-
Notifications
You must be signed in to change notification settings - Fork 261
feat(map): add heatmap layer #969
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d6bbb25
a087f9c
1b936e2
1ba7ee3
d9fce31
e84750a
47d8200
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| import { hasPos, toLngLat } from "@core/utils/geo"; | ||
| import type { Protobuf } from "@meshtastic/core"; | ||
| import type { Feature, FeatureCollection } from "geojson"; | ||
| import type { HeatmapLayerSpecification } from "maplibre-gl"; | ||
| import { useMemo } from "react"; | ||
| import { Layer, Source } from "react-map-gl/maplibre"; | ||
|
|
||
| export type HeatmapMode = "density" | "snr"; | ||
|
|
||
| export interface HeatmapLayerProps { | ||
| id: string; | ||
| filteredNodes: Protobuf.Mesh.NodeInfo[]; | ||
| mode: HeatmapMode; | ||
| } | ||
|
|
||
| export const HeatmapLayer = ({ | ||
| id, | ||
| filteredNodes, | ||
| mode, | ||
| }: HeatmapLayerProps) => { | ||
| const data: FeatureCollection = useMemo(() => { | ||
| const features: Feature[] = filteredNodes | ||
| .filter((node) => hasPos(node.position)) | ||
| .filter((node) => mode !== "snr" || node.snr !== undefined) | ||
| .map((node) => ({ | ||
| type: "Feature", | ||
| geometry: { | ||
| type: "Point", | ||
| coordinates: toLngLat(node.position), | ||
| }, | ||
| properties: { | ||
| snr: node.snr, | ||
| name: node.user?.longName, | ||
| shortName: node.user?.shortName, | ||
| num: node.num, | ||
| }, | ||
| })); | ||
|
|
||
| return { | ||
| type: "FeatureCollection", | ||
| features, | ||
| }; | ||
| }, [filteredNodes, mode]); | ||
|
|
||
| const paintProps: HeatmapLayerSpecification["paint"] = useMemo( | ||
| () => ({ | ||
| "heatmap-weight": | ||
| mode === "density" | ||
| ? 1 | ||
| : ["interpolate", ["linear"], ["get", "snr"], -20, 0, 10, 1], | ||
| "heatmap-intensity": ["interpolate", ["linear"], ["zoom"], 0, 1, 15, 3], | ||
| // Color ramp for heatmap. Domain is 0 (low) to 1 (high). | ||
| // Begin color ramp at 0-stop with a 0-transparancy color | ||
| // to create a blur-like effect. | ||
| "heatmap-color": [ | ||
| "interpolate", | ||
| ["linear"], | ||
| ["heatmap-density"], | ||
| 0, | ||
| "rgba(33,102,172,0)", | ||
| 0.2, | ||
| "rgb(103,169,207)", | ||
| 0.4, | ||
| "rgb(209,229,240)", | ||
| 0.6, | ||
| "rgb(253,219,199)", | ||
| 0.8, | ||
| "rgb(239,138,98)", | ||
| 1, | ||
| "rgb(178,24,43)", | ||
| ], | ||
| "heatmap-radius": [ | ||
| "interpolate", | ||
| ["linear"], | ||
| ["zoom"], | ||
| 0, | ||
| 2, | ||
| 9, | ||
| 20, | ||
| 15, | ||
| 30, | ||
| ], | ||
| // Opacity 0.7 to be visible but not blocking | ||
| "heatmap-opacity": 0.7, | ||
| }), | ||
| [mode], | ||
| ); | ||
|
|
||
| return ( | ||
| <Source type="geojson" data={data}> | ||
| <Layer id={id} type="heatmap" paint={paintProps} /> | ||
| <Layer | ||
| id={`${id}-interaction`} | ||
| type="circle" | ||
| paint={{ | ||
| "circle-radius": 15, | ||
| "circle-opacity": 0, | ||
| "circle-stroke-opacity": 0, | ||
| }} | ||
| /> | ||
| </Source> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import type { HeatmapMode } from "@components/PageComponents/Map/Layers/HeatmapLayer.tsx"; | ||
| import { Checkbox } from "@components/UI/Checkbox/index.tsx"; | ||
| import { | ||
| Popover, | ||
|
|
@@ -16,6 +17,7 @@ export interface VisibilityState { | |
| positionPrecision: boolean; | ||
| traceroutes: boolean; | ||
| waypoints: boolean; | ||
| heatmap: boolean; | ||
| } | ||
|
|
||
| export const defaultVisibilityState: VisibilityState = { | ||
|
|
@@ -25,11 +27,14 @@ export const defaultVisibilityState: VisibilityState = { | |
| positionPrecision: false, | ||
| traceroutes: false, | ||
| waypoints: true, | ||
| heatmap: false, | ||
| }; | ||
|
|
||
| interface MapLayerToolProps { | ||
| visibilityState: VisibilityState; | ||
| setVisibilityState: (state: VisibilityState) => void; | ||
| heatmapMode: HeatmapMode; | ||
| setHeatmapMode: (mode: HeatmapMode) => void; | ||
| } | ||
|
|
||
| interface CheckboxProps { | ||
|
|
@@ -59,6 +64,8 @@ const CheckboxItem = ({ | |
| export function MapLayerTool({ | ||
| visibilityState, | ||
| setVisibilityState, | ||
| heatmapMode, | ||
| setHeatmapMode, | ||
| }: MapLayerToolProps): ReactNode { | ||
| const { t } = useTranslation("map"); | ||
|
|
||
|
|
@@ -67,10 +74,23 @@ export function MapLayerTool({ | |
| }, [visibilityState]); | ||
|
|
||
| const handleCheckboxChange = (key: keyof VisibilityState) => { | ||
| setVisibilityState({ | ||
| ...visibilityState, | ||
| [key]: !visibilityState[key], | ||
| }); | ||
| if (key === "heatmap" && !visibilityState.heatmap) { | ||
| // If turning heatmap on, turn everything else off so the layer is visible | ||
| setVisibilityState({ | ||
| nodeMarkers: false, | ||
| directNeighbors: false, | ||
| remoteNeighbors: false, | ||
| positionPrecision: false, | ||
| traceroutes: false, | ||
| waypoints: false, | ||
| heatmap: true, | ||
| }); | ||
|
Comment on lines
+77
to
+87
|
||
| } else { | ||
| setVisibilityState({ | ||
| ...visibilityState, | ||
| [key]: !visibilityState[key], | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| const layers = useMemo( | ||
|
|
@@ -80,6 +100,7 @@ export function MapLayerTool({ | |
| { key: "directNeighbors", label: t("layerTool.directNeighbors") }, | ||
| { key: "remoteNeighbors", label: t("layerTool.remoteNeighbors") }, | ||
| { key: "positionPrecision", label: t("layerTool.positionPrecision") }, | ||
| { key: "heatmap", label: t("layerTool.heatmap") }, | ||
| // { key: "traceroutes", label: t("layerTool.traceroutes") }, | ||
| ], | ||
| [t], | ||
|
|
@@ -124,12 +145,39 @@ export function MapLayerTool({ | |
| sideOffset={7} | ||
| > | ||
| {layers.map(({ key, label }) => ( | ||
| <CheckboxItem | ||
| key={key} | ||
| label={label} | ||
| checked={visibilityState[key as keyof VisibilityState]} | ||
| onChange={() => handleCheckboxChange(key as keyof VisibilityState)} | ||
| /> | ||
| <div key={key}> | ||
| <CheckboxItem | ||
| label={label} | ||
| checked={visibilityState[key as keyof VisibilityState]} | ||
| onChange={() => | ||
| handleCheckboxChange(key as keyof VisibilityState) | ||
| } | ||
| /> | ||
| {key === "heatmap" && visibilityState.heatmap && ( | ||
| <div className="pl-6 pt-2 flex flex-col gap-1"> | ||
| <label className="flex items-center gap-2 cursor-pointer"> | ||
| <input | ||
| type="radio" | ||
| className="accent-blue-500" | ||
| checked={heatmapMode === "density"} | ||
| onChange={() => setHeatmapMode("density")} | ||
| /> | ||
| <span className="text-sm dark:text-slate-300"> | ||
| {t("layerTool.density")} | ||
| </span> | ||
| </label> | ||
| <label className="flex items-center gap-2 cursor-pointer"> | ||
| <input | ||
| type="radio" | ||
| className="accent-blue-500" | ||
| checked={heatmapMode === "snr"} | ||
| onChange={() => setHeatmapMode("snr")} | ||
| /> | ||
| <span className="text-sm dark:text-slate-300">SNR</span> | ||
| </label> | ||
| </div> | ||
|
Comment on lines
+157
to
+178
|
||
| )} | ||
| </div> | ||
| ))} | ||
| {/*<CheckboxItem | ||
| key="traceroutes" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This use of variable 'to' always evaluates to true.