let map = null; const fullmap = window.innerWidth > 800; const embed = window.parent !== window && !fullmap ? true : false; config.colors = config.colors || {}; config.colors = { rfh: config.colors.rfh || "darkorchid", rf9: config.colors.rf9 || "magenta", rf2: config.colors.rf2 || "purple", rf3: config.colors.rf3 || "blue", rf5: config.colors.rf5 || "orange", tun: config.colors.tun || "grey", xlink: config.colors.xlink || "red", supertun: config.colors.supertun || "green", longdtd: config.colors.longdtd || "limegreen" }; const rfd = { "H": { type: "FeatureCollection", features: [] }, "9": { type: "FeatureCollection", features: [] }, "2": { type: "FeatureCollection", features: [] }, "3": { type: "FeatureCollection", features: [] }, "5": { type: "FeatureCollection", features: [] }, "N": { type: "FeatureCollection", features: [] }, "S": { type: "FeatureCollection", features: [] } } const tun = { type: "FeatureCollection", features: [] }; const xlink = { type: "FeatureCollection", features: [] }; const supertun = { type: "FeatureCollection", features: [] }; const longdtd = { type: "FeatureCollection", features: [] }; const measurements = { type: "FeatureCollection", features: [] }; const mapStyles = { standard: { version: 8, sources: { openstreetmaps: { type: "raster", tiles: [ "https://tile.openstreetmap.org/{z}/{x}/{y}.png" ], tileSize: 256, attribution: "© OpenStreetMap Contributors", maxzoom: 19 }, rfh: { type: "geojson", data: rfd["H"] }, rf9: { type: "geojson", data: rfd["9"] }, rf2: { type: "geojson", data: rfd["2"] }, rf3: { type: "geojson", data: rfd["3"] }, rf5: { type: "geojson", data: rfd["5"] }, tun: { type: "geojson", data: tun }, xlink: { type: "geojson", data: xlink }, supertun: { type: "geojson", data: supertun }, longdtd: { type: "geojson", data: longdtd }, measurement: { type: "geojson", data: measurements } }, layers: [ { id: "openstreetmaps", type: "raster", source: "openstreetmaps" }, { id: "rfh", type: "line", source: "rfh", paint: { "line-color": config.colors.rfh, "line-width": 2 } }, { id: "rf9", type: "line", source: "rf9", paint: { "line-color": config.colors.rf9, "line-width": 2 } }, { id: "rf2", type: "line", source: "rf2", paint: { "line-color": config.colors.rf2, "line-width": 2 } }, { id: "rf3", type: "line", source: "rf3", paint: { "line-color": config.colors.rf3, "line-width": 2 } }, { id: "rf5", type: "line", source: "rf5", paint: { "line-color": config.colors.rf5, "line-width": 2 } }, { id: "tun", type: "line", source: "tun", paint: { "line-color": config.colors.tun, "line-width": 2, "line-dasharray": [ 3, 2 ] } }, { id: "xlink", type: "line", source: "xlink", paint: { "line-color": config.colors.xlink, "line-width": 2, "line-dasharray": [ 3, 2 ] } }, { id: "supertun", type: "line", source: "supertun", paint: { "line-color": config.colors.supertun, "line-width": 2, "line-dasharray": [ 3, 2 ] } }, { id: "longdtd", type: "line", source: "longdtd", paint: { "line-color": config.colors.longdtd, "line-width": 2, "line-dasharray": [ 1, 1 ] } }, { id: "measurement-points", type: "circle", source: "measurement", paint: { "circle-radius": 5, "circle-color": "red" }, filter: ["in", "$type", "Point"] }, { id: "measurement-lines", type: "line", source: "measurement", paint: { "line-width": 2, "line-color": "red" }, filter: ["in", "$type", "LineString"] } ] }, buildings: { version: 8, sources: { openstreetmaps: { type: "raster", tiles: [ "https://tile.openstreetmap.org/{z}/{x}/{y}.png" ], tileSize: 256, attribution: "© OpenStreetMap Contributors", maxzoom: 19 }, rfh: { type: "geojson", data: rfd["H"] }, rf9: { type: "geojson", data: rfd["9"] }, rf2: { type: "geojson", data: rfd["2"] }, rf3: { type: "geojson", data: rfd["3"] }, rf5: { type: "geojson", data: rfd["5"] }, tun: { type: "geojson", data: tun }, xlink: { type: "geojson", data: xlink }, supertun: { type: "geojson", data: supertun }, longdtd: { type: "geojson", data: longdtd }, measurement: { type: "geojson", data: measurements } }, layers: [ { id: "openstreetmaps", type: "raster", source: "openstreetmaps" }, { id: "rfh", type: "line", source: "rfh", paint: { "line-color": config.colors.rfh, "line-width": 2 } }, { id: "rf9", type: "line", source: "rf9", paint: { "line-color": config.colors.rf9, "line-width": 2 } }, { id: "rf2", type: "line", source: "rf2", paint: { "line-color": config.colors.rf2, "line-width": 2 } }, { id: "rf3", type: "line", source: "rf3", paint: { "line-color": config.colors.rf3, "line-width": 2 } }, { id: "rf5", type: "line", source: "rf5", paint: { "line-color": config.colors.rf5, "line-width": 2 } }, { id: "tun", type: "line", source: "tun", paint: { "line-color": config.colors.tun, "line-width": 2, "line-dasharray": [ 3, 2 ] } }, { id: "xlink", type: "line", source: "xlink", paint: { "line-color": config.colors.xlink, "line-width": 2, "line-dasharray": [ 3, 2 ] } }, { id: "supertun", type: "line", source: "supertun", paint: { "line-color": config.colors.supertun, "line-width": 2, "line-dasharray": [ 3, 2 ] } }, { id: "longdtd", type: "line", source: "longdtd", paint: { "line-color": config.colors.longdtd, "line-width": 2, "line-dasharray": [ 1, 1 ] } }, { id: "measurement-points", type: "circle", source: "measurement", paint: { "circle-radius": 5, "circle-color": "red" }, filter: ["in", "$type", "Point"] }, { id: "measurement-lines", type: "line", source: "measurement", paint: { "line-width": 2, "line-color": "red" }, filter: ["in", "$type", "LineString"] } ] }, topology: { version: 8, sources: { opentopomap: { type: "raster", tiles: [ "https://tile.opentopomap.org/{z}/{x}/{y}.png" ], tileSize: 256, attribution: "© OpenStreetMap Contributors", maxzoom: 17 }, rfh: { type: "geojson", data: rfd["H"] }, rf9: { type: "geojson", data: rfd["9"] }, rf2: { type: "geojson", data: rfd["2"] }, rf3: { type: "geojson", data: rfd["3"] }, rf5: { type: "geojson", data: rfd["5"] }, tun: { type: "geojson", data: tun }, xlink: { type: "geojson", data: xlink }, supertun: { type: "geojson", data: supertun }, longdtd: { type: "geojson", data: longdtd }, measurement: { type: "geojson", data: measurements } }, layers: [ { id: "opentopomap", type: "raster", source: "opentopomap" }, { id: "rfh", type: "line", source: "rfh", paint: { "line-color": config.colors.rfh, "line-width": 2 } }, { id: "rf9", type: "line", source: "rf9", paint: { "line-color": config.colors.rf9, "line-width": 2 } }, { id: "rf2", type: "line", source: "rf2", paint: { "line-color": config.colors.rf2, "line-width": 2 } }, { id: "rf3", type: "line", source: "rf3", paint: { "line-color": config.colors.rf3, "line-width": 2 } }, { id: "rf5", type: "line", source: "rf5", paint: { "line-color": config.colors.rf5, "line-width": 2 } }, { id: "tun", type: "line", source: "tun", paint: { "line-color": config.colors.tun, "line-width": 2, "line-dasharray": [ 3, 2 ] } }, { id: "xlink", type: "line", source: "xlink", paint: { "line-color": config.colors.xlink, "line-width": 2, "line-dasharray": [ 3, 2 ] } }, { id: "supertun", type: "line", source: "supertun", paint: { "line-color": config.colors.supertun, "line-width": 2, "line-dasharray": [ 3, 2 ] } }, { id: "longdtd", type: "line", source: "longdtd", paint: { "line-color": config.colors.longdtd, "line-width": 2, "line-dasharray": [ 1, 1 ] } }, { id: "measurement-points", type: "circle", source: "measurement", paint: { "circle-radius": 5, "circle-color": "red" }, filter: ["in", "$type", "Point"] }, { id: "measurement-lines", type: "line", source: "measurement", paint: { "line-width": 2, "line-color": "red" }, filter: ["in", "$type", "LineString"] } ] }, satellite: { version: 8, sources: { landsat: { type: "raster", tiles: [ "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}" ], tileSize: 256, attribution: "© Landsat / Copernicus, Maxar Technologies", maxzoom: 20 }, rfh: { type: "geojson", data: rfd["H"] }, rf9: { type: "geojson", data: rfd["9"] }, rf2: { type: "geojson", data: rfd["2"] }, rf3: { type: "geojson", data: rfd["3"] }, rf5: { type: "geojson", data: rfd["5"] }, tun: { type: "geojson", data: tun }, xlink: { type: "geojson", data: xlink }, supertun: { type: "geojson", data: supertun }, longdtd: { type: "geojson", data: longdtd }, measurement: { type: "geojson", data: measurements } }, layers: [ { id: "landsat", type: "raster", source: "landsat" }, { id: "rfh", type: "line", source: "rfh", paint: { "line-color": config.colors.rfh, "line-width": 2 } }, { id: "rf9", type: "line", source: "rf9", paint: { "line-color": config.colors.rf9, "line-width": 2 } }, { id: "rf2", type: "line", source: "rf2", paint: { "line-color": config.colors.rf2, "line-width": 2 } }, { id: "rf3", type: "line", source: "rf3", paint: { "line-color": config.colors.rf3, "line-width": 2 } }, { id: "rf5", type: "line", source: "rf5", paint: { "line-color": config.colors.rf5, "line-width": 2 } }, { id: "tun", type: "line", source: "tun", paint: { "line-color": config.colors.tun, "line-width": 2, "line-dasharray": [ 3, 2 ] } }, { id: "xlink", type: "line", source: "xlink", paint: { "line-color": config.colors.xlink, "line-width": 2, "line-dasharray": [ 3, 2 ] } }, { id: "supertun", type: "line", source: "supertun", paint: { "line-color": config.colors.supertun, "line-width": 2, "line-dasharray": [ 3, 2 ] } }, { id: "longdtd", type: "line", source: "longdtd", paint: { "line-color": config.colors.longdtd, "line-width": 2, "line-dasharray": [ 1, 1 ] } }, { id: "measurement-points", type: "circle", source: "measurement", paint: { "circle-radius": 5, "circle-color": "red" }, filter: ["in", "$type", "Point"] }, { id: "measurement-lines", type: "line", source: "measurement", paint: { "line-width": 2, "line-color": "red" }, filter: ["in", "$type", "LineString"] } ] } }; if (config.maptiler && !embed) { mapStyles.standard.sources.maptiler = { type: "raster-dem", url: `https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=${config.maptiler}`, tileSize: 512 }; mapStyles.standard.terrain = { source: "maptiler", exaggeration: 0 }; mapStyles.topology.sources.maptiler = { type: "raster-dem", url: `https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=${config.maptiler}`, tileSize: 512 }; mapStyles.topology.terrain = { source: "maptiler", exaggeration: 1.5 }; mapStyles.satellite.sources.maptiler = { type: "raster-dem", url: `https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=${config.maptiler}`, tileSize: 512 }; mapStyles.satellite.terrain = { source: "maptiler", exaggeration: 1.5 }; mapStyles.buildings.sources.openmaptiles = { type: "vector", url: `https://api.maptiler.com/tiles/v3/tiles.json?key=${config.maptiler}`, tileSize: 512 }; mapStyles.buildings.layers.push({ id: "3d-buildings", source: "openmaptiles", "source-layer": "building", type: "fill-extrusion", minzoom: 14, paint: { "fill-extrusion-color": "lightgray", "fill-extrusion-base": [ "case", [">=", ["get", "zoom"], 14], ["get", "render_min_height"], 0 ], "fill-extrusion-height": [ "interpolate", ["linear"], ["zoom"], 14, 0, 16, ["get", "render_height"] ] } }); } if (config.sources) { for (t in config.sources) { for (k in mapStyles) { if (mapStyles[k].sources[t]) { mapStyles[k].sources[t] = config.sources[t]; } } } } const nodes = {}; const markers = {}; const radioColors = { "2": config.colors.rf2, "3": config.colors.rf3, "5": config.colors.rf5, "9": config.colors.rf9, "h": config.colors.rfh, "s": config.colors.supertun, "n": config.colors.tun }; let rfh = 0; let rf9 = 0; let rf2 = 0; let rf3 = 0; let rf5 = 0; let sn = 0; let nrf = 0; let filterKeyColor = null; let linkPopup = null; let lastMarkerClickEvent = null; let currentStyle = "standard"; let channels = {}; let filterKeyChannel = "all"; let terrain; function toRadians(d) { return d * Math.PI / 180; } function toDegrees(r) { return r * 180 / Math.PI; } function getRealLatLon(n) { if (n) { return { lat: n.lat || n.mlat, lon: n.lon || n.mlon }; } return {}; } function getVirtualLatLon(n) { if (n) { return { lat: n.mlat || n.lat, lon: n.mlon || n.lon }; } return {}; } function bearingAndDistance(from, to) { const flat = toRadians(from[0]); const flon = toRadians(from[1]); const tlat = toRadians(to[0]); const tlon = toRadians(to[1]); const y = Math.sin(tlon - flon) * Math.cos(tlat); const x = Math.cos(flat) * Math.sin(tlat) - Math.sin(flat) * Math.cos(tlat) * Math.cos(tlon - flon); const dLat = toRadians(to[0] - from[0]); const dLon = toRadians(to[1] - from[1]); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(flat) * Math.cos(tlat); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return { distance: (3963 * c).toFixed(1), bearing: ((toDegrees(Math.atan2(y, x)) + 360) % 360).toFixed(0) }; } function getFreqRange(freq, chanbw) { freq = parseInt(freq - chanbw / 2); return `${freq}-${freq + parseInt(chanbw)} MHz`; } function getRfMode(mode) { switch (mode || "adhoc") { case "sta": return "Mesh Station"; case "ptp": return "Mesh PtP"; case "ptmp": return "Mesh PtMP"; case "ap": return "Mesh PtMP/PtP"; case "adhoc": default: return "Mesh"; } } function canonicalHostname(hostname) { return hostname && hostname.toUpperCase().replace(/^\./, "").replace(/^DTDLINK\./i, "").replace(/^MID\d+\./i, "").replace(/^XLINK\d+\./i, "").replace(/\.LOCAL\.MESH$/, ""); } function getMode() { const c = document.body.classList; if (c.contains("measure")) { return "measure"; } if (c.contains("find")) { return "find"; } return "normal"; } function setMode(mode) { const c = document.body.classList; if (c.contains("measure")) { document.getElementById("mb").innerHTML = "---"; document.getElementById("md").innerHTML = "--.-"; measurements.features.length = 0; map.getSource('measurement').setData(measurements); } else if (c.contains("find")) { document.querySelector("#ff input").value = ""; } c.remove("normal"); c.remove("measure"); c.remove("find"); c.add(mode); if (mode === "find") { setTimeout(() => { document.querySelector("#ff input").focus(); }, 0); } } function openPopup(chostname, zoom) { for (m in markers) { if (markers[m].getPopup().isOpen()) { markers[m].togglePopup(); } } if (linkPopup) { linkPopup.remove(); linkPopup = null; } const marker = markers[chostname]; if (marker && marker._map) { const options = { center: marker.getLngLat(), speed: 1 }; if (zoom !== undefined) { options.zoom = zoom; } map.flyTo(options); map.once("moveend", () => { marker.togglePopup(); }); } } function radioColor(d) { if (d.node_details.mesh_supernode) { return config.colors.supertun || "green"; } const rf = d.meshrf; const chan = parseInt(rf.channel); if (chan >= 3380 && chan <= 3495) { return config.colors.rf3 || "blue"; } const chanbw = parseInt(rf.chanbw); let k = (`${rf.freq}` || "X")[0]; if (k == "9" && chanbw < 10 && chanbw != 5) { k = "h"; } return radioColors[k] || config.colors.tun || "gray"; } function radioAzimuth(d) { const a = d.meshrf.azimuth; if (isNaN(a)) { return null; } return 180 + parseInt(a); } function createMarkers() { for (cname in nodes) { const data = nodes[cname].data; if (!markers[cname]) { const loc = getVirtualLatLon(data); if (loc.lat && loc.lon) { const rot = radioAzimuth(data); markers[cname] = new maplibregl.Marker({ anchor: "top", color: radioColor(data), opacity: 1, scale: 0.8, pitchAlignment: "viewport", rotationAlignment: rot === null ? "viewport" : "map", rotation: rot }).setLngLat([ loc.lon, loc.lat ]).setPopup(makePopup(data)); markers[cname].getElement().addEventListener("click", e => { lastMarkerClickEvent = e; }); } } else { if (!markers[cname].getPopup().isOpen()) { markers[cname].setPopup(makePopup(data)); } } } } function updateMarkers() { for (cname in markers) { const m = markers[cname]; if (filterKeyChannel !== "all" && m.getPopup()._channel == filterKeyChannel) { if (!m._map) { m.addTo(map); } } else if (filterKeyChannel === "all" && (!filterKeyColor || filterKeyColor == m._color)) { if (!m._map) { m.addTo(map); } } else { m.remove(); } } } function updateSources() { map.getSource("rfh").setData(rfd["H"]); map.getSource("rf9").setData(rfd["9"]); map.getSource("rf2").setData(rfd["2"]); map.getSource("rf3").setData(rfd["3"]); map.getSource("rf5").setData(rfd["5"]); map.getSource("tun").setData(tun); map.getSource("xlink").setData(xlink); map.getSource("supertun").setData(supertun); map.getSource("longdtd").setData(longdtd); } function messageLocation() { if (window.parent !== window) { map.on("move", () => { const lnglat = map.getBounds().getCenter(); window.parent.postMessage( JSON.stringify({ type: "location", lat: lnglat.lat, lon: lnglat.lng }), "*"); }); window.addEventListener("message", e => { const msg = JSON.parse(e.data); if (msg.type === "change-location") { map.flyTo({ center: [ msg.lon, msg.lat ], speed: embed ? 20 : 1 }); } }); } } function loadMap() { map = new maplibregl.Map({ container: "map", style: mapStyles.standard, center: [ config.lon, config.lat ], zoom: config.zoom, hash: true, boxZoom: false, //maxTileCacheSize: 1024 * 1024, //maxTileCacheZoomLevels: 8, refreshExpiredTiles: false, attributionControl: embed ? false : { compact: true } }); if (!embed) { map.addControl(new maplibregl.NavigationControl({ visualizePitch: true }), "bottom-right"); terrain = new maplibregl.TerrainControl({ source: 'maptiler', exaggeration: 1.5 }); map.addControl(terrain, "bottom-right"); map.once("style.load", () => terrain._toggleTerrain()); // Terrain off by default to make maps faster } createMarkers(); updateMarkers(); document.querySelector("#ctrl select").innerHTML = Object.keys(mapStyles).map(style => ``); messageLocation(); } function selectMap(v, enableTerrain) { const style = mapStyles[v]; if (style && v !== currentStyle) { currentStyle = v; map.setStyle(style, { diff: false }); document.querySelector("#ctrl select").value = v; if (!enableTerrain) { map.once("style.load", () => terrain._toggleTerrain()); } } } function downloadData(v) { switch (v) { case "csv": case "kml": case "json": const url = `${location.origin}/data/out.${v}`; const a = document.createElement("A"); a.href = url; a.download = url.split("/").pop(); document.body.appendChild(a); a.click(); document.body.removeChild(a); break; default: break; } } function filterKey(color) { if (!color) { filterKeyColor = null; } else { color = radioColors[color]; if (color === filterKeyColor) { filterKeyColor = null; } else { filterKeyColor = color; } } updateLinks(); updateKey(); updateMarkers(); updateSources(); } function filterChannel(chan) { filterKeyColor = null; filterKeyChannel = chan; updateLinks(); updateKey(); updateMarkers(); updateSources(); } function updateKey() { function sel(c) { return !filterKeyColor || filterKeyColor == radioColors[c]; } const key = document.getElementById("key"); key.innerHTML = `
| Band | Nodes |
| HaLow | " + rfh + " |
| 900 MHz | " + rf9 + " |
| 2.4 GHz | " + rf2 + " |
| 3.4 GHz | " + rf3 + " |
| 5 GHz | " + rf5 + " |
| Supernode | " + sn + " |
| No RF | " + nrf + " |
| Total | ${out.nodeInfo.length} |
| Description | " + i.description.replace("°", "\u00B0") + " |
| Location | ${rloc.lat},${rloc.lon} |
| Antenna | " + rf.antenna.description.replace("°", "\u00B0") + " |
| Polarization | " + rf.polarization + " |
| Azimuth | " + rf.azimuth + "° |
| Height | " + rf.height + " m |
| Elevation | " + rf.elevation + "° |
| Last seen | ${ d.lastseen > todayStart ? lastseen + " today" : d.lastseen > yesterdayStart ? lastseen + " yesterday" : d.lastseen > weekStart ? "The last 7 days" : "A long time ago..." } |
| RF Status | ${rf.status} |
| SSID | " + rf.ssid + " |
| Channel | " + rf.channel + " |
| Mode | " + getRfMode(rf.mode) + " |
| Frequency | " + getFreqRange(rf.freq, rf.chanbw) + " |
| Bandwidth | " + rf.chanbw + " MHz |
| MAC | " + d.interfaces[0].mac + " |
| Hardware | ${i.hardware || ""} |
| Firmware | ${i.firmware_version || ""} |
| Neighbors | ${neighbors.join("") || " None "} |