@trackunit/react-map-adapter-shared 0.0.11 → 0.0.16
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/index.cjs.js +214 -21
- package/index.esm.js +209 -21
- package/package.json +3 -3
- package/src/LayerPort.d.ts +8 -0
- package/src/geodesicDensify.d.ts +39 -0
- package/src/index.d.ts +2 -0
- package/src/layerApiTypes.d.ts +20 -0
- package/src/shapeStyleDefaults.d.ts +26 -18
- package/src/sphericalMath.d.ts +18 -0
package/index.cjs.js
CHANGED
|
@@ -762,6 +762,214 @@ const mercatorCenterFromBounds = (bounds) => {
|
|
|
762
762
|
return [centerLon, centerLat];
|
|
763
763
|
};
|
|
764
764
|
|
|
765
|
+
// ============================================================================
|
|
766
|
+
// Unit conversion
|
|
767
|
+
// ============================================================================
|
|
768
|
+
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
769
|
+
const toDeg = (rad) => (rad * 180) / Math.PI;
|
|
770
|
+
// ============================================================================
|
|
771
|
+
// Great-circle math (sphere model — WGS-84 mean radius)
|
|
772
|
+
// ============================================================================
|
|
773
|
+
/**
|
|
774
|
+
* Haversine angular distance in radians between two lng/lat points.
|
|
775
|
+
* Returns a value in [0, π].
|
|
776
|
+
*/
|
|
777
|
+
const angularDistance = (lng1, lat1, lng2, lat2) => {
|
|
778
|
+
const φ1 = toRad(lat1);
|
|
779
|
+
const φ2 = toRad(lat2);
|
|
780
|
+
const Δφ = toRad(lat2 - lat1);
|
|
781
|
+
const Δλ = toRad(lng2 - lng1);
|
|
782
|
+
const a = Math.sin(Δφ / 2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2;
|
|
783
|
+
const clampedA = Math.max(0, Math.min(1, a));
|
|
784
|
+
return 2 * Math.atan2(Math.sqrt(clampedA), Math.sqrt(1 - clampedA));
|
|
785
|
+
};
|
|
786
|
+
/**
|
|
787
|
+
* Spherical-interpolation intermediate point at fraction `f` (0 = start, 1 = end)
|
|
788
|
+
* along the great-circle arc defined by `delta` (pre-computed angular distance).
|
|
789
|
+
*
|
|
790
|
+
* Formula: http://www.movable-type.co.uk/scripts/latlong.html#intermediate-point
|
|
791
|
+
*
|
|
792
|
+
* If `start` carries an altitude the result linearly interpolates it; otherwise
|
|
793
|
+
* the result is 2-D.
|
|
794
|
+
*/
|
|
795
|
+
const intermediatePoint = (start, end, f, delta) => {
|
|
796
|
+
const sinDelta = Math.sin(delta);
|
|
797
|
+
// Degenerate edge (co-located points) — return start unchanged.
|
|
798
|
+
if (sinDelta === 0)
|
|
799
|
+
return start;
|
|
800
|
+
const A = Math.sin((1 - f) * delta) / sinDelta;
|
|
801
|
+
const B = Math.sin(f * delta) / sinDelta;
|
|
802
|
+
const φ1 = toRad(start[1]);
|
|
803
|
+
const λ1 = toRad(start[0]);
|
|
804
|
+
const φ2 = toRad(end[1]);
|
|
805
|
+
const λ2 = toRad(end[0]);
|
|
806
|
+
const x = A * Math.cos(φ1) * Math.cos(λ1) + B * Math.cos(φ2) * Math.cos(λ2);
|
|
807
|
+
const y = A * Math.cos(φ1) * Math.sin(λ1) + B * Math.cos(φ2) * Math.sin(λ2);
|
|
808
|
+
const z = A * Math.sin(φ1) + B * Math.sin(φ2);
|
|
809
|
+
const lat = toDeg(Math.atan2(z, Math.sqrt(x * x + y * y)));
|
|
810
|
+
const lng = toDeg(Math.atan2(y, x));
|
|
811
|
+
if (start.length === 3 && end.length === 3) {
|
|
812
|
+
const alt = start[2] * (1 - f) + end[2] * f;
|
|
813
|
+
return [lng, lat, alt];
|
|
814
|
+
}
|
|
815
|
+
return [lng, lat];
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// ============================================================================
|
|
819
|
+
// Constants
|
|
820
|
+
// ============================================================================
|
|
821
|
+
const EARTH_RADIUS_KM = 6371;
|
|
822
|
+
/**
|
|
823
|
+
* Default maximum arc-segment length used by {@link densifyGeodesicFeatures}.
|
|
824
|
+
*
|
|
825
|
+
* At 100 km, a 1 000 km polygon edge gets ~10 intermediate points — enough
|
|
826
|
+
* for a visually smooth great-circle curve at any map zoom level. Reduce only
|
|
827
|
+
* if you need sub-100 km accuracy for very large polygons near the poles.
|
|
828
|
+
*/
|
|
829
|
+
const GEODESIC_MAX_SEGMENT_KM = 100;
|
|
830
|
+
// ============================================================================
|
|
831
|
+
// Ring / coordinate-array densification
|
|
832
|
+
// ============================================================================
|
|
833
|
+
/**
|
|
834
|
+
* Densify an array of positions (polygon ring or line-string coordinates) by
|
|
835
|
+
* inserting great-circle intermediate points wherever an edge exceeds
|
|
836
|
+
* `maxSegmentKm`.
|
|
837
|
+
*
|
|
838
|
+
* The result preserves the first and last positions exactly (no floating-point
|
|
839
|
+
* re-computation of the original vertices). Edges shorter than `maxSegmentKm`
|
|
840
|
+
* are left untouched — for typical urban-scale site polygons this is a no-op.
|
|
841
|
+
*
|
|
842
|
+
* Returns the **original array reference** when no intermediate points were
|
|
843
|
+
* inserted, enabling the caller to cheaply detect no-op cases via reference equality.
|
|
844
|
+
*/
|
|
845
|
+
const densifyPositions = (positions, maxSegmentKm) => {
|
|
846
|
+
if (positions.length < 2)
|
|
847
|
+
return positions;
|
|
848
|
+
let modified = false;
|
|
849
|
+
const result = [];
|
|
850
|
+
for (let i = 0; i < positions.length - 1; i++) {
|
|
851
|
+
const start = positions[i];
|
|
852
|
+
const end = positions[i + 1];
|
|
853
|
+
// start is always defined because i < positions.length - 1
|
|
854
|
+
if (start === undefined || end === undefined)
|
|
855
|
+
continue;
|
|
856
|
+
result.push(start);
|
|
857
|
+
const delta = angularDistance(start[0], start[1], end[0], end[1]);
|
|
858
|
+
const distKm = delta * EARTH_RADIUS_KM;
|
|
859
|
+
if (distKm > maxSegmentKm) {
|
|
860
|
+
modified = true;
|
|
861
|
+
const nSegments = Math.ceil(distKm / maxSegmentKm);
|
|
862
|
+
// Insert nSegments-1 intermediate points (the end point is added by the
|
|
863
|
+
// next iteration, or after the loop for the final edge).
|
|
864
|
+
for (let s = 1; s < nSegments; s++) {
|
|
865
|
+
result.push(intermediatePoint(start, end, s / nSegments, delta));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// Return the original reference when nothing was inserted — allows callers to
|
|
870
|
+
// detect no-ops via reference equality without allocating a new array.
|
|
871
|
+
if (!modified)
|
|
872
|
+
return positions;
|
|
873
|
+
// Always preserve the original last position exactly.
|
|
874
|
+
const last = positions[positions.length - 1];
|
|
875
|
+
if (last !== undefined)
|
|
876
|
+
result.push(last);
|
|
877
|
+
return result;
|
|
878
|
+
};
|
|
879
|
+
// ============================================================================
|
|
880
|
+
// Feature-level densification
|
|
881
|
+
// ============================================================================
|
|
882
|
+
/**
|
|
883
|
+
* Densify all edge-bearing geometry types in a single GeoJSON feature.
|
|
884
|
+
* Point and MultiPoint features are returned unchanged.
|
|
885
|
+
*/
|
|
886
|
+
const densifyFeature = (feature, maxSegmentKm) => {
|
|
887
|
+
const { geometry } = feature;
|
|
888
|
+
if (geometry === null)
|
|
889
|
+
return feature;
|
|
890
|
+
switch (geometry.type) {
|
|
891
|
+
case "Point":
|
|
892
|
+
case "MultiPoint":
|
|
893
|
+
return feature;
|
|
894
|
+
case "LineString": {
|
|
895
|
+
const coords = densifyPositions(geometry.coordinates, maxSegmentKm);
|
|
896
|
+
if (coords === geometry.coordinates)
|
|
897
|
+
return feature;
|
|
898
|
+
return { ...feature, geometry: { ...geometry, coordinates: coords } };
|
|
899
|
+
}
|
|
900
|
+
case "MultiLineString": {
|
|
901
|
+
const lines = geometry.coordinates.map(line => densifyPositions(line, maxSegmentKm));
|
|
902
|
+
if (lines.every((line, i) => line === geometry.coordinates[i]))
|
|
903
|
+
return feature;
|
|
904
|
+
return { ...feature, geometry: { ...geometry, coordinates: lines } };
|
|
905
|
+
}
|
|
906
|
+
case "Polygon": {
|
|
907
|
+
const rings = geometry.coordinates.map(ring => densifyPositions(ring, maxSegmentKm));
|
|
908
|
+
if (rings.every((ring, i) => ring === geometry.coordinates[i]))
|
|
909
|
+
return feature;
|
|
910
|
+
return { ...feature, geometry: { ...geometry, coordinates: rings } };
|
|
911
|
+
}
|
|
912
|
+
case "MultiPolygon": {
|
|
913
|
+
const polygons = geometry.coordinates.map(polygon => polygon.map(ring => densifyPositions(ring, maxSegmentKm)));
|
|
914
|
+
if (polygons.every((polygon, pi) => polygon.every((ring, ri) => ring === geometry.coordinates[pi]?.[ri])))
|
|
915
|
+
return feature;
|
|
916
|
+
return { ...feature, geometry: { ...geometry, coordinates: polygons } };
|
|
917
|
+
}
|
|
918
|
+
default:
|
|
919
|
+
return feature;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
// ============================================================================
|
|
923
|
+
// Public API
|
|
924
|
+
// ============================================================================
|
|
925
|
+
/**
|
|
926
|
+
* Densify GeoJSON features by inserting great-circle intermediate points along
|
|
927
|
+
* polygon/line edges that exceed `maxSegmentKm`.
|
|
928
|
+
*
|
|
929
|
+
* This is the primary way to achieve geodesic rendering in adapters that lack
|
|
930
|
+
* native great-circle support (e.g. Mapbox GL). Adapters with native geodesic
|
|
931
|
+
* support (e.g. Google Maps via `geodesic: true` on `Polygon`/`Polyline`) do
|
|
932
|
+
* not need this function — they should apply the flag directly to their
|
|
933
|
+
* geometry objects instead.
|
|
934
|
+
*
|
|
935
|
+
* Geodesic eligibility is resolved per-feature:
|
|
936
|
+
* `featureStyles.get(id)?.geodesic ?? layerGeodesic ?? true`
|
|
937
|
+
*
|
|
938
|
+
* Features where geodesic resolves to `false` are passed through unchanged.
|
|
939
|
+
* Point and MultiPoint features are always passed through unchanged.
|
|
940
|
+
*
|
|
941
|
+
* The function returns the original collection reference when no feature is
|
|
942
|
+
* modified, avoiding unnecessary downstream work.
|
|
943
|
+
*
|
|
944
|
+
* Run this **before** `mergeAntimeridianFeatures` — antimeridian-merged
|
|
945
|
+
* polygons may carry longitudes outside `[-180, 180]`, which would corrupt
|
|
946
|
+
* the `atan2` result inside {@link intermediatePoint}.
|
|
947
|
+
*
|
|
948
|
+
* @param features - Source GeoJSON feature collection (RFC 7946, unmodified).
|
|
949
|
+
* @param layerGeodesic - Layer-level geodesic flag (default `true`).
|
|
950
|
+
* @param featureStyles - Per-feature style overrides; `geodesic` is read from each entry.
|
|
951
|
+
* @param maxSegmentKm - Maximum arc-segment length in kilometres. Default: {@link GEODESIC_MAX_SEGMENT_KM}.
|
|
952
|
+
*/
|
|
953
|
+
const densifyGeodesicFeatures = (features, layerGeodesic, featureStyles, maxSegmentKm = GEODESIC_MAX_SEGMENT_KM) => {
|
|
954
|
+
const densified = [];
|
|
955
|
+
let anyModified = false;
|
|
956
|
+
for (const feature of features.features) {
|
|
957
|
+
const featureId = feature.id !== undefined ? String(feature.id) : undefined;
|
|
958
|
+
const featureGeodesic = featureId !== undefined ? (featureStyles?.get(featureId)?.geodesic ?? layerGeodesic) : layerGeodesic;
|
|
959
|
+
if (!featureGeodesic) {
|
|
960
|
+
densified.push(feature);
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
const result = densifyFeature(feature, maxSegmentKm);
|
|
964
|
+
if (result !== feature)
|
|
965
|
+
anyModified = true;
|
|
966
|
+
densified.push(result);
|
|
967
|
+
}
|
|
968
|
+
if (!anyModified)
|
|
969
|
+
return features;
|
|
970
|
+
return { ...features, features: densified };
|
|
971
|
+
};
|
|
972
|
+
|
|
765
973
|
const isViewportLike = (value) => {
|
|
766
974
|
return typeof value === "object" && value !== null && "type" in value;
|
|
767
975
|
};
|
|
@@ -2314,26 +2522,6 @@ const attachSafeAreaHoverListeners = (el, onEnter, onLeave, state) => {
|
|
|
2314
2522
|
});
|
|
2315
2523
|
};
|
|
2316
2524
|
|
|
2317
|
-
// TODO (next PR): shapeStyleDefaults computes visual defaults for shapes — a map consumer
|
|
2318
|
-
// concern, not an adapter concern. Make these injectable from react-map's createMapComponent
|
|
2319
|
-
// so adapters don't need to know about default visual styles.
|
|
2320
|
-
const SHAPE_STYLE_DEFAULTS = {
|
|
2321
|
-
polygon: {
|
|
2322
|
-
fillOpacity: 0.05,
|
|
2323
|
-
strokeWidth: 1,
|
|
2324
|
-
strokeOpacity: 1,
|
|
2325
|
-
},
|
|
2326
|
-
line: {
|
|
2327
|
-
strokeWidth: 1,
|
|
2328
|
-
strokeOpacity: 1,
|
|
2329
|
-
},
|
|
2330
|
-
point: {
|
|
2331
|
-
fillOpacity: 0.2,
|
|
2332
|
-
strokeWidth: 1,
|
|
2333
|
-
strokeOpacity: 1,
|
|
2334
|
-
pointRadius: 5,
|
|
2335
|
-
},
|
|
2336
|
-
};
|
|
2337
2525
|
// ============================================================================
|
|
2338
2526
|
// Interaction style constants
|
|
2339
2527
|
// ============================================================================
|
|
@@ -2402,6 +2590,7 @@ exports.CIRCLE_SYMBOL_DEFAULT_OPACITY = CIRCLE_SYMBOL_DEFAULT_OPACITY;
|
|
|
2402
2590
|
exports.DEFAULT_CENTER = DEFAULT_CENTER;
|
|
2403
2591
|
exports.DEFAULT_MAP_APPEARANCE = DEFAULT_MAP_APPEARANCE;
|
|
2404
2592
|
exports.DEFAULT_ZOOM = DEFAULT_ZOOM;
|
|
2593
|
+
exports.GEODESIC_MAX_SEGMENT_KM = GEODESIC_MAX_SEGMENT_KM;
|
|
2405
2594
|
exports.HIT_SURFACE_SELECTOR = HIT_SURFACE_SELECTOR;
|
|
2406
2595
|
exports.INITIAL_CAMERA_STATE = INITIAL_CAMERA_STATE;
|
|
2407
2596
|
exports.INITIAL_INTERACTION_STATE = INITIAL_INTERACTION_STATE;
|
|
@@ -2414,10 +2603,10 @@ exports.MAP_CURSORS = MAP_CURSORS;
|
|
|
2414
2603
|
exports.MAX_ZOOM = MAX_ZOOM;
|
|
2415
2604
|
exports.MIN_ZOOM = MIN_ZOOM;
|
|
2416
2605
|
exports.SAFE_AREA_DEFAULT_BUFFER_PX = SAFE_AREA_DEFAULT_BUFFER_PX;
|
|
2417
|
-
exports.SHAPE_STYLE_DEFAULTS = SHAPE_STYLE_DEFAULTS;
|
|
2418
2606
|
exports.WORLD_BBOX = WORLD_BBOX;
|
|
2419
2607
|
exports.allocSafeAreaDebugId = allocSafeAreaDebugId;
|
|
2420
2608
|
exports.anchorFromBottomCenter = anchorFromBottomCenter;
|
|
2609
|
+
exports.angularDistance = angularDistance;
|
|
2421
2610
|
exports.attachSafeAreaHoverListeners = attachSafeAreaHoverListeners;
|
|
2422
2611
|
exports.bboxEquals = bboxEquals;
|
|
2423
2612
|
exports.bufferRectCorners = bufferRectCorners;
|
|
@@ -2439,6 +2628,7 @@ exports.createDefaultClusterElement = createDefaultClusterElement;
|
|
|
2439
2628
|
exports.createSymbolDotElement = createSymbolDotElement;
|
|
2440
2629
|
exports.darkenColor = darkenColor;
|
|
2441
2630
|
exports.defineAdapter = defineAdapter;
|
|
2631
|
+
exports.densifyGeodesicFeatures = densifyGeodesicFeatures;
|
|
2442
2632
|
exports.disableSafeAreaDebug = disableSafeAreaDebug;
|
|
2443
2633
|
exports.discriminateRenderResult = discriminateRenderResult;
|
|
2444
2634
|
exports.enableSafeAreaDebug = enableSafeAreaDebug;
|
|
@@ -2455,6 +2645,7 @@ exports.getAnchorRect = getAnchorRect;
|
|
|
2455
2645
|
exports.getEffectiveRestrictBounds = getEffectiveRestrictBounds;
|
|
2456
2646
|
exports.getHitSurfaceRect = getHitSurfaceRect;
|
|
2457
2647
|
exports.hasSameFeatureIds = hasSameFeatureIds;
|
|
2648
|
+
exports.intermediatePoint = intermediatePoint;
|
|
2458
2649
|
exports.isCanvasMarkerMode = isCanvasMarkerMode;
|
|
2459
2650
|
exports.isEventOfType = isEventOfType;
|
|
2460
2651
|
exports.isSafeAreaDebugEnabled = isSafeAreaDebugEnabled;
|
|
@@ -2477,5 +2668,7 @@ exports.resolveSelectedStyle = resolveSelectedStyle;
|
|
|
2477
2668
|
exports.resolveStrokeColors = resolveStrokeColors;
|
|
2478
2669
|
exports.resolveSymbolDescriptor = resolveSymbolDescriptor;
|
|
2479
2670
|
exports.safePolygon = safePolygon;
|
|
2671
|
+
exports.toDeg = toDeg;
|
|
2672
|
+
exports.toRad = toRad;
|
|
2480
2673
|
exports.validateInitialViewport = validateInitialViewport;
|
|
2481
2674
|
exports.watchSafeAreaLeave = watchSafeAreaLeave;
|
package/index.esm.js
CHANGED
|
@@ -760,6 +760,214 @@ const mercatorCenterFromBounds = (bounds) => {
|
|
|
760
760
|
return [centerLon, centerLat];
|
|
761
761
|
};
|
|
762
762
|
|
|
763
|
+
// ============================================================================
|
|
764
|
+
// Unit conversion
|
|
765
|
+
// ============================================================================
|
|
766
|
+
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
767
|
+
const toDeg = (rad) => (rad * 180) / Math.PI;
|
|
768
|
+
// ============================================================================
|
|
769
|
+
// Great-circle math (sphere model — WGS-84 mean radius)
|
|
770
|
+
// ============================================================================
|
|
771
|
+
/**
|
|
772
|
+
* Haversine angular distance in radians between two lng/lat points.
|
|
773
|
+
* Returns a value in [0, π].
|
|
774
|
+
*/
|
|
775
|
+
const angularDistance = (lng1, lat1, lng2, lat2) => {
|
|
776
|
+
const φ1 = toRad(lat1);
|
|
777
|
+
const φ2 = toRad(lat2);
|
|
778
|
+
const Δφ = toRad(lat2 - lat1);
|
|
779
|
+
const Δλ = toRad(lng2 - lng1);
|
|
780
|
+
const a = Math.sin(Δφ / 2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2;
|
|
781
|
+
const clampedA = Math.max(0, Math.min(1, a));
|
|
782
|
+
return 2 * Math.atan2(Math.sqrt(clampedA), Math.sqrt(1 - clampedA));
|
|
783
|
+
};
|
|
784
|
+
/**
|
|
785
|
+
* Spherical-interpolation intermediate point at fraction `f` (0 = start, 1 = end)
|
|
786
|
+
* along the great-circle arc defined by `delta` (pre-computed angular distance).
|
|
787
|
+
*
|
|
788
|
+
* Formula: http://www.movable-type.co.uk/scripts/latlong.html#intermediate-point
|
|
789
|
+
*
|
|
790
|
+
* If `start` carries an altitude the result linearly interpolates it; otherwise
|
|
791
|
+
* the result is 2-D.
|
|
792
|
+
*/
|
|
793
|
+
const intermediatePoint = (start, end, f, delta) => {
|
|
794
|
+
const sinDelta = Math.sin(delta);
|
|
795
|
+
// Degenerate edge (co-located points) — return start unchanged.
|
|
796
|
+
if (sinDelta === 0)
|
|
797
|
+
return start;
|
|
798
|
+
const A = Math.sin((1 - f) * delta) / sinDelta;
|
|
799
|
+
const B = Math.sin(f * delta) / sinDelta;
|
|
800
|
+
const φ1 = toRad(start[1]);
|
|
801
|
+
const λ1 = toRad(start[0]);
|
|
802
|
+
const φ2 = toRad(end[1]);
|
|
803
|
+
const λ2 = toRad(end[0]);
|
|
804
|
+
const x = A * Math.cos(φ1) * Math.cos(λ1) + B * Math.cos(φ2) * Math.cos(λ2);
|
|
805
|
+
const y = A * Math.cos(φ1) * Math.sin(λ1) + B * Math.cos(φ2) * Math.sin(λ2);
|
|
806
|
+
const z = A * Math.sin(φ1) + B * Math.sin(φ2);
|
|
807
|
+
const lat = toDeg(Math.atan2(z, Math.sqrt(x * x + y * y)));
|
|
808
|
+
const lng = toDeg(Math.atan2(y, x));
|
|
809
|
+
if (start.length === 3 && end.length === 3) {
|
|
810
|
+
const alt = start[2] * (1 - f) + end[2] * f;
|
|
811
|
+
return [lng, lat, alt];
|
|
812
|
+
}
|
|
813
|
+
return [lng, lat];
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
// ============================================================================
|
|
817
|
+
// Constants
|
|
818
|
+
// ============================================================================
|
|
819
|
+
const EARTH_RADIUS_KM = 6371;
|
|
820
|
+
/**
|
|
821
|
+
* Default maximum arc-segment length used by {@link densifyGeodesicFeatures}.
|
|
822
|
+
*
|
|
823
|
+
* At 100 km, a 1 000 km polygon edge gets ~10 intermediate points — enough
|
|
824
|
+
* for a visually smooth great-circle curve at any map zoom level. Reduce only
|
|
825
|
+
* if you need sub-100 km accuracy for very large polygons near the poles.
|
|
826
|
+
*/
|
|
827
|
+
const GEODESIC_MAX_SEGMENT_KM = 100;
|
|
828
|
+
// ============================================================================
|
|
829
|
+
// Ring / coordinate-array densification
|
|
830
|
+
// ============================================================================
|
|
831
|
+
/**
|
|
832
|
+
* Densify an array of positions (polygon ring or line-string coordinates) by
|
|
833
|
+
* inserting great-circle intermediate points wherever an edge exceeds
|
|
834
|
+
* `maxSegmentKm`.
|
|
835
|
+
*
|
|
836
|
+
* The result preserves the first and last positions exactly (no floating-point
|
|
837
|
+
* re-computation of the original vertices). Edges shorter than `maxSegmentKm`
|
|
838
|
+
* are left untouched — for typical urban-scale site polygons this is a no-op.
|
|
839
|
+
*
|
|
840
|
+
* Returns the **original array reference** when no intermediate points were
|
|
841
|
+
* inserted, enabling the caller to cheaply detect no-op cases via reference equality.
|
|
842
|
+
*/
|
|
843
|
+
const densifyPositions = (positions, maxSegmentKm) => {
|
|
844
|
+
if (positions.length < 2)
|
|
845
|
+
return positions;
|
|
846
|
+
let modified = false;
|
|
847
|
+
const result = [];
|
|
848
|
+
for (let i = 0; i < positions.length - 1; i++) {
|
|
849
|
+
const start = positions[i];
|
|
850
|
+
const end = positions[i + 1];
|
|
851
|
+
// start is always defined because i < positions.length - 1
|
|
852
|
+
if (start === undefined || end === undefined)
|
|
853
|
+
continue;
|
|
854
|
+
result.push(start);
|
|
855
|
+
const delta = angularDistance(start[0], start[1], end[0], end[1]);
|
|
856
|
+
const distKm = delta * EARTH_RADIUS_KM;
|
|
857
|
+
if (distKm > maxSegmentKm) {
|
|
858
|
+
modified = true;
|
|
859
|
+
const nSegments = Math.ceil(distKm / maxSegmentKm);
|
|
860
|
+
// Insert nSegments-1 intermediate points (the end point is added by the
|
|
861
|
+
// next iteration, or after the loop for the final edge).
|
|
862
|
+
for (let s = 1; s < nSegments; s++) {
|
|
863
|
+
result.push(intermediatePoint(start, end, s / nSegments, delta));
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
// Return the original reference when nothing was inserted — allows callers to
|
|
868
|
+
// detect no-ops via reference equality without allocating a new array.
|
|
869
|
+
if (!modified)
|
|
870
|
+
return positions;
|
|
871
|
+
// Always preserve the original last position exactly.
|
|
872
|
+
const last = positions[positions.length - 1];
|
|
873
|
+
if (last !== undefined)
|
|
874
|
+
result.push(last);
|
|
875
|
+
return result;
|
|
876
|
+
};
|
|
877
|
+
// ============================================================================
|
|
878
|
+
// Feature-level densification
|
|
879
|
+
// ============================================================================
|
|
880
|
+
/**
|
|
881
|
+
* Densify all edge-bearing geometry types in a single GeoJSON feature.
|
|
882
|
+
* Point and MultiPoint features are returned unchanged.
|
|
883
|
+
*/
|
|
884
|
+
const densifyFeature = (feature, maxSegmentKm) => {
|
|
885
|
+
const { geometry } = feature;
|
|
886
|
+
if (geometry === null)
|
|
887
|
+
return feature;
|
|
888
|
+
switch (geometry.type) {
|
|
889
|
+
case "Point":
|
|
890
|
+
case "MultiPoint":
|
|
891
|
+
return feature;
|
|
892
|
+
case "LineString": {
|
|
893
|
+
const coords = densifyPositions(geometry.coordinates, maxSegmentKm);
|
|
894
|
+
if (coords === geometry.coordinates)
|
|
895
|
+
return feature;
|
|
896
|
+
return { ...feature, geometry: { ...geometry, coordinates: coords } };
|
|
897
|
+
}
|
|
898
|
+
case "MultiLineString": {
|
|
899
|
+
const lines = geometry.coordinates.map(line => densifyPositions(line, maxSegmentKm));
|
|
900
|
+
if (lines.every((line, i) => line === geometry.coordinates[i]))
|
|
901
|
+
return feature;
|
|
902
|
+
return { ...feature, geometry: { ...geometry, coordinates: lines } };
|
|
903
|
+
}
|
|
904
|
+
case "Polygon": {
|
|
905
|
+
const rings = geometry.coordinates.map(ring => densifyPositions(ring, maxSegmentKm));
|
|
906
|
+
if (rings.every((ring, i) => ring === geometry.coordinates[i]))
|
|
907
|
+
return feature;
|
|
908
|
+
return { ...feature, geometry: { ...geometry, coordinates: rings } };
|
|
909
|
+
}
|
|
910
|
+
case "MultiPolygon": {
|
|
911
|
+
const polygons = geometry.coordinates.map(polygon => polygon.map(ring => densifyPositions(ring, maxSegmentKm)));
|
|
912
|
+
if (polygons.every((polygon, pi) => polygon.every((ring, ri) => ring === geometry.coordinates[pi]?.[ri])))
|
|
913
|
+
return feature;
|
|
914
|
+
return { ...feature, geometry: { ...geometry, coordinates: polygons } };
|
|
915
|
+
}
|
|
916
|
+
default:
|
|
917
|
+
return feature;
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
// ============================================================================
|
|
921
|
+
// Public API
|
|
922
|
+
// ============================================================================
|
|
923
|
+
/**
|
|
924
|
+
* Densify GeoJSON features by inserting great-circle intermediate points along
|
|
925
|
+
* polygon/line edges that exceed `maxSegmentKm`.
|
|
926
|
+
*
|
|
927
|
+
* This is the primary way to achieve geodesic rendering in adapters that lack
|
|
928
|
+
* native great-circle support (e.g. Mapbox GL). Adapters with native geodesic
|
|
929
|
+
* support (e.g. Google Maps via `geodesic: true` on `Polygon`/`Polyline`) do
|
|
930
|
+
* not need this function — they should apply the flag directly to their
|
|
931
|
+
* geometry objects instead.
|
|
932
|
+
*
|
|
933
|
+
* Geodesic eligibility is resolved per-feature:
|
|
934
|
+
* `featureStyles.get(id)?.geodesic ?? layerGeodesic ?? true`
|
|
935
|
+
*
|
|
936
|
+
* Features where geodesic resolves to `false` are passed through unchanged.
|
|
937
|
+
* Point and MultiPoint features are always passed through unchanged.
|
|
938
|
+
*
|
|
939
|
+
* The function returns the original collection reference when no feature is
|
|
940
|
+
* modified, avoiding unnecessary downstream work.
|
|
941
|
+
*
|
|
942
|
+
* Run this **before** `mergeAntimeridianFeatures` — antimeridian-merged
|
|
943
|
+
* polygons may carry longitudes outside `[-180, 180]`, which would corrupt
|
|
944
|
+
* the `atan2` result inside {@link intermediatePoint}.
|
|
945
|
+
*
|
|
946
|
+
* @param features - Source GeoJSON feature collection (RFC 7946, unmodified).
|
|
947
|
+
* @param layerGeodesic - Layer-level geodesic flag (default `true`).
|
|
948
|
+
* @param featureStyles - Per-feature style overrides; `geodesic` is read from each entry.
|
|
949
|
+
* @param maxSegmentKm - Maximum arc-segment length in kilometres. Default: {@link GEODESIC_MAX_SEGMENT_KM}.
|
|
950
|
+
*/
|
|
951
|
+
const densifyGeodesicFeatures = (features, layerGeodesic, featureStyles, maxSegmentKm = GEODESIC_MAX_SEGMENT_KM) => {
|
|
952
|
+
const densified = [];
|
|
953
|
+
let anyModified = false;
|
|
954
|
+
for (const feature of features.features) {
|
|
955
|
+
const featureId = feature.id !== undefined ? String(feature.id) : undefined;
|
|
956
|
+
const featureGeodesic = featureId !== undefined ? (featureStyles?.get(featureId)?.geodesic ?? layerGeodesic) : layerGeodesic;
|
|
957
|
+
if (!featureGeodesic) {
|
|
958
|
+
densified.push(feature);
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
const result = densifyFeature(feature, maxSegmentKm);
|
|
962
|
+
if (result !== feature)
|
|
963
|
+
anyModified = true;
|
|
964
|
+
densified.push(result);
|
|
965
|
+
}
|
|
966
|
+
if (!anyModified)
|
|
967
|
+
return features;
|
|
968
|
+
return { ...features, features: densified };
|
|
969
|
+
};
|
|
970
|
+
|
|
763
971
|
const isViewportLike = (value) => {
|
|
764
972
|
return typeof value === "object" && value !== null && "type" in value;
|
|
765
973
|
};
|
|
@@ -2312,26 +2520,6 @@ const attachSafeAreaHoverListeners = (el, onEnter, onLeave, state) => {
|
|
|
2312
2520
|
});
|
|
2313
2521
|
};
|
|
2314
2522
|
|
|
2315
|
-
// TODO (next PR): shapeStyleDefaults computes visual defaults for shapes — a map consumer
|
|
2316
|
-
// concern, not an adapter concern. Make these injectable from react-map's createMapComponent
|
|
2317
|
-
// so adapters don't need to know about default visual styles.
|
|
2318
|
-
const SHAPE_STYLE_DEFAULTS = {
|
|
2319
|
-
polygon: {
|
|
2320
|
-
fillOpacity: 0.05,
|
|
2321
|
-
strokeWidth: 1,
|
|
2322
|
-
strokeOpacity: 1,
|
|
2323
|
-
},
|
|
2324
|
-
line: {
|
|
2325
|
-
strokeWidth: 1,
|
|
2326
|
-
strokeOpacity: 1,
|
|
2327
|
-
},
|
|
2328
|
-
point: {
|
|
2329
|
-
fillOpacity: 0.2,
|
|
2330
|
-
strokeWidth: 1,
|
|
2331
|
-
strokeOpacity: 1,
|
|
2332
|
-
pointRadius: 5,
|
|
2333
|
-
},
|
|
2334
|
-
};
|
|
2335
2523
|
// ============================================================================
|
|
2336
2524
|
// Interaction style constants
|
|
2337
2525
|
// ============================================================================
|
|
@@ -2394,4 +2582,4 @@ const resolveStrokeColors = (style, shapeType, theme) => {
|
|
|
2394
2582
|
};
|
|
2395
2583
|
};
|
|
2396
2584
|
|
|
2397
|
-
export { ANCHOR_SELECTOR, CIRCLE_SYMBOL_DEFAULT_DIAMETER_PX, CIRCLE_SYMBOL_DEFAULT_OPACITY, DEFAULT_CENTER, DEFAULT_MAP_APPEARANCE, DEFAULT_ZOOM, HIT_SURFACE_SELECTOR, INITIAL_CAMERA_STATE, INITIAL_INTERACTION_STATE, INITIAL_MAP_STATE, INITIAL_MAP_STATUS, KEYBOARD_PAN_AMOUNT, KEYBOARD_ZOOM_AMOUNT, LAYER_FADE_DURATION_MS, MAP_CURSORS, MAX_ZOOM, MIN_ZOOM, SAFE_AREA_DEFAULT_BUFFER_PX,
|
|
2585
|
+
export { ANCHOR_SELECTOR, CIRCLE_SYMBOL_DEFAULT_DIAMETER_PX, CIRCLE_SYMBOL_DEFAULT_OPACITY, DEFAULT_CENTER, DEFAULT_MAP_APPEARANCE, DEFAULT_ZOOM, GEODESIC_MAX_SEGMENT_KM, HIT_SURFACE_SELECTOR, INITIAL_CAMERA_STATE, INITIAL_INTERACTION_STATE, INITIAL_MAP_STATE, INITIAL_MAP_STATUS, KEYBOARD_PAN_AMOUNT, KEYBOARD_ZOOM_AMOUNT, LAYER_FADE_DURATION_MS, MAP_CURSORS, MAX_ZOOM, MIN_ZOOM, SAFE_AREA_DEFAULT_BUFFER_PX, WORLD_BBOX, allocSafeAreaDebugId, anchorFromBottomCenter, angularDistance, attachSafeAreaHoverListeners, bboxEquals, bufferRectCorners, buildAdaptiveDomEntries, buildAdaptiveDomRenderFn, buildAdaptiveSymbolStyleFn, cameraStateEquals, canPatchAdaptiveMarker, canPatchAdaptiveViewport, canPatchMarkerInPlace, clearSafeArea, collectFeatureIdSet, colorWithOpacity, computeInitialState, computeMarkerDomPortalZIndex, convexHull, createClusterPinElement, createDefaultClusterElement, createSymbolDotElement, darkenColor, defineAdapter, densifyGeodesicFeatures, disableSafeAreaDebug, discriminateRenderResult, enableSafeAreaDebug, estimateZoomFromBounds, extractLineCoordinates, extractPointCoordinates, extractPolygonPaths, extractSourceData, fadeInElement, filterRectsByCursorDirection, geometryTypeToShapeType, getAdaptiveDomFeatureIds, getAnchorRect, getEffectiveRestrictBounds, getHitSurfaceRect, hasSameFeatureIds, intermediatePoint, isCanvasMarkerMode, isEventOfType, isSafeAreaDebugEnabled, lightenColor, mapAppearanceSchema, mapStateEquals, mapThemeSchema, mapTypeSchema, mercatorCenterFromBounds, mergeAntimeridianFeatures, mixColor, patchPortalDescriptors, pointInPolygon, removeGoneIndexedMarkers, renderSafeArea, resetColorUtilsForTesting, resolveCircleSymbolDefaults, resolveHoveredStyle, resolveSelectedStyle, resolveStrokeColors, resolveSymbolDescriptor, safePolygon, toDeg, toRad, validateInitialViewport, watchSafeAreaLeave };
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-map-adapter-shared",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=24.x"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@trackunit/geo-json-utils": "1.14.
|
|
11
|
-
"@trackunit/ui-design-tokens": "1.13.
|
|
10
|
+
"@trackunit/geo-json-utils": "1.14.35",
|
|
11
|
+
"@trackunit/ui-design-tokens": "1.13.32",
|
|
12
12
|
"es-toolkit": "^1.39.10",
|
|
13
13
|
"zod": "^3.25.76"
|
|
14
14
|
},
|
package/src/LayerPort.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GeoJsonBbox } from "@trackunit/geo-json-utils";
|
|
2
2
|
import type { Entity } from "./interactionTypes";
|
|
3
|
+
import type { ShapeStyleDefaults } from "./shapeStyleDefaults";
|
|
3
4
|
import type { AdaptiveMarkerResolution, ClusterConfig, ClusterRenderConfig, ClusterRenderState, DomRenderState, GeoJsonFeatureCollection, GeoJsonGeometry, RenderConfig, RouteStyle, ShapeInteractiveMode, ShapeStyle } from "./layerApiTypes";
|
|
4
5
|
type ReactNode = import("react").ReactNode;
|
|
5
6
|
/**
|
|
@@ -231,6 +232,13 @@ export type LayerPort = Readonly<{
|
|
|
231
232
|
* @returns Unsubscribe function.
|
|
232
233
|
*/
|
|
233
234
|
onSourceReady: (configId: string, callback: () => void) => () => void;
|
|
235
|
+
/**
|
|
236
|
+
* Injects the per-shape-type visual defaults from the map consumer.
|
|
237
|
+
*
|
|
238
|
+
* Called by `createMapComponent` before any snapshot is sent, so adapters
|
|
239
|
+
* always receive the consumer-owned defaults rather than hard-coding them.
|
|
240
|
+
*/
|
|
241
|
+
setShapeStyleDefaults: (defaults: ShapeStyleDefaults) => void;
|
|
234
242
|
/**
|
|
235
243
|
* Subscription for DOM-rendered individual markers (including adaptive DOM
|
|
236
244
|
* features). `<Layers>` routes these descriptors to `markerRender`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { GeoJsonFeatureCollection } from "@trackunit/geo-json-utils";
|
|
2
|
+
import type { ShapeStyle } from "./layerApiTypes";
|
|
3
|
+
/**
|
|
4
|
+
* Default maximum arc-segment length used by {@link densifyGeodesicFeatures}.
|
|
5
|
+
*
|
|
6
|
+
* At 100 km, a 1 000 km polygon edge gets ~10 intermediate points — enough
|
|
7
|
+
* for a visually smooth great-circle curve at any map zoom level. Reduce only
|
|
8
|
+
* if you need sub-100 km accuracy for very large polygons near the poles.
|
|
9
|
+
*/
|
|
10
|
+
export declare const GEODESIC_MAX_SEGMENT_KM = 100;
|
|
11
|
+
/**
|
|
12
|
+
* Densify GeoJSON features by inserting great-circle intermediate points along
|
|
13
|
+
* polygon/line edges that exceed `maxSegmentKm`.
|
|
14
|
+
*
|
|
15
|
+
* This is the primary way to achieve geodesic rendering in adapters that lack
|
|
16
|
+
* native great-circle support (e.g. Mapbox GL). Adapters with native geodesic
|
|
17
|
+
* support (e.g. Google Maps via `geodesic: true` on `Polygon`/`Polyline`) do
|
|
18
|
+
* not need this function — they should apply the flag directly to their
|
|
19
|
+
* geometry objects instead.
|
|
20
|
+
*
|
|
21
|
+
* Geodesic eligibility is resolved per-feature:
|
|
22
|
+
* `featureStyles.get(id)?.geodesic ?? layerGeodesic ?? true`
|
|
23
|
+
*
|
|
24
|
+
* Features where geodesic resolves to `false` are passed through unchanged.
|
|
25
|
+
* Point and MultiPoint features are always passed through unchanged.
|
|
26
|
+
*
|
|
27
|
+
* The function returns the original collection reference when no feature is
|
|
28
|
+
* modified, avoiding unnecessary downstream work.
|
|
29
|
+
*
|
|
30
|
+
* Run this **before** `mergeAntimeridianFeatures` — antimeridian-merged
|
|
31
|
+
* polygons may carry longitudes outside `[-180, 180]`, which would corrupt
|
|
32
|
+
* the `atan2` result inside {@link intermediatePoint}.
|
|
33
|
+
*
|
|
34
|
+
* @param features - Source GeoJSON feature collection (RFC 7946, unmodified).
|
|
35
|
+
* @param layerGeodesic - Layer-level geodesic flag (default `true`).
|
|
36
|
+
* @param featureStyles - Per-feature style overrides; `geodesic` is read from each entry.
|
|
37
|
+
* @param maxSegmentKm - Maximum arc-segment length in kilometres. Default: {@link GEODESIC_MAX_SEGMENT_KM}.
|
|
38
|
+
*/
|
|
39
|
+
export declare const densifyGeodesicFeatures: (features: GeoJsonFeatureCollection, layerGeodesic: boolean, featureStyles: ReadonlyMap<string, ShapeStyle> | undefined, maxSegmentKm?: number) => GeoJsonFeatureCollection;
|
package/src/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from "./adapterContract";
|
|
|
2
2
|
export * from "./antimeridianMerge";
|
|
3
3
|
export * from "./colorUtils";
|
|
4
4
|
export * from "./constants";
|
|
5
|
+
export * from "./geodesicDensify";
|
|
5
6
|
export * from "./initialViewportValidation";
|
|
6
7
|
export * from "./interactionTypes";
|
|
7
8
|
export * from "./layerApiTypes";
|
|
@@ -18,4 +19,5 @@ export * from "./safeArea/safeAreaDebug";
|
|
|
18
19
|
export * from "./safeArea/safePolygon";
|
|
19
20
|
export * from "./safeArea/watchSafeAreaLeave";
|
|
20
21
|
export * from "./shapeStyleDefaults";
|
|
22
|
+
export * from "./sphericalMath";
|
|
21
23
|
export * from "./types";
|
package/src/layerApiTypes.d.ts
CHANGED
|
@@ -46,6 +46,26 @@ export type ShapeStyleOverrides = Readonly<{
|
|
|
46
46
|
export interface ShapeStyle extends ShapeStyleOverrides {
|
|
47
47
|
/** Radius of point features in pixels. Default: 5 */
|
|
48
48
|
readonly pointRadius?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Whether polygon/line edges follow great-circle arcs (geodesics) rather than
|
|
51
|
+
* rhumb lines between the given coordinates.
|
|
52
|
+
*
|
|
53
|
+
* This is a semantic property of the shape — both adapters honour it, but via
|
|
54
|
+
* different mechanisms:
|
|
55
|
+
*
|
|
56
|
+
* - **Google Maps adapter**: passes `geodesic: true` directly to
|
|
57
|
+
* `google.maps.Polygon` / `Polyline` (native support).
|
|
58
|
+
* - **Mapbox adapter**: pre-processes the GeoJSON with `densifyGeodesicFeatures`,
|
|
59
|
+
* inserting great-circle intermediate points before the data reaches the source.
|
|
60
|
+
* Mapbox GL has no native geodesic option; densification is the workaround.
|
|
61
|
+
*
|
|
62
|
+
* Set to `false` for shapes whose edges are intentionally straight (e.g. a
|
|
63
|
+
* boundary defined as a rhumb line, or a small urban polygon where the
|
|
64
|
+
* difference is sub-pixel).
|
|
65
|
+
*
|
|
66
|
+
* Default: `true`
|
|
67
|
+
*/
|
|
68
|
+
readonly geodesic?: boolean;
|
|
49
69
|
/** Visual overrides applied when this shape is hovered */
|
|
50
70
|
readonly hovered?: ShapeStyleOverrides;
|
|
51
71
|
/** Visual overrides applied when this shape is selected */
|
|
@@ -1,23 +1,31 @@
|
|
|
1
|
-
import type { MapTheme } from "./primitiveMapTypes";
|
|
2
1
|
import type { ShapeType } from "./interactionTypes";
|
|
3
2
|
import type { ShapeStyle, ShapeStyleOverrides } from "./layerApiTypes";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
3
|
+
import type { MapTheme } from "./primitiveMapTypes";
|
|
4
|
+
/**
|
|
5
|
+
* Per-shape-type visual defaults injected by the map consumer via `createMapComponent`.
|
|
6
|
+
* Adapters receive these via `LayerPort.setShapeStyleDefaults` rather than importing
|
|
7
|
+
* hardcoded values directly.
|
|
8
|
+
*
|
|
9
|
+
* Each shape type exposes the required numeric fields adapters use as fallbacks so
|
|
10
|
+
* TypeScript can infer non-nullable numbers without extra guards.
|
|
11
|
+
*/
|
|
12
|
+
export type ShapeStyleDefaults = Readonly<{
|
|
13
|
+
polygon: Readonly<{
|
|
14
|
+
fillOpacity: number;
|
|
15
|
+
strokeWidth: number;
|
|
16
|
+
strokeOpacity: number;
|
|
17
|
+
}>;
|
|
18
|
+
line: Readonly<{
|
|
19
|
+
strokeWidth: number;
|
|
20
|
+
strokeOpacity: number;
|
|
21
|
+
}>;
|
|
22
|
+
point: Readonly<{
|
|
23
|
+
fillOpacity: number;
|
|
24
|
+
strokeWidth: number;
|
|
25
|
+
strokeOpacity: number;
|
|
26
|
+
pointRadius: number;
|
|
27
|
+
}>;
|
|
28
|
+
}>;
|
|
21
29
|
/**
|
|
22
30
|
* Resolve the visual style for a hovered shape.
|
|
23
31
|
*
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GeoJsonPosition } from "@trackunit/geo-json-utils";
|
|
2
|
+
export declare const toRad: (deg: number) => number;
|
|
3
|
+
export declare const toDeg: (rad: number) => number;
|
|
4
|
+
/**
|
|
5
|
+
* Haversine angular distance in radians between two lng/lat points.
|
|
6
|
+
* Returns a value in [0, π].
|
|
7
|
+
*/
|
|
8
|
+
export declare const angularDistance: (lng1: number, lat1: number, lng2: number, lat2: number) => number;
|
|
9
|
+
/**
|
|
10
|
+
* Spherical-interpolation intermediate point at fraction `f` (0 = start, 1 = end)
|
|
11
|
+
* along the great-circle arc defined by `delta` (pre-computed angular distance).
|
|
12
|
+
*
|
|
13
|
+
* Formula: http://www.movable-type.co.uk/scripts/latlong.html#intermediate-point
|
|
14
|
+
*
|
|
15
|
+
* If `start` carries an altitude the result linearly interpolates it; otherwise
|
|
16
|
+
* the result is 2-D.
|
|
17
|
+
*/
|
|
18
|
+
export declare const intermediatePoint: (start: GeoJsonPosition, end: GeoJsonPosition, f: number, delta: number) => GeoJsonPosition;
|