@zendir/ui 0.2.0 → 0.2.2
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.
- package/CHANGELOG.md +26 -0
- package/dist/react/charts/GroundTrackMap.d.ts +43 -1
- package/dist/react/charts/GroundTrackMap.js +117 -66
- package/dist/react/charts/GroundTrackMap.js.map +1 -1
- package/dist/react/charts/GroundTrackMapLeaflet.d.ts +13 -2
- package/dist/react/charts/GroundTrackMapLeaflet.js +103 -56
- package/dist/react/charts/GroundTrackMapLeaflet.js.map +1 -1
- package/dist/react/charts/index.d.ts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @zendir/ui
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
#### GroundTrackMap — Night Visualization
|
|
8
|
+
|
|
9
|
+
- **Seamless night polygon** — completely rewritten terminator geometry: sunset/sunrise curves are now computed as continuous longitude pairs, producing a single closed polygon with no antimeridian artifacts. Eliminates all "jerk" lines and rendering glitches at the poles.
|
|
10
|
+
- **`nightTileUrl` prop** — optional second tile layer (e.g. NASA Black Marble, VIIRS city lights) rendered beneath the terminator overlay. The graduated twilight gradient naturally masks the day-to-night tile transition. Leaflet backend only.
|
|
11
|
+
- **`lightSources` prop** — array of point light sources (`LightSource` interface) rendered as soft glow dots on the night side of the map. Each light is masked by the terminator: invisible in daylight, fading in through twilight, full brightness at night. Supports custom colors, radii, intensity, and labels.
|
|
12
|
+
- **Solar elevation masking** — light source visibility is computed per-point using the exact solar elevation angle (not polygon containment), giving a smooth and physically correct twilight fade for each individual light.
|
|
13
|
+
|
|
14
|
+
### Documentation
|
|
15
|
+
|
|
16
|
+
- **City Lights at Night** story — 35+ world cities as `lightSources`, demonstrating realistic nighttime glow.
|
|
17
|
+
- **City Lights Time Travel** story — interactive UTC hour slider showing lights switching on/off as the terminator sweeps.
|
|
18
|
+
- **Custom Light Sources** story — mission scenario with colored RF emitters, bases, and relay nodes.
|
|
19
|
+
- **Night Tile Layer** story — demonstrates `nightTileUrl` combined with `lightSources`.
|
|
20
|
+
- Updated component description with Night Tile Layer and Dynamic Light Sources sections.
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
- Fixed terminator night polygon antimeridian wrapping — sunset and sunrise curves are now generated as continuous coordinate pairs (no `lon ± 180` mirroring), preventing the polygon from crossing the day side.
|
|
25
|
+
- Removed unused `buildBandPolygon` function from Leaflet backend.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
3
29
|
## 0.2.0
|
|
4
30
|
|
|
5
31
|
### Features
|
|
@@ -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[];
|
|
@@ -132,6 +155,25 @@ export interface GroundTrackMapProps {
|
|
|
132
155
|
mapProvider?: 'leaflet' | 'canvas';
|
|
133
156
|
/** Tile URL for Leaflet (default: CartoDB dark). Ignored when mapProvider is 'canvas'. */
|
|
134
157
|
tileUrl?: string;
|
|
158
|
+
/**
|
|
159
|
+
* URL template for a "night" tile layer (e.g. NASA Black Marble, VIIRS city lights).
|
|
160
|
+
* When provided, this layer is rendered beneath the terminator overlay so that it
|
|
161
|
+
* appears only on the night side of the map — creating a realistic day/night transition.
|
|
162
|
+
* The day tiles remain visible on the sunlit side.
|
|
163
|
+
*
|
|
164
|
+
* Requires `showTerminator` to be enabled. Leaflet backend only.
|
|
165
|
+
*
|
|
166
|
+
* Example: `'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/VIIRS_CityLights_2012/...'`
|
|
167
|
+
*/
|
|
168
|
+
nightTileUrl?: string;
|
|
169
|
+
/**
|
|
170
|
+
* Array of point light sources rendered on the night side of the map.
|
|
171
|
+
* Each light appears as a soft glow dot that is masked by the terminator —
|
|
172
|
+
* invisible in daylight, fading in through twilight, full brightness at night.
|
|
173
|
+
*
|
|
174
|
+
* Use cases: city lights, base indicators, RF emitters, targets, population centers.
|
|
175
|
+
*/
|
|
176
|
+
lightSources?: LightSource[];
|
|
135
177
|
/** Array of user-placed pins displayed on the map */
|
|
136
178
|
pins?: MapPin[];
|
|
137
179
|
/**
|
|
@@ -147,5 +189,5 @@ export interface GroundTrackMapProps {
|
|
|
147
189
|
/** Called when the user removes a pin */
|
|
148
190
|
onPinRemove?: (pinId: string) => void;
|
|
149
191
|
}
|
|
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;
|
|
192
|
+
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, nightTileUrl, lightSources, pins, pinsEditable, onPinAdd, onPinUpdate, onPinRemove, }: GroundTrackMapProps): React.ReactElement;
|
|
151
193
|
export default GroundTrackMap;
|
|
@@ -68,25 +68,47 @@ function drawCoverageEllipse(ctx, centerLat, centerLon, radiusDeg, W, H, lonToX,
|
|
|
68
68
|
}
|
|
69
69
|
ctx.closePath();
|
|
70
70
|
}
|
|
71
|
-
function
|
|
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
|
+
}
|
|
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));
|
|
74
84
|
const decRad = declination * Math.PI / 180;
|
|
75
85
|
const hourAngle = (date.getUTCHours() + date.getUTCMinutes() / 60) / 24 * 360 - 180;
|
|
76
86
|
const depRad = depressionDeg * Math.PI / 180;
|
|
77
|
-
const
|
|
87
|
+
const sunset = [];
|
|
88
|
+
const sunrise = [];
|
|
78
89
|
for (let i = 0; i <= numPoints; i++) {
|
|
79
|
-
const
|
|
90
|
+
const lat = -89.999 + i / numPoints * 179.998;
|
|
91
|
+
const latRad = lat * (Math.PI / 180);
|
|
80
92
|
const cosH = -(Math.sin(depRad) + Math.sin(latRad) * Math.sin(decRad)) / (Math.cos(latRad) * Math.cos(decRad));
|
|
81
|
-
|
|
82
|
-
if (cosH
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
if (cosH < -1) ;
|
|
94
|
+
else if (cosH > 1) {
|
|
95
|
+
sunset.push([lat, hourAngle]);
|
|
96
|
+
sunrise.push([lat, hourAngle + 360]);
|
|
97
|
+
} else {
|
|
98
|
+
const H = Math.acos(cosH) * 180 / Math.PI;
|
|
99
|
+
sunset.push([lat, hourAngle + H]);
|
|
100
|
+
sunrise.push([lat, hourAngle + 360 - H]);
|
|
101
|
+
}
|
|
88
102
|
}
|
|
89
|
-
return
|
|
103
|
+
return { sunset, sunrise };
|
|
104
|
+
}
|
|
105
|
+
function buildNightPolygon(terminator) {
|
|
106
|
+
const { sunset, sunrise } = terminator;
|
|
107
|
+
if (!sunset.length || !sunrise.length) return [];
|
|
108
|
+
const poly = [];
|
|
109
|
+
poly.push(...sunset);
|
|
110
|
+
poly.push(...[...sunrise].reverse());
|
|
111
|
+
return poly;
|
|
90
112
|
}
|
|
91
113
|
function GroundTrackMap({
|
|
92
114
|
groundTrack,
|
|
@@ -112,6 +134,8 @@ function GroundTrackMap({
|
|
|
112
134
|
onStationClick,
|
|
113
135
|
mapProvider = "leaflet",
|
|
114
136
|
tileUrl,
|
|
137
|
+
nightTileUrl,
|
|
138
|
+
lightSources,
|
|
115
139
|
pins,
|
|
116
140
|
pinsEditable = false,
|
|
117
141
|
onPinAdd,
|
|
@@ -202,63 +226,52 @@ function GroundTrackMap({
|
|
|
202
226
|
ctx.fillRect(0, 0, W, H);
|
|
203
227
|
if (showTerminator) {
|
|
204
228
|
const now = terminatorTime ?? /* @__PURE__ */ new Date();
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
{ depression:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
ctx.beginPath();
|
|
223
|
-
if (isSunrise) {
|
|
224
|
-
ctx.moveTo(0, 0);
|
|
225
|
-
ctx.lineTo(0, H);
|
|
226
|
-
} else {
|
|
227
|
-
ctx.moveTo(W, 0);
|
|
228
|
-
ctx.lineTo(W, H);
|
|
229
|
+
const BAND_STEP = 2;
|
|
230
|
+
const MAX_DEP = 24;
|
|
231
|
+
const NIGHT_ALPHA = 0.55;
|
|
232
|
+
const bandSteps = [];
|
|
233
|
+
for (let d = 0; d <= MAX_DEP; d += BAND_STEP) {
|
|
234
|
+
const t = Math.min(d / 18, 1);
|
|
235
|
+
const alpha = t * t * (NIGHT_ALPHA * 0.82) + (d > 18 ? (d - 18) / 6 * (NIGHT_ALPHA * 0.18) : 0);
|
|
236
|
+
bandSteps.push({ depression: d, alpha: Math.min(alpha, NIGHT_ALPHA) });
|
|
237
|
+
}
|
|
238
|
+
bandSteps.push({ depression: 90, alpha: NIGHT_ALPHA });
|
|
239
|
+
let prevAlpha = 0;
|
|
240
|
+
for (const zone of bandSteps) {
|
|
241
|
+
const dep = Math.min(zone.depression, 89);
|
|
242
|
+
const bandAlpha = zone.alpha - prevAlpha;
|
|
243
|
+
if (bandAlpha < 2e-3) {
|
|
244
|
+
prevAlpha = zone.alpha;
|
|
245
|
+
continue;
|
|
229
246
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const y = latToY(terminator[i].lat, H);
|
|
235
|
-
if (Math.abs(x - prevX) > W * 0.5 && i < terminator.length - 1) {
|
|
236
|
-
const edgeX = isSunrise ? prevX < W / 2 ? 0 : W : prevX > W / 2 ? W : 0;
|
|
237
|
-
ctx.lineTo(edgeX, y);
|
|
238
|
-
ctx.closePath();
|
|
239
|
-
ctx.fill();
|
|
240
|
-
ctx.beginPath();
|
|
241
|
-
ctx.moveTo(isSunrise ? 0 : W, y);
|
|
242
|
-
}
|
|
243
|
-
ctx.lineTo(x, y);
|
|
244
|
-
prevX = x;
|
|
247
|
+
const terminator = calculateTerminatorContinuous(now, dep);
|
|
248
|
+
if (terminator.sunset.length === 0) {
|
|
249
|
+
prevAlpha = zone.alpha;
|
|
250
|
+
continue;
|
|
245
251
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
252
|
+
const poly = buildNightPolygon(terminator);
|
|
253
|
+
if (poly.length < 3) {
|
|
254
|
+
prevAlpha = zone.alpha;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
ctx.fillStyle = `rgba(0, 6, 24, ${bandAlpha.toFixed(4)})`;
|
|
258
|
+
for (const offset of [-360, 0, 360]) {
|
|
259
|
+
ctx.beginPath();
|
|
260
|
+
for (let i = 0; i < poly.length; i++) {
|
|
261
|
+
const lat = poly[i][0];
|
|
262
|
+
const lon = poly[i][1] + offset;
|
|
263
|
+
const x = lonToX(lon, W);
|
|
264
|
+
const y = latToY(lat, H);
|
|
265
|
+
if (i === 0) {
|
|
266
|
+
ctx.moveTo(x, y);
|
|
267
|
+
} else {
|
|
268
|
+
ctx.lineTo(x, y);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
ctx.closePath();
|
|
272
|
+
ctx.fill();
|
|
260
273
|
}
|
|
261
|
-
|
|
274
|
+
prevAlpha = zone.alpha;
|
|
262
275
|
}
|
|
263
276
|
}
|
|
264
277
|
ctx.fillStyle = COLORS.land;
|
|
@@ -482,6 +495,42 @@ function GroundTrackMap({
|
|
|
482
495
|
ctx.textAlign = "center";
|
|
483
496
|
ctx.fillText(gs.name.length > 14 ? gs.name.slice(0, 14) + "…" : gs.name, x, y + halfIcon + 12);
|
|
484
497
|
}
|
|
498
|
+
if (lightSources && lightSources.length > 0 && showTerminator) {
|
|
499
|
+
const now = terminatorTime ?? /* @__PURE__ */ new Date();
|
|
500
|
+
for (const light of lightSources) {
|
|
501
|
+
const lx = lonToX(light.longitude, W);
|
|
502
|
+
const ly = latToY(light.latitude, H);
|
|
503
|
+
const r = light.radius ?? 4;
|
|
504
|
+
const intensity = light.intensity ?? 0.8;
|
|
505
|
+
const color = light.color ?? "#ffeedd";
|
|
506
|
+
const dayOfYear = Math.floor((now.getTime() - new Date(now.getFullYear(), 0, 0).getTime()) / 864e5);
|
|
507
|
+
const declination = -23.44 * Math.cos(2 * Math.PI / 365 * (dayOfYear + 10));
|
|
508
|
+
const decRad = declination * Math.PI / 180;
|
|
509
|
+
const hourAngle = (now.getUTCHours() + now.getUTCMinutes() / 60) / 24 * 360 - 180;
|
|
510
|
+
const latRad = light.latitude * Math.PI / 180;
|
|
511
|
+
const sinAlt = Math.sin(latRad) * Math.sin(decRad) + Math.cos(latRad) * Math.cos(decRad) * Math.cos((light.longitude - hourAngle) * Math.PI / 180);
|
|
512
|
+
const solarAltDeg = Math.asin(sinAlt) * 180 / Math.PI;
|
|
513
|
+
let nightFactor;
|
|
514
|
+
if (solarAltDeg >= 0) nightFactor = 0;
|
|
515
|
+
else if (solarAltDeg <= -18) nightFactor = 1;
|
|
516
|
+
else nightFactor = Math.min(1, -solarAltDeg / 18);
|
|
517
|
+
if (nightFactor < 0.01) continue;
|
|
518
|
+
const alpha = intensity * nightFactor;
|
|
519
|
+
const [cr, cg, cb] = hexToRgb(color);
|
|
520
|
+
const grad = ctx.createRadialGradient(lx, ly, 0, lx, ly, r * 2);
|
|
521
|
+
grad.addColorStop(0, `rgba(${cr}, ${cg}, ${cb}, ${alpha})`);
|
|
522
|
+
grad.addColorStop(0.4, `rgba(${cr}, ${cg}, ${cb}, ${alpha * 0.6})`);
|
|
523
|
+
grad.addColorStop(1, `rgba(${cr}, ${cg}, ${cb}, 0)`);
|
|
524
|
+
ctx.fillStyle = grad;
|
|
525
|
+
ctx.beginPath();
|
|
526
|
+
ctx.arc(lx, ly, r * 2, 0, Math.PI * 2);
|
|
527
|
+
ctx.fill();
|
|
528
|
+
ctx.fillStyle = `rgba(${cr}, ${cg}, ${cb}, ${Math.min(1, alpha * 1.3)})`;
|
|
529
|
+
ctx.beginPath();
|
|
530
|
+
ctx.arc(lx, ly, Math.max(1, r * 0.4), 0, Math.PI * 2);
|
|
531
|
+
ctx.fill();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
485
534
|
allSatellites.forEach((sat, satIdx) => {
|
|
486
535
|
const track = sat.groundTrack;
|
|
487
536
|
if (!track || track.length === 0) return;
|
|
@@ -790,7 +839,7 @@ function GroundTrackMap({
|
|
|
790
839
|
ctx.textAlign = "center";
|
|
791
840
|
ctx.fillText(emptyMessage, W / 2, H / 2);
|
|
792
841
|
}
|
|
793
|
-
}, [allSatellites, groundStations, height, showTerminator, terminatorTime, showGrid, showLegend, showEquator, lonToX, latToY, COLORS, emptyMessage, pins, tokens.colors.accent.primary]);
|
|
842
|
+
}, [allSatellites, groundStations, height, showTerminator, terminatorTime, showGrid, showLegend, showEquator, lonToX, latToY, COLORS, emptyMessage, pins, lightSources, tokens.colors.accent.primary]);
|
|
794
843
|
const handleMouseMove = useCallback((e) => {
|
|
795
844
|
const canvas = canvasRef.current;
|
|
796
845
|
if (!canvas) return;
|
|
@@ -918,6 +967,8 @@ function GroundTrackMap({
|
|
|
918
967
|
minHeight,
|
|
919
968
|
emptyMessage,
|
|
920
969
|
tileUrl,
|
|
970
|
+
nightTileUrl,
|
|
971
|
+
lightSources,
|
|
921
972
|
className,
|
|
922
973
|
onSatelliteClick,
|
|
923
974
|
onStationClick,
|