diff --git a/db.go b/db.go
index e42292e..01e9751 100644
--- a/db.go
+++ b/db.go
@@ -25,6 +25,7 @@ CREATE TABLE IF NOT EXISTS repeaters (
trustee TEXT NOT NULL DEFAULT '',
ipsc_network TEXT NOT NULL DEFAULT '',
network TEXT NOT NULL DEFAULT '',
+ hotspot INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'ACTIVE'
);
@@ -49,6 +50,7 @@ type Repeater struct {
Trustee string `json:"trustee"`
IpscNetwork string `json:"ipsc_network"`
Network string `json:"network"`
+ Hotspot int `json:"hotspot"`
Status string `json:"status"`
}
@@ -64,9 +66,9 @@ func openDB(path string) (*sql.DB, error) {
return db, nil
}
-func queryRepeaters(db *sql.DB, minLat, maxLat, minLng, maxLng float64, band string, networks []string) ([]Repeater, error) {
+func queryRepeaters(db *sql.DB, minLat, maxLat, minLng, maxLng float64, band string, networks []string, showHotspots bool) ([]Repeater, error) {
query := `SELECT id, callsign, frequency, band, lat, lng, city, state, country,
- color_code, offset, ts_linked, trustee, ipsc_network, network, status
+ color_code, offset, ts_linked, trustee, ipsc_network, network, hotspot, status
FROM repeaters WHERE lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?`
args := []interface{}{minLat, maxLat, minLng, maxLng}
@@ -82,6 +84,10 @@ func queryRepeaters(db *sql.DB, minLat, maxLat, minLng, maxLng float64, band str
query += " AND band IN ('2m', '70cm')"
}
+ if !showHotspots {
+ query += " AND hotspot = 0"
+ }
+
// Network filter: only apply when not all 4 categories are selected
if len(networks) > 0 && len(networks) < 4 {
var placeholders []string
@@ -118,7 +124,7 @@ func queryRepeaters(db *sql.DB, minLat, maxLat, minLng, maxLng float64, band str
if err := rows.Scan(&r.ID, &r.Callsign, &r.Frequency, &r.Band,
&r.Lat, &r.Lng, &r.City, &r.State, &r.Country,
&r.ColorCode, &r.Offset, &r.TsLinked, &r.Trustee,
- &r.IpscNetwork, &r.Network, &r.Status); err != nil {
+ &r.IpscNetwork, &r.Network, &r.Hotspot, &r.Status); err != nil {
return nil, err
}
results = append(results, r)
@@ -131,7 +137,7 @@ func queryRepeaters(db *sql.DB, minLat, maxLat, minLng, maxLng float64, band str
}
// Route corridor query: find repeaters within corridorKm of a polyline.
-func queryRepeatersAlongRoute(db *sql.DB, points [][2]float64, corridorKm float64, band string, networks []string) ([]Repeater, error) {
+func queryRepeatersAlongRoute(db *sql.DB, points [][2]float64, corridorKm float64, band string, networks []string, showHotspots bool) ([]Repeater, error) {
if len(points) == 0 {
return []Repeater{}, nil
}
@@ -162,7 +168,7 @@ func queryRepeatersAlongRoute(db *sql.DB, points [][2]float64, corridorKm float6
maxLng += lngPad
// Fetch candidates from bounding box
- candidates, err := queryRepeaters(db, minLat, maxLat, minLng, maxLng, band, networks)
+ candidates, err := queryRepeaters(db, minLat, maxLat, minLng, maxLng, band, networks, showHotspots)
if err != nil {
return nil, err
}
diff --git a/handlers.go b/handlers.go
index 7e3929a..8b65294 100644
--- a/handlers.go
+++ b/handlers.go
@@ -18,6 +18,7 @@ type routeRequest struct {
Band string `json:"band"`
Corridor float64 `json:"corridor"`
Network []string `json:"network"`
+ Hotspots bool `json:"hotspots"`
}
func handleRepeaters(db *sql.DB) http.HandlerFunc {
@@ -52,7 +53,9 @@ func handleRepeaters(db *sql.DB) http.HandlerFunc {
networks = strings.Split(net, ",")
}
- repeaters, err := queryRepeaters(db, minLat, maxLat, minLng, maxLng, band, networks)
+ showHotspots := q.Get("hotspots") == "1"
+
+ repeaters, err := queryRepeaters(db, minLat, maxLat, minLng, maxLng, band, networks, showHotspots)
if err != nil {
http.Error(w, `{"error":"database query failed"}`, http.StatusInternalServerError)
return
@@ -99,7 +102,7 @@ func handleRouteRepeaters(db *sql.DB) http.HandlerFunc {
req.Corridor = 10
}
- repeaters, err := queryRepeatersAlongRoute(db, req.Points, req.Corridor, req.Band, req.Network)
+ repeaters, err := queryRepeatersAlongRoute(db, req.Points, req.Corridor, req.Band, req.Network, req.Hotspots)
if err != nil {
http.Error(w, `{"error":"database query failed"}`, http.StatusInternalServerError)
return
diff --git a/seed.go b/seed.go
index e2af855..eecd2e2 100644
--- a/seed.go
+++ b/seed.go
@@ -24,6 +24,7 @@ type rawRepeater struct {
TsLinked string `json:"ts_linked"`
Trustee string `json:"trustee"`
IpscNetwork string `json:"ipsc_network"`
+ MapInfo string `json:"map_info"`
Status string `json:"status"`
}
@@ -42,12 +43,6 @@ func seedDatabase(dbPath, jsonPath string) error {
return fmt.Errorf("create schema: %w", err)
}
- // Migration: add network column for existing databases
- if _, err := db.Exec("ALTER TABLE repeaters ADD COLUMN network TEXT NOT NULL DEFAULT ''"); err == nil {
- db.Exec("DELETE FROM repeaters")
- log.Println("Added network column, re-seeding database")
- }
-
var count int
if err := db.QueryRow("SELECT COUNT(*) FROM repeaters").Scan(&count); err != nil {
return fmt.Errorf("count check: %w", err)
@@ -75,15 +70,15 @@ func seedDatabase(dbPath, jsonPath string) error {
stmt, err := tx.Prepare(`INSERT INTO repeaters
(id, callsign, frequency, band, lat, lng, city, state, country,
- color_code, offset, ts_linked, trustee, ipsc_network, network, status)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
+ color_code, offset, ts_linked, trustee, ipsc_network, network, hotspot, status)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
if err != nil {
tx.Rollback()
return fmt.Errorf("prepare stmt: %w", err)
}
defer stmt.Close()
- inserted, skipped := 0, 0
+ inserted, skipped, hotspots := 0, 0, 0
for _, r := range raw.Rptrs {
lat, lng, ok := parseCoords(r.Lat, r.Lng)
if !ok {
@@ -98,9 +93,14 @@ func seedDatabase(dbPath, jsonPath string) error {
band := classifyBand(freq)
network := classifyNetwork(r.IpscNetwork)
+ hotspot := 0
+ if isHotspot(r.Offset, r.City, r.MapInfo) {
+ hotspot = 1
+ hotspots++
+ }
if _, err := stmt.Exec(r.ID, r.Callsign, freq, band, lat, lng,
r.City, r.State, r.Country, r.ColorCode, r.Offset,
- r.TsLinked, r.Trustee, r.IpscNetwork, network, r.Status); err != nil {
+ r.TsLinked, r.Trustee, r.IpscNetwork, network, hotspot, r.Status); err != nil {
log.Printf("Warning: skipping repeater %d (%s): %v", r.ID, r.Callsign, err)
skipped++
continue
@@ -112,7 +112,7 @@ func seedDatabase(dbPath, jsonPath string) error {
return fmt.Errorf("commit: %w", err)
}
- log.Printf("Seeded %d repeaters (%d skipped)", inserted, skipped)
+ log.Printf("Seeded %d repeaters (%d skipped, %d hotspots)", inserted, skipped, hotspots)
return nil
}
@@ -172,6 +172,23 @@ func classifyBand(freq float64) string {
return "other"
}
+func isHotspot(offset, city, mapInfo string) bool {
+ // Zero offset indicates simplex (likely a personal hotspot)
+ if off, err := strconv.ParseFloat(strings.TrimSpace(offset), 64); err == nil && math.Abs(off) < 0.01 {
+ return true
+ }
+ // Hotspot keywords in city or map_info
+ check := strings.ToLower(city) + " " + strings.ToLower(mapInfo)
+ if strings.Contains(check, "hotspot") ||
+ strings.Contains(check, "pistar") ||
+ strings.Contains(check, "pi-star") ||
+ strings.Contains(check, "mmdvm") ||
+ strings.Contains(check, "simplex") {
+ return true
+ }
+ return false
+}
+
func classifyNetwork(raw string) string {
s := strings.ToLower(strings.TrimSpace(raw))
if s == "" || s == "none" || s == "n/a" || s == "?" || s == "-" || s == "no" {
diff --git a/static/app.js b/static/app.js
index 1a76348..f35515d 100644
--- a/static/app.js
+++ b/static/app.js
@@ -45,6 +45,7 @@
var netDmrplus = document.getElementById("net-dmrplus");
var netTgif = document.getElementById("net-tgif");
var netOther = document.getElementById("net-other");
+ var showHotspots = document.getElementById("show-hotspots");
var countEl = document.getElementById("count");
var fromInput = document.getElementById("route-from");
var toInput = document.getElementById("route-to");
@@ -317,6 +318,7 @@
maxLng: bounds.getEast(),
band: getSelectedBand(),
network: getSelectedNetworks(),
+ hotspots: showHotspots.checked ? "1" : "0",
});
fetch("/api/repeaters?" + params, { signal: controller.signal })
@@ -397,6 +399,7 @@
band: getSelectedBand(),
corridor: 10,
network: getSelectedNetworks() === "all" ? [] : getSelectedNetworks().split(","),
+ hotspots: showHotspots.checked,
}),
})
.then(function (resp) {
@@ -494,6 +497,11 @@
});
});
+ showHotspots.addEventListener("change", function () {
+ if (isRouteMode) fetchRouteRepeaters();
+ else fetchRepeaters();
+ });
+
routeBtn.addEventListener("click", findRoute);
clearBtn.addEventListener("click", clearRoute);
diff --git a/static/index.html b/static/index.html
index 7b4f4e5..c6ab2ed 100644
--- a/static/index.html
+++ b/static/index.html
@@ -22,6 +22,10 @@
+ Experimental
+
+