@zendir/ui 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import L from "leaflet";
4
4
  import "leaflet/dist/leaflet.css";
5
5
  /* empty css */
6
6
  import { geodesicCirclePoints, splitRingAtAntimeridian, segmentsWithWorldCopies, splitPolylineAtAntimeridian } from "./groundTrackMapLeafletUtils.js";
7
- import { DEFAULT_TILE, CARTO_ATTRIBUTION, OSM_ATTRIBUTION, FALLBACK_TILE } from "./groundTrackMapLeafletTiles.js";
7
+ import { CARTO_ATTRIBUTION, OSM_ATTRIBUTION, TILE_PRESETS, FALLBACK_TILE } from "./groundTrackMapLeafletTiles.js";
8
8
  import { useTheme } from "../theme/ThemeProvider.js";
9
9
  const STATUS_COLOR_MAP = {
10
10
  normal: "#56f000",
@@ -143,13 +143,16 @@ function GroundTrackMapLeaflet({
143
143
  showLegend = true,
144
144
  showEquator = false,
145
145
  showRecenterButton = true,
146
+ showMapStyleToggle = true,
146
147
  defaultCenter = [20, 0],
147
148
  defaultZoom = 2,
148
149
  height = "100%",
149
150
  width = "100%",
150
151
  minHeight = "400px",
151
152
  emptyMessage = "No orbital data available",
152
- tileUrl = DEFAULT_TILE,
153
+ tileUrl,
154
+ nightTileUrl,
155
+ lightSources,
153
156
  className = "",
154
157
  onSatelliteClick,
155
158
  onStationClick,
@@ -167,6 +170,9 @@ function GroundTrackMapLeaflet({
167
170
  const controlsRef = useRef([]);
168
171
  const pinsGroupRef = useRef(null);
169
172
  const [ready, setReady] = useState(false);
173
+ const [tileStyle, setTileStyle] = useState("dark");
174
+ const isExplicitTileUrl = tileUrl !== void 0;
175
+ const effectiveTileUrl = isExplicitTileUrl ? tileUrl : TILE_PRESETS[tileStyle];
170
176
  const clearLayers = useCallback(() => {
171
177
  var _a;
172
178
  const map = mapRef.current;
@@ -200,38 +206,6 @@ function GroundTrackMapLeaflet({
200
206
  });
201
207
  L.control.zoom({ position: "topright" }).addTo(map);
202
208
  map.attributionControl.setPrefix("");
203
- const isCartoTiles = tileUrl.includes("cartocdn");
204
- if (isCartoTiles) {
205
- map.attributionControl.addAttribution(CARTO_ATTRIBUTION);
206
- }
207
- const tileOptions = {
208
- maxZoom: 19,
209
- subdomains: isCartoTiles ? "abcd" : "abc",
210
- // crossOrigin avoids tainted-canvas errors when Leaflet tries to read tile pixels
211
- crossOrigin: true
212
- };
213
- const tile = L.tileLayer(tileUrl, tileOptions);
214
- tile.addTo(map);
215
- tileLayerRef.current = tile;
216
- let hasSwitchedToFallback = false;
217
- const onTileError = () => {
218
- if (hasSwitchedToFallback) return;
219
- hasSwitchedToFallback = true;
220
- tile.off("tileerror", onTileError);
221
- tile.remove();
222
- if (isCartoTiles) {
223
- map.attributionControl.removeAttribution(CARTO_ATTRIBUTION);
224
- map.attributionControl.addAttribution(OSM_ATTRIBUTION);
225
- }
226
- const fallback = L.tileLayer(FALLBACK_TILE, {
227
- maxZoom: 19,
228
- subdomains: "abc",
229
- crossOrigin: true
230
- });
231
- fallback.addTo(map);
232
- tileLayerRef.current = fallback;
233
- };
234
- tile.on("tileerror", onTileError);
235
209
  const overlayGroup = L.layerGroup();
236
210
  overlayGroup.addTo(map);
237
211
  overlayGroupRef.current = overlayGroup;
@@ -259,11 +233,61 @@ function GroundTrackMapLeaflet({
259
233
  mapRef.current = null;
260
234
  overlayGroupRef.current = null;
261
235
  };
262
- }, [tileUrl, clearLayers]);
236
+ }, [clearLayers]);
237
+ useEffect(() => {
238
+ const map = mapRef.current;
239
+ if (!map || !ready) return;
240
+ if (tileLayerRef.current) {
241
+ tileLayerRef.current.remove();
242
+ tileLayerRef.current = null;
243
+ }
244
+ try {
245
+ map.attributionControl.removeAttribution(CARTO_ATTRIBUTION);
246
+ map.attributionControl.removeAttribution(OSM_ATTRIBUTION);
247
+ } catch {
248
+ }
249
+ const isCartoTiles = effectiveTileUrl.includes("cartocdn");
250
+ if (isCartoTiles) {
251
+ map.attributionControl.addAttribution(CARTO_ATTRIBUTION);
252
+ }
253
+ const tile = L.tileLayer(effectiveTileUrl, {
254
+ maxZoom: 19,
255
+ subdomains: isCartoTiles ? "abcd" : "abc",
256
+ crossOrigin: true
257
+ });
258
+ tile.addTo(map);
259
+ tile.bringToBack();
260
+ tileLayerRef.current = tile;
261
+ let hasSwitchedToFallback = false;
262
+ const onTileError = () => {
263
+ if (hasSwitchedToFallback) return;
264
+ hasSwitchedToFallback = true;
265
+ tile.off("tileerror", onTileError);
266
+ tile.remove();
267
+ const fallback = L.tileLayer(FALLBACK_TILE, {
268
+ maxZoom: 19,
269
+ subdomains: "abc",
270
+ crossOrigin: true
271
+ });
272
+ fallback.addTo(map);
273
+ fallback.bringToBack();
274
+ tileLayerRef.current = fallback;
275
+ };
276
+ tile.on("tileerror", onTileError);
277
+ }, [effectiveTileUrl, ready]);
263
278
  useEffect(() => {
264
279
  if (!ready || !mapRef.current) return;
265
280
  const map = mapRef.current;
266
281
  clearLayers();
282
+ if (nightTileUrl && showTerminator) {
283
+ const nightTile = L.tileLayer(nightTileUrl, {
284
+ maxZoom: 19,
285
+ crossOrigin: true,
286
+ opacity: 0.7,
287
+ className: "zendir-night-tiles"
288
+ });
289
+ addLayer(nightTile);
290
+ }
267
291
  if (showTerminator) {
268
292
  const now = terminatorTime ?? /* @__PURE__ */ new Date();
269
293
  const BAND_STEP = 2;
@@ -297,6 +321,75 @@ function GroundTrackMapLeaflet({
297
321
  });
298
322
  prevOpacity = b.opacity;
299
323
  }
324
+ const terminatorEdge = calculateTerminatorContinuous(now, 0);
325
+ if (terminatorEdge.sunset.length > 2) {
326
+ const sunsetLine = terminatorEdge.sunset.map(([lat, lon]) => [lat, lon]);
327
+ const sunriseLine = terminatorEdge.sunrise.map(([lat, lon]) => [lat, lon]);
328
+ [0, 360, -360].forEach((offset) => {
329
+ addLayer(L.polyline(
330
+ sunsetLine.map(([lat, lon]) => [lat, lon + offset]),
331
+ { color: "#5a8ec8", weight: 4, opacity: 0.12, interactive: false, smoothFactor: 1.5 }
332
+ ));
333
+ addLayer(L.polyline(
334
+ sunriseLine.map(([lat, lon]) => [lat, lon + offset]),
335
+ { color: "#5a8ec8", weight: 4, opacity: 0.12, interactive: false, smoothFactor: 1.5 }
336
+ ));
337
+ addLayer(L.polyline(
338
+ sunsetLine.map(([lat, lon]) => [lat, lon + offset]),
339
+ { color: "#7aa4d4", weight: 1, opacity: 0.5, interactive: false, smoothFactor: 1.5 }
340
+ ));
341
+ addLayer(L.polyline(
342
+ sunriseLine.map(([lat, lon]) => [lat, lon + offset]),
343
+ { color: "#7aa4d4", weight: 1, opacity: 0.5, interactive: false, smoothFactor: 1.5 }
344
+ ));
345
+ });
346
+ }
347
+ }
348
+ if (lightSources && lightSources.length > 0 && showTerminator) {
349
+ const now = terminatorTime ?? /* @__PURE__ */ new Date();
350
+ const dayOfYear = Math.floor((now.getTime() - new Date(now.getFullYear(), 0, 0).getTime()) / 864e5);
351
+ const declination = -23.44 * Math.cos(2 * Math.PI / 365 * (dayOfYear + 10));
352
+ const decRad = declination * Math.PI / 180;
353
+ const hourAngle = (now.getUTCHours() + now.getUTCMinutes() / 60) / 24 * 360 - 180;
354
+ for (const light of lightSources) {
355
+ const latRad = light.latitude * Math.PI / 180;
356
+ const sinAlt = Math.sin(latRad) * Math.sin(decRad) + Math.cos(latRad) * Math.cos(decRad) * Math.cos((light.longitude - hourAngle) * Math.PI / 180);
357
+ const solarAltDeg = Math.asin(sinAlt) * 180 / Math.PI;
358
+ let nightFactor;
359
+ if (solarAltDeg >= 0) nightFactor = 0;
360
+ else if (solarAltDeg <= -18) nightFactor = 1;
361
+ else nightFactor = Math.min(1, -solarAltDeg / 18);
362
+ if (nightFactor < 0.01) continue;
363
+ const r = light.radius ?? 4;
364
+ const intensity = light.intensity ?? 0.8;
365
+ const color = light.color ?? "#ffeedd";
366
+ const alpha = intensity * nightFactor;
367
+ const glowMarker = L.circleMarker([light.latitude, light.longitude], {
368
+ radius: r * 2,
369
+ fillColor: color,
370
+ fillOpacity: alpha * 0.4,
371
+ color,
372
+ weight: 0,
373
+ interactive: !!light.label
374
+ });
375
+ addLayer(glowMarker);
376
+ const coreMarker = L.circleMarker([light.latitude, light.longitude], {
377
+ radius: Math.max(1, r * 0.5),
378
+ fillColor: color,
379
+ fillOpacity: Math.min(1, alpha * 1.2),
380
+ color: "transparent",
381
+ weight: 0,
382
+ interactive: false
383
+ });
384
+ addLayer(coreMarker);
385
+ if (light.label) {
386
+ glowMarker.bindTooltip(light.label, {
387
+ permanent: false,
388
+ direction: "top",
389
+ className: "zendir-leaflet-tooltip"
390
+ });
391
+ }
392
+ }
300
393
  }
301
394
  if (showGrid) {
302
395
  const gridStyle = { color: "rgba(157, 112, 255, 0.12)", weight: 0.5, interactive: false };
@@ -492,6 +585,8 @@ function GroundTrackMapLeaflet({
492
585
  groundStations,
493
586
  showTerminator,
494
587
  terminatorTime,
588
+ nightTileUrl,
589
+ lightSources,
495
590
  showGrid,
496
591
  showEquator,
497
592
  showLegend,
@@ -630,35 +725,109 @@ function GroundTrackMapLeaflet({
630
725
  children: emptyMessage
631
726
  }
632
727
  ),
633
- showRecenterButton && /* @__PURE__ */ jsxs(
634
- "button",
728
+ (showRecenterButton || showMapStyleToggle && !isExplicitTileUrl) && /* @__PURE__ */ jsxs(
729
+ "div",
635
730
  {
636
- type: "button",
637
- onClick: handleRecenter,
638
- title: "Recenter map",
639
- "aria-label": "Recenter map",
731
+ className: "zendir-map-controls",
640
732
  style: {
641
733
  position: "absolute",
642
- top: 8,
643
- right: 50,
734
+ bottom: 28,
735
+ left: 10,
644
736
  zIndex: 1e3,
645
- background: "rgba(24, 29, 46, 0.9)",
646
- border: "1px solid rgba(157, 112, 255, 0.25)",
647
- borderRadius: 6,
648
- color: "#e4e0f0",
649
- cursor: "pointer",
650
- padding: "6px 10px",
651
- fontSize: 12,
652
737
  display: "flex",
653
- alignItems: "center",
654
- gap: 4
738
+ flexDirection: "row",
739
+ gap: 6,
740
+ alignItems: "flex-end"
655
741
  },
656
742
  children: [
657
- /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
658
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
659
- /* @__PURE__ */ jsx("path", { d: "M12 2v4M12 18v4M2 12h4M18 12h4" })
660
- ] }),
661
- "Recenter"
743
+ showRecenterButton && /* @__PURE__ */ jsxs(
744
+ "button",
745
+ {
746
+ type: "button",
747
+ onClick: handleRecenter,
748
+ title: "Recenter map to fit all assets and ground stations",
749
+ "aria-label": "Recenter map",
750
+ style: {
751
+ background: "rgba(20, 24, 38, 0.92)",
752
+ backdropFilter: "blur(8px)",
753
+ WebkitBackdropFilter: "blur(8px)",
754
+ border: "1px solid rgba(120, 100, 180, 0.2)",
755
+ borderRadius: 4,
756
+ color: "#c8c0d8",
757
+ cursor: "pointer",
758
+ padding: "6px 10px",
759
+ fontSize: 11,
760
+ fontWeight: 500,
761
+ display: "flex",
762
+ alignItems: "center",
763
+ gap: 5,
764
+ transition: "background 0.15s, border-color 0.15s",
765
+ letterSpacing: "0.02em"
766
+ },
767
+ onMouseEnter: (e) => {
768
+ e.currentTarget.style.background = "rgba(30, 34, 52, 0.95)";
769
+ e.currentTarget.style.borderColor = "rgba(157, 112, 255, 0.4)";
770
+ },
771
+ onMouseLeave: (e) => {
772
+ e.currentTarget.style.background = "rgba(20, 24, 38, 0.92)";
773
+ e.currentTarget.style.borderColor = "rgba(120, 100, 180, 0.2)";
774
+ },
775
+ children: [
776
+ /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
777
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
778
+ /* @__PURE__ */ jsx("path", { d: "M12 2v4M12 18v4M2 12h4M18 12h4" })
779
+ ] }),
780
+ "Recenter"
781
+ ]
782
+ }
783
+ ),
784
+ showMapStyleToggle && !isExplicitTileUrl && /* @__PURE__ */ jsx(
785
+ "div",
786
+ {
787
+ style: {
788
+ background: "rgba(20, 24, 38, 0.92)",
789
+ backdropFilter: "blur(8px)",
790
+ WebkitBackdropFilter: "blur(8px)",
791
+ border: "1px solid rgba(120, 100, 180, 0.2)",
792
+ borderRadius: 4,
793
+ display: "flex",
794
+ overflow: "hidden"
795
+ },
796
+ children: ["dark", "satellite"].map((style) => /* @__PURE__ */ jsxs(
797
+ "button",
798
+ {
799
+ type: "button",
800
+ onClick: () => setTileStyle(style),
801
+ title: style === "dark" ? "Dark ops map" : "Satellite imagery",
802
+ "aria-label": style === "dark" ? "Dark map style" : "Satellite imagery",
803
+ "aria-pressed": tileStyle === style,
804
+ style: {
805
+ background: tileStyle === style ? "rgba(157, 112, 255, 0.22)" : "transparent",
806
+ border: "none",
807
+ borderRight: style === "dark" ? "1px solid rgba(120, 100, 180, 0.15)" : "none",
808
+ color: tileStyle === style ? "#d0c4ee" : "#7a748e",
809
+ cursor: "pointer",
810
+ padding: "5px 8px",
811
+ fontSize: 10,
812
+ fontWeight: tileStyle === style ? 600 : 400,
813
+ display: "flex",
814
+ alignItems: "center",
815
+ gap: 4,
816
+ transition: "all 0.15s ease",
817
+ letterSpacing: "0.02em"
818
+ },
819
+ children: [
820
+ style === "dark" ? /* @__PURE__ */ jsx("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "currentColor", stroke: "none", children: /* @__PURE__ */ jsx("path", { d: "M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" }) }) : /* @__PURE__ */ jsxs("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
821
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
822
+ /* @__PURE__ */ jsx("path", { d: "M2 12h20M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z" })
823
+ ] }),
824
+ style === "dark" ? "Dark" : "Satellite"
825
+ ]
826
+ },
827
+ style
828
+ ))
829
+ }
830
+ )
662
831
  ]
663
832
  }
664
833
  )