@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.
@@ -83,6 +83,29 @@ export interface MapPin {
83
83
  /** Who placed the pin (display-only; not enforced) */
84
84
  createdBy?: string;
85
85
  }
86
+ /**
87
+ * A point light source rendered on the night side of the map.
88
+ * Use this to display city lights, base locations, RF emitters,
89
+ * or any glow that should only be visible after sunset.
90
+ *
91
+ * Light sources are masked by the terminator — they fade in
92
+ * through the twilight gradient and reach full brightness in
93
+ * deep night, matching real-world light-pollution behavior.
94
+ */
95
+ export interface LightSource {
96
+ /** Latitude in degrees (−90 … 90) */
97
+ latitude: number;
98
+ /** Longitude in degrees (−180 … 180) */
99
+ longitude: number;
100
+ /** Glow radius in pixels (default 4). Larger values create wider halos. */
101
+ radius?: number;
102
+ /** Glow intensity 0–1 (default 0.8). Controls the peak alpha of the light dot. */
103
+ intensity?: number;
104
+ /** Light color — any CSS color string (default '#ffeedd', warm white). */
105
+ color?: string;
106
+ /** Optional label shown on hover (Leaflet) or in tooltip (Canvas). */
107
+ label?: string;
108
+ }
86
109
  export interface GroundTrackMapProps {
87
110
  /** Single satellite ground track (legacy API) */
88
111
  groundTrack?: GroundTrackPoint[];
@@ -108,6 +131,8 @@ export interface GroundTrackMapProps {
108
131
  showEquator?: boolean;
109
132
  /** Show recenter button (SRO compat) */
110
133
  showRecenterButton?: boolean;
134
+ /** Show a toggle button to switch between Dark and Satellite tile styles. Default true for Leaflet. */
135
+ showMapStyleToggle?: boolean;
111
136
  /** Whether data is still loading (SRO compat) */
112
137
  isLoading?: boolean;
113
138
  /** Message when there is no data (SRO compat) */
@@ -132,6 +157,25 @@ export interface GroundTrackMapProps {
132
157
  mapProvider?: 'leaflet' | 'canvas';
133
158
  /** Tile URL for Leaflet (default: CartoDB dark). Ignored when mapProvider is 'canvas'. */
134
159
  tileUrl?: string;
160
+ /**
161
+ * URL template for a "night" tile layer (e.g. NASA Black Marble, VIIRS city lights).
162
+ * When provided, this layer is rendered beneath the terminator overlay so that it
163
+ * appears only on the night side of the map — creating a realistic day/night transition.
164
+ * The day tiles remain visible on the sunlit side.
165
+ *
166
+ * Requires `showTerminator` to be enabled. Leaflet backend only.
167
+ *
168
+ * Example: `'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/VIIRS_CityLights_2012/...'`
169
+ */
170
+ nightTileUrl?: string;
171
+ /**
172
+ * Array of point light sources rendered on the night side of the map.
173
+ * Each light appears as a soft glow dot that is masked by the terminator —
174
+ * invisible in daylight, fading in through twilight, full brightness at night.
175
+ *
176
+ * Use cases: city lights, base indicators, RF emitters, targets, population centers.
177
+ */
178
+ lightSources?: LightSource[];
135
179
  /** Array of user-placed pins displayed on the map */
136
180
  pins?: MapPin[];
137
181
  /**
@@ -147,5 +191,5 @@ export interface GroundTrackMapProps {
147
191
  /** Called when the user removes a pin */
148
192
  onPinRemove?: (pinId: string) => void;
149
193
  }
150
- export declare function GroundTrackMap({ groundTrack, satellites, groundStations, accessMask, teamPaths, showTerminator, terminatorTime, showGrid, showLegend, showEquator, showRecenterButton, isLoading, emptyMessage, width, height, minHeight, defaultCenter, defaultZoom, className, onSatelliteClick, onStationClick, mapProvider, tileUrl, pins, pinsEditable, onPinAdd, onPinUpdate, onPinRemove, }: GroundTrackMapProps): React.ReactElement;
194
+ export declare function GroundTrackMap({ groundTrack, satellites, groundStations, accessMask, teamPaths, showTerminator, terminatorTime, showGrid, showLegend, showEquator, showRecenterButton, showMapStyleToggle, isLoading, emptyMessage, width, height, minHeight, defaultCenter, defaultZoom, className, onSatelliteClick, onStationClick, mapProvider, tileUrl, nightTileUrl, lightSources, pins, pinsEditable, onPinAdd, onPinUpdate, onPinRemove, }: GroundTrackMapProps): React.ReactElement;
151
195
  export default GroundTrackMap;
@@ -68,6 +68,16 @@ function drawCoverageEllipse(ctx, centerLat, centerLon, radiusDeg, W, H, lonToX,
68
68
  }
69
69
  ctx.closePath();
70
70
  }
71
+ function hexToRgb(hex) {
72
+ const h = hex.replace("#", "");
73
+ if (h.length === 3) {
74
+ return [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)];
75
+ }
76
+ if (h.length >= 6) {
77
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
78
+ }
79
+ return [255, 238, 221];
80
+ }
71
81
  function calculateTerminatorContinuous(date, depressionDeg = 0, numPoints = 360) {
72
82
  const dayOfYear = Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 864e5);
73
83
  const declination = -23.44 * Math.cos(2 * Math.PI / 365 * (dayOfYear + 10));
@@ -112,6 +122,7 @@ function GroundTrackMap({
112
122
  showLegend = true,
113
123
  showEquator = true,
114
124
  showRecenterButton = false,
125
+ showMapStyleToggle = true,
115
126
  isLoading = false,
116
127
  emptyMessage = "No orbital data available",
117
128
  width = "100%",
@@ -124,6 +135,8 @@ function GroundTrackMap({
124
135
  onStationClick,
125
136
  mapProvider = "leaflet",
126
137
  tileUrl,
138
+ nightTileUrl,
139
+ lightSources,
127
140
  pins,
128
141
  pinsEditable = false,
129
142
  onPinAdd,
@@ -261,6 +274,39 @@ function GroundTrackMap({
261
274
  }
262
275
  prevAlpha = zone.alpha;
263
276
  }
277
+ const terminatorEdge = calculateTerminatorContinuous(now, 0);
278
+ if (terminatorEdge.sunset.length > 2) {
279
+ ctx.save();
280
+ ctx.strokeStyle = "rgba(90, 142, 200, 0.12)";
281
+ ctx.lineWidth = 4;
282
+ for (const curve of [terminatorEdge.sunset, terminatorEdge.sunrise]) {
283
+ for (const offset of [-360, 0, 360]) {
284
+ ctx.beginPath();
285
+ for (let i = 0; i < curve.length; i++) {
286
+ const x = lonToX(curve[i][1] + offset, W);
287
+ const y = latToY(curve[i][0], H);
288
+ if (i === 0) ctx.moveTo(x, y);
289
+ else ctx.lineTo(x, y);
290
+ }
291
+ ctx.stroke();
292
+ }
293
+ }
294
+ ctx.strokeStyle = "rgba(122, 164, 212, 0.5)";
295
+ ctx.lineWidth = 1;
296
+ for (const curve of [terminatorEdge.sunset, terminatorEdge.sunrise]) {
297
+ for (const offset of [-360, 0, 360]) {
298
+ ctx.beginPath();
299
+ for (let i = 0; i < curve.length; i++) {
300
+ const x = lonToX(curve[i][1] + offset, W);
301
+ const y = latToY(curve[i][0], H);
302
+ if (i === 0) ctx.moveTo(x, y);
303
+ else ctx.lineTo(x, y);
304
+ }
305
+ ctx.stroke();
306
+ }
307
+ }
308
+ ctx.restore();
309
+ }
264
310
  }
265
311
  ctx.fillStyle = COLORS.land;
266
312
  ctx.strokeStyle = "rgba(100, 120, 160, 0.25)";
@@ -483,6 +529,42 @@ function GroundTrackMap({
483
529
  ctx.textAlign = "center";
484
530
  ctx.fillText(gs.name.length > 14 ? gs.name.slice(0, 14) + "…" : gs.name, x, y + halfIcon + 12);
485
531
  }
532
+ if (lightSources && lightSources.length > 0 && showTerminator) {
533
+ const now = terminatorTime ?? /* @__PURE__ */ new Date();
534
+ for (const light of lightSources) {
535
+ const lx = lonToX(light.longitude, W);
536
+ const ly = latToY(light.latitude, H);
537
+ const r = light.radius ?? 4;
538
+ const intensity = light.intensity ?? 0.8;
539
+ const color = light.color ?? "#ffeedd";
540
+ const dayOfYear = Math.floor((now.getTime() - new Date(now.getFullYear(), 0, 0).getTime()) / 864e5);
541
+ const declination = -23.44 * Math.cos(2 * Math.PI / 365 * (dayOfYear + 10));
542
+ const decRad = declination * Math.PI / 180;
543
+ const hourAngle = (now.getUTCHours() + now.getUTCMinutes() / 60) / 24 * 360 - 180;
544
+ const latRad = light.latitude * Math.PI / 180;
545
+ const sinAlt = Math.sin(latRad) * Math.sin(decRad) + Math.cos(latRad) * Math.cos(decRad) * Math.cos((light.longitude - hourAngle) * Math.PI / 180);
546
+ const solarAltDeg = Math.asin(sinAlt) * 180 / Math.PI;
547
+ let nightFactor;
548
+ if (solarAltDeg >= 0) nightFactor = 0;
549
+ else if (solarAltDeg <= -18) nightFactor = 1;
550
+ else nightFactor = Math.min(1, -solarAltDeg / 18);
551
+ if (nightFactor < 0.01) continue;
552
+ const alpha = intensity * nightFactor;
553
+ const [cr, cg, cb] = hexToRgb(color);
554
+ const grad = ctx.createRadialGradient(lx, ly, 0, lx, ly, r * 2);
555
+ grad.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, ${alpha})`);
556
+ grad.addColorStop(0.4, `rgba(${cr}, ${cg}, ${cb}, ${alpha * 0.6})`);
557
+ grad.addColorStop(1, `rgba(${cr}, ${cg}, ${cb}, 0)`);
558
+ ctx.fillStyle = grad;
559
+ ctx.beginPath();
560
+ ctx.arc(lx, ly, r * 2, 0, Math.PI * 2);
561
+ ctx.fill();
562
+ ctx.fillStyle = `rgba(${cr}, ${cg}, ${cb}, ${Math.min(1, alpha * 1.3)})`;
563
+ ctx.beginPath();
564
+ ctx.arc(lx, ly, Math.max(1, r * 0.4), 0, Math.PI * 2);
565
+ ctx.fill();
566
+ }
567
+ }
486
568
  allSatellites.forEach((sat, satIdx) => {
487
569
  const track = sat.groundTrack;
488
570
  if (!track || track.length === 0) return;
@@ -791,7 +873,7 @@ function GroundTrackMap({
791
873
  ctx.textAlign = "center";
792
874
  ctx.fillText(emptyMessage, W / 2, H / 2);
793
875
  }
794
- }, [allSatellites, groundStations, height, showTerminator, terminatorTime, showGrid, showLegend, showEquator, lonToX, latToY, COLORS, emptyMessage, pins, tokens.colors.accent.primary]);
876
+ }, [allSatellites, groundStations, height, showTerminator, terminatorTime, showGrid, showLegend, showEquator, lonToX, latToY, COLORS, emptyMessage, pins, lightSources, tokens.colors.accent.primary]);
795
877
  const handleMouseMove = useCallback((e) => {
796
878
  const canvas = canvasRef.current;
797
879
  if (!canvas) return;
@@ -912,6 +994,7 @@ function GroundTrackMap({
912
994
  showLegend,
913
995
  showEquator,
914
996
  showRecenterButton,
997
+ showMapStyleToggle,
915
998
  defaultCenter: leafletDefaultCenter,
916
999
  defaultZoom: leafletDefaultZoom,
917
1000
  height,
@@ -919,6 +1002,8 @@ function GroundTrackMap({
919
1002
  minHeight,
920
1003
  emptyMessage,
921
1004
  tileUrl,
1005
+ nightTileUrl,
1006
+ lightSources,
922
1007
  className,
923
1008
  onSatelliteClick,
924
1009
  onStationClick,