mirror of
https://github.com/dpup/meshstream.git
synced 2026-03-28 17:42:37 +01:00
Gate map layers behind onLoad to fix missing markers
Source/Layer components mounted before the map style finishes loading fail silently. Add mapLoaded state + onLoad callback to LocationMap, NodeLocationMap, and NetworkMap so GeoJSON sources and layers are only added after the style is ready. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useState, useEffect, useMemo } from "react";
|
||||
import React, { useRef, useState, useEffect, useMemo, useCallback } from "react";
|
||||
import ReactMap, { Source, Layer } from "react-map-gl/maplibre";
|
||||
import type { FeatureCollection } from "geojson";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
@@ -26,6 +26,8 @@ export const LocationMap: React.FC<LocationMapProps> = ({
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [mapLoaded, setMapLoaded] = useState(false);
|
||||
const handleLoad = useCallback(() => setMapLoaded(true), []);
|
||||
|
||||
// Only mount the WebGL map when the container enters the viewport.
|
||||
// This prevents exhausting the browser's WebGL context limit (~8-16)
|
||||
@@ -79,25 +81,30 @@ export const LocationMap: React.FC<LocationMapProps> = ({
|
||||
initialViewState={{ longitude, latitude, zoom: effectiveZoom }}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
attributionControl={{ compact: true }}
|
||||
onLoad={handleLoad}
|
||||
>
|
||||
{showAccuracyCircle && (
|
||||
<Source id="circle" type="geojson" data={circleGeoJSON}>
|
||||
<Layer id="circle-fill" type="fill" paint={{ "fill-color": "#4ade80", "fill-opacity": 0.15 }} />
|
||||
<Layer id="circle-outline" type="line" paint={{ "line-color": "#22c55e", "line-width": 1.5, "line-opacity": 0.8 }} />
|
||||
</Source>
|
||||
{mapLoaded && (
|
||||
<>
|
||||
{showAccuracyCircle && (
|
||||
<Source id="circle" type="geojson" data={circleGeoJSON}>
|
||||
<Layer id="circle-fill" type="fill" paint={{ "fill-color": "#4ade80", "fill-opacity": 0.15 }} />
|
||||
<Layer id="circle-outline" type="line" paint={{ "line-color": "#22c55e", "line-width": 1.5, "line-opacity": 0.8 }} />
|
||||
</Source>
|
||||
)}
|
||||
<Source id="marker" type="geojson" data={markerGeoJSON}>
|
||||
<Layer
|
||||
id="marker-dot"
|
||||
type="circle"
|
||||
paint={{
|
||||
"circle-radius": 5,
|
||||
"circle-color": "#4ade80",
|
||||
"circle-stroke-width": 2,
|
||||
"circle-stroke-color": "#22c55e",
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
</>
|
||||
)}
|
||||
<Source id="marker" type="geojson" data={markerGeoJSON}>
|
||||
<Layer
|
||||
id="marker-dot"
|
||||
type="circle"
|
||||
paint={{
|
||||
"circle-radius": 5,
|
||||
"circle-color": "#4ade80",
|
||||
"circle-stroke-width": 2,
|
||||
"circle-stroke-color": "#22c55e",
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
</ReactMap>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import ReactMap, { Source, Layer } from "react-map-gl/maplibre";
|
||||
import type { FeatureCollection } from "geojson";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
@@ -30,14 +30,12 @@ export const NodeLocationMap: React.FC<NodeLocationMapProps> = ({
|
||||
}) => {
|
||||
const accuracyMeters = calculateAccuracyFromPrecisionBits(precisionBits);
|
||||
const effectiveZoom = zoom ?? calculateZoomFromAccuracy(accuracyMeters);
|
||||
const showCenterDot = true;
|
||||
const [mapLoaded, setMapLoaded] = useState(false);
|
||||
|
||||
const markerGeoJSON = useMemo((): FeatureCollection => ({
|
||||
type: "FeatureCollection",
|
||||
features: showCenterDot
|
||||
? [{ type: "Feature", geometry: { type: "Point", coordinates: [lng, lat] }, properties: {} }]
|
||||
: [],
|
||||
}), [lat, lng, showCenterDot]);
|
||||
features: [{ type: "Feature", geometry: { type: "Point", coordinates: [lng, lat] }, properties: {} }],
|
||||
}), [lat, lng]);
|
||||
|
||||
const circleGeoJSON = useMemo((): FeatureCollection => ({
|
||||
type: "FeatureCollection",
|
||||
@@ -59,33 +57,38 @@ export const NodeLocationMap: React.FC<NodeLocationMapProps> = ({
|
||||
initialViewState={{ longitude: lng, latitude: lat, zoom: effectiveZoom }}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
attributionControl={{ compact: true }}
|
||||
onLoad={() => setMapLoaded(true)}
|
||||
>
|
||||
<Source id="circle" type="geojson" data={circleGeoJSON}>
|
||||
<Layer
|
||||
id="circle-fill"
|
||||
type="fill"
|
||||
paint={{ "fill-color": "#4ade80", "fill-opacity": 0.15 }}
|
||||
/>
|
||||
<Layer
|
||||
id="circle-outline"
|
||||
type="line"
|
||||
paint={{ "line-color": "#22c55e", "line-width": 2, "line-opacity": 0.8 }}
|
||||
/>
|
||||
</Source>
|
||||
{mapLoaded && (
|
||||
<>
|
||||
<Source id="circle" type="geojson" data={circleGeoJSON}>
|
||||
<Layer
|
||||
id="circle-fill"
|
||||
type="fill"
|
||||
paint={{ "fill-color": "#4ade80", "fill-opacity": 0.15 }}
|
||||
/>
|
||||
<Layer
|
||||
id="circle-outline"
|
||||
type="line"
|
||||
paint={{ "line-color": "#22c55e", "line-width": 2, "line-opacity": 0.8 }}
|
||||
/>
|
||||
</Source>
|
||||
|
||||
<Source id="marker" type="geojson" data={markerGeoJSON}>
|
||||
<Layer
|
||||
id="marker-dot"
|
||||
type="circle"
|
||||
paint={{
|
||||
"circle-radius": 6,
|
||||
"circle-color": "#4ade80",
|
||||
"circle-stroke-width": 2,
|
||||
"circle-stroke-color": "#22c55e",
|
||||
"circle-opacity": 1,
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
<Source id="marker" type="geojson" data={markerGeoJSON}>
|
||||
<Layer
|
||||
id="marker-dot"
|
||||
type="circle"
|
||||
paint={{
|
||||
"circle-radius": 6,
|
||||
"circle-color": "#4ade80",
|
||||
"circle-stroke-width": 2,
|
||||
"circle-stroke-color": "#22c55e",
|
||||
"circle-opacity": 1,
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
</>
|
||||
)}
|
||||
</ReactMap>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -187,55 +187,58 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ
|
||||
onZoomStart={handleUserInteraction}
|
||||
onLoad={() => setMapLoaded(true)}
|
||||
>
|
||||
{/* Topology links — always mounted, visibility controlled via layout property */}
|
||||
<Source id="links" type="geojson" data={linksGeoJSON}>
|
||||
<Layer
|
||||
id="links-line"
|
||||
type="line"
|
||||
layout={{
|
||||
"line-join": "round",
|
||||
"line-cap": "round",
|
||||
"visibility": showLinks ? "visible" : "none",
|
||||
}}
|
||||
paint={{
|
||||
"line-color": ["get", "color"],
|
||||
"line-width": 2,
|
||||
"line-opacity": ["get", "opacity"],
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
{/* Sources and layers — only after map style has loaded */}
|
||||
{mapLoaded && (
|
||||
<>
|
||||
<Source id="links" type="geojson" data={linksGeoJSON}>
|
||||
<Layer
|
||||
id="links-line"
|
||||
type="line"
|
||||
layout={{
|
||||
"line-join": "round",
|
||||
"line-cap": "round",
|
||||
"visibility": showLinks ? "visible" : "none",
|
||||
}}
|
||||
paint={{
|
||||
"line-color": ["get", "color"],
|
||||
"line-width": 2,
|
||||
"line-opacity": ["get", "opacity"],
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
|
||||
{/* Node circles */}
|
||||
<Source id="nodes" type="geojson" data={nodesGeoJSON}>
|
||||
<Layer
|
||||
id="nodes-circles"
|
||||
type="circle"
|
||||
paint={{
|
||||
"circle-radius": ["get", "radius"],
|
||||
"circle-color": ["get", "fillColor"],
|
||||
"circle-stroke-width": 2,
|
||||
"circle-stroke-color": ["get", "strokeColor"],
|
||||
"circle-opacity": 0.9,
|
||||
"circle-stroke-opacity": 1,
|
||||
}}
|
||||
/>
|
||||
<Layer
|
||||
id="nodes-labels"
|
||||
type="symbol"
|
||||
layout={{
|
||||
"text-field": ["get", "name"],
|
||||
"text-size": 11,
|
||||
"text-offset": [0, 1.5],
|
||||
"text-anchor": "top",
|
||||
"text-optional": true,
|
||||
}}
|
||||
paint={{
|
||||
"text-color": "#e5e7eb",
|
||||
"text-halo-color": "#111827",
|
||||
"text-halo-width": 1.5,
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
<Source id="nodes" type="geojson" data={nodesGeoJSON}>
|
||||
<Layer
|
||||
id="nodes-circles"
|
||||
type="circle"
|
||||
paint={{
|
||||
"circle-radius": ["get", "radius"],
|
||||
"circle-color": ["get", "fillColor"],
|
||||
"circle-stroke-width": 2,
|
||||
"circle-stroke-color": ["get", "strokeColor"],
|
||||
"circle-opacity": 0.9,
|
||||
"circle-stroke-opacity": 1,
|
||||
}}
|
||||
/>
|
||||
<Layer
|
||||
id="nodes-labels"
|
||||
type="symbol"
|
||||
layout={{
|
||||
"text-field": ["get", "name"],
|
||||
"text-size": 11,
|
||||
"text-offset": [0, 1.5],
|
||||
"text-anchor": "top",
|
||||
"text-optional": true,
|
||||
}}
|
||||
paint={{
|
||||
"text-color": "#e5e7eb",
|
||||
"text-halo-color": "#111827",
|
||||
"text-halo-width": 1.5,
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Node popup */}
|
||||
{selectedNode && (
|
||||
|
||||
Reference in New Issue
Block a user