@geolibre/core 3.6.0 → 3.7.0
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/dist/ellipsoids-BdHOMRmm.d.mts +448 -0
- package/dist/ellipsoids-BdHOMRmm.d.mts.map +1 -0
- package/dist/ellipsoids-DlTLddDu.mjs +496 -0
- package/dist/ellipsoids-DlTLddDu.mjs.map +1 -0
- package/dist/geodesic.d.mts +70 -0
- package/dist/geodesic.d.mts.map +1 -0
- package/dist/geodesic.mjs +155 -0
- package/dist/geodesic.mjs.map +1 -0
- package/dist/index.d.mts +535 -14
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1481 -1942
- package/dist/index.mjs.map +1 -1
- package/dist/{project-diff-BJDslj2U.d.mts → project-diff-B6AfZk8o.d.mts} +95 -449
- package/dist/project-diff-B6AfZk8o.d.mts.map +1 -0
- package/dist/{project-diff-CSG1AgSK.mjs → project-diff-DPGO-zCS.mjs} +1593 -498
- package/dist/project-diff-DPGO-zCS.mjs.map +1 -0
- package/dist/worker-safe.d.mts +2 -1
- package/dist/worker-safe.mjs +3 -1
- package/package.json +9 -4
- package/dist/project-diff-BJDslj2U.d.mts.map +0 -1
- package/dist/project-diff-CSG1AgSK.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { $ as GEOCODING_PROVIDERS, $i as
|
|
1
|
+
import { $ as GEOCODING_PROVIDERS, $a as MAX_PROCESSING_HISTORY, $i as createEqualIntervalBreaks, $n as groupMemberIndices, $r as mapZoomStepOutputs, $t as CESIUM_BING_AERIAL_ASSET_ID, A as normalizeSecondaryMapViews, Aa as REGIONAL_BASEMAP_GROUPS, Ai as substituteExpressionVariables, An as createStyleLibraryEntryId, Ar as isValidMetadataDate, At as selectedGeocodingLocator, B as storyMapHasContent, Ba as DEFAULT_DASHBOARD_COLUMNS, Bi as getMapboxAccessToken, Bn as normalizePrintLayoutConfig, Br as effectiveVectorRules, Bt as PROJECT_CREDENTIAL_FIELDS, C as normalizeModelGraph, Ca as CESIUM_BASEMAPS, Ci as compileFeatureExpression, Cn as isGeoArrowBatch, Cr as reapplyLayerJoins, Ct as normalizeArcGISLocatorUrl, D as normalizeProcessingHistory, Da as sameCesiumImagery, Di as isStyleSpecColor, Dn as STYLE_LIBRARY_BUNDLE_TYPE, Dr as STAC_PROCESSING_EXTENSION, Dt as parseArcGISSuggestions, E as normalizePrimaryRenderer, Ea as normalizeCesiumBasemap, Ei as inferFieldTypes, En as RAMP_STYLE_KEYS, Er as stripVirtualFieldColumns, Et as parseArcGISLocatorInfo, F as projectFromStore, Fa as isRegionalBasemapSentinel, Fi as invertSelection, Fn as sanitizeLayerStylePatch, Fr as parseMetadataKeywords, Ft as DEFAULT_STORY_CHAPTER_DURATION_SECONDS, G as DEFAULT_REVERSE_GEOCODE_ENDPOINT, Ga as DEFAULT_PROJECT_PREFERENCES, Gi as batchDecodePolylines, Gn as scrubCommentsForRemovedLayers, Gr as generatorCircleRadiusValue, Gt as redactCredentials, H as CARTOCIUDAD_PUBLIC_HOST, Ha as DEFAULT_LEGEND_CONFIG, Hi as getProtomapsStyleUrl, Hn as scrubPrintLayoutForLayers, Hr as extrusionColorValue, Ht as isCredentialFieldName, I as resolveMapGrid, Ia as BLANK_BASEMAP, Ii as getArcgisApiKey, In as serializeStyleLibrary, Ir as stacAssetMediaType, It as MAX_STORY_CHAPTER_DURATION_SECONDS, J as GEOCODE_LAT_KEY, Ja as DUCKDB_VECTOR_ROUTE_BYTES, Ji as encodePolyline, Jn as DEFAULT_LAYER_GROUP_OPACITY, Jr as heatmapRampColors, Jt as setRegistryPublishableSettings, K as GEOCODE_ATTRIBUTE_PREFIX, Ka as DEFAULT_STORY_MAP, Ki as decodePolyline, Kn as scrubLegendForRemovedLayers, Kr as generatorSizeRange, Kt as redactProjectCredentials, L as serializeProject, La as BLEND_MODES, Li as getBuildEnvironment, Ln as DEFAULT_PRINT_LAYOUT, Lr as validateLayerDescriptiveMetadata, Lt as MIN_STORY_CHAPTER_DURATION_SECONDS, M as normalizeWidgets, Ma as gateRegionalBasemap, Mi as SELECTION_MODES, Mn as isStyleLibraryTargetLayer, Mr as isValidMetadataUrl, Mt as setGeocodingTokenProvider, N as parseProject, Na as getRegionalBasemapById, Ni as applySelectionMode, Nn as normalizeStyleLibraryEntries, Nr as metadataDateToRfc3339, Nt as shouldThrottle, O as normalizeProjectComments, Oa as CHINA_BASEMAPS, Oi as matchFeaturesByExpression, On as STYLE_LIBRARY_BUNDLE_VERSION, Or as STAC_VERSION, Ot as resolveGeocoderConfig, P as portableWmsTileUrl, Pa as getRegionalBasemapByStyleUrl, Pi as featureSelectionId, Pn as parseStyleLibrary, Pr as normalizeLayerDescriptiveMetadata, Pt as unmatchedGeocodeFeature, Q as GEOCODE_STATUS_KEY, Qa as MAX_MAP_GRID_DIM, Qi as VECTOR_COLOR_RAMPS, Qn as effectiveLayerRenderState, Qr as lineWidthValue, Qt as resolveHeaderReferences, R as serializeProjectWithLayerCache, Ra as DEFAULT_BASEMAP, Ri as getCesiumIonToken, Rn as createDefaultPrintLayout, Rr as MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0, Rt as normalizeStoryChapterDurationSeconds, S as normalizeMapViewState, Sa as normalizeArcgisWkid, Si as EXPRESSION_FUNCTION_CATEGORIES, Sn as geoArrowToFeatureCollection, Sr as layerJoinKey, St as nominatimReverseResultToDisplay, T as normalizeObiaWorkbench, Ta as basemapToCesiumImagery, Ti as formatExpressionPreviewValue, Tn as BUILT_IN_STYLE_PRESETS, Tr as applyLayerVirtualFields, Tt as normalizeGeocodingProviderId, U as DEFAULT_FORWARD_GEOCODE_ENDPOINT, Ua as DEFAULT_MAP_GRID_LAYOUT, Ui as getRuntimeEnvironment, Un as scrubPrintLayoutForRemovedLayers, Ur as extrusionFloorHeightValue, Ut as isCredentialUrlParam, V as CARTOCIUDAD_MIN_INTERVAL_MS, Va as DEFAULT_LAYER_STYLE, Vi as getProtomapsApiKey, Vn as printLayoutConfigsEqual, Vr as extrusionBaseValue, Vt as PUBLISHABLE_PLUGIN_SETTINGS, W as DEFAULT_GEOCODING_PROVIDER_ID, Wa as DEFAULT_PRIMARY_RENDERER, Wi as getSpatialExtensionPath, Wn as removedLayerIdSet, Wr as extrusionHeightValue, Wt as redactConfigurationCredentials, X as GEOCODE_PROVIDER_KEY, Xa as LAYER_TYPES, Xi as polylineStrToGeoJSON, Xn as buildLayerPanelUnits, Xr as isStrokeDashPattern, Xt as hasHeaderReferences, Y as GEOCODE_LON_KEY, Ya as LARGE_VECTOR_FEATURE_THRESHOLD, Yi as geoJSONToPolylineStr, Yn as applyGroupEffects, Yr as isHexColor, Yt as allowsCredentialHeaders, Z as GEOCODE_SCORE_KEY, Za as MAX_DASHBOARD_COLUMNS, Zi as unescapePolyline, Zn as buildLayerTree, Zr as isVectorColorExpression, Zt as isHeaderReferenceOnly, _ as normalizeBookmarkGroups, _a as getMapCrsPreset, _i as vectorFillColorValue, _n as czmlSource, _r as resolveRolePrivileges, _t as geocoderSupportsSuggest, a as DEFAULT_PROJECT_NAME, aa as interpolateRampColors, ai as readSimpleStyleNumberLookup, an as cesiumIonAssetKind, ao as SQL_QUERY_SOURCE_KIND, ar as reorderLayerGroupInPanel, at as arcgisLocatorInputField, b as normalizeLegendConfig, ba as parseMapCrsEpsgCode, bi as vectorOutlineColorValue, bn as computeGeoArrowBBox, br as cascadeLayerJoinRefresh, bt as nextDelayMs, c as createDefaultMapView, ca as parseHexColorList, ci as ruleBasedVisibilityFilter, cn as isCesiumOnlyLayer, co as isDuckDBQueryLayer, cr as ROLE_PRIVILEGES, ct as csvRowsToGeocodeRequests, d as createProjectTemplate, da as horizontalBbox, di as strokeDashPresetName, dn as cesiumKmlSource, dr as hasAppPrivilege, dt as geocodeMatchToFeature, ea as createGraduatedClassBreaks, ei as mercatorMetersPerPixelAtZoom0, en as CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID, eo as MIN_DASHBOARD_COLUMNS, er as layerGroupDepth, et as GeocoderServiceError, f as detachProjectCopy, fa as transformGeojsonElevation, fi as strokeDashValue, fn as createCesiumKmlLayer, fr as inferLayerCapabilities, ft as geocodeReverse, g as normalizeBlankBackgroundColor, ga as deriveTileMatrixSet, gi as vectorColorExpression, gn as createCzmlLayer, gr as resolveLayerCapabilities, gt as geocoderSupportsReverse, h as isSessionOnlyLayer, ha as adaptBboxTemplateToCrs, hi as vectorCircleColorValue, hn as CZML_SOURCE_KIND, hr as normalizeLayerCapabilities, ht as geocoderNeedsApiKey, i as previewValue, ia as interpolateHexColor, ii as proportionalSizeRange, in as cesiumIonAssetId, io as PROTOMAPS_BASEMAPS, ir as normalizeGroupContiguity, it as PUBLIC_GEOCODE_ROW_CAP, j as normalizeStoryMap, ja as REGIONAL_BASEMAP_SENTINEL_PREFIX, ji as validateMapExpression, jn as extractStyleLibraryStyle, jr as isValidMetadataEmail, jt as setGeocodingFetch, k as normalizeProjectInteraction, ka as REGIONAL_BASEMAPS, ki as removeTrailingJsonCommas, kn as SYMBOL_STYLE_KEYS, kr as buildLayerStacItem, kt as rowCap, l as createEmptyProject, la as rgbToHex, li as simpleStyleNumberValue, ln as parseCesiumIonAssetId, lo as shouldUseTiledRendering, lr as appPrivilegeReason, lt as fetchArcGISLocatorInfo, m as isAllowedPluginManifestUrl, ma as MAP_CRS_PRESETS, mi as strokeOpacityValue, mn as CZML_QUICK_PICKS, mr as normalizeAppPrivileges, mt as geocoderMinIntervalMs, n as diffProjects, na as getVectorColorRamp, ni as parseJsonExpression, nn as CESIUM_ION_SOURCE_KIND, no as OPENFREEMAP_BASEMAPS, nr as layerGroupSortability, nt as NOMINATIM_MIN_INTERVAL_MS, o as MAX_PROJECT_BOOKMARKS, oa as normalizeHexColor, oi as readsSimpleStyleProperties, on as createCesiumIonLayer, oo as STROKE_DASH_PRESETS, or as sortLayerGroupInPanel, ot as buildForwardGeocodeUrl, p as hydrateProjectLayer, pa as MAP_CRS_CHANGE_EVENT, pi as strokeOpacityPaintValue, pn as isCesiumKmlLayer, pr as intersectPrivileges, pt as geocodeSuggest, q as GEOCODE_DISPLAY_NAME_KEY, qa as DUCKDB_VECTOR_FEATURE_WARN_COUNT, qi as decodePolylineDetailed, qn as scrubWidgetsForRemovedLayers, qr as hasSimpleStyleProperties, qt as redactUrlCredentials, r as jsonValuesEqual, ra as interpolateColors, ri as proportionalRadiusExpression, rn as CESIUM_OSM_BUILDINGS_ASSET_ID, ro as PROJECT_VERSION, rr as layerPanelGroupHeaders, rt as NOMINATIM_PUBLIC_HOST, s as applyProjectToStore, sa as parseHexColor, si as ruleBasedColorExpression, sn as isCesiumIonLayer, so as detectNonGeographicCoordinates, sr as ALL_APP_PRIVILEGES, st as buildReverseGeocodeUrl, t as diffFeatureCollections, ta as createQuantileBreaks, ti as metersWidthExpression, tn as CESIUM_ION_QUICK_PICKS, to as NETCDF_IMAGE_SOURCE_KIND, tr as layerGroupMoveability, tt as GeocoderUnsupportedError, u as createProjectLayerSerializationCache, ua as geojsonHasZCoordinates, ui as strokeBlurValue, un as CESIUM_KML_SOURCE_KIND, uo as styleValue, ur as createDefaultAppCapabilities, ut as geocodeForward, v as normalizeBookmarks, va as mapCrsPreferencePatch, vi as vectorFillOpacityValue, vn as isCzmlLayer, vr as applyJoinsToLayer, vt as getGeocoderConfig, w as normalizeModels, wa as availableCesiumBasemap, wi as evaluateMapExpression, wn as withRasterSourceDefaults, wr as stripJoinFields, wt as normalizeGeocodingLocator, x as normalizeMapLayout, xa as ARCGIS_PROJECTION_PRESETS, xi as vectorStrokeWidthValue, xn as featureCollectionToGeoArrow, xr as collectTransitiveJoinSourceIds, xt as nominatimResultToFeature, y as normalizeDashboardColumns, ya as normalizeMapCrs, yi as vectorLineColorValue, yn as parseCzml, yr as applyLayerJoins, yt as getGeocodingProvider, z as serializeProjectWithLayerCacheAsync, za as DEFAULT_BLEND_MODE, zi as getGoogleMapsApiKey, zn as isDefaultPrintLayout, zr as circleRadiusValue, zt as MAX_REDACT_DEPTH } from "./project-diff-DPGO-zCS.mjs";
|
|
2
|
+
import { _ as getPlanetaryBasemapByStyleUrl, a as PLANETARY_BASEMAP_GROUPS, c as bodyLengthToEarth, d as getActiveBodyRadiusRatio, f as getActiveEllipsoid, g as getPlanetaryBasemapById, h as getEllipsoid, i as PLANETARY_BASEMAPS, l as earthAreaToBody, m as getActiveSemiMajorAxisMeters, n as EARTH_MEAN_RADIUS_METERS, o as PLANETARY_BASEMAP_SENTINEL_PREFIX, p as getActiveMeanRadiusMeters, r as ELLIPSOIDS, s as PLANET_SWITCHER_OPTIONS, t as DEFAULT_ELLIPSOID_ID, u as earthLengthToBody, v as meanRadiusMeters, y as setActiveEllipsoidId } from "./ellipsoids-DlTLddDu.mjs";
|
|
3
|
+
import { centralAngle, geodesicDistance, geodesicLength, geodesicRadiusBounds, geodesicRingArea, isLonLat, latLonBoxArea, withinGeodesicDistance } from "./geodesic.mjs";
|
|
2
4
|
import { Color, featureFilter, latest, normalizePropertyExpression } from "@maplibre/maplibre-gl-style-spec";
|
|
3
5
|
import { create } from "zustand";
|
|
4
6
|
import { temporal } from "zundo";
|
|
@@ -461,534 +463,6 @@ const PHOTO_PROPERTY = "photo";
|
|
|
461
463
|
* format a browser cannot display at native size (TIFF/HEIC).
|
|
462
464
|
*/
|
|
463
465
|
const PHOTO_FULL_PROPERTY = "photo_full";
|
|
464
|
-
// src/color-ramp.ts
|
|
465
|
-
/** The built-in color ramps offered by the style panels. */
|
|
466
|
-
const VECTOR_COLOR_RAMPS = [
|
|
467
|
-
{
|
|
468
|
-
value: "viridis",
|
|
469
|
-
label: "Viridis",
|
|
470
|
-
colors: [
|
|
471
|
-
"#440154",
|
|
472
|
-
"#31688e",
|
|
473
|
-
"#35b779",
|
|
474
|
-
"#fde725"
|
|
475
|
-
]
|
|
476
|
-
},
|
|
477
|
-
{
|
|
478
|
-
value: "plasma",
|
|
479
|
-
label: "Plasma",
|
|
480
|
-
colors: [
|
|
481
|
-
"#0d0887",
|
|
482
|
-
"#9c179e",
|
|
483
|
-
"#ed7953",
|
|
484
|
-
"#f0f921"
|
|
485
|
-
]
|
|
486
|
-
},
|
|
487
|
-
{
|
|
488
|
-
value: "inferno",
|
|
489
|
-
label: "Inferno",
|
|
490
|
-
colors: [
|
|
491
|
-
"#000004",
|
|
492
|
-
"#781c6d",
|
|
493
|
-
"#ed6925",
|
|
494
|
-
"#fcffa4"
|
|
495
|
-
]
|
|
496
|
-
},
|
|
497
|
-
{
|
|
498
|
-
value: "magma",
|
|
499
|
-
label: "Magma",
|
|
500
|
-
colors: [
|
|
501
|
-
"#000004",
|
|
502
|
-
"#721f81",
|
|
503
|
-
"#f1605d",
|
|
504
|
-
"#fcfdbf"
|
|
505
|
-
]
|
|
506
|
-
},
|
|
507
|
-
{
|
|
508
|
-
value: "cividis",
|
|
509
|
-
label: "Cividis",
|
|
510
|
-
colors: [
|
|
511
|
-
"#00204d",
|
|
512
|
-
"#575d6d",
|
|
513
|
-
"#a59c74",
|
|
514
|
-
"#ffea46"
|
|
515
|
-
]
|
|
516
|
-
},
|
|
517
|
-
{
|
|
518
|
-
value: "turbo",
|
|
519
|
-
label: "Turbo",
|
|
520
|
-
colors: [
|
|
521
|
-
"#30123b",
|
|
522
|
-
"#4777ef",
|
|
523
|
-
"#1ccfd0",
|
|
524
|
-
"#b9e642",
|
|
525
|
-
"#fb8022",
|
|
526
|
-
"#7a0403"
|
|
527
|
-
]
|
|
528
|
-
},
|
|
529
|
-
{
|
|
530
|
-
value: "spectral",
|
|
531
|
-
label: "Spectral",
|
|
532
|
-
colors: [
|
|
533
|
-
"#9e0142",
|
|
534
|
-
"#f46d43",
|
|
535
|
-
"#ffffbf",
|
|
536
|
-
"#66c2a5",
|
|
537
|
-
"#5e4fa2"
|
|
538
|
-
]
|
|
539
|
-
},
|
|
540
|
-
{
|
|
541
|
-
value: "blues",
|
|
542
|
-
label: "Blues",
|
|
543
|
-
colors: [
|
|
544
|
-
"#eff6ff",
|
|
545
|
-
"#93c5fd",
|
|
546
|
-
"#2563eb",
|
|
547
|
-
"#1e3a8a"
|
|
548
|
-
]
|
|
549
|
-
},
|
|
550
|
-
{
|
|
551
|
-
value: "greens",
|
|
552
|
-
label: "Greens",
|
|
553
|
-
colors: [
|
|
554
|
-
"#f0fdf4",
|
|
555
|
-
"#86efac",
|
|
556
|
-
"#16a34a",
|
|
557
|
-
"#14532d"
|
|
558
|
-
]
|
|
559
|
-
},
|
|
560
|
-
{
|
|
561
|
-
value: "oranges",
|
|
562
|
-
label: "Oranges",
|
|
563
|
-
colors: [
|
|
564
|
-
"#fff7ed",
|
|
565
|
-
"#fdba74",
|
|
566
|
-
"#f97316",
|
|
567
|
-
"#7c2d12"
|
|
568
|
-
]
|
|
569
|
-
},
|
|
570
|
-
{
|
|
571
|
-
value: "reds",
|
|
572
|
-
label: "Reds",
|
|
573
|
-
colors: [
|
|
574
|
-
"#fff5f0",
|
|
575
|
-
"#fcae91",
|
|
576
|
-
"#fb6a4a",
|
|
577
|
-
"#cb181d",
|
|
578
|
-
"#67000d"
|
|
579
|
-
]
|
|
580
|
-
},
|
|
581
|
-
{
|
|
582
|
-
value: "purples",
|
|
583
|
-
label: "Purples",
|
|
584
|
-
colors: [
|
|
585
|
-
"#fcfbfd",
|
|
586
|
-
"#bcbddc",
|
|
587
|
-
"#807dba",
|
|
588
|
-
"#54278f",
|
|
589
|
-
"#3f007d"
|
|
590
|
-
]
|
|
591
|
-
},
|
|
592
|
-
{
|
|
593
|
-
value: "terrain",
|
|
594
|
-
label: "Terrain",
|
|
595
|
-
colors: [
|
|
596
|
-
"#333399",
|
|
597
|
-
"#21bcb3",
|
|
598
|
-
"#79d05a",
|
|
599
|
-
"#e8e85a",
|
|
600
|
-
"#a87b54",
|
|
601
|
-
"#ffffff"
|
|
602
|
-
]
|
|
603
|
-
},
|
|
604
|
-
{
|
|
605
|
-
value: "rdylgn",
|
|
606
|
-
label: "Red-Yellow-Green",
|
|
607
|
-
colors: [
|
|
608
|
-
"#a50026",
|
|
609
|
-
"#f46d43",
|
|
610
|
-
"#ffffbf",
|
|
611
|
-
"#66bd63",
|
|
612
|
-
"#006837"
|
|
613
|
-
]
|
|
614
|
-
},
|
|
615
|
-
{
|
|
616
|
-
value: "rdylbu",
|
|
617
|
-
label: "Red-Yellow-Blue",
|
|
618
|
-
colors: [
|
|
619
|
-
"#a50026",
|
|
620
|
-
"#f46d43",
|
|
621
|
-
"#ffffbf",
|
|
622
|
-
"#74add1",
|
|
623
|
-
"#313695"
|
|
624
|
-
]
|
|
625
|
-
},
|
|
626
|
-
{
|
|
627
|
-
value: "rdbu",
|
|
628
|
-
label: "Red-Blue",
|
|
629
|
-
colors: [
|
|
630
|
-
"#b2182b",
|
|
631
|
-
"#ef8a62",
|
|
632
|
-
"#f7f7f7",
|
|
633
|
-
"#67a9cf",
|
|
634
|
-
"#2166ac"
|
|
635
|
-
]
|
|
636
|
-
},
|
|
637
|
-
{
|
|
638
|
-
value: "coolwarm",
|
|
639
|
-
label: "Cool-Warm",
|
|
640
|
-
colors: [
|
|
641
|
-
"#3b4cc0",
|
|
642
|
-
"#7b9ff9",
|
|
643
|
-
"#dddcdc",
|
|
644
|
-
"#f49a7b",
|
|
645
|
-
"#b40426"
|
|
646
|
-
]
|
|
647
|
-
},
|
|
648
|
-
{
|
|
649
|
-
value: "jet",
|
|
650
|
-
label: "Jet",
|
|
651
|
-
colors: [
|
|
652
|
-
"#000080",
|
|
653
|
-
"#0000ff",
|
|
654
|
-
"#00ffff",
|
|
655
|
-
"#ffff00",
|
|
656
|
-
"#ff0000",
|
|
657
|
-
"#800000"
|
|
658
|
-
]
|
|
659
|
-
},
|
|
660
|
-
{
|
|
661
|
-
value: "greys",
|
|
662
|
-
label: "Greys",
|
|
663
|
-
colors: [
|
|
664
|
-
"#ffffff",
|
|
665
|
-
"#bdbdbd",
|
|
666
|
-
"#636363",
|
|
667
|
-
"#000000"
|
|
668
|
-
]
|
|
669
|
-
},
|
|
670
|
-
{
|
|
671
|
-
value: "gray",
|
|
672
|
-
label: "Grayscale",
|
|
673
|
-
colors: ["#000000", "#ffffff"]
|
|
674
|
-
}
|
|
675
|
-
];
|
|
676
|
-
/**
|
|
677
|
-
* Resolves a ramp definition by name, falling back to the first ramp when the
|
|
678
|
-
* name is unknown.
|
|
679
|
-
*
|
|
680
|
-
* @param value - The ramp `value` (e.g. "viridis").
|
|
681
|
-
* @returns The matching ramp, or the first ramp.
|
|
682
|
-
*/
|
|
683
|
-
function getVectorColorRamp(value) {
|
|
684
|
-
return VECTOR_COLOR_RAMPS.find((colorRamp) => colorRamp.value === value) ?? VECTOR_COLOR_RAMPS[0];
|
|
685
|
-
}
|
|
686
|
-
/**
|
|
687
|
-
* Samples a ramp into `count` evenly spaced hex colors by linearly
|
|
688
|
-
* interpolating between the ramp's anchor colors.
|
|
689
|
-
*
|
|
690
|
-
* @param colorRamp - The ramp `value`.
|
|
691
|
-
* @param count - Number of colors to produce.
|
|
692
|
-
* @returns An array of `count` hex colors (a single end color when count <= 1).
|
|
693
|
-
*/
|
|
694
|
-
function interpolateRampColors(colorRamp, count) {
|
|
695
|
-
return interpolateColors(getVectorColorRamp(colorRamp).colors, count);
|
|
696
|
-
}
|
|
697
|
-
/**
|
|
698
|
-
* Samples an explicit list of anchor colors into `count` evenly spaced hex
|
|
699
|
-
* colors by linear interpolation. Backs both the named ramps and user-defined
|
|
700
|
-
* custom ramps, so a list of hex codes is sampled identically to a built-in.
|
|
701
|
-
*
|
|
702
|
-
* @param colors - The anchor colors (at least one).
|
|
703
|
-
* @param count - Number of colors to produce.
|
|
704
|
-
* @returns An array of `count` hex colors (a single end color when count <= 1).
|
|
705
|
-
*/
|
|
706
|
-
function interpolateColors(colors, count) {
|
|
707
|
-
const anchors = colors.length > 0 ? colors : ["#000000"];
|
|
708
|
-
if (count <= 0) return [];
|
|
709
|
-
if (count === 1) return [anchors[anchors.length - 1]];
|
|
710
|
-
if (anchors.length === 1) return Array.from({ length: count }, () => anchors[0]);
|
|
711
|
-
return Array.from({ length: count }, (_, index) => {
|
|
712
|
-
const scaled = index / (count - 1) * (anchors.length - 1);
|
|
713
|
-
const lowerIndex = Math.floor(scaled);
|
|
714
|
-
const upperIndex = Math.min(anchors.length - 1, Math.ceil(scaled));
|
|
715
|
-
const ratio = scaled - lowerIndex;
|
|
716
|
-
return interpolateHexColor(anchors[lowerIndex], anchors[upperIndex], ratio);
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* Normalizes a single user-entered color token into a canonical `#rrggbb`
|
|
721
|
-
* lowercase hex string, or null when it is not a valid 3- or 6-digit hex
|
|
722
|
-
* color. Accepts values with or without a leading `#` and expands shorthand
|
|
723
|
-
* (`#abc` → `#aabbcc`).
|
|
724
|
-
*
|
|
725
|
-
* @param token - A raw color token (e.g. "FF0000", "#f00", " #AaBbCc ").
|
|
726
|
-
* @returns The canonical `#rrggbb` color, or null when invalid.
|
|
727
|
-
*/
|
|
728
|
-
function normalizeHexColor(token) {
|
|
729
|
-
let value = token.trim().toLowerCase();
|
|
730
|
-
if (value === "") return null;
|
|
731
|
-
if (!value.startsWith("#")) value = `#${value}`;
|
|
732
|
-
if (/^#[0-9a-f]{3}$/.test(value)) value = `#${value.slice(1).split("").map((channel) => channel + channel).join("")}`;
|
|
733
|
-
return /^#[0-9a-f]{6}$/.test(value) ? value : null;
|
|
734
|
-
}
|
|
735
|
-
/**
|
|
736
|
-
* Parses a free-text list of hex color codes into canonical `#rrggbb` colors,
|
|
737
|
-
* for the custom color-ramp inputs. Tokens may be separated by commas,
|
|
738
|
-
* semicolons, or any whitespace (including newlines); invalid tokens are
|
|
739
|
-
* dropped so a partial paste still yields the colors it can.
|
|
740
|
-
*
|
|
741
|
-
* @param input - The raw text the user entered.
|
|
742
|
-
* @returns The valid, normalized colors in input order.
|
|
743
|
-
*/
|
|
744
|
-
function parseHexColorList(input) {
|
|
745
|
-
return input.split(/[\s,;]+/).map(normalizeHexColor).filter((color) => color !== null);
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Linearly interpolates between two hex colors.
|
|
749
|
-
*
|
|
750
|
-
* @param from - Start hex color (e.g. "#440154").
|
|
751
|
-
* @param to - End hex color.
|
|
752
|
-
* @param ratio - Blend factor in [0, 1].
|
|
753
|
-
* @returns The interpolated hex color.
|
|
754
|
-
*/
|
|
755
|
-
function interpolateHexColor(from, to, ratio) {
|
|
756
|
-
const start = parseHexColor(from);
|
|
757
|
-
const end = parseHexColor(to);
|
|
758
|
-
return rgbToHex({
|
|
759
|
-
r: Math.round(start.r + (end.r - start.r) * ratio),
|
|
760
|
-
g: Math.round(start.g + (end.g - start.g) * ratio),
|
|
761
|
-
b: Math.round(start.b + (end.b - start.b) * ratio)
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
/**
|
|
765
|
-
* Parses a `#rrggbb` hex color into its RGB channels.
|
|
766
|
-
*
|
|
767
|
-
* @param value - A `#rrggbb` hex color.
|
|
768
|
-
* @returns The red, green, and blue channels (0-255).
|
|
769
|
-
*/
|
|
770
|
-
function parseHexColor(value) {
|
|
771
|
-
const numeric = Number.parseInt(value.slice(1), 16);
|
|
772
|
-
return {
|
|
773
|
-
r: numeric >> 16 & 255,
|
|
774
|
-
g: numeric >> 8 & 255,
|
|
775
|
-
b: numeric & 255
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
/**
|
|
779
|
-
* Formats RGB channels as a `#rrggbb` hex color.
|
|
780
|
-
*
|
|
781
|
-
* @param color - The red, green, and blue channels (0-255).
|
|
782
|
-
* @returns A `#rrggbb` hex color.
|
|
783
|
-
*/
|
|
784
|
-
function rgbToHex(color) {
|
|
785
|
-
return `#${[
|
|
786
|
-
color.r,
|
|
787
|
-
color.g,
|
|
788
|
-
color.b
|
|
789
|
-
].map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
|
|
790
|
-
}
|
|
791
|
-
/**
|
|
792
|
-
* Builds `count` evenly spaced break values across [min, max].
|
|
793
|
-
*
|
|
794
|
-
* @param min - The minimum value.
|
|
795
|
-
* @param max - The maximum value.
|
|
796
|
-
* @param count - Number of breaks to produce.
|
|
797
|
-
* @returns The break values (min..max inclusive when count > 1).
|
|
798
|
-
*/
|
|
799
|
-
function createEqualIntervalBreaks(min, max, count) {
|
|
800
|
-
return Array.from({ length: count }, (_, index) => {
|
|
801
|
-
const ratio = count === 1 ? 0 : index / (count - 1);
|
|
802
|
-
return min + (max - min) * ratio;
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
/**
|
|
806
|
-
* Builds `count` quantile break values from a sample of numeric values.
|
|
807
|
-
*
|
|
808
|
-
* These are ramp *edges* spanning the whole sample (the first break is the
|
|
809
|
-
* minimum, the last the maximum), matching {@link createEqualIntervalBreaks}.
|
|
810
|
-
* Graduated vector classes want class *lower bounds* instead; use
|
|
811
|
-
* {@link createGraduatedClassBreaks}.
|
|
812
|
-
*
|
|
813
|
-
* @param values - The sample values.
|
|
814
|
-
* @param count - Number of breaks to produce.
|
|
815
|
-
* @returns The quantile break values.
|
|
816
|
-
*/
|
|
817
|
-
function createQuantileBreaks(values, count) {
|
|
818
|
-
if (count <= 0) return [];
|
|
819
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
820
|
-
if (sorted.length === 0) return [];
|
|
821
|
-
return Array.from({ length: count }, (_, index) => {
|
|
822
|
-
const position = count === 1 ? 0 : index / (count - 1) * (sorted.length - 1);
|
|
823
|
-
return quantileAtPosition(sorted, position);
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
/** Linearly interpolated order statistic at a fractional index into `sorted`. */
|
|
827
|
-
function quantileAtPosition(sorted, position) {
|
|
828
|
-
const lowerIndex = Math.floor(position);
|
|
829
|
-
const upperIndex = Math.min(sorted.length - 1, Math.ceil(position));
|
|
830
|
-
const ratio = position - lowerIndex;
|
|
831
|
-
return sorted[lowerIndex] + (sorted[upperIndex] - sorted[lowerIndex]) * ratio;
|
|
832
|
-
}
|
|
833
|
-
/**
|
|
834
|
-
* Builds the class *lower bounds* for a graduated vector renderer.
|
|
835
|
-
*
|
|
836
|
-
* A graduated renderer's stops are lower bounds, not ramp edges: with N stops
|
|
837
|
-
* sorted ascending, class `i` covers `[stops[i], stops[i + 1])` and the last
|
|
838
|
-
* class is open-ended above. The legend (`≥ value`) and the QML/SLD exporters
|
|
839
|
-
* already read them that way, so the breaks must follow the same convention:
|
|
840
|
-
* `count` classes yield `count` breaks whose first entry is the sample minimum
|
|
841
|
-
* and whose last entry opens the top class. Producing edges instead (ending at
|
|
842
|
-
* the maximum) leaves the top class holding only the single largest feature and
|
|
843
|
-
* puts the interior breaks at the wrong percentiles.
|
|
844
|
-
*
|
|
845
|
-
* The result is strictly ascending: duplicate breaks (a skewed sample can push
|
|
846
|
-
* several quantiles onto the same value) are collapsed, so the caller may get
|
|
847
|
-
* fewer than `count` breaks and must size its colors off `breaks.length`.
|
|
848
|
-
* MapLibre rejects a `step` expression whose inputs are not strictly ascending,
|
|
849
|
-
* so this de-duplication is load-bearing, not cosmetic.
|
|
850
|
-
*
|
|
851
|
-
* Standard deviation and geometric interval derive their breaks from the data,
|
|
852
|
-
* so `count` is a ceiling there: a standard-deviation break that falls outside
|
|
853
|
-
* the sample range is dropped rather than producing an empty class. See
|
|
854
|
-
* {@link standardDeviationClassBreaks} and {@link geometricIntervalClassBreaks}
|
|
855
|
-
* for how each treats its edge cases.
|
|
856
|
-
*
|
|
857
|
-
* @param values - The finite numeric values of the classified property.
|
|
858
|
-
* @param count - Number of classes requested.
|
|
859
|
-
* @param scheme - The classification scheme.
|
|
860
|
-
* @returns Up to `count` strictly ascending class lower bounds.
|
|
861
|
-
*/
|
|
862
|
-
function createGraduatedClassBreaks(values, count, scheme) {
|
|
863
|
-
if (count <= 0 || values.length === 0) return [];
|
|
864
|
-
if (scheme === "natural-breaks") return ascendingUnique(naturalClassBreaks(values, count));
|
|
865
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
866
|
-
if (scheme === "quantile") return ascendingUnique(Array.from({ length: count }, (_, index) => quantileAtPosition(sorted, index / count * (sorted.length - 1))));
|
|
867
|
-
if (scheme === "standard-deviation") return ascendingUnique(standardDeviationClassBreaks(sorted, count));
|
|
868
|
-
if (scheme === "geometric-interval") {
|
|
869
|
-
const breaks = geometricIntervalClassBreaks(sorted, count);
|
|
870
|
-
if (breaks) return ascendingUnique(breaks);
|
|
871
|
-
}
|
|
872
|
-
return ascendingUnique(equalIntervalClassBreaks(sorted, count));
|
|
873
|
-
}
|
|
874
|
-
/** Equal-interval lower bounds over the range of an ascending sample. */
|
|
875
|
-
function equalIntervalClassBreaks(sorted, count) {
|
|
876
|
-
const min = sorted[0];
|
|
877
|
-
const max = sorted[sorted.length - 1];
|
|
878
|
-
return Array.from({ length: count }, (_, index) => min + (max - min) * index / count);
|
|
879
|
-
}
|
|
880
|
-
/**
|
|
881
|
-
* Standard-deviation lower bounds: classes one standard deviation wide,
|
|
882
|
-
* centered on the mean. An even `count` puts a break on the mean (classes
|
|
883
|
-
* `[mean - σ, mean)` and `[mean, mean + σ)` meet there); an odd `count` makes
|
|
884
|
-
* the middle class straddle it (`[mean - σ/2, mean + σ/2)`). The first break is
|
|
885
|
-
* the sample minimum, and interior breaks outside `(min, max]` are dropped, so
|
|
886
|
-
* the open-ended bottom and top classes absorb the tails and no class is empty.
|
|
887
|
-
* A constant sample (σ = 0) yields the minimum alone.
|
|
888
|
-
*
|
|
889
|
-
* Uses the population standard deviation, as QGIS and ArcGIS do.
|
|
890
|
-
*/
|
|
891
|
-
function standardDeviationClassBreaks(sorted, count) {
|
|
892
|
-
const min = sorted[0];
|
|
893
|
-
const max = sorted[sorted.length - 1];
|
|
894
|
-
const mean = sorted.reduce((sum, value) => sum + value, 0) / sorted.length;
|
|
895
|
-
const variance = sorted.reduce((sum, value) => sum + (value - mean) * (value - mean), 0) / sorted.length;
|
|
896
|
-
const deviation = Math.sqrt(variance);
|
|
897
|
-
if (!(deviation > 0)) return [min];
|
|
898
|
-
const interior = [];
|
|
899
|
-
for (let index = 1; index < count; index += 1) {
|
|
900
|
-
const value = mean + (index - count / 2) * deviation;
|
|
901
|
-
if (value > min && value <= max) interior.push(value);
|
|
902
|
-
}
|
|
903
|
-
return [min, ...interior];
|
|
904
|
-
}
|
|
905
|
-
/**
|
|
906
|
-
* Geometric-interval lower bounds: class edges growing by a constant ratio
|
|
907
|
-
* from the smallest positive value to the maximum, so each class spans the
|
|
908
|
-
* same multiple of its lower bound. Suits skewed data spanning orders of
|
|
909
|
-
* magnitude (population, income, concentrations).
|
|
910
|
-
*
|
|
911
|
-
* A geometric series cannot start at or below zero, so it starts at the
|
|
912
|
-
* smallest positive value and the first break stays the sample minimum: zero
|
|
913
|
-
* and negative values all fall into the first class. When no value is
|
|
914
|
-
* positive, the result is `null` and the caller falls back to equal interval.
|
|
915
|
-
*/
|
|
916
|
-
function geometricIntervalClassBreaks(sorted, count) {
|
|
917
|
-
const max = sorted[sorted.length - 1];
|
|
918
|
-
const lowest = sorted.find((value) => value > 0);
|
|
919
|
-
if (lowest === void 0) return null;
|
|
920
|
-
const logLowest = Math.log(lowest);
|
|
921
|
-
const logStep = (Math.log(max) - logLowest) / count;
|
|
922
|
-
return [sorted[0], ...Array.from({ length: count - 1 }, (_, index) => Math.exp(logLowest + logStep * (index + 1)))];
|
|
923
|
-
}
|
|
924
|
-
/** Drops non-finite breaks and collapses repeats, keeping ascending order. */
|
|
925
|
-
function ascendingUnique(breaks) {
|
|
926
|
-
const out = [];
|
|
927
|
-
for (const value of breaks) {
|
|
928
|
-
if (!Number.isFinite(value)) continue;
|
|
929
|
-
if (out.length > 0 && value <= out[out.length - 1]) continue;
|
|
930
|
-
out.push(value);
|
|
931
|
-
}
|
|
932
|
-
return out;
|
|
933
|
-
}
|
|
934
|
-
/** Cap on the Jenks input size; the DP below is roughly O(n^2 * k). */
|
|
935
|
-
const MAX_NATURAL_BREAK_SAMPLES = 1e3;
|
|
936
|
-
/** Evenly thins a sorted array down to at most `maxSamples` entries. */
|
|
937
|
-
function downsampleSortedValues(values, maxSamples) {
|
|
938
|
-
if (values.length <= maxSamples) return values;
|
|
939
|
-
const result = [];
|
|
940
|
-
const step = (values.length - 1) / (maxSamples - 1);
|
|
941
|
-
for (let index = 0; index < maxSamples; index += 1) result.push(values[Math.round(index * step)]);
|
|
942
|
-
return result;
|
|
943
|
-
}
|
|
944
|
-
/**
|
|
945
|
-
* Jenks natural breaks: the class lower bounds that minimize the summed
|
|
946
|
-
* within-class variance. Returns the sample minimum first, then the first value
|
|
947
|
-
* of each subsequent class.
|
|
948
|
-
*/
|
|
949
|
-
function naturalClassBreaks(values, count) {
|
|
950
|
-
const sorted = downsampleSortedValues(Array.from(new Set(values)).sort((a, b) => a - b), MAX_NATURAL_BREAK_SAMPLES);
|
|
951
|
-
if (sorted.length <= count) return sorted;
|
|
952
|
-
const lowerClassLimits = Array.from({ length: sorted.length + 1 }, () => Array(count + 1).fill(0));
|
|
953
|
-
const varianceCombinations = Array.from({ length: sorted.length + 1 }, () => Array(count + 1).fill(Number.POSITIVE_INFINITY));
|
|
954
|
-
for (let classIndex = 1; classIndex <= count; classIndex += 1) {
|
|
955
|
-
lowerClassLimits[1][classIndex] = 1;
|
|
956
|
-
varianceCombinations[1][classIndex] = 0;
|
|
957
|
-
}
|
|
958
|
-
for (let valueIndex = 2; valueIndex <= sorted.length; valueIndex += 1) {
|
|
959
|
-
let sum = 0;
|
|
960
|
-
let sumSquares = 0;
|
|
961
|
-
let weight = 0;
|
|
962
|
-
for (let lowerIndex = 1; lowerIndex <= valueIndex; lowerIndex += 1) {
|
|
963
|
-
const currentIndex = valueIndex - lowerIndex + 1;
|
|
964
|
-
const value = sorted[currentIndex - 1];
|
|
965
|
-
weight += 1;
|
|
966
|
-
sum += value;
|
|
967
|
-
sumSquares += value * value;
|
|
968
|
-
const variance = sumSquares - sum * sum / weight;
|
|
969
|
-
const previousIndex = currentIndex - 1;
|
|
970
|
-
if (previousIndex === 0) continue;
|
|
971
|
-
for (let classIndex = 2; classIndex <= count; classIndex += 1) {
|
|
972
|
-
const candidate = variance + varianceCombinations[previousIndex][classIndex - 1];
|
|
973
|
-
if (varianceCombinations[valueIndex][classIndex] >= candidate) {
|
|
974
|
-
lowerClassLimits[valueIndex][classIndex] = currentIndex;
|
|
975
|
-
varianceCombinations[valueIndex][classIndex] = candidate;
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
lowerClassLimits[valueIndex][1] = 1;
|
|
980
|
-
varianceCombinations[valueIndex][1] = sumSquares - sum * sum / Math.max(1, weight);
|
|
981
|
-
}
|
|
982
|
-
const breaks = Array(count).fill(sorted[0]);
|
|
983
|
-
let valueIndex = sorted.length;
|
|
984
|
-
for (let classIndex = count; classIndex >= 2; classIndex -= 1) {
|
|
985
|
-
const lowerClassLimit = Math.max(1, lowerClassLimits[valueIndex][classIndex]);
|
|
986
|
-
breaks[classIndex - 1] = sorted[lowerClassLimit - 1];
|
|
987
|
-
valueIndex = lowerClassLimit - 1;
|
|
988
|
-
}
|
|
989
|
-
breaks[0] = sorted[0];
|
|
990
|
-
return breaks;
|
|
991
|
-
}
|
|
992
466
|
// src/paths.ts
|
|
993
467
|
const PATH_TRAVERSAL = /(?:^|[/\\])\.\.(?:[/\\]|$)/;
|
|
994
468
|
/**
|
|
@@ -1345,900 +819,119 @@ function requestRoute(endpoint, body, signal) {
|
|
|
1345
819
|
function requestNavigationRoute(endpoint, body, signal) {
|
|
1346
820
|
return postJson(`${stripTrailingSlash(endpoint)}/route`, body, signal);
|
|
1347
821
|
}
|
|
1348
|
-
// src/
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
* Whether a string is a 6-digit hex color (`#rrggbb`), the form the vector
|
|
1355
|
-
* renderer treats as valid. Exported so the symbology exporters can apply the
|
|
1356
|
-
* same validity check the live `match`/`case` expressions use rather than
|
|
1357
|
-
* re-deriving the regex.
|
|
1358
|
-
*/
|
|
1359
|
-
function isHexColor(value) {
|
|
1360
|
-
return typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value.trim());
|
|
1361
|
-
}
|
|
1362
|
-
/** A 3- or 6-digit hex color, as emitted by the simplestyle spec. */
|
|
1363
|
-
function isSimpleStyleColor(value) {
|
|
1364
|
-
return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value.trim());
|
|
1365
|
-
}
|
|
1366
|
-
/**
|
|
1367
|
-
* simplestyle-spec color and numeric property names. Color keys carry CSS hex
|
|
1368
|
-
* colors; numeric keys carry plain numbers. See
|
|
1369
|
-
* https://github.com/mapbox/simplestyle-spec.
|
|
1370
|
-
*/
|
|
1371
|
-
const SIMPLE_STYLE_COLOR_KEYS = [
|
|
1372
|
-
"fill",
|
|
1373
|
-
"stroke",
|
|
1374
|
-
"marker-color"
|
|
1375
|
-
];
|
|
1376
|
-
const SIMPLE_STYLE_NUMBER_KEYS = [
|
|
1377
|
-
"fill-opacity",
|
|
1378
|
-
"stroke-width",
|
|
1379
|
-
"stroke-opacity",
|
|
1380
|
-
"marker-opacity"
|
|
1381
|
-
];
|
|
1382
|
-
function isSimpleStyleEnabled(style) {
|
|
1383
|
-
return styleValue(style, "simpleStyleEnabled") === true;
|
|
1384
|
-
}
|
|
822
|
+
// src/style-layer-evaluator.ts
|
|
823
|
+
const GEOMETRY_TYPE = {
|
|
824
|
+
Point: 1,
|
|
825
|
+
LineString: 2,
|
|
826
|
+
Polygon: 3
|
|
827
|
+
};
|
|
1385
828
|
/**
|
|
1386
|
-
*
|
|
1387
|
-
*
|
|
1388
|
-
*
|
|
829
|
+
* Compile a MapLibre style layer's filter and properties for evaluation
|
|
830
|
+
* outside MapLibre, for a renderer that draws the layer itself (the ArcGIS
|
|
831
|
+
* renderer drawing a plugin's own GeoJSON overlay, say). Uses the style spec's
|
|
832
|
+
* own evaluator, so data-driven and zoom expressions, legacy functions and
|
|
833
|
+
* defaults behave as they do on MapLibre. Each property compiles once.
|
|
1389
834
|
*
|
|
1390
|
-
* @param
|
|
1391
|
-
* @
|
|
1392
|
-
* @param base - The flat color or expression to fall back to.
|
|
1393
|
-
* @returns A `coalesce` expression, or the base value when disabled.
|
|
835
|
+
* @param layer - The style layer (its `type`, `filter`, `paint`, `layout`).
|
|
836
|
+
* @returns The layer's evaluator.
|
|
1394
837
|
*/
|
|
1395
|
-
function
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
/**
|
|
1404
|
-
* Apply a per-feature transform to a value, descending into the outputs of a
|
|
1405
|
-
* top-level `["step", ["zoom"], …]` expression (as produced by the rule-based
|
|
1406
|
-
* per-rule zoom compilation) so the zoom step stays outermost. MapLibre
|
|
1407
|
-
* rejects any expression where `["zoom"]` is not the input of a top-level
|
|
1408
|
-
* `step`/`interpolate`, so wrappers (simplestyle coalesce, opacity folds)
|
|
1409
|
-
* must be pushed inside the step's outputs rather than wrapped around it.
|
|
1410
|
-
* Non-step values are transformed directly.
|
|
1411
|
-
*/
|
|
1412
|
-
function mapZoomStepOutputs(value, transform) {
|
|
1413
|
-
if (Array.isArray(value) && value[0] === "step" && Array.isArray(value[1]) && value[1][0] === "zoom") {
|
|
1414
|
-
const out = [
|
|
1415
|
-
"step",
|
|
1416
|
-
value[1],
|
|
1417
|
-
transform(value[2])
|
|
1418
|
-
];
|
|
1419
|
-
for (let index = 3; index < value.length; index += 2) out.push(value[index], transform(value[index + 1]));
|
|
1420
|
-
return out;
|
|
838
|
+
function createStyleLayerEvaluator(layer) {
|
|
839
|
+
const root = `layers[${layer.id ?? ""}]`;
|
|
840
|
+
let filter;
|
|
841
|
+
try {
|
|
842
|
+
const compiled = featureFilter(layer.filter, `${root}.filter`);
|
|
843
|
+
filter = (zoom, feature) => compiled.filter({ zoom }, feature);
|
|
844
|
+
} catch {
|
|
845
|
+
filter = () => false;
|
|
1421
846
|
}
|
|
1422
|
-
|
|
847
|
+
const compiled = /* @__PURE__ */ new Map();
|
|
848
|
+
const compile = (group, name) => {
|
|
849
|
+
const key = `${group}/${name}`;
|
|
850
|
+
if (compiled.has(key)) return compiled.get(key);
|
|
851
|
+
const spec = latest[`${group}_${layer.type}`]?.[name];
|
|
852
|
+
let evaluate = null;
|
|
853
|
+
if (spec) {
|
|
854
|
+
const raw = layer[group]?.[name];
|
|
855
|
+
try {
|
|
856
|
+
const expression = normalizePropertyExpression(raw ?? spec.default, `${root}.${group}.${name}`, spec);
|
|
857
|
+
evaluate = (globals, feature) => expression.evaluate(globals, feature);
|
|
858
|
+
} catch {
|
|
859
|
+
evaluate = null;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
compiled.set(key, evaluate);
|
|
863
|
+
return evaluate;
|
|
864
|
+
};
|
|
865
|
+
const input = (feature) => ({
|
|
866
|
+
type: GEOMETRY_TYPE[feature.kind],
|
|
867
|
+
id: feature.id,
|
|
868
|
+
properties: feature.properties ?? {}
|
|
869
|
+
});
|
|
870
|
+
return {
|
|
871
|
+
passes: (zoom, feature) => {
|
|
872
|
+
try {
|
|
873
|
+
return filter(zoom, input(feature));
|
|
874
|
+
} catch {
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
value: (group, name, zoom, feature) => {
|
|
879
|
+
const evaluate = compile(group, name);
|
|
880
|
+
if (!evaluate) return void 0;
|
|
881
|
+
let result;
|
|
882
|
+
try {
|
|
883
|
+
result = evaluate({ zoom }, input(feature));
|
|
884
|
+
} catch {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (result instanceof Color) {
|
|
888
|
+
const [r, g, b, a] = result.rgb;
|
|
889
|
+
return [
|
|
890
|
+
Math.round(r * 255),
|
|
891
|
+
Math.round(g * 255),
|
|
892
|
+
Math.round(b * 255),
|
|
893
|
+
a
|
|
894
|
+
];
|
|
895
|
+
}
|
|
896
|
+
if (result && typeof result === "object" && "sections" in result) return String(result);
|
|
897
|
+
return result;
|
|
898
|
+
}
|
|
899
|
+
};
|
|
1423
900
|
}
|
|
901
|
+
// src/document-locale.ts
|
|
1424
902
|
/**
|
|
1425
|
-
*
|
|
1426
|
-
* override the layer value when {@link LayerStyle.simpleStyleEnabled} is set.
|
|
1427
|
-
*
|
|
1428
|
-
* The lookup is wrapped in a `coalesce` rather than leaning on `to-number`'s
|
|
1429
|
-
* own fallback argument, because that fallback only fires when a value fails
|
|
1430
|
-
* to convert — and a *missing* property does not fail, it converts to 0. Since
|
|
1431
|
-
* simplestyle is enabled for the whole layer as soon as any feature carries any
|
|
1432
|
-
* one of these keys, features that carry only some of them (an ArcGIS KML
|
|
1433
|
-
* export writes `fill`/`fill-opacity` but no `marker-opacity`, for instance)
|
|
1434
|
-
* would otherwise be painted at opacity 0 or width 0 — present in the layer,
|
|
1435
|
-
* invisible on the map (#1552).
|
|
903
|
+
* The document language, so formatted numbers and dates follow the UI locale.
|
|
1436
904
|
*
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
1440
|
-
*
|
|
1441
|
-
*/
|
|
1442
|
-
function simpleStyleNumberValue(style, property, base) {
|
|
1443
|
-
if (!isSimpleStyleEnabled(style)) return base;
|
|
1444
|
-
return [
|
|
1445
|
-
"to-number",
|
|
1446
|
-
[
|
|
1447
|
-
"coalesce",
|
|
1448
|
-
["get", property],
|
|
1449
|
-
base
|
|
1450
|
-
],
|
|
1451
|
-
base
|
|
1452
|
-
];
|
|
1453
|
-
}
|
|
1454
|
-
/**
|
|
1455
|
-
* Ground resolution (meters per pixel) at MapLibre zoom 0 on the equator, for
|
|
1456
|
-
* the Web Mercator projection: earth circumference (2*pi*6378137) over the
|
|
1457
|
-
* 512px world at zoom 0. Resolution halves with every zoom level. Exported so
|
|
1458
|
-
* the Mapbox-style importer can reverse {@link metersWidthExpression} without a
|
|
1459
|
-
* second copy of the magic number.
|
|
1460
|
-
*/
|
|
1461
|
-
const MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0 = 2 * Math.PI * 6378137 / 512;
|
|
1462
|
-
/**
|
|
1463
|
-
* Same ground resolution as {@link MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0} but for
|
|
1464
|
-
* the project's active body: its circumference over the 512px zoom-0 world. On
|
|
1465
|
-
* Earth this equals the constant; on the Moon/Mars it uses that body's radius so
|
|
1466
|
-
* a stroke width given in ground meters renders at the correct on-screen size.
|
|
1467
|
-
* Exported so the Mapbox-style importer can reverse {@link metersWidthExpression}
|
|
1468
|
-
* with the same ellipsoid-aware factor the exporter used (keeping the round trip
|
|
1469
|
-
* correct on non-Earth projects), rather than the Earth-only constant.
|
|
905
|
+
* Shared by the Identify popup's value formatting and by label number
|
|
906
|
+
* formatting, which both fall back to it when the author has not pinned an
|
|
907
|
+
* explicit locale. Returns `undefined` outside a browser (tests, the headless
|
|
908
|
+
* project authoring paths), which leaves `Intl` on its own default.
|
|
1470
909
|
*/
|
|
1471
|
-
function
|
|
1472
|
-
return
|
|
910
|
+
function documentLocale() {
|
|
911
|
+
return (typeof document !== "undefined" ? document.documentElement.lang.trim() : "") || void 0;
|
|
1473
912
|
}
|
|
1474
|
-
|
|
913
|
+
// src/label-number-format.ts
|
|
1475
914
|
/**
|
|
1476
|
-
*
|
|
1477
|
-
* map scale, so a width given in ground meters renders thicker when zoomed in
|
|
1478
|
-
* and thinner when zoomed out (QGIS "map units" behavior).
|
|
915
|
+
* Locale-aware number formatting for map labels (issue #2336).
|
|
1479
916
|
*
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1482
|
-
*
|
|
1483
|
-
*
|
|
1484
|
-
*
|
|
1485
|
-
*
|
|
917
|
+
* A numeric attribute rendered straight into a label reads as `1234567.5`,
|
|
918
|
+
* which is hard to compare at a glance. QGIS solves this with
|
|
919
|
+
* `format_number(field, places, language)`; the equivalent here is three
|
|
920
|
+
* {@link LabelStyle} fields — {@link LabelStyle.numberFormatEnabled},
|
|
921
|
+
* {@link LabelStyle.numberDecimals} and {@link LabelStyle.numberLocale} —
|
|
922
|
+
* whose effect this module renders two ways:
|
|
1486
923
|
*
|
|
1487
|
-
*
|
|
1488
|
-
*
|
|
924
|
+
* - {@link labelFieldTextField} builds the MapLibre `text-field` expression the
|
|
925
|
+
* 2D map and the Mapbox/MapLibre style export both use, so the map and the
|
|
926
|
+
* exported style agree.
|
|
927
|
+
* - {@link formatLabelNumber} does the same in JavaScript for the paths that
|
|
928
|
+
* resolve label text themselves: the Cesium globe and the duplicate-label
|
|
929
|
+
* collapsing modes.
|
|
1489
930
|
*
|
|
1490
|
-
*
|
|
1491
|
-
* @
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
const widthAtZoom0 = meters / mercatorMetersPerPixelAtZoom0();
|
|
1495
|
-
return [
|
|
1496
|
-
"interpolate",
|
|
1497
|
-
["exponential", 2],
|
|
1498
|
-
["zoom"],
|
|
1499
|
-
0,
|
|
1500
|
-
widthAtZoom0,
|
|
1501
|
-
MAX_MERCATOR_ZOOM,
|
|
1502
|
-
widthAtZoom0 * 2 ** MAX_MERCATOR_ZOOM
|
|
1503
|
-
];
|
|
1504
|
-
}
|
|
1505
|
-
/**
|
|
1506
|
-
* Resolve the `line-width` paint value for a layer style, honoring the
|
|
1507
|
-
* {@link LayerStyle.strokeWidthUnit}:
|
|
1508
|
-
*
|
|
1509
|
-
* - `"meters"`: a zoom-driven {@link metersWidthExpression} from the flat
|
|
1510
|
-
* `strokeWidth`, so the stroke scales with the map. A per-feature pixel
|
|
1511
|
-
* `stroke-width` override no longer applies in this mode.
|
|
1512
|
-
* - `"pixels"` (default): the constant pixel width, still honoring any
|
|
1513
|
-
* per-feature simplestyle `stroke-width`.
|
|
1514
|
-
*
|
|
1515
|
-
* Shared by the map style-mapper and the geo-editor plugin so the Sketches
|
|
1516
|
-
* store layer and Geoman's interaction display layers render an identical
|
|
1517
|
-
* width.
|
|
1518
|
-
*
|
|
1519
|
-
* @param style - The layer style.
|
|
1520
|
-
* @returns A number (constant pixels) or a MapLibre expression array.
|
|
1521
|
-
*/
|
|
1522
|
-
function lineWidthValue(style) {
|
|
1523
|
-
const proportionalWidth = proportionalRadiusExpression(style);
|
|
1524
|
-
if (proportionalWidth) return vectorStrokeWidthValue(style, proportionalWidth);
|
|
1525
|
-
if (styleValue(style, "strokeWidthUnit") === "meters") return metersWidthExpression(styleValue(style, "strokeWidth"));
|
|
1526
|
-
return vectorStrokeWidthValue(style, simpleStyleNumberValue(style, "stroke-width", styleValue(style, "strokeWidth")));
|
|
1527
|
-
}
|
|
1528
|
-
/**
|
|
1529
|
-
* Whether a FeatureCollection carries per-feature simplestyle-spec properties
|
|
1530
|
-
* worth honoring: at least one feature with a valid hex color in a color key
|
|
1531
|
-
* (`fill`/`stroke`/`marker-color`) or a finite number in a numeric key
|
|
1532
|
-
* (`fill-opacity`/`stroke-width`/`stroke-opacity`). The scan is capped so very
|
|
1533
|
-
* large collections do not pay a full pass.
|
|
1534
|
-
*
|
|
1535
|
-
* @param geojson - The collection to inspect (may be undefined).
|
|
1536
|
-
* @returns `true` when simplestyle rendering should be enabled for the layer.
|
|
1537
|
-
*/
|
|
1538
|
-
function hasSimpleStyleProperties(geojson) {
|
|
1539
|
-
const features = geojson?.features;
|
|
1540
|
-
if (!features?.length) return false;
|
|
1541
|
-
const limit = Math.min(features.length, 1e3);
|
|
1542
|
-
for (let index = 0; index < limit; index += 1) {
|
|
1543
|
-
const properties = features[index]?.properties;
|
|
1544
|
-
if (!properties) continue;
|
|
1545
|
-
for (const key of SIMPLE_STYLE_COLOR_KEYS) {
|
|
1546
|
-
const value = properties[key];
|
|
1547
|
-
if (typeof value === "string" && isSimpleStyleColor(value)) return true;
|
|
1548
|
-
}
|
|
1549
|
-
for (const key of SIMPLE_STYLE_NUMBER_KEYS) {
|
|
1550
|
-
const value = properties[key];
|
|
1551
|
-
if (typeof value === "number" && Number.isFinite(value)) return true;
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
return false;
|
|
1555
|
-
}
|
|
1556
|
-
/**
|
|
1557
|
-
* Parses a user-entered MapLibre expression string into an expression array,
|
|
1558
|
-
* tolerating trailing commas. Returns null when the text is empty or not a
|
|
1559
|
-
* JSON array.
|
|
1560
|
-
*/
|
|
1561
|
-
function parseJsonExpression(expression) {
|
|
1562
|
-
const trimmed = expression.trim();
|
|
1563
|
-
if (!trimmed) return null;
|
|
1564
|
-
try {
|
|
1565
|
-
const parsed = JSON.parse(removeTrailingJsonCommas(trimmed));
|
|
1566
|
-
if (!Array.isArray(parsed)) return null;
|
|
1567
|
-
return parsed;
|
|
1568
|
-
} catch {
|
|
1569
|
-
return null;
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
/**
|
|
1573
|
-
* Builds the data-driven color value for a vector layer's current style mode.
|
|
1574
|
-
* `single` (or any mode that cannot produce a valid expression) returns the
|
|
1575
|
-
* flat fallback color; `categorized` returns a `match` expression, `graduated`
|
|
1576
|
-
* a `step` expression, and `expression` the parsed user expression.
|
|
1577
|
-
*
|
|
1578
|
-
* Graduated stops are class lower bounds (see
|
|
1579
|
-
* {@link createGraduatedClassBreaks}), so the classes are discrete: the map
|
|
1580
|
-
* paints exactly one color per class, which is what the legend (`≥ value`) and
|
|
1581
|
-
* the QML/SLD exporters already describe. Rendering them as a continuous
|
|
1582
|
-
* `interpolate` instead blends a shade per feature, so a 4-class layer draws in
|
|
1583
|
-
* many more than 4 colors and stops matching its own legend (#1384).
|
|
1584
|
-
*
|
|
1585
|
-
* @param style - The layer style.
|
|
1586
|
-
* @param fallbackColor - The flat color used for `single` mode and as the
|
|
1587
|
-
* expression fallback.
|
|
1588
|
-
* @returns A flat color string or a MapLibre color expression.
|
|
1589
|
-
*/
|
|
1590
|
-
function vectorColorExpression(style, fallbackColor) {
|
|
1591
|
-
const mode = styleValue(style, "vectorStyleMode");
|
|
1592
|
-
if (mode === "single") return fallbackColor;
|
|
1593
|
-
if (mode === "expression") return parseJsonExpression(styleValue(style, "vectorStyleExpression")) ?? fallbackColor;
|
|
1594
|
-
if (mode === "rule-based") return ruleBasedColorExpression(style, fallbackColor);
|
|
1595
|
-
const property = styleValue(style, "vectorStyleProperty").trim();
|
|
1596
|
-
if (!property) return fallbackColor;
|
|
1597
|
-
if (mode === "categorized") {
|
|
1598
|
-
const stops = styleValue(style, "vectorStyleStops").filter((stop) => String(stop.value).trim().length > 0 && isHexColor(stop.color));
|
|
1599
|
-
if (stops.length === 0) return fallbackColor;
|
|
1600
|
-
return [
|
|
1601
|
-
"match",
|
|
1602
|
-
["to-string", ["get", property]],
|
|
1603
|
-
...stops.flatMap((stop) => [String(stop.value).trim(), stop.color]),
|
|
1604
|
-
fallbackColor
|
|
1605
|
-
];
|
|
1606
|
-
}
|
|
1607
|
-
const stops = graduatedStops(style);
|
|
1608
|
-
if (stops.length < 2) return fallbackColor;
|
|
1609
|
-
return [
|
|
1610
|
-
"step",
|
|
1611
|
-
[
|
|
1612
|
-
"to-number",
|
|
1613
|
-
["get", property],
|
|
1614
|
-
stops[0].value
|
|
1615
|
-
],
|
|
1616
|
-
stops[0].color,
|
|
1617
|
-
...stops.slice(1).flatMap((stop) => [stop.value, stop.color])
|
|
1618
|
-
];
|
|
1619
|
-
}
|
|
1620
|
-
/**
|
|
1621
|
-
* The graduated stops of a style, cleaned for rendering: non-numeric values and
|
|
1622
|
-
* invalid colors dropped, sorted ascending, and de-duplicated. MapLibre rejects
|
|
1623
|
-
* a `step` expression whose inputs are not strictly ascending, and stops reach
|
|
1624
|
-
* here from hand-edited projects and style imports as well as the Style panel,
|
|
1625
|
-
* so repeats are dropped rather than trusted away.
|
|
1626
|
-
*/
|
|
1627
|
-
function graduatedStops(style) {
|
|
1628
|
-
const sorted = styleValue(style, "vectorStyleStops").map((stop) => ({
|
|
1629
|
-
color: stop.color,
|
|
1630
|
-
value: typeof stop.value === "number" ? stop.value : Number.parseFloat(stop.value)
|
|
1631
|
-
})).filter((stop) => Number.isFinite(stop.value) && isHexColor(stop.color)).sort((a, b) => a.value - b.value);
|
|
1632
|
-
return sorted.filter((stop, index) => index === 0 || stop.value > sorted[index - 1].value);
|
|
1633
|
-
}
|
|
1634
|
-
/** A finite per-rule zoom bound, or undefined when unset/invalid. */
|
|
1635
|
-
function ruleZoom(value) {
|
|
1636
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1637
|
-
}
|
|
1638
|
-
/**
|
|
1639
|
-
* Resolve the layer's rule tree into the drawable leaf rules (see
|
|
1640
|
-
* {@link EffectiveVectorRule}) plus the catch-all else rule. Disabled rules
|
|
1641
|
-
* (and every descendant of a disabled group), rules with an invalid color or
|
|
1642
|
-
* filter, subtrees under a group whose non-blank filter does not parse, and
|
|
1643
|
-
* rules whose intersected zoom range is empty are all dropped. Cycles in
|
|
1644
|
-
* `parentId` are treated as broken chains and dropped rather than looping.
|
|
1645
|
-
*
|
|
1646
|
-
* @param style - The layer style (reads {@link LayerStyle.vectorRules}).
|
|
1647
|
-
* @returns The ordered drawable rules and the enabled else rule, if any.
|
|
1648
|
-
*/
|
|
1649
|
-
function effectiveVectorRules(style) {
|
|
1650
|
-
const all = styleValue(style, "vectorRules");
|
|
1651
|
-
const elseRule = all.find((rule) => rule.isElse && rule.enabled !== false) ?? null;
|
|
1652
|
-
const byId = /* @__PURE__ */ new Map();
|
|
1653
|
-
for (const rule of all) if (!rule.isElse) byId.set(rule.id, rule);
|
|
1654
|
-
const groupIds = /* @__PURE__ */ new Set();
|
|
1655
|
-
for (const rule of byId.values()) if (rule.parentId && rule.parentId !== rule.id && byId.has(rule.parentId)) groupIds.add(rule.parentId);
|
|
1656
|
-
const rules = [];
|
|
1657
|
-
for (const rule of all) {
|
|
1658
|
-
if (rule.isElse || groupIds.has(rule.id)) continue;
|
|
1659
|
-
if (rule.enabled === false || !isHexColor(rule.color)) continue;
|
|
1660
|
-
const own = parseJsonExpression(rule.filter);
|
|
1661
|
-
if (!own || typeof own[0] !== "string") continue;
|
|
1662
|
-
const filters = [own];
|
|
1663
|
-
let minZoom = ruleZoom(rule.minZoom);
|
|
1664
|
-
let maxZoom = ruleZoom(rule.maxZoom);
|
|
1665
|
-
let dropped = false;
|
|
1666
|
-
const seen = /* @__PURE__ */ new Set([rule.id]);
|
|
1667
|
-
let parent = rule.parentId && rule.parentId !== rule.id ? byId.get(rule.parentId) : void 0;
|
|
1668
|
-
while (parent) {
|
|
1669
|
-
if (seen.has(parent.id) || parent.enabled === false) {
|
|
1670
|
-
dropped = true;
|
|
1671
|
-
break;
|
|
1672
|
-
}
|
|
1673
|
-
seen.add(parent.id);
|
|
1674
|
-
if (parent.filter.trim()) {
|
|
1675
|
-
const parentFilter = parseJsonExpression(parent.filter);
|
|
1676
|
-
if (!parentFilter || typeof parentFilter[0] !== "string") {
|
|
1677
|
-
dropped = true;
|
|
1678
|
-
break;
|
|
1679
|
-
}
|
|
1680
|
-
filters.unshift(parentFilter);
|
|
1681
|
-
}
|
|
1682
|
-
const parentMin = ruleZoom(parent.minZoom);
|
|
1683
|
-
const parentMax = ruleZoom(parent.maxZoom);
|
|
1684
|
-
if (parentMin !== void 0) minZoom = minZoom === void 0 ? parentMin : Math.max(minZoom, parentMin);
|
|
1685
|
-
if (parentMax !== void 0) maxZoom = maxZoom === void 0 ? parentMax : Math.min(maxZoom, parentMax);
|
|
1686
|
-
parent = parent.parentId && parent.parentId !== parent.id ? byId.get(parent.parentId) : void 0;
|
|
1687
|
-
}
|
|
1688
|
-
if (dropped) continue;
|
|
1689
|
-
if (minZoom !== void 0 && maxZoom !== void 0 && minZoom >= maxZoom) continue;
|
|
1690
|
-
rules.push({
|
|
1691
|
-
id: rule.id,
|
|
1692
|
-
label: rule.label,
|
|
1693
|
-
color: rule.color,
|
|
1694
|
-
filter: filters.length === 1 ? filters[0] : ["all", ...filters],
|
|
1695
|
-
...minZoom !== void 0 ? { minZoom } : {},
|
|
1696
|
-
...maxZoom !== void 0 ? { maxZoom } : {},
|
|
1697
|
-
...isHexColor(rule.strokeColor) ? { strokeColor: rule.strokeColor } : {},
|
|
1698
|
-
...typeof rule.strokeWidth === "number" && Number.isFinite(rule.strokeWidth) ? { strokeWidth: rule.strokeWidth } : {},
|
|
1699
|
-
...typeof rule.fillOpacity === "number" && Number.isFinite(rule.fillOpacity) ? { fillOpacity: rule.fillOpacity } : {},
|
|
1700
|
-
...typeof rule.circleRadius === "number" && Number.isFinite(rule.circleRadius) ? { circleRadius: rule.circleRadius } : {}
|
|
1701
|
-
});
|
|
1702
|
-
}
|
|
1703
|
-
return {
|
|
1704
|
-
rules,
|
|
1705
|
-
elseRule
|
|
1706
|
-
};
|
|
1707
|
-
}
|
|
1708
|
-
/**
|
|
1709
|
-
* A rule's match condition as a MapLibre filter: its (ancestor-combined)
|
|
1710
|
-
* attribute filter, with any scale range folded in as `["zoom"]` comparisons.
|
|
1711
|
-
* The zoom window is half-open (`min <= zoom < max`), matching how
|
|
1712
|
-
* {@link zoomWrappedRuleValue} activates rules per zoom segment. MapLibre only
|
|
1713
|
-
* re-evaluates `["zoom"]` in filters at integer zoom levels, so a scale-ranged
|
|
1714
|
-
* rule's features appear/disappear on whole zooms — the paint expressions keep
|
|
1715
|
-
* the fractional-zoom precision.
|
|
1716
|
-
*/
|
|
1717
|
-
function ruleMatchFilter(rule) {
|
|
1718
|
-
const conditions = [rule.filter];
|
|
1719
|
-
if (rule.minZoom !== void 0) conditions.push([
|
|
1720
|
-
">=",
|
|
1721
|
-
["zoom"],
|
|
1722
|
-
rule.minZoom
|
|
1723
|
-
]);
|
|
1724
|
-
if (rule.maxZoom !== void 0) conditions.push([
|
|
1725
|
-
"<",
|
|
1726
|
-
["zoom"],
|
|
1727
|
-
rule.maxZoom
|
|
1728
|
-
]);
|
|
1729
|
-
return conditions.length === 1 ? rule.filter : ["all", ...conditions];
|
|
1730
|
-
}
|
|
1731
|
-
const visibilityFilterCache = /* @__PURE__ */ new WeakMap();
|
|
1732
|
-
/**
|
|
1733
|
-
* The MapLibre filter that hides features matching no rule, for a rule-based
|
|
1734
|
-
* layer whose catch-all else rule has been switched off. Returns `null` — and
|
|
1735
|
-
* callers then leave their geometry filters untouched — unless the layer is in
|
|
1736
|
-
* `"rule-based"` mode AND carries an else record explicitly disabled
|
|
1737
|
-
* (`enabled: false`). An absent else record keeps the historical fallback
|
|
1738
|
-
* rendering, so projects saved before the toggle existed are unaffected.
|
|
1739
|
-
*
|
|
1740
|
-
* With the else rule off, the returned `["any", filter1, filter2, …]` keeps
|
|
1741
|
-
* only features matched by a drawable rule (see {@link effectiveVectorRules});
|
|
1742
|
-
* with no drawable rules it returns the vacuous `["any"]`, which matches
|
|
1743
|
-
* nothing, so every feature hides. Sub-layers combine it with their geometry
|
|
1744
|
-
* filters, which also drops unmatched features from labels and hit-testing —
|
|
1745
|
-
* mirroring QGIS, where features matching no rule are simply not drawn.
|
|
1746
|
-
*
|
|
1747
|
-
* @param style - The layer style (reads {@link LayerStyle.vectorRules}).
|
|
1748
|
-
* @returns The filter to `["all", …]`-combine into each sub-layer, or `null`.
|
|
1749
|
-
*/
|
|
1750
|
-
function ruleBasedVisibilityFilter(style) {
|
|
1751
|
-
if (styleValue(style, "vectorStyleMode") !== "rule-based") return null;
|
|
1752
|
-
const all = styleValue(style, "vectorRules");
|
|
1753
|
-
const cached = visibilityFilterCache.get(all);
|
|
1754
|
-
if (cached !== void 0) return cached;
|
|
1755
|
-
const elseRecord = all.find((rule) => rule.isElse);
|
|
1756
|
-
const filter = !elseRecord || elseRecord.enabled !== false ? null : ["any", ...effectiveVectorRules(style).rules.map(ruleMatchFilter)];
|
|
1757
|
-
visibilityFilterCache.set(all, filter);
|
|
1758
|
-
return filter;
|
|
1759
|
-
}
|
|
1760
|
-
/**
|
|
1761
|
-
* Wrap a per-zoom-segment value builder in a `["step", ["zoom"], …]` expression
|
|
1762
|
-
* when any rule carries a zoom bound. MapLibre only allows `["zoom"]` as the
|
|
1763
|
-
* top-level input of a `step`/`interpolate` in paint properties (not inside a
|
|
1764
|
-
* `case` condition), so per-rule scale ranges compile by splitting the zoom
|
|
1765
|
-
* axis at every rule bound and rebuilding the `case` expression per segment
|
|
1766
|
-
* with only the rules active in that segment. With no zoom bounds the builder
|
|
1767
|
-
* runs once and its plain value is returned unchanged, keeping the historical
|
|
1768
|
-
* `case` shape (which the Mapbox-style importer recognizes).
|
|
1769
|
-
*/
|
|
1770
|
-
function zoomWrappedRuleValue(rules, build) {
|
|
1771
|
-
const bounds = /* @__PURE__ */ new Set();
|
|
1772
|
-
for (const rule of rules) {
|
|
1773
|
-
if (rule.minZoom !== void 0) bounds.add(rule.minZoom);
|
|
1774
|
-
if (rule.maxZoom !== void 0) bounds.add(rule.maxZoom);
|
|
1775
|
-
}
|
|
1776
|
-
if (bounds.size === 0) return build(rules);
|
|
1777
|
-
const breaks = [...bounds].sort((a, b) => a - b);
|
|
1778
|
-
const activeAt = (segmentStart) => rules.filter((rule) => (rule.minZoom ?? -Infinity) <= segmentStart && segmentStart < (rule.maxZoom ?? Infinity));
|
|
1779
|
-
const expression = [
|
|
1780
|
-
"step",
|
|
1781
|
-
["zoom"],
|
|
1782
|
-
build(activeAt(-Infinity))
|
|
1783
|
-
];
|
|
1784
|
-
for (const zoom of breaks) expression.push(zoom, build(activeAt(zoom)));
|
|
1785
|
-
return expression;
|
|
1786
|
-
}
|
|
1787
|
-
/**
|
|
1788
|
-
* Compiles the `"rule-based"` renderer's ordered rules into a MapLibre `case`
|
|
1789
|
-
* color expression: `["case", filter1, color1, filter2, color2, …, elseColor]`.
|
|
1790
|
-
* Rules are evaluated top to bottom; the first matching filter wins. Disabled
|
|
1791
|
-
* rules and rules with an invalid filter JSON or a non-hex color are skipped;
|
|
1792
|
-
* nested rules AND their ancestors' filters (see {@link effectiveVectorRules}).
|
|
1793
|
-
* When any rule carries a zoom range the `case` is rebuilt per zoom segment
|
|
1794
|
-
* inside a `["step", ["zoom"], …]` wrapper. The catch-all (`isElse`) rule
|
|
1795
|
-
* supplies the trailing fallback; when absent or invalid the layer
|
|
1796
|
-
* `fallbackColor` is used. With no usable rules the flat fallback color is
|
|
1797
|
-
* returned.
|
|
1798
|
-
*
|
|
1799
|
-
* @param style - The layer style (reads {@link LayerStyle.vectorRules}).
|
|
1800
|
-
* @param fallbackColor - The color used when no else rule defines one.
|
|
1801
|
-
* @returns A MapLibre expression, or a flat color when no rule applies.
|
|
1802
|
-
*/
|
|
1803
|
-
function ruleBasedColorExpression(style, fallbackColor) {
|
|
1804
|
-
const { rules, elseRule } = effectiveVectorRules(style);
|
|
1805
|
-
const elseColor = elseRule && isHexColor(elseRule.color) ? elseRule.color : fallbackColor;
|
|
1806
|
-
if (rules.length === 0) return elseColor;
|
|
1807
|
-
return zoomWrappedRuleValue(rules, (active) => {
|
|
1808
|
-
if (active.length === 0) return elseColor;
|
|
1809
|
-
return [
|
|
1810
|
-
"case",
|
|
1811
|
-
...active.flatMap((rule) => [rule.filter, rule.color]),
|
|
1812
|
-
elseColor
|
|
1813
|
-
];
|
|
1814
|
-
});
|
|
1815
|
-
}
|
|
1816
|
-
/**
|
|
1817
|
-
* Build a paint value honoring per-rule symbol overrides for one field: a
|
|
1818
|
-
* `case` expression (zoom-wrapped when rules carry scale ranges) where each
|
|
1819
|
-
* drawable rule's branch yields its override, or the layer fallback when that
|
|
1820
|
-
* rule does not override the field. Returns the fallback unchanged when the
|
|
1821
|
-
* layer is not in rule-based mode or no rule (including the else rule)
|
|
1822
|
-
* overrides the field, so simple styles keep their flat paint values.
|
|
1823
|
-
*
|
|
1824
|
-
* First-match semantics are preserved: every drawable rule contributes a
|
|
1825
|
-
* branch (even without an override) so a feature matched by an earlier rule
|
|
1826
|
-
* never picks up a later rule's override.
|
|
1827
|
-
*/
|
|
1828
|
-
function ruleOverrideValue(style, key, fallback) {
|
|
1829
|
-
if (styleValue(style, "vectorStyleMode") !== "rule-based") return fallback;
|
|
1830
|
-
const { rules, elseRule } = effectiveVectorRules(style);
|
|
1831
|
-
const elseOverride = readRuleOverride(elseRule ?? void 0, key);
|
|
1832
|
-
if (elseOverride === void 0 && !rules.some((rule) => rule[key] !== void 0)) return fallback;
|
|
1833
|
-
const elseValue = elseOverride ?? fallback;
|
|
1834
|
-
return zoomWrappedRuleValue(rules, (active) => {
|
|
1835
|
-
if (active.length === 0) return elseValue;
|
|
1836
|
-
return [
|
|
1837
|
-
"case",
|
|
1838
|
-
...active.flatMap((rule) => [rule.filter, rule[key] ?? fallback]),
|
|
1839
|
-
elseValue
|
|
1840
|
-
];
|
|
1841
|
-
});
|
|
1842
|
-
}
|
|
1843
|
-
/** A validated override read straight off a (possibly else) {@link VectorRule}. */
|
|
1844
|
-
function readRuleOverride(rule, key) {
|
|
1845
|
-
const value = rule?.[key];
|
|
1846
|
-
if (key === "strokeColor") return isHexColor(value) ? value : void 0;
|
|
1847
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1848
|
-
}
|
|
1849
|
-
/**
|
|
1850
|
-
* The stroke/outline color for polygon outlines and circle strokes, honoring
|
|
1851
|
-
* per-rule {@link VectorRule.strokeColor} overrides in rule-based mode.
|
|
1852
|
-
* Falls back to the flat layer stroke color.
|
|
1853
|
-
*/
|
|
1854
|
-
function vectorOutlineColorValue(style) {
|
|
1855
|
-
return ruleOverrideValue(style, "strokeColor", styleValue(style, "strokeColor"));
|
|
1856
|
-
}
|
|
1857
|
-
/**
|
|
1858
|
-
* A stroke-width paint value honoring per-rule {@link VectorRule.strokeWidth}
|
|
1859
|
-
* overrides in rule-based mode; `fallback` is the layer-level width (a number
|
|
1860
|
-
* or an already-built expression such as the meters-based interpolation).
|
|
1861
|
-
*/
|
|
1862
|
-
function vectorStrokeWidthValue(style, fallback) {
|
|
1863
|
-
return ruleOverrideValue(style, "strokeWidth", fallback);
|
|
1864
|
-
}
|
|
1865
|
-
/**
|
|
1866
|
-
* A fill/circle opacity paint value honoring per-rule
|
|
1867
|
-
* {@link VectorRule.fillOpacity} overrides in rule-based mode; `fallback` is
|
|
1868
|
-
* the layer-level opacity value (number or expression).
|
|
1869
|
-
*/
|
|
1870
|
-
function vectorFillOpacityValue(style, fallback) {
|
|
1871
|
-
return ruleOverrideValue(style, "fillOpacity", fallback);
|
|
1872
|
-
}
|
|
1873
|
-
/**
|
|
1874
|
-
* The color-ramp colors a density heatmap paints with.
|
|
1875
|
-
*
|
|
1876
|
-
* {@link getVectorColorRamp} falls back to the *first* ramp ("viridis") for an
|
|
1877
|
-
* unknown name, which is not the heatmap default ("turbo"). Both the map paint
|
|
1878
|
-
* (`heatmapPaint` in `@geolibre/map`) and the auto-legend gradient resolve the
|
|
1879
|
-
* ramp here so a stale or hand-edited `heatmapColorRamp` cannot make the legend
|
|
1880
|
-
* and the rendered heatmap disagree about which ramp is in use.
|
|
1881
|
-
*
|
|
1882
|
-
* @param style - The layer style.
|
|
1883
|
-
* @returns The ramp's colors, or the default ramp's colors for an unknown name.
|
|
1884
|
-
*/
|
|
1885
|
-
function heatmapRampColors(style) {
|
|
1886
|
-
const value = styleValue(style, "heatmapColorRamp");
|
|
1887
|
-
const ramp = getVectorColorRamp(value);
|
|
1888
|
-
return (ramp.value === value ? ramp : getVectorColorRamp(DEFAULT_LAYER_STYLE.heatmapColorRamp)).colors;
|
|
1889
|
-
}
|
|
1890
|
-
/**
|
|
1891
|
-
* The proportional-size configuration, or `null` when proportional sizing
|
|
1892
|
-
* does not apply: disabled, no field chosen, non-finite or degenerate
|
|
1893
|
-
* (`maxValue <= minValue`) value range, or non-finite radii. This is the
|
|
1894
|
-
* single validation chain for every proportional consumer — circle radius,
|
|
1895
|
-
* line width, and marker icon-size (`@geolibre/map`) — so their notion of
|
|
1896
|
-
* "active" can never drift apart.
|
|
1897
|
-
*
|
|
1898
|
-
* @param style - The layer style.
|
|
1899
|
-
* @returns The validated range, or `null` when proportional sizing is off.
|
|
1900
|
-
*/
|
|
1901
|
-
function proportionalSizeRange(style) {
|
|
1902
|
-
if (!styleValue(style, "proportionalSizeEnabled")) return null;
|
|
1903
|
-
return validatedSizeRange(styleValue(style, "proportionalSizeProperty"), styleValue(style, "proportionalSizeMinValue"), styleValue(style, "proportionalSizeMaxValue"), styleValue(style, "proportionalSizeMinRadius"), styleValue(style, "proportionalSizeMaxRadius"));
|
|
1904
|
-
}
|
|
1905
|
-
/**
|
|
1906
|
-
* The validation every field-driven size range shares: a chosen field, a
|
|
1907
|
-
* finite non-degenerate value span, and finite radii. Kept in one place so a
|
|
1908
|
-
* future tightening (rejecting negative radii, say) cannot land on the
|
|
1909
|
-
* layer-wide proportional renderer without also landing on the geometry
|
|
1910
|
-
* generator's centroids.
|
|
1911
|
-
*/
|
|
1912
|
-
function validatedSizeRange(rawProperty, minValue, maxValue, minRadius, maxRadius) {
|
|
1913
|
-
const property = rawProperty.trim();
|
|
1914
|
-
if (!property) return null;
|
|
1915
|
-
if (!(Number.isFinite(minValue) && Number.isFinite(maxValue))) return null;
|
|
1916
|
-
if (maxValue <= minValue) return null;
|
|
1917
|
-
if (!(Number.isFinite(minRadius) && Number.isFinite(maxRadius))) return null;
|
|
1918
|
-
return {
|
|
1919
|
-
property,
|
|
1920
|
-
minValue,
|
|
1921
|
-
maxValue,
|
|
1922
|
-
minRadius,
|
|
1923
|
-
maxRadius
|
|
1924
|
-
};
|
|
1925
|
-
}
|
|
1926
|
-
/**
|
|
1927
|
-
* The proportional `interpolate` expression mapping
|
|
1928
|
-
* `proportionalSizeMinValue..proportionalSizeMaxValue` onto
|
|
1929
|
-
* `proportionalSizeMinRadius..proportionalSizeMaxRadius`, or `null` when
|
|
1930
|
-
* proportional sizing does not apply (see {@link proportionalSizeRange}).
|
|
1931
|
-
*
|
|
1932
|
-
* @param style - The layer style.
|
|
1933
|
-
* @returns The `interpolate` expression, or `null`.
|
|
1934
|
-
*/
|
|
1935
|
-
function proportionalRadiusExpression(style) {
|
|
1936
|
-
const range = proportionalSizeRange(style);
|
|
1937
|
-
return range ? radiusInterpolateExpression(range) : null;
|
|
1938
|
-
}
|
|
1939
|
-
/**
|
|
1940
|
-
* The `interpolate` that maps a validated range's value span onto its radius
|
|
1941
|
-
* span. Shared by the layer-wide proportional renderer and the geometry
|
|
1942
|
-
* generator's centroid sizing so both scale identically (linear, with the
|
|
1943
|
-
* value clamped to the range's ends by `interpolate`'s own stop behavior).
|
|
1944
|
-
*/
|
|
1945
|
-
function radiusInterpolateExpression(range) {
|
|
1946
|
-
return [
|
|
1947
|
-
"interpolate",
|
|
1948
|
-
["linear"],
|
|
1949
|
-
[
|
|
1950
|
-
"to-number",
|
|
1951
|
-
["get", range.property],
|
|
1952
|
-
range.minValue
|
|
1953
|
-
],
|
|
1954
|
-
range.minValue,
|
|
1955
|
-
Math.max(0, range.minRadius),
|
|
1956
|
-
range.maxValue,
|
|
1957
|
-
Math.max(0, range.maxRadius)
|
|
1958
|
-
];
|
|
1959
|
-
}
|
|
1960
|
-
/**
|
|
1961
|
-
* The geometry generator's centroid-size configuration, or `null` when the
|
|
1962
|
-
* generated points should all take the flat
|
|
1963
|
-
* {@link LayerStyle.geometryGeneratorCircleRadius}: no field chosen, non-finite
|
|
1964
|
-
* or degenerate (`maxValue <= minValue`) value range, or non-finite radii.
|
|
1965
|
-
* Mirrors {@link proportionalSizeRange} against the generator's own keys.
|
|
1966
|
-
*
|
|
1967
|
-
* @param style - The layer style.
|
|
1968
|
-
* @returns The validated range, or `null` when field-driven sizing is off.
|
|
1969
|
-
*/
|
|
1970
|
-
function generatorSizeRange(style) {
|
|
1971
|
-
return validatedSizeRange(styleValue(style, "geometryGeneratorSizeProperty"), styleValue(style, "geometryGeneratorSizeMinValue"), styleValue(style, "geometryGeneratorSizeMaxValue"), styleValue(style, "geometryGeneratorSizeMinRadius"), styleValue(style, "geometryGeneratorSizeMaxRadius"));
|
|
1972
|
-
}
|
|
1973
|
-
/**
|
|
1974
|
-
* Builds the `circle-radius` paint value for the geometry generator's derived
|
|
1975
|
-
* centroid points: an `interpolate` over
|
|
1976
|
-
* {@link LayerStyle.geometryGeneratorSizeProperty} when field-driven sizing
|
|
1977
|
-
* applies (see {@link generatorSizeRange}), otherwise the flat
|
|
1978
|
-
* {@link LayerStyle.geometryGeneratorCircleRadius}.
|
|
1979
|
-
*
|
|
1980
|
-
* The flat fallback keeps the 1px floor the generator has always applied; the
|
|
1981
|
-
* interpolate is left as authored, so a 0px min radius still hides the
|
|
1982
|
-
* smallest symbols on purpose.
|
|
1983
|
-
*
|
|
1984
|
-
* @param style - The layer style.
|
|
1985
|
-
* @returns A constant radius (pixels) or a MapLibre `interpolate` expression.
|
|
1986
|
-
*/
|
|
1987
|
-
function generatorCircleRadiusValue(style) {
|
|
1988
|
-
const range = generatorSizeRange(style);
|
|
1989
|
-
if (range) return radiusInterpolateExpression(range);
|
|
1990
|
-
return Math.max(1, styleValue(style, "geometryGeneratorCircleRadius"));
|
|
1991
|
-
}
|
|
1992
|
-
/**
|
|
1993
|
-
* Builds the `circle-radius` paint value, honoring proportional (graduated)
|
|
1994
|
-
* symbol sizing. When {@link LayerStyle.proportionalSizeEnabled} is set with a
|
|
1995
|
-
* chosen numeric field and a valid value range, returns an `interpolate` that
|
|
1996
|
-
* maps `proportionalSizeMinValue..proportionalSizeMaxValue` onto
|
|
1997
|
-
* `proportionalSizeMinRadius..proportionalSizeMaxRadius`; otherwise the constant
|
|
1998
|
-
* {@link LayerStyle.circleRadius}.
|
|
1999
|
-
*
|
|
2000
|
-
* @param style - The layer style.
|
|
2001
|
-
* @returns A constant radius (pixels) or a MapLibre `interpolate` expression.
|
|
2002
|
-
*/
|
|
2003
|
-
function circleRadiusValue(style) {
|
|
2004
|
-
return ruleOverrideValue(style, "circleRadius", baseCircleRadiusValue(style));
|
|
2005
|
-
}
|
|
2006
|
-
/** The layer-level circle radius before per-rule overrides. */
|
|
2007
|
-
function baseCircleRadiusValue(style) {
|
|
2008
|
-
return proportionalRadiusExpression(style) ?? styleValue(style, "circleRadius");
|
|
2009
|
-
}
|
|
2010
|
-
/** Fill color value for a polygon layer (fallback: the layer fill color). */
|
|
2011
|
-
function vectorFillColorValue(style) {
|
|
2012
|
-
return withSimpleStyleColor(style, "fill", vectorColorExpression(style, styleValue(style, "fillColor")));
|
|
2013
|
-
}
|
|
2014
|
-
/**
|
|
2015
|
-
* Circle color value for a point layer. Intentionally identical to
|
|
2016
|
-
* `vectorFillColorValue`: GeoLibre has no separate point-fill color, so point
|
|
2017
|
-
* circles share the polygon fill color (matching `circlePaint` in the map
|
|
2018
|
-
* package). Kept as its own function so the per-geometry callers read in
|
|
2019
|
-
* parallel and a future dedicated circle color stays a one-line change here.
|
|
2020
|
-
*/
|
|
2021
|
-
function vectorCircleColorValue(style) {
|
|
2022
|
-
return withSimpleStyleColor(style, "marker-color", vectorColorExpression(style, styleValue(style, "fillColor")));
|
|
2023
|
-
}
|
|
2024
|
-
/**
|
|
2025
|
-
* Line color value for line geometry and polygon outlines (fallback: the
|
|
2026
|
-
* layer stroke color). For non-`expression` modes the data-driven color is
|
|
2027
|
-
* applied to line geometry only, while polygon outlines keep the flat stroke
|
|
2028
|
-
* color, matching the polygon-fill-only behavior of categorized/graduated
|
|
2029
|
-
* styling.
|
|
2030
|
-
*/
|
|
2031
|
-
function vectorLineColorValue(style) {
|
|
2032
|
-
const strokeColor = styleValue(style, "strokeColor");
|
|
2033
|
-
if (styleValue(style, "vectorStyleMode") === "rule-based") return withSimpleStyleColor(style, "stroke", ruleBasedLineColorExpression(style));
|
|
2034
|
-
const vectorColor = vectorColorExpression(style, strokeColor);
|
|
2035
|
-
return withSimpleStyleColor(style, "stroke", vectorColor === strokeColor ? strokeColor : styleValue(style, "vectorStyleMode") === "expression" ? vectorColor : [
|
|
2036
|
-
"case",
|
|
2037
|
-
[
|
|
2038
|
-
"==",
|
|
2039
|
-
["geometry-type"],
|
|
2040
|
-
"Polygon"
|
|
2041
|
-
],
|
|
2042
|
-
strokeColor,
|
|
2043
|
-
vectorColor
|
|
2044
|
-
]);
|
|
2045
|
-
}
|
|
2046
|
-
/**
|
|
2047
|
-
* The line color for the rule-based renderer: line geometry takes the rule
|
|
2048
|
-
* color, polygon outlines take the per-rule stroke color override (falling
|
|
2049
|
-
* back to the flat layer stroke). Both channels are built inside one shared
|
|
2050
|
-
* zoom wrapper, since composing two independently zoom-stepped expressions in
|
|
2051
|
-
* a single `case` would nest `["zoom"]` below the top level, which MapLibre
|
|
2052
|
-
* rejects.
|
|
2053
|
-
*/
|
|
2054
|
-
function ruleBasedLineColorExpression(style) {
|
|
2055
|
-
const strokeColor = styleValue(style, "strokeColor");
|
|
2056
|
-
const { rules, elseRule } = effectiveVectorRules(style);
|
|
2057
|
-
const elseColor = elseRule && isHexColor(elseRule.color) ? elseRule.color : strokeColor;
|
|
2058
|
-
const elseOutline = readRuleOverride(elseRule ?? void 0, "strokeColor") ?? strokeColor;
|
|
2059
|
-
const hasOutlineOverride = rules.some((rule) => rule.strokeColor !== void 0) || elseOutline !== strokeColor;
|
|
2060
|
-
if (rules.length === 0 && elseColor === strokeColor && !hasOutlineOverride) return strokeColor;
|
|
2061
|
-
return zoomWrappedRuleValue(rules, (active) => {
|
|
2062
|
-
const colorValue = active.length === 0 ? elseColor : [
|
|
2063
|
-
"case",
|
|
2064
|
-
...active.flatMap((rule) => [rule.filter, rule.color]),
|
|
2065
|
-
elseColor
|
|
2066
|
-
];
|
|
2067
|
-
const outlineValue = !hasOutlineOverride ? strokeColor : active.length === 0 ? elseOutline : [
|
|
2068
|
-
"case",
|
|
2069
|
-
...active.flatMap((rule) => [rule.filter, rule.strokeColor ?? strokeColor]),
|
|
2070
|
-
elseOutline
|
|
2071
|
-
];
|
|
2072
|
-
if (colorValue === strokeColor && outlineValue === strokeColor) return strokeColor;
|
|
2073
|
-
return [
|
|
2074
|
-
"case",
|
|
2075
|
-
[
|
|
2076
|
-
"==",
|
|
2077
|
-
["geometry-type"],
|
|
2078
|
-
"Polygon"
|
|
2079
|
-
],
|
|
2080
|
-
outlineValue,
|
|
2081
|
-
colorValue
|
|
2082
|
-
];
|
|
2083
|
-
});
|
|
2084
|
-
}
|
|
2085
|
-
/**
|
|
2086
|
-
* Resolves the 3D-extrusion height for a layer style into a MapLibre value: a
|
|
2087
|
-
* plain meters number, or a data-driven expression. In advanced mode a valid
|
|
2088
|
-
* `extrusionHeightExpression` wins; otherwise the height is the chosen property
|
|
2089
|
-
* scaled by `extrusionHeightScale` (`["*", ["to-number", ["get", prop], 0],
|
|
2090
|
-
* scale]`), or a flat `0` when no property is set (so the layer renders flat
|
|
2091
|
-
* rather than erroring). Shared by the map's fill-extrusion paint and the
|
|
2092
|
-
* Add Vector Layer control's extrusion mapping so both extrude identically.
|
|
2093
|
-
*
|
|
2094
|
-
* @param style - The layer style.
|
|
2095
|
-
* @returns The extrusion height as a number or a MapLibre expression array.
|
|
2096
|
-
*/
|
|
2097
|
-
function extrusionHeightValue(style) {
|
|
2098
|
-
const advancedExpression = parseJsonExpression(styleValue(style, "extrusionAdvancedStyleEnabled") ? styleValue(style, "extrusionHeightExpression") : "");
|
|
2099
|
-
if (advancedExpression) return advancedExpression;
|
|
2100
|
-
const property = styleValue(style, "extrusionHeightProperty").trim();
|
|
2101
|
-
if (!property) return 0;
|
|
2102
|
-
const scale = styleValue(style, "extrusionHeightScale");
|
|
2103
|
-
return [
|
|
2104
|
-
"*",
|
|
2105
|
-
[
|
|
2106
|
-
"to-number",
|
|
2107
|
-
["get", property],
|
|
2108
|
-
0
|
|
2109
|
-
],
|
|
2110
|
-
scale
|
|
2111
|
-
];
|
|
2112
|
-
}
|
|
2113
|
-
/**
|
|
2114
|
-
* Resolves the 3D-extrusion color for a layer style: a data-driven expression
|
|
2115
|
-
* when the layer's symbology mode produces one (categorized/graduated/rule/
|
|
2116
|
-
* expression) or an advanced `extrusionColorExpression` is set, otherwise the
|
|
2117
|
-
* flat `extrusionColor`. Mirrors the fill-color contract so an extruded layer
|
|
2118
|
-
* honors the same attribute-driven styling.
|
|
2119
|
-
*
|
|
2120
|
-
* @param style - The layer style.
|
|
2121
|
-
* @returns A flat color string or a MapLibre color expression array.
|
|
2122
|
-
*/
|
|
2123
|
-
function extrusionColorValue(style) {
|
|
2124
|
-
const flat = styleValue(style, "extrusionColor");
|
|
2125
|
-
const vectorExpression = vectorColorExpression(style, flat);
|
|
2126
|
-
if (vectorExpression !== flat) return vectorExpression;
|
|
2127
|
-
return parseJsonExpression(styleValue(style, "extrusionAdvancedStyleEnabled") ? styleValue(style, "extrusionColorExpression") : "") ?? flat;
|
|
2128
|
-
}
|
|
2129
|
-
// src/style-layer-evaluator.ts
|
|
2130
|
-
const GEOMETRY_TYPE = {
|
|
2131
|
-
Point: 1,
|
|
2132
|
-
LineString: 2,
|
|
2133
|
-
Polygon: 3
|
|
2134
|
-
};
|
|
2135
|
-
/**
|
|
2136
|
-
* Compile a MapLibre style layer's filter and properties for evaluation
|
|
2137
|
-
* outside MapLibre, for a renderer that draws the layer itself (the ArcGIS
|
|
2138
|
-
* renderer drawing a plugin's own GeoJSON overlay, say). Uses the style spec's
|
|
2139
|
-
* own evaluator, so data-driven and zoom expressions, legacy functions and
|
|
2140
|
-
* defaults behave as they do on MapLibre. Each property compiles once.
|
|
2141
|
-
*
|
|
2142
|
-
* @param layer - The style layer (its `type`, `filter`, `paint`, `layout`).
|
|
2143
|
-
* @returns The layer's evaluator.
|
|
2144
|
-
*/
|
|
2145
|
-
function createStyleLayerEvaluator(layer) {
|
|
2146
|
-
const root = `layers[${layer.id ?? ""}]`;
|
|
2147
|
-
let filter;
|
|
2148
|
-
try {
|
|
2149
|
-
const compiled = featureFilter(layer.filter, `${root}.filter`);
|
|
2150
|
-
filter = (zoom, feature) => compiled.filter({ zoom }, feature);
|
|
2151
|
-
} catch {
|
|
2152
|
-
filter = () => false;
|
|
2153
|
-
}
|
|
2154
|
-
const compiled = /* @__PURE__ */ new Map();
|
|
2155
|
-
const compile = (group, name) => {
|
|
2156
|
-
const key = `${group}/${name}`;
|
|
2157
|
-
if (compiled.has(key)) return compiled.get(key);
|
|
2158
|
-
const spec = latest[`${group}_${layer.type}`]?.[name];
|
|
2159
|
-
let evaluate = null;
|
|
2160
|
-
if (spec) {
|
|
2161
|
-
const raw = layer[group]?.[name];
|
|
2162
|
-
try {
|
|
2163
|
-
const expression = normalizePropertyExpression(raw ?? spec.default, `${root}.${group}.${name}`, spec);
|
|
2164
|
-
evaluate = (globals, feature) => expression.evaluate(globals, feature);
|
|
2165
|
-
} catch {
|
|
2166
|
-
evaluate = null;
|
|
2167
|
-
}
|
|
2168
|
-
}
|
|
2169
|
-
compiled.set(key, evaluate);
|
|
2170
|
-
return evaluate;
|
|
2171
|
-
};
|
|
2172
|
-
const input = (feature) => ({
|
|
2173
|
-
type: GEOMETRY_TYPE[feature.kind],
|
|
2174
|
-
id: feature.id,
|
|
2175
|
-
properties: feature.properties ?? {}
|
|
2176
|
-
});
|
|
2177
|
-
return {
|
|
2178
|
-
passes: (zoom, feature) => {
|
|
2179
|
-
try {
|
|
2180
|
-
return filter(zoom, input(feature));
|
|
2181
|
-
} catch {
|
|
2182
|
-
return false;
|
|
2183
|
-
}
|
|
2184
|
-
},
|
|
2185
|
-
value: (group, name, zoom, feature) => {
|
|
2186
|
-
const evaluate = compile(group, name);
|
|
2187
|
-
if (!evaluate) return void 0;
|
|
2188
|
-
let result;
|
|
2189
|
-
try {
|
|
2190
|
-
result = evaluate({ zoom }, input(feature));
|
|
2191
|
-
} catch {
|
|
2192
|
-
return;
|
|
2193
|
-
}
|
|
2194
|
-
if (result instanceof Color) {
|
|
2195
|
-
const [r, g, b, a] = result.rgb;
|
|
2196
|
-
return [
|
|
2197
|
-
Math.round(r * 255),
|
|
2198
|
-
Math.round(g * 255),
|
|
2199
|
-
Math.round(b * 255),
|
|
2200
|
-
a
|
|
2201
|
-
];
|
|
2202
|
-
}
|
|
2203
|
-
if (result && typeof result === "object" && "sections" in result) return String(result);
|
|
2204
|
-
return result;
|
|
2205
|
-
}
|
|
2206
|
-
};
|
|
2207
|
-
}
|
|
2208
|
-
// src/document-locale.ts
|
|
2209
|
-
/**
|
|
2210
|
-
* The document language, so formatted numbers and dates follow the UI locale.
|
|
2211
|
-
*
|
|
2212
|
-
* Shared by the Identify popup's value formatting and by label number
|
|
2213
|
-
* formatting, which both fall back to it when the author has not pinned an
|
|
2214
|
-
* explicit locale. Returns `undefined` outside a browser (tests, the headless
|
|
2215
|
-
* project authoring paths), which leaves `Intl` on its own default.
|
|
2216
|
-
*/
|
|
2217
|
-
function documentLocale() {
|
|
2218
|
-
return (typeof document !== "undefined" ? document.documentElement.lang.trim() : "") || void 0;
|
|
2219
|
-
}
|
|
2220
|
-
// src/label-number-format.ts
|
|
2221
|
-
/**
|
|
2222
|
-
* Locale-aware number formatting for map labels (issue #2336).
|
|
2223
|
-
*
|
|
2224
|
-
* A numeric attribute rendered straight into a label reads as `1234567.5`,
|
|
2225
|
-
* which is hard to compare at a glance. QGIS solves this with
|
|
2226
|
-
* `format_number(field, places, language)`; the equivalent here is three
|
|
2227
|
-
* {@link LabelStyle} fields — {@link LabelStyle.numberFormatEnabled},
|
|
2228
|
-
* {@link LabelStyle.numberDecimals} and {@link LabelStyle.numberLocale} —
|
|
2229
|
-
* whose effect this module renders two ways:
|
|
2230
|
-
*
|
|
2231
|
-
* - {@link labelFieldTextField} builds the MapLibre `text-field` expression the
|
|
2232
|
-
* 2D map and the Mapbox/MapLibre style export both use, so the map and the
|
|
2233
|
-
* exported style agree.
|
|
2234
|
-
* - {@link formatLabelNumber} does the same in JavaScript for the paths that
|
|
2235
|
-
* resolve label text themselves: the Cesium globe and the duplicate-label
|
|
2236
|
-
* collapsing modes.
|
|
2237
|
-
*
|
|
2238
|
-
* Formatting applies to {@link LabelStyle.field} only. A label
|
|
2239
|
-
* {@link LabelStyle.expression} formats its own output (with `number-format`,
|
|
2240
|
-
* offered in the Expression Builder), so wrapping it here would fight the
|
|
2241
|
-
* author.
|
|
931
|
+
* Formatting applies to {@link LabelStyle.field} only. A label
|
|
932
|
+
* {@link LabelStyle.expression} formats its own output (with `number-format`,
|
|
933
|
+
* offered in the Expression Builder), so wrapping it here would fight the
|
|
934
|
+
* author.
|
|
2242
935
|
*/
|
|
2243
936
|
/**
|
|
2244
937
|
* Locales offered for {@link LabelStyle.numberLocale}, alongside the default
|
|
@@ -3375,28 +2068,364 @@ function clearQuickFilterValues(filters) {
|
|
|
3375
2068
|
text: filter.kind === "text" ? "" : filter.text
|
|
3376
2069
|
}));
|
|
3377
2070
|
}
|
|
3378
|
-
// src/
|
|
3379
|
-
/**
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
2071
|
+
// src/level-filter.ts
|
|
2072
|
+
/** Field names that usually hold a floor, in preference order (compared case-insensitively). */
|
|
2073
|
+
const LEVEL_FIELD_CANDIDATES = [
|
|
2074
|
+
"level",
|
|
2075
|
+
"ordinal",
|
|
2076
|
+
"floor",
|
|
2077
|
+
"level_ordinal",
|
|
2078
|
+
"floor_number",
|
|
2079
|
+
"floornumber",
|
|
2080
|
+
"floor_no",
|
|
2081
|
+
"storey",
|
|
2082
|
+
"story",
|
|
2083
|
+
"etage",
|
|
2084
|
+
"level_name"
|
|
2085
|
+
];
|
|
2086
|
+
const NUMBER = /^-?\d+(?:\.\d+)?$/;
|
|
2087
|
+
const RANGE = /^(-?\d+(?:\.\d+)?)\s*-\s*(-?\d+(?:\.\d+)?)$/;
|
|
2088
|
+
/** A range wider than this is read as its two ends, not every floor between. */
|
|
2089
|
+
const MAX_RANGE_STEPS = 200;
|
|
2090
|
+
function numericToken(value) {
|
|
2091
|
+
return {
|
|
2092
|
+
label: String(value),
|
|
2093
|
+
ordinal: value
|
|
2094
|
+
};
|
|
3389
2095
|
}
|
|
3390
2096
|
/**
|
|
3391
|
-
*
|
|
3392
|
-
* expression. An explicit expression and Quick Filter controls narrow the
|
|
3393
|
-
* layer together.
|
|
2097
|
+
* Parse one attribute value into the floors it names.
|
|
3394
2098
|
*
|
|
3395
|
-
*
|
|
3396
|
-
*
|
|
2099
|
+
* Numbers are taken as they are. Text is split on `;` (OSM's list separator),
|
|
2100
|
+
* each part read as a number, an integer span such as `0-2` or `-2--1`
|
|
2101
|
+
* (expanded to every floor it covers), or kept as a named floor.
|
|
2102
|
+
*
|
|
2103
|
+
* @param raw - The attribute value.
|
|
2104
|
+
* @returns The floors it names, possibly empty (for `null`, booleans, blanks).
|
|
3397
2105
|
*/
|
|
3398
|
-
function
|
|
3399
|
-
|
|
2106
|
+
function parseLevelValue(raw) {
|
|
2107
|
+
if (typeof raw === "number") return Number.isFinite(raw) ? [numericToken(raw)] : [];
|
|
2108
|
+
if (typeof raw !== "string") return [];
|
|
2109
|
+
const tokens = [];
|
|
2110
|
+
for (const part of raw.split(";")) {
|
|
2111
|
+
const text = part.trim();
|
|
2112
|
+
if (!text) continue;
|
|
2113
|
+
if (NUMBER.test(text)) {
|
|
2114
|
+
tokens.push(numericToken(Number(text)));
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
const range = RANGE.exec(text);
|
|
2118
|
+
if (range) {
|
|
2119
|
+
const a = Number(range[1]);
|
|
2120
|
+
const b = Number(range[2]);
|
|
2121
|
+
const lo = Math.min(a, b);
|
|
2122
|
+
const hi = Math.max(a, b);
|
|
2123
|
+
if (Number.isSafeInteger(lo) && Number.isSafeInteger(hi) && hi - lo <= MAX_RANGE_STEPS) for (let level = lo; level <= hi; level += 1) tokens.push(numericToken(level));
|
|
2124
|
+
else tokens.push(numericToken(lo), numericToken(hi));
|
|
2125
|
+
continue;
|
|
2126
|
+
}
|
|
2127
|
+
tokens.push({
|
|
2128
|
+
label: text,
|
|
2129
|
+
ordinal: null
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
return tokens;
|
|
2133
|
+
}
|
|
2134
|
+
/** The ordinal of a picker label: its number, or `null` for a named floor. */
|
|
2135
|
+
function labelOrdinal(label) {
|
|
2136
|
+
return NUMBER.test(label.trim()) ? Number(label) : null;
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Whether a feature's level value falls on the chosen floor.
|
|
2140
|
+
*
|
|
2141
|
+
* @param raw - The feature's level attribute.
|
|
2142
|
+
* @param level - The chosen floor's label.
|
|
2143
|
+
* @param mode - `only` matches that floor; `below` also matches every
|
|
2144
|
+
* numbered floor under it (a named floor matches only itself). `all`
|
|
2145
|
+
* matches everything.
|
|
2146
|
+
* @returns True when the feature should be drawn.
|
|
2147
|
+
*/
|
|
2148
|
+
function levelValueMatches(raw, level, mode) {
|
|
2149
|
+
if (mode === "all") return true;
|
|
2150
|
+
const tokens = parseLevelValue(raw);
|
|
2151
|
+
const target = labelOrdinal(level);
|
|
2152
|
+
const label = target === null ? level.trim() : String(target);
|
|
2153
|
+
return tokens.some((token) => {
|
|
2154
|
+
if (token.label === label) return true;
|
|
2155
|
+
return mode === "below" && target !== null && token.ordinal !== null && token.ordinal <= target;
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
/**
|
|
2159
|
+
* Order floors top down, the way a building directory or lift panel lists
|
|
2160
|
+
* them: numbered floors from highest to lowest, then named floors by name.
|
|
2161
|
+
*/
|
|
2162
|
+
function compareLevels(a, b) {
|
|
2163
|
+
if (a.ordinal !== null && b.ordinal !== null) return b.ordinal - a.ordinal;
|
|
2164
|
+
if (a.ordinal !== null) return -1;
|
|
2165
|
+
if (b.ordinal !== null) return 1;
|
|
2166
|
+
return a.label.localeCompare(b.label);
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* The distinct floors a set of features holds in one field, top floor first.
|
|
2170
|
+
*
|
|
2171
|
+
* @param features - The layer's features.
|
|
2172
|
+
* @param field - The property holding the floor.
|
|
2173
|
+
* @returns One entry per floor with its feature count.
|
|
2174
|
+
*/
|
|
2175
|
+
function collectLevels(features, field) {
|
|
2176
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
2177
|
+
for (const feature of features ?? []) {
|
|
2178
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2179
|
+
for (const token of parseLevelValue(feature?.properties?.[field])) {
|
|
2180
|
+
if (seen.has(token.label)) continue;
|
|
2181
|
+
seen.add(token.label);
|
|
2182
|
+
const entry = byLabel.get(token.label);
|
|
2183
|
+
if (entry) entry.count += 1;
|
|
2184
|
+
else byLabel.set(token.label, {
|
|
2185
|
+
label: token.label,
|
|
2186
|
+
ordinal: token.ordinal,
|
|
2187
|
+
count: 1
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
return [...byLabel.values()].sort(compareLevels);
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Merge the floor lists of several layers into one, top floor first, summing
|
|
2195
|
+
* the counts of a floor that appears in more than one layer.
|
|
2196
|
+
*
|
|
2197
|
+
* @param lists - One {@link collectLevels} result per linked layer.
|
|
2198
|
+
* @returns The combined floor list.
|
|
2199
|
+
*/
|
|
2200
|
+
function mergeLevels(lists) {
|
|
2201
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
2202
|
+
for (const list of lists) for (const entry of list) {
|
|
2203
|
+
const existing = byLabel.get(entry.label);
|
|
2204
|
+
if (existing) existing.count += entry.count;
|
|
2205
|
+
else byLabel.set(entry.label, { ...entry });
|
|
2206
|
+
}
|
|
2207
|
+
return [...byLabel.values()].sort(compareLevels);
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* The distinct raw values of `field` that match a floor choice, in first-seen
|
|
2211
|
+
* order. These are what {@link compileLevelFilter} tests with `in`.
|
|
2212
|
+
*
|
|
2213
|
+
* @param features - The layer's features.
|
|
2214
|
+
* @param field - The property holding the floor.
|
|
2215
|
+
* @param level - The chosen floor's label.
|
|
2216
|
+
* @param mode - The floor picker mode.
|
|
2217
|
+
* @returns The matching values (strings and numbers kept as stored).
|
|
2218
|
+
*/
|
|
2219
|
+
function matchingLevelValues(features, field, level, mode) {
|
|
2220
|
+
const values = [];
|
|
2221
|
+
const checked = /* @__PURE__ */ new Set();
|
|
2222
|
+
for (const feature of features ?? []) {
|
|
2223
|
+
const raw = feature?.properties?.[field];
|
|
2224
|
+
if (typeof raw !== "string" && typeof raw !== "number") continue;
|
|
2225
|
+
if (checked.has(raw)) continue;
|
|
2226
|
+
checked.add(raw);
|
|
2227
|
+
if (levelValueMatches(raw, level, mode)) values.push(raw);
|
|
2228
|
+
}
|
|
2229
|
+
return values;
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* Apply a floor choice to a layer's link, resolving the matching values from
|
|
2233
|
+
* the layer's features.
|
|
2234
|
+
*
|
|
2235
|
+
* Without features (a tile-backed layer, or data not loaded yet) the values
|
|
2236
|
+
* stay unresolved, which draws every floor, rather than resolving to an empty
|
|
2237
|
+
* list that would hide the layer once its data arrives.
|
|
2238
|
+
*
|
|
2239
|
+
* @param filter - The layer's current link.
|
|
2240
|
+
* @param features - The layer's features, or `null`/`undefined` when none are loaded.
|
|
2241
|
+
* @param level - The chosen floor's label, or `null` for every floor.
|
|
2242
|
+
* @param mode - The floor picker mode.
|
|
2243
|
+
* @returns The updated link.
|
|
2244
|
+
*/
|
|
2245
|
+
function selectLevel(filter, features, level, mode) {
|
|
2246
|
+
if (level === null || mode === "all") return {
|
|
2247
|
+
field: filter.field,
|
|
2248
|
+
mode: "all",
|
|
2249
|
+
level: null
|
|
2250
|
+
};
|
|
2251
|
+
if (!features) return {
|
|
2252
|
+
field: filter.field,
|
|
2253
|
+
mode,
|
|
2254
|
+
level
|
|
2255
|
+
};
|
|
2256
|
+
return {
|
|
2257
|
+
field: filter.field,
|
|
2258
|
+
mode,
|
|
2259
|
+
level,
|
|
2260
|
+
values: matchingLevelValues(features, filter.field, level, mode)
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* The floor choice the picker shows: the first linked layer that has a floor
|
|
2265
|
+
* chosen, so a layer linked later (still on `all`) does not reset the display.
|
|
2266
|
+
*
|
|
2267
|
+
* @param linked - The linked layers.
|
|
2268
|
+
* @returns The chosen level and mode, or every floor.
|
|
2269
|
+
*/
|
|
2270
|
+
function currentLevelChoice(linked) {
|
|
2271
|
+
for (const layer of linked) {
|
|
2272
|
+
const link = layer.levelFilter;
|
|
2273
|
+
if (link && link.mode && link.mode !== "all" && typeof link.level === "string") return {
|
|
2274
|
+
level: link.level,
|
|
2275
|
+
mode: link.mode
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
return {
|
|
2279
|
+
level: null,
|
|
2280
|
+
mode: "all"
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Whether two links store the same choice, independent of key order (a
|
|
2285
|
+
* project written by the Python API may order the keys differently).
|
|
2286
|
+
*
|
|
2287
|
+
* @param a - One link.
|
|
2288
|
+
* @param b - The other.
|
|
2289
|
+
* @returns True when they draw the same features.
|
|
2290
|
+
*/
|
|
2291
|
+
function sameLevelLink(a, b) {
|
|
2292
|
+
if (a.field !== b.field) return false;
|
|
2293
|
+
if ((a.mode ?? "all") !== (b.mode ?? "all")) return false;
|
|
2294
|
+
if ((a.level ?? null) !== (b.level ?? null)) return false;
|
|
2295
|
+
const av = a.values;
|
|
2296
|
+
const bv = b.values;
|
|
2297
|
+
if (!Array.isArray(av) || !Array.isArray(bv)) return av === bv;
|
|
2298
|
+
return av.length === bv.length && av.every((value, index) => value === bv[index]);
|
|
2299
|
+
}
|
|
2300
|
+
/** Resolved links per feature array and choice, so an unrelated layer edit does not rescan. */
|
|
2301
|
+
const resolvedLinks = /* @__PURE__ */ new WeakMap();
|
|
2302
|
+
/** {@link selectLevel}, memoized on the feature array (replaced whenever the data changes). */
|
|
2303
|
+
function cachedSelectLevel(link, features, choice) {
|
|
2304
|
+
const key = JSON.stringify([
|
|
2305
|
+
link.field,
|
|
2306
|
+
choice.level,
|
|
2307
|
+
choice.mode
|
|
2308
|
+
]);
|
|
2309
|
+
let byChoice = resolvedLinks.get(features);
|
|
2310
|
+
if (!byChoice) {
|
|
2311
|
+
byChoice = /* @__PURE__ */ new Map();
|
|
2312
|
+
resolvedLinks.set(features, byChoice);
|
|
2313
|
+
}
|
|
2314
|
+
let next = byChoice.get(key);
|
|
2315
|
+
if (!next) {
|
|
2316
|
+
next = selectLevel(link, features, choice.level, choice.mode);
|
|
2317
|
+
byChoice.set(key, next);
|
|
2318
|
+
}
|
|
2319
|
+
return next;
|
|
2320
|
+
}
|
|
2321
|
+
/**
|
|
2322
|
+
* The link updates that put every linked layer on one floor choice, with the
|
|
2323
|
+
* matching values resolved from each layer's own features: a layer linked
|
|
2324
|
+
* after a floor was chosen, a project written without the values (the Python
|
|
2325
|
+
* API without inline data), or data that changed since the choice was made.
|
|
2326
|
+
*
|
|
2327
|
+
* @param linked - The linked layers.
|
|
2328
|
+
* @param choice - The floor to apply; defaults to {@link currentLevelChoice}.
|
|
2329
|
+
* @returns One update per layer whose stored link differs.
|
|
2330
|
+
*/
|
|
2331
|
+
function resolveLevelLinks(linked, choice = currentLevelChoice(linked)) {
|
|
2332
|
+
const updates = [];
|
|
2333
|
+
for (const layer of linked) {
|
|
2334
|
+
const link = layer.levelFilter;
|
|
2335
|
+
if (!link) continue;
|
|
2336
|
+
const features = layer.geojson?.features;
|
|
2337
|
+
if (!features) continue;
|
|
2338
|
+
const next = cachedSelectLevel(link, features, choice);
|
|
2339
|
+
if (!sameLevelLink(link, next)) updates.push({
|
|
2340
|
+
id: layer.id,
|
|
2341
|
+
levelFilter: next
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
return updates;
|
|
2345
|
+
}
|
|
2346
|
+
/**
|
|
2347
|
+
* Whether a link narrows the layer: a floor is chosen and its values resolved.
|
|
2348
|
+
*
|
|
2349
|
+
* @param filter - The layer's link, if any.
|
|
2350
|
+
* @returns True when {@link compileLevelFilter} would return a filter.
|
|
2351
|
+
*/
|
|
2352
|
+
function isLevelFilterActive(filter) {
|
|
2353
|
+
return !!filter && typeof filter.field === "string" && filter.field.length > 0 && (filter.mode === "only" || filter.mode === "below") && typeof filter.level === "string" && Array.isArray(filter.values);
|
|
2354
|
+
}
|
|
2355
|
+
/**
|
|
2356
|
+
* Compile a floor picker link into a MapLibre filter expression.
|
|
2357
|
+
*
|
|
2358
|
+
* @param filter - The layer's link, if any.
|
|
2359
|
+
* @returns An `in` test over the resolved values, or `null` when every floor
|
|
2360
|
+
* is drawn (no link, `all` mode, no floor chosen, or values unresolved).
|
|
2361
|
+
*/
|
|
2362
|
+
function compileLevelFilter(filter) {
|
|
2363
|
+
if (!isLevelFilterActive(filter)) return null;
|
|
2364
|
+
const values = (filter.values ?? []).filter((value) => typeof value === "string" || typeof value === "number" && Number.isFinite(value));
|
|
2365
|
+
return [
|
|
2366
|
+
"in",
|
|
2367
|
+
["get", filter.field],
|
|
2368
|
+
["literal", values]
|
|
2369
|
+
];
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* The floor a feature is on, as the floor picker labels it: the first floor its
|
|
2373
|
+
* level value names that the picker lists (a room spanning floors `0;1` goes
|
|
2374
|
+
* to floor 0), or `null` when it names none of them.
|
|
2375
|
+
*
|
|
2376
|
+
* @param raw - The feature's level attribute.
|
|
2377
|
+
* @param levels - The floors the picker lists.
|
|
2378
|
+
* @returns The floor's label, or `null`.
|
|
2379
|
+
*/
|
|
2380
|
+
function featureLevel(raw, levels) {
|
|
2381
|
+
const listed = new Set(levels.map((entry) => entry.label));
|
|
2382
|
+
for (const token of parseLevelValue(raw)) if (listed.has(token.label)) return token.label;
|
|
2383
|
+
return null;
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Guess which field holds the floor.
|
|
2387
|
+
*
|
|
2388
|
+
* @param fields - The layer's field names.
|
|
2389
|
+
* @returns The best candidate, or `null` when none looks like a floor field.
|
|
2390
|
+
*/
|
|
2391
|
+
function guessLevelField(fields) {
|
|
2392
|
+
const byLower = /* @__PURE__ */ new Map();
|
|
2393
|
+
for (const field of fields) {
|
|
2394
|
+
const lower = field.toLowerCase();
|
|
2395
|
+
if (!byLower.has(lower)) byLower.set(lower, field);
|
|
2396
|
+
}
|
|
2397
|
+
for (const candidate of LEVEL_FIELD_CANDIDATES) {
|
|
2398
|
+
const field = byLower.get(candidate);
|
|
2399
|
+
if (field) return field;
|
|
2400
|
+
}
|
|
2401
|
+
return null;
|
|
2402
|
+
}
|
|
2403
|
+
// src/layer-filters.ts
|
|
2404
|
+
/**
|
|
2405
|
+
* Return a layer's persisted expression filter when it has a usable array
|
|
2406
|
+
* shape, otherwise return null.
|
|
2407
|
+
*
|
|
2408
|
+
* @param layer - The layer whose expression filter should be read.
|
|
2409
|
+
* @returns The active expression filter, or null when none is configured.
|
|
2410
|
+
*/
|
|
2411
|
+
function activeLayerFilterExpression(layer) {
|
|
2412
|
+
const expression = layer.filterExpression;
|
|
2413
|
+
return Array.isArray(expression) && expression.length > 0 ? expression : null;
|
|
2414
|
+
}
|
|
2415
|
+
/**
|
|
2416
|
+
* Compile all project-authored filters for a layer into one MapLibre boolean
|
|
2417
|
+
* expression. An explicit expression, Quick Filter controls and the floor
|
|
2418
|
+
* picker's choice narrow the layer together.
|
|
2419
|
+
*
|
|
2420
|
+
* @param layer - The layer whose authored filters should be composed.
|
|
2421
|
+
* @returns A MapLibre filter expression, or null when no authored filter is active.
|
|
2422
|
+
*/
|
|
2423
|
+
function compileLayerFilters(layer) {
|
|
2424
|
+
const filters = [
|
|
2425
|
+
activeLayerFilterExpression(layer),
|
|
2426
|
+
compileQuickFilters(layer.quickFilters),
|
|
2427
|
+
compileLevelFilter(layer.levelFilter)
|
|
2428
|
+
].filter((filter) => filter !== null);
|
|
3400
2429
|
if (filters.length === 0) return null;
|
|
3401
2430
|
return filters.length === 1 ? filters[0] : ["all", ...filters];
|
|
3402
2431
|
}
|
|
@@ -3404,11 +2433,379 @@ function compileLayerFilters(layer) {
|
|
|
3404
2433
|
* Whether a project-authored expression or Quick Filter currently narrows a
|
|
3405
2434
|
* layer. This drives the filtered indicator and clear action in the layer UI.
|
|
3406
2435
|
*
|
|
2436
|
+
* The floor picker's choice is left out on purpose: the picker is its own
|
|
2437
|
+
* on-map control that shows and resets the choice for every linked layer at
|
|
2438
|
+
* once, so a per-layer "clear filters" action must not desynchronize it.
|
|
2439
|
+
* Renderers still apply it through {@link compileLayerFilters}.
|
|
2440
|
+
*
|
|
3407
2441
|
* @param layer - The layer to inspect.
|
|
3408
|
-
* @returns True when at least one persisted
|
|
2442
|
+
* @returns True when at least one persisted expression or Quick Filter is active.
|
|
3409
2443
|
*/
|
|
3410
2444
|
function hasActiveLayerFilter(layer) {
|
|
3411
|
-
return
|
|
2445
|
+
return activeLayerFilterExpression(layer) !== null || compileQuickFilters(layer.quickFilters) !== null;
|
|
2446
|
+
}
|
|
2447
|
+
/** MapLibre's feature type codes, which `featureFilter` reads for `$type`. */
|
|
2448
|
+
const FEATURE_TYPE_CODES = {
|
|
2449
|
+
Point: 1,
|
|
2450
|
+
MultiPoint: 1,
|
|
2451
|
+
LineString: 2,
|
|
2452
|
+
MultiLineString: 2,
|
|
2453
|
+
Polygon: 3,
|
|
2454
|
+
MultiPolygon: 3
|
|
2455
|
+
};
|
|
2456
|
+
/** The last filtered result per collection, so an unchanged filter reuses it. */
|
|
2457
|
+
const filteredCollections = /* @__PURE__ */ new WeakMap();
|
|
2458
|
+
/**
|
|
2459
|
+
* A layer's features narrowed by its authored filters (an expression, Quick
|
|
2460
|
+
* Filters, the floor picker), for a renderer that draws GeoJSON itself rather
|
|
2461
|
+
* than through a MapLibre layer filter, such as the deck.gl 3D Z-value path.
|
|
2462
|
+
*
|
|
2463
|
+
* The same collection comes back while no filter is active, and the same
|
|
2464
|
+
* filtered collection while the filter is unchanged, so caches keyed on the
|
|
2465
|
+
* collection keep hitting. A filter that does not compile leaves every
|
|
2466
|
+
* feature in, as MapLibre would refuse it rather than hide the layer.
|
|
2467
|
+
*
|
|
2468
|
+
* @param layer - The layer whose filters apply.
|
|
2469
|
+
* @param geojson - The features to filter.
|
|
2470
|
+
* @param zoom - The zoom a `["zoom"]` expression sees.
|
|
2471
|
+
* @returns The matching features.
|
|
2472
|
+
*/
|
|
2473
|
+
function applyLayerFilters(layer, geojson, zoom = 0) {
|
|
2474
|
+
const filter = compileLayerFilters(layer);
|
|
2475
|
+
if (!filter) return geojson;
|
|
2476
|
+
const key = `${zoom}|${JSON.stringify(filter)}`;
|
|
2477
|
+
const cached = filteredCollections.get(geojson);
|
|
2478
|
+
if (cached?.key === key) return cached.result;
|
|
2479
|
+
let compiled;
|
|
2480
|
+
try {
|
|
2481
|
+
compiled = featureFilter(filter, "layers[0].filter");
|
|
2482
|
+
} catch {
|
|
2483
|
+
return geojson;
|
|
2484
|
+
}
|
|
2485
|
+
const result = {
|
|
2486
|
+
...geojson,
|
|
2487
|
+
features: geojson.features.filter((feature) => compiled.filter({ zoom }, {
|
|
2488
|
+
type: FEATURE_TYPE_CODES[feature.geometry?.type ?? ""] ?? 1,
|
|
2489
|
+
properties: feature.properties ?? {},
|
|
2490
|
+
...feature.id !== void 0 ? { id: feature.id } : {}
|
|
2491
|
+
}))
|
|
2492
|
+
};
|
|
2493
|
+
filteredCollections.set(geojson, {
|
|
2494
|
+
key,
|
|
2495
|
+
result
|
|
2496
|
+
});
|
|
2497
|
+
return result;
|
|
2498
|
+
}
|
|
2499
|
+
// src/zip-directory.ts
|
|
2500
|
+
const EOCD_SIGNATURE = 101010256;
|
|
2501
|
+
const EOCD_SIZE = 22;
|
|
2502
|
+
const ZIP64_LOCATOR_SIGNATURE = 117853008;
|
|
2503
|
+
const ZIP64_LOCATOR_SIZE = 20;
|
|
2504
|
+
const ZIP64_EOCD_SIGNATURE = 101075792;
|
|
2505
|
+
const ZIP64_EOCD_SIZE = 56;
|
|
2506
|
+
const CENTRAL_ENTRY_SIGNATURE = 33639248;
|
|
2507
|
+
const CENTRAL_ENTRY_SIZE = 46;
|
|
2508
|
+
/** General purpose flag bit 11: the entry name is UTF-8 rather than CP437. */
|
|
2509
|
+
const UTF8_NAME_FLAG = 2048;
|
|
2510
|
+
/** The upper half (0x80-0xff) of code page 437, the zip default for names. */
|
|
2511
|
+
const CP437_HIGH = "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\xA0";
|
|
2512
|
+
/** Longest possible zip comment plus the record that precedes it. */
|
|
2513
|
+
const MAX_EOCD_SEARCH = 65557;
|
|
2514
|
+
/** End-record candidates tried before giving up, so a hostile comment stays cheap. */
|
|
2515
|
+
const MAX_EOCD_CANDIDATES = 16;
|
|
2516
|
+
const UTF8_DECODER = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
2517
|
+
const STRICT_UTF8_DECODER = new TextDecoder("utf-8", {
|
|
2518
|
+
fatal: true,
|
|
2519
|
+
ignoreBOM: true
|
|
2520
|
+
});
|
|
2521
|
+
/** A central directory beyond this is not a file a handler should inspect. */
|
|
2522
|
+
const MAX_CENTRAL_DIRECTORY_BYTES = 16777216;
|
|
2523
|
+
function viewOf(bytes) {
|
|
2524
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
2525
|
+
}
|
|
2526
|
+
function decodeCp437(bytes) {
|
|
2527
|
+
let name = "";
|
|
2528
|
+
for (const byte of bytes) name += byte < 128 ? String.fromCharCode(byte) : CP437_HIGH[byte - 128];
|
|
2529
|
+
return name;
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* Decode an entry name. Flag bit 11 says UTF-8. Without it the zip default is
|
|
2533
|
+
* CP437, but some writers (macOS "Compress") store UTF-8 without the flag, so
|
|
2534
|
+
* a name that is valid UTF-8 is read as such before falling back to CP437.
|
|
2535
|
+
*/
|
|
2536
|
+
function decodeEntryName(name, flags) {
|
|
2537
|
+
if (flags & UTF8_NAME_FLAG) return UTF8_DECODER.decode(name);
|
|
2538
|
+
if (name.some((byte) => byte >= 128)) try {
|
|
2539
|
+
return STRICT_UTF8_DECODER.decode(name);
|
|
2540
|
+
} catch {
|
|
2541
|
+
return decodeCp437(name);
|
|
2542
|
+
}
|
|
2543
|
+
return decodeCp437(name);
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* List the paths inside a zip from its central directory, reading only the tail
|
|
2547
|
+
* of the archive rather than the whole file.
|
|
2548
|
+
*
|
|
2549
|
+
* Deliberately not listed (the result is undefined, so a handler can still claim
|
|
2550
|
+
* by file name): archives with bytes in front of them (self-extracting zips),
|
|
2551
|
+
* and spanned or multi-disk archives. The entries are a hint for `claim`, not a full zip
|
|
2552
|
+
* parser; a handler that needs these cases unpacks the file itself in `open`.
|
|
2553
|
+
*
|
|
2554
|
+
* @param size - The archive's size in bytes.
|
|
2555
|
+
* @param readRange - Reads a byte range of the archive.
|
|
2556
|
+
* @returns The entry paths in archive order, or undefined when the file has no
|
|
2557
|
+
* readable central directory (not a zip, or truncated).
|
|
2558
|
+
*/
|
|
2559
|
+
async function listZipEntryPaths(size, readRange) {
|
|
2560
|
+
if (size < EOCD_SIZE) return void 0;
|
|
2561
|
+
const tailLength = Math.min(size, MAX_EOCD_SEARCH);
|
|
2562
|
+
const tailOffset = size - tailLength;
|
|
2563
|
+
const tail = await readRange(tailOffset, tailLength);
|
|
2564
|
+
const tailView = viewOf(tail);
|
|
2565
|
+
/** Whether a central directory entry starts at `offset`, with `length` bytes in range. */
|
|
2566
|
+
const startsDirectory = async (offset, length) => {
|
|
2567
|
+
if (length < CENTRAL_ENTRY_SIZE) return false;
|
|
2568
|
+
if (offset + length > size) return false;
|
|
2569
|
+
if (offset >= tailOffset && offset + 4 <= tailOffset + tail.byteLength) return tailView.getUint32(offset - tailOffset, true) === CENTRAL_ENTRY_SIGNATURE;
|
|
2570
|
+
const head = await readRange(offset, 4);
|
|
2571
|
+
return head.byteLength === 4 && viewOf(head).getUint32(0, true) === CENTRAL_ENTRY_SIGNATURE;
|
|
2572
|
+
};
|
|
2573
|
+
/** The directory the end record at `eocd` declares, if one really starts there. */
|
|
2574
|
+
const checkRecord = async (eocd) => {
|
|
2575
|
+
const disk = tailView.getUint16(eocd + 4, true);
|
|
2576
|
+
const directoryDisk = tailView.getUint16(eocd + 6, true);
|
|
2577
|
+
if (disk !== 0 || directoryDisk !== 0) return void 0;
|
|
2578
|
+
let total = tailView.getUint16(eocd + 10, true);
|
|
2579
|
+
let recordOffset = tailOffset + eocd;
|
|
2580
|
+
let length = tailView.getUint32(eocd + 12, true);
|
|
2581
|
+
let offset = tailView.getUint32(eocd + 16, true);
|
|
2582
|
+
if (length === 4294967295 || offset === 4294967295) {
|
|
2583
|
+
const locator = eocd - ZIP64_LOCATOR_SIZE;
|
|
2584
|
+
if (locator < 0 || tailView.getUint32(locator, true) !== ZIP64_LOCATOR_SIGNATURE) return;
|
|
2585
|
+
if (tailView.getUint32(locator + 4, true) !== 0) return void 0;
|
|
2586
|
+
if (tailView.getUint32(locator + 16, true) > 1) return void 0;
|
|
2587
|
+
const zip64Offset = Number(tailView.getBigUint64(locator + 8, true));
|
|
2588
|
+
if (zip64Offset + ZIP64_EOCD_SIZE > size) return void 0;
|
|
2589
|
+
const zip64Bytes = await readRange(zip64Offset, ZIP64_EOCD_SIZE);
|
|
2590
|
+
if (zip64Bytes.byteLength !== ZIP64_EOCD_SIZE) return void 0;
|
|
2591
|
+
const zip64 = viewOf(zip64Bytes);
|
|
2592
|
+
if (zip64.getUint32(0, true) !== ZIP64_EOCD_SIGNATURE) return void 0;
|
|
2593
|
+
if (zip64.getUint32(16, true) !== 0 || zip64.getUint32(20, true) !== 0) return void 0;
|
|
2594
|
+
recordOffset = zip64Offset;
|
|
2595
|
+
total = Number(zip64.getBigUint64(32, true));
|
|
2596
|
+
length = Number(zip64.getBigUint64(40, true));
|
|
2597
|
+
offset = Number(zip64.getBigUint64(48, true));
|
|
2598
|
+
}
|
|
2599
|
+
if (length === 0) return total === 0 && offset === recordOffset ? [offset, 0] : void 0;
|
|
2600
|
+
return await startsDirectory(offset, length) ? [offset, length] : void 0;
|
|
2601
|
+
};
|
|
2602
|
+
const exact = [];
|
|
2603
|
+
const others = [];
|
|
2604
|
+
for (let eocd = tail.byteLength - EOCD_SIZE; eocd >= 0; eocd -= 1) {
|
|
2605
|
+
if (tailView.getUint32(eocd, true) !== EOCD_SIGNATURE) continue;
|
|
2606
|
+
if (eocd + EOCD_SIZE + tailView.getUint16(eocd + 20, true) === tail.byteLength) exact.push(eocd);
|
|
2607
|
+
else others.push(eocd);
|
|
2608
|
+
}
|
|
2609
|
+
const candidates = [...exact.reverse(), ...others];
|
|
2610
|
+
let located;
|
|
2611
|
+
for (const eocd of candidates.slice(0, MAX_EOCD_CANDIDATES)) {
|
|
2612
|
+
located = await checkRecord(eocd);
|
|
2613
|
+
if (located) break;
|
|
2614
|
+
}
|
|
2615
|
+
if (!located) return void 0;
|
|
2616
|
+
const [directoryOffset, directorySize] = located;
|
|
2617
|
+
if (directorySize > MAX_CENTRAL_DIRECTORY_BYTES || directoryOffset + directorySize > size) return;
|
|
2618
|
+
const directory = await readRange(directoryOffset, directorySize);
|
|
2619
|
+
const view = viewOf(directory);
|
|
2620
|
+
const paths = [];
|
|
2621
|
+
let cursor = 0;
|
|
2622
|
+
while (cursor + CENTRAL_ENTRY_SIZE <= directory.byteLength) {
|
|
2623
|
+
if (view.getUint32(cursor, true) !== CENTRAL_ENTRY_SIGNATURE) return void 0;
|
|
2624
|
+
const flags = view.getUint16(cursor + 8, true);
|
|
2625
|
+
const nameLength = view.getUint16(cursor + 28, true);
|
|
2626
|
+
const extraLength = view.getUint16(cursor + 30, true);
|
|
2627
|
+
const commentLength = view.getUint16(cursor + 32, true);
|
|
2628
|
+
const nameStart = cursor + CENTRAL_ENTRY_SIZE;
|
|
2629
|
+
if (nameStart + nameLength > directory.byteLength) return void 0;
|
|
2630
|
+
const name = directory.subarray(nameStart, nameStart + nameLength);
|
|
2631
|
+
paths.push(decodeEntryName(name, flags));
|
|
2632
|
+
cursor = nameStart + nameLength + extraLength + commentLength;
|
|
2633
|
+
}
|
|
2634
|
+
return paths;
|
|
2635
|
+
}
|
|
2636
|
+
// src/imdf-detect.ts
|
|
2637
|
+
/**
|
|
2638
|
+
* Recognizing an IMDF (Indoor Mapping Data Format) archive without unpacking it
|
|
2639
|
+
* (issue #3341). One implementation for every entry point: the app's file and
|
|
2640
|
+
* drop loaders and the Add Vector panel's picker and URL loads.
|
|
2641
|
+
*
|
|
2642
|
+
* Only the zip's central directory is read (its tail, through a byte-range
|
|
2643
|
+
* reader), so ruling out a large shapefile zip costs a few kilobytes.
|
|
2644
|
+
*/
|
|
2645
|
+
/** Bytes read from the end of an archive: the longest zip comment plus its end record. */
|
|
2646
|
+
const TAIL_BYTES = 65557;
|
|
2647
|
+
/**
|
|
2648
|
+
* Largest archive downloaded whole just to look inside it, when a server will
|
|
2649
|
+
* not serve a byte range. IMDF venues are small; a larger zip is left to the
|
|
2650
|
+
* vector control, which downloads it once anyway.
|
|
2651
|
+
*/
|
|
2652
|
+
const MAX_PROBE_DOWNLOAD_BYTES = 33554432;
|
|
2653
|
+
/** Whether an archive entry is macOS resource-fork noise (`__MACOSX/`, `._name`). */
|
|
2654
|
+
function isMacOsZipMetadata(path) {
|
|
2655
|
+
const base = path.split("/").pop() ?? "";
|
|
2656
|
+
return path.startsWith("__MACOSX/") || path.includes("/__MACOSX/") || base.startsWith("._");
|
|
2657
|
+
}
|
|
2658
|
+
/**
|
|
2659
|
+
* Whether a list of zip entry paths is an IMDF archive: a `manifest.json` beside
|
|
2660
|
+
* a `venue`, `level` or `unit` GeoJSON file, in any folder, ignoring case and
|
|
2661
|
+
* macOS metadata entries.
|
|
2662
|
+
*
|
|
2663
|
+
* @param paths - The archive's entry paths.
|
|
2664
|
+
* @returns True for an IMDF archive.
|
|
2665
|
+
*/
|
|
2666
|
+
function isImdfArchiveEntries(paths) {
|
|
2667
|
+
let manifest = false;
|
|
2668
|
+
let core = false;
|
|
2669
|
+
for (const path of paths) {
|
|
2670
|
+
if (isMacOsZipMetadata(path)) continue;
|
|
2671
|
+
const name = path.split("/").pop()?.toLowerCase() ?? "";
|
|
2672
|
+
if (name === "manifest.json") manifest = true;
|
|
2673
|
+
if (name === "venue.geojson" || name === "level.geojson" || name === "unit.geojson") core = true;
|
|
2674
|
+
}
|
|
2675
|
+
return manifest && core;
|
|
2676
|
+
}
|
|
2677
|
+
/** A byte-range reader over a `Blob` (or `File`), reading only the slices asked for. */
|
|
2678
|
+
function blobRangeReader(blob) {
|
|
2679
|
+
return async (offset, length) => new Uint8Array(await blob.slice(offset, offset + length).arrayBuffer());
|
|
2680
|
+
}
|
|
2681
|
+
/** A byte-range reader over bytes already in memory. */
|
|
2682
|
+
function bytesRangeReader(bytes) {
|
|
2683
|
+
return async (offset, length) => bytes.subarray(offset, offset + length);
|
|
2684
|
+
}
|
|
2685
|
+
/**
|
|
2686
|
+
* Whether an archive of `size` bytes is IMDF, reading only its central
|
|
2687
|
+
* directory through `readRange`.
|
|
2688
|
+
*
|
|
2689
|
+
* @param size - The archive's size in bytes.
|
|
2690
|
+
* @param readRange - Reads a byte range of the archive.
|
|
2691
|
+
* @returns True for an IMDF archive; false for any other zip or a non-zip.
|
|
2692
|
+
*/
|
|
2693
|
+
async function isImdfZipRange(size, readRange) {
|
|
2694
|
+
try {
|
|
2695
|
+
const paths = await listZipEntryPaths(size, readRange);
|
|
2696
|
+
return paths !== void 0 && isImdfArchiveEntries(paths);
|
|
2697
|
+
} catch {
|
|
2698
|
+
return false;
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Whether a `Blob` (a picked or dropped file) is an IMDF zip, reading only its
|
|
2703
|
+
* central directory.
|
|
2704
|
+
*
|
|
2705
|
+
* @param blob - The file.
|
|
2706
|
+
* @returns True for an IMDF archive.
|
|
2707
|
+
*/
|
|
2708
|
+
function isImdfZipBlob(blob) {
|
|
2709
|
+
return isImdfZipRange(blob.size, blobRangeReader(blob));
|
|
2710
|
+
}
|
|
2711
|
+
/**
|
|
2712
|
+
* Probe a zip URL for IMDF with HTTP range requests: the tail first (which
|
|
2713
|
+
* also gives the archive's size), then the central directory if it starts
|
|
2714
|
+
* before the tail.
|
|
2715
|
+
*
|
|
2716
|
+
* Two hosts need more. One that ignores `Range` answers with the whole file
|
|
2717
|
+
* (status 200); one that serves ranges cross-origin without exposing
|
|
2718
|
+
* `Content-Range` leaves the archive's size unknown. Either way the archive is
|
|
2719
|
+
* read whole instead, up to {@link MAX_PROBE_DOWNLOAD_BYTES}: a larger one is
|
|
2720
|
+
* reported as not IMDF without reading its body, so the control downloads it
|
|
2721
|
+
* once rather than twice. Below the cap, a non-IMDF archive read this way is
|
|
2722
|
+
* downloaded again by the control.
|
|
2723
|
+
*
|
|
2724
|
+
* Detection also needs the page to send `Range` cross-origin: a host whose
|
|
2725
|
+
* preflight refuses it makes the first request reject, which throws here. The
|
|
2726
|
+
* caller then passes the URL to the control, which loads an IMDF zip as a
|
|
2727
|
+
* zipped shapefile.
|
|
2728
|
+
*
|
|
2729
|
+
* @param url - The archive's URL.
|
|
2730
|
+
* @param fetchImpl - `fetch`, injectable for tests.
|
|
2731
|
+
* @returns Whether the archive is IMDF, and its bytes if they were downloaded.
|
|
2732
|
+
* @throws Error when the URL cannot be fetched at all.
|
|
2733
|
+
*/
|
|
2734
|
+
async function probeImdfZipUrl(url, fetchImpl = fetch) {
|
|
2735
|
+
const first = await fetchImpl(url, { headers: { Range: `bytes=-${TAIL_BYTES}` } });
|
|
2736
|
+
if (!first.ok) throw new Error(`HTTP ${first.status}`);
|
|
2737
|
+
if (first.status !== 206) return probeWholeResponse(first);
|
|
2738
|
+
const total = Number(/\/(\d+)\s*$/.exec(first.headers.get("content-range") ?? "")?.[1]);
|
|
2739
|
+
if (!Number.isFinite(total) || total <= 0) {
|
|
2740
|
+
await first.body?.cancel().catch(() => {});
|
|
2741
|
+
const whole = await fetchImpl(url);
|
|
2742
|
+
if (!whole.ok) throw new Error(`HTTP ${whole.status}`);
|
|
2743
|
+
return probeWholeResponse(whole);
|
|
2744
|
+
}
|
|
2745
|
+
const tail = new Uint8Array(await first.arrayBuffer());
|
|
2746
|
+
const tailOffset = total - tail.byteLength;
|
|
2747
|
+
const readRange = async (offset, length) => {
|
|
2748
|
+
if (offset >= tailOffset && offset + length <= total) return tail.subarray(offset - tailOffset, offset - tailOffset + length);
|
|
2749
|
+
const response = await fetchImpl(url, { headers: { Range: `bytes=${offset}-${offset + length - 1}` } });
|
|
2750
|
+
if (response.status !== 206) throw new Error("range request not honoured");
|
|
2751
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
2752
|
+
};
|
|
2753
|
+
return { imdf: await isImdfZipRange(total, readRange) };
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Classify an archive from a response carrying all of it, reading the body
|
|
2757
|
+
* only when it is small enough to be an IMDF venue.
|
|
2758
|
+
*
|
|
2759
|
+
* @param response - A full (status 200) response.
|
|
2760
|
+
* @returns The probe, with the bytes when they were read.
|
|
2761
|
+
*/
|
|
2762
|
+
async function probeWholeResponse(response) {
|
|
2763
|
+
const bytes = await readBodyUpTo(response, MAX_PROBE_DOWNLOAD_BYTES);
|
|
2764
|
+
if (!bytes) return { imdf: false };
|
|
2765
|
+
return {
|
|
2766
|
+
imdf: await isImdfZipRange(bytes.byteLength, bytesRangeReader(bytes)),
|
|
2767
|
+
bytes
|
|
2768
|
+
};
|
|
2769
|
+
}
|
|
2770
|
+
/**
|
|
2771
|
+
* Read a response body, giving up (and cancelling the rest) once it passes
|
|
2772
|
+
* `limit` bytes. The size header cannot be trusted for this: it may be absent
|
|
2773
|
+
* (a chunked response) or hidden from script on a cross-origin one.
|
|
2774
|
+
*
|
|
2775
|
+
* @param response - The response.
|
|
2776
|
+
* @param limit - The most bytes to read.
|
|
2777
|
+
* @returns The body, or `null` when it is larger than `limit`.
|
|
2778
|
+
*/
|
|
2779
|
+
async function readBodyUpTo(response, limit) {
|
|
2780
|
+
const declared = response.headers.get("content-length");
|
|
2781
|
+
if (declared !== null && Number(declared) > limit) {
|
|
2782
|
+
await response.body?.cancel().catch(() => {});
|
|
2783
|
+
return null;
|
|
2784
|
+
}
|
|
2785
|
+
if (!response.body) {
|
|
2786
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
2787
|
+
return bytes.byteLength > limit ? null : bytes;
|
|
2788
|
+
}
|
|
2789
|
+
const reader = response.body.getReader();
|
|
2790
|
+
const chunks = [];
|
|
2791
|
+
let length = 0;
|
|
2792
|
+
for (;;) {
|
|
2793
|
+
const { done, value } = await reader.read();
|
|
2794
|
+
if (done) break;
|
|
2795
|
+
length += value.byteLength;
|
|
2796
|
+
if (length > limit) {
|
|
2797
|
+
await reader.cancel().catch(() => {});
|
|
2798
|
+
return null;
|
|
2799
|
+
}
|
|
2800
|
+
chunks.push(value);
|
|
2801
|
+
}
|
|
2802
|
+
const bytes = new Uint8Array(length);
|
|
2803
|
+
let offset = 0;
|
|
2804
|
+
for (const chunk of chunks) {
|
|
2805
|
+
bytes.set(chunk, offset);
|
|
2806
|
+
offset += chunk.byteLength;
|
|
2807
|
+
}
|
|
2808
|
+
return bytes;
|
|
3412
2809
|
}
|
|
3413
2810
|
// src/deployment-capabilities.ts
|
|
3414
2811
|
/** Every capability, in a stable order for docs and tests. */
|
|
@@ -4599,6 +3996,32 @@ const createLayersSlice = (set, get) => ({
|
|
|
4599
3996
|
}),
|
|
4600
3997
|
setLayerQuickFilters: (id, filters) => get().updateLayer(id, { quickFilters: filters.length > 0 ? filters : void 0 }),
|
|
4601
3998
|
setLayerFilterExpression: (id, expression) => get().updateLayer(id, { filterExpression: expression ?? void 0 }),
|
|
3999
|
+
setLayerLevelFilter: (id, filter) => get().updateLayer(id, { levelFilter: filter ?? void 0 }),
|
|
4000
|
+
setLevelSelection: (level, mode) => set((s) => {
|
|
4001
|
+
let changed = false;
|
|
4002
|
+
const layers = s.layers.map((layer) => {
|
|
4003
|
+
if (!layer.levelFilter) return layer;
|
|
4004
|
+
const next = selectLevel(layer.levelFilter, layer.geojson?.features, level, mode);
|
|
4005
|
+
if (sameLevelLink(layer.levelFilter, next)) return layer;
|
|
4006
|
+
changed = true;
|
|
4007
|
+
return {
|
|
4008
|
+
...layer,
|
|
4009
|
+
levelFilter: next
|
|
4010
|
+
};
|
|
4011
|
+
});
|
|
4012
|
+
return changed ? {
|
|
4013
|
+
layers,
|
|
4014
|
+
isDirty: true
|
|
4015
|
+
} : {};
|
|
4016
|
+
}),
|
|
4017
|
+
reconcileLevelLinks: (updates) => set((s) => {
|
|
4018
|
+
if (updates.length === 0) return {};
|
|
4019
|
+
const byId = new Map(updates.map((update) => [update.id, update.levelFilter]));
|
|
4020
|
+
return { layers: s.layers.map((layer) => byId.has(layer.id) ? {
|
|
4021
|
+
...layer,
|
|
4022
|
+
levelFilter: byId.get(layer.id)
|
|
4023
|
+
} : layer) };
|
|
4024
|
+
}),
|
|
4602
4025
|
setLayerVisibility: (id, visible) => get().updateLayer(id, { visible }),
|
|
4603
4026
|
setLayerOpacity: (id, opacity) => get().updateLayer(id, { opacity }),
|
|
4604
4027
|
setLayerStyle: (id, style) => set((s) => ({
|
|
@@ -5493,526 +4916,56 @@ const createLibrariesSlice = (set) => ({
|
|
|
5493
4916
|
saveTemplateEntry: (entry) => set((s) => ({ templateLibrary: s.templateLibrary.some((t) => t.id === entry.id) ? s.templateLibrary.map((t) => t.id === entry.id ? entry : t) : [...s.templateLibrary, entry] })),
|
|
5494
4917
|
deleteTemplateEntry: (id) => set((s) => ({ templateLibrary: s.templateLibrary.filter((t) => t.id !== id) }))
|
|
5495
4918
|
});
|
|
5496
|
-
// src/
|
|
4919
|
+
// src/history.ts
|
|
5497
4920
|
/**
|
|
5498
|
-
*
|
|
5499
|
-
*
|
|
5500
|
-
*
|
|
4921
|
+
* Runtime-settable window (ms) used to coalesce rapid history-producing changes
|
|
4922
|
+
* (e.g. a continuous opacity/style slider drag) into a single undo entry. Tests
|
|
4923
|
+
* set this to 0 for deterministic one-entry-per-action behavior.
|
|
5501
4924
|
*/
|
|
5502
|
-
|
|
5503
|
-
function
|
|
5504
|
-
|
|
5505
|
-
const clearCesium = state.preferences.map.cesiumBasemap !== "project" && (state.primaryRenderer === "cesium" || state.secondaryMapViews.some((pane) => pane.viewKind === "cesium"));
|
|
5506
|
-
const clearArcgis = state.preferences.map.arcgisBasemap !== void 0 && (state.primaryRenderer === "arcgis" || state.secondaryMapViews.some((pane) => pane.viewKind === "arcgis"));
|
|
5507
|
-
if (!clearMapbox && !clearCesium && !clearArcgis && ellipsoidId === state.preferences.map.ellipsoidId) return state.preferences;
|
|
5508
|
-
return {
|
|
5509
|
-
...state.preferences,
|
|
5510
|
-
map: {
|
|
5511
|
-
...state.preferences.map,
|
|
5512
|
-
...clearMapbox ? { mapboxStyleUrl: void 0 } : {},
|
|
5513
|
-
...clearCesium ? { cesiumBasemap: "project" } : {},
|
|
5514
|
-
...clearArcgis ? { arcgisBasemap: void 0 } : {},
|
|
5515
|
-
ellipsoidId
|
|
5516
|
-
}
|
|
5517
|
-
};
|
|
5518
|
-
}
|
|
5519
|
-
/** True when two camera states have identical center/zoom/bearing/pitch. */
|
|
5520
|
-
function sameCamera(a, b) {
|
|
5521
|
-
return a.center[0] === b.center[0] && a.center[1] === b.center[1] && a.zoom === b.zoom && a.bearing === b.bearing && a.pitch === b.pitch;
|
|
4925
|
+
let historyCoalesceMs = 400;
|
|
4926
|
+
function setHistoryCoalesceMs(ms) {
|
|
4927
|
+
historyCoalesceMs = ms;
|
|
5522
4928
|
}
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
if (!Number.isFinite(value)) return 1;
|
|
5526
|
-
return Math.max(1, Math.min(4, Math.floor(value)));
|
|
4929
|
+
function getHistoryCoalesceMs() {
|
|
4930
|
+
return historyCoalesceMs;
|
|
5527
4931
|
}
|
|
5528
4932
|
/**
|
|
5529
|
-
*
|
|
5530
|
-
*
|
|
5531
|
-
*
|
|
5532
|
-
*
|
|
5533
|
-
*
|
|
4933
|
+
* Soft budget (in total feature count) for the geometry/attribute payload
|
|
4934
|
+
* retained across undo snapshots. Each in-app edit of a vector layer pushes a
|
|
4935
|
+
* new snapshot that holds the layer's full `geojson`; without a bound, editing
|
|
4936
|
+
* a large layer many times pins several copies of its feature set in memory
|
|
4937
|
+
* (issue #341). When the distinct features across snapshots exceed this budget,
|
|
4938
|
+
* the oldest snapshots are dropped (see {@link trimHistoryBySize}).
|
|
5534
4939
|
*
|
|
5535
|
-
*
|
|
5536
|
-
*
|
|
5537
|
-
*
|
|
5538
|
-
*
|
|
5539
|
-
* rather than returning an out-of-range dimension.
|
|
4940
|
+
* Feature count is a cheap proxy for payload size; it avoids serializing
|
|
4941
|
+
* geometry on every edit. The default is generous enough that ordinary
|
|
4942
|
+
* multi-step editing of small/medium layers keeps the full history depth, while
|
|
4943
|
+
* very large layers trade history depth for a bounded footprint.
|
|
5540
4944
|
*/
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
const capacity = rows * cols;
|
|
5549
|
-
if (capacity < total) continue;
|
|
5550
|
-
const empty = capacity - total;
|
|
5551
|
-
const score = Math.abs(cols - preferredCols);
|
|
5552
|
-
if (best === null || empty < best.empty || empty === best.empty && (score < best.score || score === best.score && cols > best.cols)) best = {
|
|
5553
|
-
rows,
|
|
5554
|
-
cols,
|
|
5555
|
-
empty,
|
|
5556
|
-
score
|
|
5557
|
-
};
|
|
5558
|
-
}
|
|
5559
|
-
return best ? {
|
|
5560
|
-
rows: best.rows,
|
|
5561
|
-
cols: best.cols
|
|
5562
|
-
} : {
|
|
5563
|
-
rows: 1,
|
|
5564
|
-
cols: 1
|
|
5565
|
-
};
|
|
4945
|
+
let maxHistoryFeatureCount = 5e5;
|
|
4946
|
+
function setMaxHistoryFeatureCount(count) {
|
|
4947
|
+
if (!Number.isFinite(count) || count < 0) throw new RangeError(`maxHistoryFeatureCount must be a non-negative finite number, got ${count}`);
|
|
4948
|
+
maxHistoryFeatureCount = count;
|
|
4949
|
+
}
|
|
4950
|
+
function getMaxHistoryFeatureCount() {
|
|
4951
|
+
return maxHistoryFeatureCount;
|
|
5566
4952
|
}
|
|
5567
|
-
const createMapViewSlice = (set) => ({
|
|
5568
|
-
mapView: createDefaultMapView(),
|
|
5569
|
-
basemapStyleUrl: DEFAULT_BASEMAP,
|
|
5570
|
-
basemapVisible: true,
|
|
5571
|
-
basemapOpacity: 1,
|
|
5572
|
-
blankBackgroundColor: null,
|
|
5573
|
-
preferences: DEFAULT_PROJECT_PREFERENCES,
|
|
5574
|
-
mapLayout: { ...DEFAULT_MAP_GRID_LAYOUT },
|
|
5575
|
-
secondaryMapViews: [],
|
|
5576
|
-
primaryMapLabel: "",
|
|
5577
|
-
primaryRenderer: DEFAULT_PRIMARY_RENDERER,
|
|
5578
|
-
setMapView: (view, markDirty = false) => set((s) => ({
|
|
5579
|
-
mapView: {
|
|
5580
|
-
...s.mapView,
|
|
5581
|
-
...view
|
|
5582
|
-
},
|
|
5583
|
-
isDirty: markDirty || s.isDirty
|
|
5584
|
-
})),
|
|
5585
|
-
setMapGrid: (rows, cols) => set((s) => {
|
|
5586
|
-
const clampedRows = clampGridDim(rows);
|
|
5587
|
-
const clampedCols = clampGridDim(cols);
|
|
5588
|
-
const desiredSecondary = clampedRows * clampedCols - 1;
|
|
5589
|
-
let secondaryMapViews = s.secondaryMapViews;
|
|
5590
|
-
if (desiredSecondary < secondaryMapViews.length) secondaryMapViews = secondaryMapViews.slice(0, desiredSecondary);
|
|
5591
|
-
else if (desiredSecondary > secondaryMapViews.length) {
|
|
5592
|
-
const additions = [];
|
|
5593
|
-
for (let i = secondaryMapViews.length; i < desiredSecondary; i++) additions.push({
|
|
5594
|
-
id: v4(),
|
|
5595
|
-
view: { ...s.mapView },
|
|
5596
|
-
layerVisibility: {}
|
|
5597
|
-
});
|
|
5598
|
-
secondaryMapViews = [...secondaryMapViews, ...additions];
|
|
5599
|
-
}
|
|
5600
|
-
return {
|
|
5601
|
-
mapLayout: {
|
|
5602
|
-
...s.mapLayout,
|
|
5603
|
-
rows: clampedRows,
|
|
5604
|
-
cols: clampedCols
|
|
5605
|
-
},
|
|
5606
|
-
secondaryMapViews,
|
|
5607
|
-
isDirty: true
|
|
5608
|
-
};
|
|
5609
|
-
}),
|
|
5610
|
-
setSyncView: (syncView) => set((s) => ({
|
|
5611
|
-
mapLayout: {
|
|
5612
|
-
...s.mapLayout,
|
|
5613
|
-
syncView
|
|
5614
|
-
},
|
|
5615
|
-
isDirty: true
|
|
5616
|
-
})),
|
|
5617
|
-
setSecondaryMapView: (id, view, markDirty = false) => set((s) => {
|
|
5618
|
-
let changed = false;
|
|
5619
|
-
const secondaryMapViews = s.secondaryMapViews.map((pane) => {
|
|
5620
|
-
if (pane.id !== id) return pane;
|
|
5621
|
-
const merged = {
|
|
5622
|
-
...pane.view,
|
|
5623
|
-
...view
|
|
5624
|
-
};
|
|
5625
|
-
if (sameCamera(pane.view, merged)) return pane;
|
|
5626
|
-
changed = true;
|
|
5627
|
-
return {
|
|
5628
|
-
...pane,
|
|
5629
|
-
view: merged
|
|
5630
|
-
};
|
|
5631
|
-
});
|
|
5632
|
-
if (!changed) return s;
|
|
5633
|
-
return {
|
|
5634
|
-
secondaryMapViews,
|
|
5635
|
-
isDirty: markDirty || s.isDirty
|
|
5636
|
-
};
|
|
5637
|
-
}),
|
|
5638
|
-
setSecondaryLayerVisibility: (id, layerId, visible) => set((s) => {
|
|
5639
|
-
let changed = false;
|
|
5640
|
-
const secondaryMapViews = s.secondaryMapViews.map((pane) => {
|
|
5641
|
-
if (pane.id !== id) return pane;
|
|
5642
|
-
changed = true;
|
|
5643
|
-
return {
|
|
5644
|
-
...pane,
|
|
5645
|
-
layerVisibility: {
|
|
5646
|
-
...pane.layerVisibility,
|
|
5647
|
-
[layerId]: visible
|
|
5648
|
-
}
|
|
5649
|
-
};
|
|
5650
|
-
});
|
|
5651
|
-
if (!changed) return s;
|
|
5652
|
-
return {
|
|
5653
|
-
secondaryMapViews,
|
|
5654
|
-
isDirty: true
|
|
5655
|
-
};
|
|
5656
|
-
}),
|
|
5657
|
-
setPrimaryMapLabel: (label) => set({
|
|
5658
|
-
primaryMapLabel: label,
|
|
5659
|
-
isDirty: true
|
|
5660
|
-
}),
|
|
5661
|
-
setPrimaryRenderer: (renderer) => set((s) => s.primaryRenderer === renderer ? s : {
|
|
5662
|
-
primaryRenderer: renderer,
|
|
5663
|
-
isDirty: true
|
|
5664
|
-
}),
|
|
5665
|
-
setSecondaryMapLabel: (id, label) => set((s) => {
|
|
5666
|
-
let changed = false;
|
|
5667
|
-
const secondaryMapViews = s.secondaryMapViews.map((pane) => {
|
|
5668
|
-
if (pane.id !== id) return pane;
|
|
5669
|
-
changed = true;
|
|
5670
|
-
return {
|
|
5671
|
-
...pane,
|
|
5672
|
-
label
|
|
5673
|
-
};
|
|
5674
|
-
});
|
|
5675
|
-
if (!changed) return s;
|
|
5676
|
-
return {
|
|
5677
|
-
secondaryMapViews,
|
|
5678
|
-
isDirty: true
|
|
5679
|
-
};
|
|
5680
|
-
}),
|
|
5681
|
-
setSecondaryViewKind: (id, viewKind) => set((s) => {
|
|
5682
|
-
let changed = false;
|
|
5683
|
-
const secondaryMapViews = s.secondaryMapViews.map((pane) => {
|
|
5684
|
-
if (pane.id !== id) return pane;
|
|
5685
|
-
if ((pane.viewKind ?? "maplibre") === viewKind) return pane;
|
|
5686
|
-
changed = true;
|
|
5687
|
-
return {
|
|
5688
|
-
...pane,
|
|
5689
|
-
viewKind
|
|
5690
|
-
};
|
|
5691
|
-
});
|
|
5692
|
-
if (!changed) return s;
|
|
5693
|
-
return {
|
|
5694
|
-
secondaryMapViews,
|
|
5695
|
-
isDirty: true
|
|
5696
|
-
};
|
|
5697
|
-
}),
|
|
5698
|
-
removeSecondaryMapView: (id) => set((s) => {
|
|
5699
|
-
const secondaryMapViews = s.secondaryMapViews.filter((pane) => pane.id !== id);
|
|
5700
|
-
if (secondaryMapViews.length === s.secondaryMapViews.length) return s;
|
|
5701
|
-
const { rows, cols } = fitGrid(secondaryMapViews.length + 1, s.mapLayout.cols);
|
|
5702
|
-
return {
|
|
5703
|
-
secondaryMapViews,
|
|
5704
|
-
mapLayout: {
|
|
5705
|
-
...s.mapLayout,
|
|
5706
|
-
rows,
|
|
5707
|
-
cols
|
|
5708
|
-
},
|
|
5709
|
-
isDirty: true
|
|
5710
|
-
};
|
|
5711
|
-
}),
|
|
5712
|
-
setBasemapStyleUrl: (url) => set((state) => ({
|
|
5713
|
-
basemapStyleUrl: url,
|
|
5714
|
-
preferences: preferencesForBasemap(state),
|
|
5715
|
-
isDirty: true
|
|
5716
|
-
})),
|
|
5717
|
-
applyPlanetaryBasemap: (basemap) => set((state) => ({
|
|
5718
|
-
basemapStyleUrl: basemap.styleUrl,
|
|
5719
|
-
preferences: preferencesForBasemap(state, basemap.ellipsoidId),
|
|
5720
|
-
isDirty: true
|
|
5721
|
-
})),
|
|
5722
|
-
restoreEarthBasemap: (styleUrl) => set((state) => ({
|
|
5723
|
-
basemapStyleUrl: styleUrl,
|
|
5724
|
-
preferences: preferencesForBasemap(state, DEFAULT_ELLIPSOID_ID),
|
|
5725
|
-
isDirty: true
|
|
5726
|
-
})),
|
|
5727
|
-
setBasemapVisible: (visible) => set({
|
|
5728
|
-
basemapVisible: visible,
|
|
5729
|
-
isDirty: true
|
|
5730
|
-
}),
|
|
5731
|
-
setBasemapOpacity: (opacity) => set({
|
|
5732
|
-
basemapOpacity: opacity,
|
|
5733
|
-
isDirty: true
|
|
5734
|
-
}),
|
|
5735
|
-
setBlankBackgroundColor: (color) => set({
|
|
5736
|
-
blankBackgroundColor: normalizeBlankBackgroundColor(color),
|
|
5737
|
-
isDirty: true
|
|
5738
|
-
}),
|
|
5739
|
-
setPreferences: (preferences) => set({
|
|
5740
|
-
preferences,
|
|
5741
|
-
isDirty: true
|
|
5742
|
-
})
|
|
5743
|
-
});
|
|
5744
|
-
// src/store/processing-slice.ts
|
|
5745
4953
|
/**
|
|
5746
|
-
*
|
|
5747
|
-
*
|
|
4954
|
+
* Bytes of inline payload charged as one feature-equivalent, so a `data:` URL
|
|
4955
|
+
* competes for the same budget as geometry. A rough vector feature costs a few
|
|
4956
|
+
* hundred bytes of live JS objects, which is the order this matches.
|
|
5748
4957
|
*/
|
|
5749
|
-
const
|
|
5750
|
-
models: [],
|
|
5751
|
-
processingHistory: [],
|
|
5752
|
-
obiaWorkbench: null,
|
|
5753
|
-
saveModel: (model) => set((s) => {
|
|
5754
|
-
return {
|
|
5755
|
-
models: s.models.some((m) => m.id === model.id) ? s.models.map((m) => m.id === model.id ? model : m) : [...s.models, model],
|
|
5756
|
-
isDirty: true
|
|
5757
|
-
};
|
|
5758
|
-
}),
|
|
5759
|
-
deleteModel: (id) => set((s) => ({
|
|
5760
|
-
models: s.models.filter((m) => m.id !== id),
|
|
5761
|
-
isDirty: true
|
|
5762
|
-
})),
|
|
5763
|
-
addProcessingRun: (run) => set((s) => {
|
|
5764
|
-
if (s.processingHistory.some((r) => r.id === run.id)) return s;
|
|
5765
|
-
return {
|
|
5766
|
-
processingHistory: [...s.processingHistory, run].slice(-100),
|
|
5767
|
-
isDirty: true
|
|
5768
|
-
};
|
|
5769
|
-
}),
|
|
5770
|
-
updateProcessingRun: (id, patch) => set((s) => {
|
|
5771
|
-
if (!s.processingHistory.some((r) => r.id === id)) return s;
|
|
5772
|
-
return {
|
|
5773
|
-
processingHistory: s.processingHistory.map((r) => r.id === id ? {
|
|
5774
|
-
...r,
|
|
5775
|
-
...patch,
|
|
5776
|
-
id: r.id
|
|
5777
|
-
} : r),
|
|
5778
|
-
isDirty: true
|
|
5779
|
-
};
|
|
5780
|
-
}),
|
|
5781
|
-
clearProcessingHistory: () => set((s) => s.processingHistory.length === 0 ? s : {
|
|
5782
|
-
processingHistory: [],
|
|
5783
|
-
isDirty: true
|
|
5784
|
-
}),
|
|
5785
|
-
setObiaWorkbench: (obiaWorkbench, markDirty = true) => set(markDirty ? {
|
|
5786
|
-
obiaWorkbench,
|
|
5787
|
-
isDirty: true
|
|
5788
|
-
} : { obiaWorkbench })
|
|
5789
|
-
});
|
|
5790
|
-
// src/store/project-content-slice.ts
|
|
4958
|
+
const BYTES_PER_FEATURE_EQUIVALENT = 200;
|
|
5791
4959
|
/**
|
|
5792
|
-
*
|
|
5793
|
-
*
|
|
5794
|
-
*
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
bookmarkGroups: [],
|
|
5802
|
-
comments: [],
|
|
5803
|
-
projectInteraction: null,
|
|
5804
|
-
setBookmarks: (bookmarks, bookmarkGroups) => set({
|
|
5805
|
-
bookmarks,
|
|
5806
|
-
bookmarkGroups,
|
|
5807
|
-
isDirty: true
|
|
5808
|
-
}),
|
|
5809
|
-
addComment: (comment) => set((s) => {
|
|
5810
|
-
if (s.comments.some((c) => c.id === comment.id)) return s;
|
|
5811
|
-
return {
|
|
5812
|
-
comments: [...s.comments, comment],
|
|
5813
|
-
isDirty: true
|
|
5814
|
-
};
|
|
5815
|
-
}),
|
|
5816
|
-
replyToComment: (commentId, reply) => set((s) => {
|
|
5817
|
-
let appended = false;
|
|
5818
|
-
const nextComments = s.comments.map((c) => {
|
|
5819
|
-
if (c.id !== commentId) return c;
|
|
5820
|
-
if (c.replies.some((r) => r.id === reply.id)) return c;
|
|
5821
|
-
appended = true;
|
|
5822
|
-
return {
|
|
5823
|
-
...c,
|
|
5824
|
-
replies: [...c.replies, reply]
|
|
5825
|
-
};
|
|
5826
|
-
});
|
|
5827
|
-
if (!appended) return s;
|
|
5828
|
-
return {
|
|
5829
|
-
comments: nextComments,
|
|
5830
|
-
isDirty: true
|
|
5831
|
-
};
|
|
5832
|
-
}),
|
|
5833
|
-
toggleResolveComment: (commentId, resolved) => set((s) => ({
|
|
5834
|
-
comments: s.comments.map((c) => c.id === commentId ? {
|
|
5835
|
-
...c,
|
|
5836
|
-
resolved: resolved !== void 0 ? resolved : !c.resolved
|
|
5837
|
-
} : c),
|
|
5838
|
-
isDirty: true
|
|
5839
|
-
})),
|
|
5840
|
-
deleteComment: (commentId) => set((s) => ({
|
|
5841
|
-
comments: s.comments.filter((c) => c.id !== commentId),
|
|
5842
|
-
isDirty: true
|
|
5843
|
-
})),
|
|
5844
|
-
setComments: (comments) => set({
|
|
5845
|
-
comments,
|
|
5846
|
-
isDirty: true
|
|
5847
|
-
}),
|
|
5848
|
-
addWidget: (widget) => set((s) => {
|
|
5849
|
-
if (s.widgets.some((w) => w.id === widget.id)) return s;
|
|
5850
|
-
return {
|
|
5851
|
-
widgets: [...s.widgets, widget],
|
|
5852
|
-
isDirty: true
|
|
5853
|
-
};
|
|
5854
|
-
}),
|
|
5855
|
-
updateWidget: (id, patch) => set((s) => {
|
|
5856
|
-
if (!s.widgets.some((w) => w.id === id)) return s;
|
|
5857
|
-
return {
|
|
5858
|
-
widgets: s.widgets.map((w) => w.id === id ? {
|
|
5859
|
-
...w,
|
|
5860
|
-
...patch,
|
|
5861
|
-
id: w.id
|
|
5862
|
-
} : w),
|
|
5863
|
-
isDirty: true
|
|
5864
|
-
};
|
|
5865
|
-
}),
|
|
5866
|
-
replaceWidget: (id, widget) => set((s) => {
|
|
5867
|
-
if (!s.widgets.some((w) => w.id === id)) return s;
|
|
5868
|
-
return {
|
|
5869
|
-
widgets: s.widgets.map((w) => w.id === id ? {
|
|
5870
|
-
...widget,
|
|
5871
|
-
id
|
|
5872
|
-
} : w),
|
|
5873
|
-
isDirty: true
|
|
5874
|
-
};
|
|
5875
|
-
}),
|
|
5876
|
-
removeWidget: (id) => set((s) => ({
|
|
5877
|
-
widgets: s.widgets.filter((w) => w.id !== id),
|
|
5878
|
-
isDirty: true
|
|
5879
|
-
})),
|
|
5880
|
-
moveWidget: (id, toIndex) => set((s) => {
|
|
5881
|
-
const from = s.widgets.findIndex((w) => w.id === id);
|
|
5882
|
-
if (from < 0) return s;
|
|
5883
|
-
const target = Math.max(0, Math.min(s.widgets.length - 1, toIndex));
|
|
5884
|
-
if (target === from) return s;
|
|
5885
|
-
const widgets = [...s.widgets];
|
|
5886
|
-
const [moved] = widgets.splice(from, 1);
|
|
5887
|
-
widgets.splice(target, 0, moved);
|
|
5888
|
-
return {
|
|
5889
|
-
widgets,
|
|
5890
|
-
isDirty: true
|
|
5891
|
-
};
|
|
5892
|
-
}),
|
|
5893
|
-
setDashboardColumns: (columns) => set((s) => {
|
|
5894
|
-
if (!Number.isFinite(columns)) return s;
|
|
5895
|
-
return {
|
|
5896
|
-
dashboardColumns: Math.max(1, Math.min(6, Math.trunc(columns))),
|
|
5897
|
-
isDirty: true
|
|
5898
|
-
};
|
|
5899
|
-
}),
|
|
5900
|
-
setStorymap: (storymap) => set({
|
|
5901
|
-
storymap,
|
|
5902
|
-
isDirty: true
|
|
5903
|
-
}),
|
|
5904
|
-
updateStorymapSettings: (patch) => set((s) => ({
|
|
5905
|
-
storymap: {
|
|
5906
|
-
...s.storymap ?? DEFAULT_STORY_MAP,
|
|
5907
|
-
...patch
|
|
5908
|
-
},
|
|
5909
|
-
isDirty: true
|
|
5910
|
-
})),
|
|
5911
|
-
addStoryChapter: (chapter, atIndex) => set((s) => {
|
|
5912
|
-
const base = s.storymap ?? DEFAULT_STORY_MAP;
|
|
5913
|
-
const chapters = [...base.chapters];
|
|
5914
|
-
const index = atIndex === void 0 ? chapters.length : Math.min(Math.max(atIndex, 0), chapters.length);
|
|
5915
|
-
chapters.splice(index, 0, chapter);
|
|
5916
|
-
return {
|
|
5917
|
-
storymap: {
|
|
5918
|
-
...base,
|
|
5919
|
-
chapters
|
|
5920
|
-
},
|
|
5921
|
-
isDirty: true
|
|
5922
|
-
};
|
|
5923
|
-
}),
|
|
5924
|
-
updateStoryChapter: (id, patch) => set((s) => {
|
|
5925
|
-
if (!s.storymap) return s;
|
|
5926
|
-
return {
|
|
5927
|
-
storymap: {
|
|
5928
|
-
...s.storymap,
|
|
5929
|
-
chapters: s.storymap.chapters.map((chapter) => chapter.id === id ? {
|
|
5930
|
-
...chapter,
|
|
5931
|
-
...patch
|
|
5932
|
-
} : chapter)
|
|
5933
|
-
},
|
|
5934
|
-
isDirty: true
|
|
5935
|
-
};
|
|
5936
|
-
}),
|
|
5937
|
-
removeStoryChapter: (id) => set((s) => {
|
|
5938
|
-
if (!s.storymap) return s;
|
|
5939
|
-
return {
|
|
5940
|
-
storymap: {
|
|
5941
|
-
...s.storymap,
|
|
5942
|
-
chapters: s.storymap.chapters.filter((chapter) => chapter.id !== id)
|
|
5943
|
-
},
|
|
5944
|
-
isDirty: true
|
|
5945
|
-
};
|
|
5946
|
-
}),
|
|
5947
|
-
moveStoryChapter: (id, targetIndex) => set((s) => {
|
|
5948
|
-
if (!s.storymap) return s;
|
|
5949
|
-
const current = s.storymap.chapters.findIndex((chapter) => chapter.id === id);
|
|
5950
|
-
if (current < 0) return s;
|
|
5951
|
-
const chapters = [...s.storymap.chapters];
|
|
5952
|
-
const [chapter] = chapters.splice(current, 1);
|
|
5953
|
-
if (!chapter) return s;
|
|
5954
|
-
const next = Math.min(Math.max(targetIndex, 0), chapters.length);
|
|
5955
|
-
chapters.splice(next, 0, chapter);
|
|
5956
|
-
if (chapters.every((item, i) => item.id === s.storymap?.chapters[i]?.id)) return s;
|
|
5957
|
-
return {
|
|
5958
|
-
storymap: {
|
|
5959
|
-
...s.storymap,
|
|
5960
|
-
chapters
|
|
5961
|
-
},
|
|
5962
|
-
isDirty: true
|
|
5963
|
-
};
|
|
5964
|
-
})
|
|
5965
|
-
});
|
|
5966
|
-
// src/history.ts
|
|
5967
|
-
/**
|
|
5968
|
-
* Runtime-settable window (ms) used to coalesce rapid history-producing changes
|
|
5969
|
-
* (e.g. a continuous opacity/style slider drag) into a single undo entry. Tests
|
|
5970
|
-
* set this to 0 for deterministic one-entry-per-action behavior.
|
|
5971
|
-
*/
|
|
5972
|
-
let historyCoalesceMs = 400;
|
|
5973
|
-
function setHistoryCoalesceMs(ms) {
|
|
5974
|
-
historyCoalesceMs = ms;
|
|
5975
|
-
}
|
|
5976
|
-
function getHistoryCoalesceMs() {
|
|
5977
|
-
return historyCoalesceMs;
|
|
5978
|
-
}
|
|
5979
|
-
/**
|
|
5980
|
-
* Soft budget (in total feature count) for the geometry/attribute payload
|
|
5981
|
-
* retained across undo snapshots. Each in-app edit of a vector layer pushes a
|
|
5982
|
-
* new snapshot that holds the layer's full `geojson`; without a bound, editing
|
|
5983
|
-
* a large layer many times pins several copies of its feature set in memory
|
|
5984
|
-
* (issue #341). When the distinct features across snapshots exceed this budget,
|
|
5985
|
-
* the oldest snapshots are dropped (see {@link trimHistoryBySize}).
|
|
5986
|
-
*
|
|
5987
|
-
* Feature count is a cheap proxy for payload size; it avoids serializing
|
|
5988
|
-
* geometry on every edit. The default is generous enough that ordinary
|
|
5989
|
-
* multi-step editing of small/medium layers keeps the full history depth, while
|
|
5990
|
-
* very large layers trade history depth for a bounded footprint.
|
|
5991
|
-
*/
|
|
5992
|
-
let maxHistoryFeatureCount = 5e5;
|
|
5993
|
-
function setMaxHistoryFeatureCount(count) {
|
|
5994
|
-
if (!Number.isFinite(count) || count < 0) throw new RangeError(`maxHistoryFeatureCount must be a non-negative finite number, got ${count}`);
|
|
5995
|
-
maxHistoryFeatureCount = count;
|
|
5996
|
-
}
|
|
5997
|
-
function getMaxHistoryFeatureCount() {
|
|
5998
|
-
return maxHistoryFeatureCount;
|
|
5999
|
-
}
|
|
6000
|
-
/**
|
|
6001
|
-
* Bytes of inline payload charged as one feature-equivalent, so a `data:` URL
|
|
6002
|
-
* competes for the same budget as geometry. A rough vector feature costs a few
|
|
6003
|
-
* hundred bytes of live JS objects, which is the order this matches.
|
|
6004
|
-
*/
|
|
6005
|
-
const BYTES_PER_FEATURE_EQUIVALENT = 200;
|
|
6006
|
-
/**
|
|
6007
|
-
* The size a snapshot adds to the budget, skipping any payload already in
|
|
6008
|
-
* `seen` so payloads shared across snapshots (unchanged layers keep the same
|
|
6009
|
-
* reference) are counted once. Mutates `seen` with each newly counted payload.
|
|
6010
|
-
*
|
|
6011
|
-
* Two payload kinds count. Vector `geojson` contributes its feature count. A
|
|
6012
|
-
* layer whose `source.url` is an inline `data:` URL contributes its length as
|
|
6013
|
-
* feature-equivalents: a NetCDF grid baked to pixels holds a whole PNG there,
|
|
6014
|
-
* and re-symbolizing it writes a fresh multi-megabyte string on every change, so
|
|
6015
|
-
* without this the pruner sees a "free" snapshot and history grows unbounded.
|
|
4960
|
+
* The size a snapshot adds to the budget, skipping any payload already in
|
|
4961
|
+
* `seen` so payloads shared across snapshots (unchanged layers keep the same
|
|
4962
|
+
* reference) are counted once. Mutates `seen` with each newly counted payload.
|
|
4963
|
+
*
|
|
4964
|
+
* Two payload kinds count. Vector `geojson` contributes its feature count. A
|
|
4965
|
+
* layer whose `source.url` is an inline `data:` URL contributes its length as
|
|
4966
|
+
* feature-equivalents: a NetCDF grid baked to pixels holds a whole PNG there,
|
|
4967
|
+
* and re-symbolizing it writes a fresh multi-megabyte string on every change, so
|
|
4968
|
+
* without this the pruner sees a "free" snapshot and history grows unbounded.
|
|
6016
4969
|
*/
|
|
6017
4970
|
function distinctFeatureCount(snapshot, seen) {
|
|
6018
4971
|
let count = 0;
|
|
@@ -6248,6 +5201,7 @@ function finishHistoryStep(previousBasemapStyleUrl) {
|
|
|
6248
5201
|
ellipsoidId: restoredEllipsoidId
|
|
6249
5202
|
}
|
|
6250
5203
|
} } : {};
|
|
5204
|
+
const basemapPatch = s.regionalBasemapPolicy === "block" && getRegionalBasemapByStyleUrl(s.basemapStyleUrl) ? { basemapStyleUrl: DEFAULT_BASEMAP } : {};
|
|
6251
5205
|
boundStore().setState(selectionDangling ? {
|
|
6252
5206
|
isDirty: true,
|
|
6253
5207
|
selectedLayerId: null,
|
|
@@ -6260,6 +5214,7 @@ function finishHistoryStep(previousBasemapStyleUrl) {
|
|
|
6260
5214
|
...ellipsoidPatch,
|
|
6261
5215
|
...referencePatch
|
|
6262
5216
|
});
|
|
5217
|
+
if (basemapPatch.basemapStyleUrl) withoutHistory(() => boundStore().setState(basemapPatch));
|
|
6263
5218
|
cancelHistoryCoalesce();
|
|
6264
5219
|
}
|
|
6265
5220
|
/**
|
|
@@ -6276,7 +5231,8 @@ function undo() {
|
|
|
6276
5231
|
try {
|
|
6277
5232
|
boundStore().getState().loadProject(entry.before, entry.beforePath, {
|
|
6278
5233
|
rememberRecent: false,
|
|
6279
|
-
presenting: false
|
|
5234
|
+
presenting: false,
|
|
5235
|
+
trustBasemap: true
|
|
6280
5236
|
});
|
|
6281
5237
|
projectRestoreUndo = null;
|
|
6282
5238
|
boundStore().setState({ isDirty: true });
|
|
@@ -6304,7 +5260,8 @@ function redo() {
|
|
|
6304
5260
|
try {
|
|
6305
5261
|
boundStore().getState().loadProject(entry.after, entry.afterPath, {
|
|
6306
5262
|
rememberRecent: false,
|
|
6307
|
-
presenting: false
|
|
5263
|
+
presenting: false,
|
|
5264
|
+
trustBasemap: true
|
|
6308
5265
|
});
|
|
6309
5266
|
projectRestoreRedo = null;
|
|
6310
5267
|
boundStore().setState({ isDirty: true });
|
|
@@ -6340,6 +5297,29 @@ function withoutHistory(change) {
|
|
|
6340
5297
|
temporal.resume();
|
|
6341
5298
|
}
|
|
6342
5299
|
}
|
|
5300
|
+
/**
|
|
5301
|
+
* Rewrite the basemap recorded in every undo/redo snapshot from `from` to `to`.
|
|
5302
|
+
*
|
|
5303
|
+
* Used when the user accepts a regional basemap a loaded project asked for:
|
|
5304
|
+
* snapshots taken while the confirmation was open recorded the fallback, which
|
|
5305
|
+
* is not a basemap anyone chose, so undo must not bring it back.
|
|
5306
|
+
*
|
|
5307
|
+
* @param from - The basemap style URL to replace (the fallback).
|
|
5308
|
+
* @param to - The basemap style URL to record instead.
|
|
5309
|
+
*/
|
|
5310
|
+
function replaceHistoryBasemap(from, to) {
|
|
5311
|
+
const temporal = boundStore().temporal;
|
|
5312
|
+
const swap = (states) => states.map((s) => s.basemapStyleUrl === from ? {
|
|
5313
|
+
...s,
|
|
5314
|
+
basemapStyleUrl: to
|
|
5315
|
+
} : s);
|
|
5316
|
+
const { pastStates, futureStates } = temporal.getState();
|
|
5317
|
+
if (pastStates.length === 0 && futureStates.length === 0) return;
|
|
5318
|
+
temporal.setState({
|
|
5319
|
+
pastStates: swap(pastStates),
|
|
5320
|
+
futureStates: swap(futureStates)
|
|
5321
|
+
});
|
|
5322
|
+
}
|
|
6343
5323
|
/** Empty both the undo and redo stacks (e.g. on new/loaded project). */
|
|
6344
5324
|
function clearHistory() {
|
|
6345
5325
|
cancelHistoryCoalesce();
|
|
@@ -6350,6 +5330,550 @@ function clearHistory() {
|
|
|
6350
5330
|
notifyProjectRestoreHistory();
|
|
6351
5331
|
}
|
|
6352
5332
|
}
|
|
5333
|
+
// src/store/map-view-slice.ts
|
|
5334
|
+
/**
|
|
5335
|
+
* The map workspace: the primary camera, the basemap, project preferences, the
|
|
5336
|
+
* multi-map grid and its secondary panes, and which engine draws the primary
|
|
5337
|
+
* map. The basemap fields are tracked by undo history; the camera is not.
|
|
5338
|
+
*/
|
|
5339
|
+
/** An explicit background choice replaces the active renderer's override. */
|
|
5340
|
+
function preferencesForBasemap(state, ellipsoidId = state.preferences.map.ellipsoidId) {
|
|
5341
|
+
const clearMapbox = state.primaryRenderer === "mapbox" && state.preferences.map.mapboxStyleUrl !== void 0;
|
|
5342
|
+
const clearCesium = state.preferences.map.cesiumBasemap !== "project" && (state.primaryRenderer === "cesium" || state.secondaryMapViews.some((pane) => pane.viewKind === "cesium"));
|
|
5343
|
+
const clearArcgis = state.preferences.map.arcgisBasemap !== void 0 && (state.primaryRenderer === "arcgis" || state.secondaryMapViews.some((pane) => pane.viewKind === "arcgis"));
|
|
5344
|
+
if (!clearMapbox && !clearCesium && !clearArcgis && ellipsoidId === state.preferences.map.ellipsoidId) return state.preferences;
|
|
5345
|
+
return {
|
|
5346
|
+
...state.preferences,
|
|
5347
|
+
map: {
|
|
5348
|
+
...state.preferences.map,
|
|
5349
|
+
...clearMapbox ? { mapboxStyleUrl: void 0 } : {},
|
|
5350
|
+
...clearCesium ? { cesiumBasemap: "project" } : {},
|
|
5351
|
+
...clearArcgis ? { arcgisBasemap: void 0 } : {},
|
|
5352
|
+
ellipsoidId
|
|
5353
|
+
}
|
|
5354
|
+
};
|
|
5355
|
+
}
|
|
5356
|
+
/**
|
|
5357
|
+
* The basemap fields for a basemap the user (or code acting for them) picked.
|
|
5358
|
+
*
|
|
5359
|
+
* A deployment that disabled regional basemaps refuses them from every caller
|
|
5360
|
+
* (plugins, scripts, the assistant), not only the hidden picker. An explicit
|
|
5361
|
+
* pick is the user's own choice, so `"prompt"` never applies, and it supersedes
|
|
5362
|
+
* a pending prompt: answering Load afterwards must not replace it.
|
|
5363
|
+
*
|
|
5364
|
+
* Plugins, scripts and the assistant go through here too. They are code the
|
|
5365
|
+
* user installed or ran to act for them, like the picker, so they are trusted
|
|
5366
|
+
* the same way; only a project file (data someone else may have authored) is
|
|
5367
|
+
* gated by `"prompt"`.
|
|
5368
|
+
*
|
|
5369
|
+
* @param state - The current store state.
|
|
5370
|
+
* @param url - The requested basemap style URL.
|
|
5371
|
+
* @returns The `basemapStyleUrl` to apply, the updated hold and choice counter.
|
|
5372
|
+
*/
|
|
5373
|
+
function explicitBasemapChoice(state, url) {
|
|
5374
|
+
const gated = gateRegionalBasemap(url, state.regionalBasemapPolicy, DEFAULT_BASEMAP, true);
|
|
5375
|
+
if (gated.held) console.warn(`Regional basemaps are disabled by this deployment; ignoring "${url}".`);
|
|
5376
|
+
return {
|
|
5377
|
+
regionalBasemapAnswers: !gated.held && getRegionalBasemapByStyleUrl(url) !== void 0 ? {
|
|
5378
|
+
...state.regionalBasemapAnswers,
|
|
5379
|
+
[url]: true
|
|
5380
|
+
} : state.regionalBasemapAnswers,
|
|
5381
|
+
basemapStyleUrl: gated.styleUrl,
|
|
5382
|
+
regionalBasemapHold: state.regionalBasemapHold?.reason === "prompt" ? null : state.regionalBasemapHold,
|
|
5383
|
+
basemapChoiceGeneration: state.basemapChoiceGeneration + 1
|
|
5384
|
+
};
|
|
5385
|
+
}
|
|
5386
|
+
/** True when two camera states have identical center/zoom/bearing/pitch. */
|
|
5387
|
+
function sameCamera(a, b) {
|
|
5388
|
+
return a.center[0] === b.center[0] && a.center[1] === b.center[1] && a.zoom === b.zoom && a.bearing === b.bearing && a.pitch === b.pitch;
|
|
5389
|
+
}
|
|
5390
|
+
/** Clamp a requested grid row/column count into the supported [1, MAX] range. */
|
|
5391
|
+
function clampGridDim(value) {
|
|
5392
|
+
if (!Number.isFinite(value)) return 1;
|
|
5393
|
+
return Math.max(1, Math.min(4, Math.floor(value)));
|
|
5394
|
+
}
|
|
5395
|
+
/**
|
|
5396
|
+
* Pick a grid that holds at least `total` panes within the supported
|
|
5397
|
+
* `MAX_MAP_GRID_DIM x MAX_MAP_GRID_DIM` bound, minimizing empty cells first and
|
|
5398
|
+
* then preferring a column count close to (and, on ties, no smaller than)
|
|
5399
|
+
* `preferredCols` so removing a pane keeps the layout's orientation. Used when
|
|
5400
|
+
* collapsing the grid after a pane is removed.
|
|
5401
|
+
*
|
|
5402
|
+
* Bounding both dimensions matters: a prime `total` (e.g. 5 panes left after
|
|
5403
|
+
* removing one from a 2x3 grid) has no gap-free factor pair inside the bound, so
|
|
5404
|
+
* the only gap-free options (1x5 / 5x1) would exceed `MAX_MAP_GRID_DIM`. In that
|
|
5405
|
+
* case we accept the smallest bounded grid with one empty trailing cell (2x3)
|
|
5406
|
+
* rather than returning an out-of-range dimension.
|
|
5407
|
+
*/
|
|
5408
|
+
function fitGrid(total, preferredCols) {
|
|
5409
|
+
if (total <= 1) return {
|
|
5410
|
+
rows: 1,
|
|
5411
|
+
cols: 1
|
|
5412
|
+
};
|
|
5413
|
+
let best = null;
|
|
5414
|
+
for (let rows = 1; rows <= 4; rows++) for (let cols = 1; cols <= 4; cols++) {
|
|
5415
|
+
const capacity = rows * cols;
|
|
5416
|
+
if (capacity < total) continue;
|
|
5417
|
+
const empty = capacity - total;
|
|
5418
|
+
const score = Math.abs(cols - preferredCols);
|
|
5419
|
+
if (best === null || empty < best.empty || empty === best.empty && (score < best.score || score === best.score && cols > best.cols)) best = {
|
|
5420
|
+
rows,
|
|
5421
|
+
cols,
|
|
5422
|
+
empty,
|
|
5423
|
+
score
|
|
5424
|
+
};
|
|
5425
|
+
}
|
|
5426
|
+
return best ? {
|
|
5427
|
+
rows: best.rows,
|
|
5428
|
+
cols: best.cols
|
|
5429
|
+
} : {
|
|
5430
|
+
rows: 1,
|
|
5431
|
+
cols: 1
|
|
5432
|
+
};
|
|
5433
|
+
}
|
|
5434
|
+
const createMapViewSlice = (set, get) => ({
|
|
5435
|
+
mapView: createDefaultMapView(),
|
|
5436
|
+
basemapStyleUrl: DEFAULT_BASEMAP,
|
|
5437
|
+
basemapVisible: true,
|
|
5438
|
+
basemapOpacity: 1,
|
|
5439
|
+
blankBackgroundColor: null,
|
|
5440
|
+
preferences: DEFAULT_PROJECT_PREFERENCES,
|
|
5441
|
+
mapLayout: { ...DEFAULT_MAP_GRID_LAYOUT },
|
|
5442
|
+
secondaryMapViews: [],
|
|
5443
|
+
primaryMapLabel: "",
|
|
5444
|
+
primaryRenderer: DEFAULT_PRIMARY_RENDERER,
|
|
5445
|
+
regionalBasemapPolicy: "allow",
|
|
5446
|
+
regionalBasemapHold: null,
|
|
5447
|
+
basemapChoiceGeneration: 0,
|
|
5448
|
+
regionalBasemapAnswers: {},
|
|
5449
|
+
setMapView: (view, markDirty = false) => set((s) => ({
|
|
5450
|
+
mapView: {
|
|
5451
|
+
...s.mapView,
|
|
5452
|
+
...view
|
|
5453
|
+
},
|
|
5454
|
+
isDirty: markDirty || s.isDirty
|
|
5455
|
+
})),
|
|
5456
|
+
setMapGrid: (rows, cols) => set((s) => {
|
|
5457
|
+
const clampedRows = clampGridDim(rows);
|
|
5458
|
+
const clampedCols = clampGridDim(cols);
|
|
5459
|
+
const desiredSecondary = clampedRows * clampedCols - 1;
|
|
5460
|
+
let secondaryMapViews = s.secondaryMapViews;
|
|
5461
|
+
if (desiredSecondary < secondaryMapViews.length) secondaryMapViews = secondaryMapViews.slice(0, desiredSecondary);
|
|
5462
|
+
else if (desiredSecondary > secondaryMapViews.length) {
|
|
5463
|
+
const additions = [];
|
|
5464
|
+
for (let i = secondaryMapViews.length; i < desiredSecondary; i++) additions.push({
|
|
5465
|
+
id: v4(),
|
|
5466
|
+
view: { ...s.mapView },
|
|
5467
|
+
layerVisibility: {}
|
|
5468
|
+
});
|
|
5469
|
+
secondaryMapViews = [...secondaryMapViews, ...additions];
|
|
5470
|
+
}
|
|
5471
|
+
return {
|
|
5472
|
+
mapLayout: {
|
|
5473
|
+
...s.mapLayout,
|
|
5474
|
+
rows: clampedRows,
|
|
5475
|
+
cols: clampedCols
|
|
5476
|
+
},
|
|
5477
|
+
secondaryMapViews,
|
|
5478
|
+
isDirty: true
|
|
5479
|
+
};
|
|
5480
|
+
}),
|
|
5481
|
+
setSyncView: (syncView) => set((s) => ({
|
|
5482
|
+
mapLayout: {
|
|
5483
|
+
...s.mapLayout,
|
|
5484
|
+
syncView
|
|
5485
|
+
},
|
|
5486
|
+
isDirty: true
|
|
5487
|
+
})),
|
|
5488
|
+
setSecondaryMapView: (id, view, markDirty = false) => set((s) => {
|
|
5489
|
+
let changed = false;
|
|
5490
|
+
const secondaryMapViews = s.secondaryMapViews.map((pane) => {
|
|
5491
|
+
if (pane.id !== id) return pane;
|
|
5492
|
+
const merged = {
|
|
5493
|
+
...pane.view,
|
|
5494
|
+
...view
|
|
5495
|
+
};
|
|
5496
|
+
if (sameCamera(pane.view, merged)) return pane;
|
|
5497
|
+
changed = true;
|
|
5498
|
+
return {
|
|
5499
|
+
...pane,
|
|
5500
|
+
view: merged
|
|
5501
|
+
};
|
|
5502
|
+
});
|
|
5503
|
+
if (!changed) return s;
|
|
5504
|
+
return {
|
|
5505
|
+
secondaryMapViews,
|
|
5506
|
+
isDirty: markDirty || s.isDirty
|
|
5507
|
+
};
|
|
5508
|
+
}),
|
|
5509
|
+
setSecondaryLayerVisibility: (id, layerId, visible) => set((s) => {
|
|
5510
|
+
let changed = false;
|
|
5511
|
+
const secondaryMapViews = s.secondaryMapViews.map((pane) => {
|
|
5512
|
+
if (pane.id !== id) return pane;
|
|
5513
|
+
changed = true;
|
|
5514
|
+
return {
|
|
5515
|
+
...pane,
|
|
5516
|
+
layerVisibility: {
|
|
5517
|
+
...pane.layerVisibility,
|
|
5518
|
+
[layerId]: visible
|
|
5519
|
+
}
|
|
5520
|
+
};
|
|
5521
|
+
});
|
|
5522
|
+
if (!changed) return s;
|
|
5523
|
+
return {
|
|
5524
|
+
secondaryMapViews,
|
|
5525
|
+
isDirty: true
|
|
5526
|
+
};
|
|
5527
|
+
}),
|
|
5528
|
+
setPrimaryMapLabel: (label) => set({
|
|
5529
|
+
primaryMapLabel: label,
|
|
5530
|
+
isDirty: true
|
|
5531
|
+
}),
|
|
5532
|
+
setPrimaryRenderer: (renderer) => set((s) => s.primaryRenderer === renderer ? s : {
|
|
5533
|
+
primaryRenderer: renderer,
|
|
5534
|
+
isDirty: true
|
|
5535
|
+
}),
|
|
5536
|
+
setSecondaryMapLabel: (id, label) => set((s) => {
|
|
5537
|
+
let changed = false;
|
|
5538
|
+
const secondaryMapViews = s.secondaryMapViews.map((pane) => {
|
|
5539
|
+
if (pane.id !== id) return pane;
|
|
5540
|
+
changed = true;
|
|
5541
|
+
return {
|
|
5542
|
+
...pane,
|
|
5543
|
+
label
|
|
5544
|
+
};
|
|
5545
|
+
});
|
|
5546
|
+
if (!changed) return s;
|
|
5547
|
+
return {
|
|
5548
|
+
secondaryMapViews,
|
|
5549
|
+
isDirty: true
|
|
5550
|
+
};
|
|
5551
|
+
}),
|
|
5552
|
+
setSecondaryViewKind: (id, viewKind) => set((s) => {
|
|
5553
|
+
let changed = false;
|
|
5554
|
+
const secondaryMapViews = s.secondaryMapViews.map((pane) => {
|
|
5555
|
+
if (pane.id !== id) return pane;
|
|
5556
|
+
if ((pane.viewKind ?? "maplibre") === viewKind) return pane;
|
|
5557
|
+
changed = true;
|
|
5558
|
+
return {
|
|
5559
|
+
...pane,
|
|
5560
|
+
viewKind
|
|
5561
|
+
};
|
|
5562
|
+
});
|
|
5563
|
+
if (!changed) return s;
|
|
5564
|
+
return {
|
|
5565
|
+
secondaryMapViews,
|
|
5566
|
+
isDirty: true
|
|
5567
|
+
};
|
|
5568
|
+
}),
|
|
5569
|
+
removeSecondaryMapView: (id) => set((s) => {
|
|
5570
|
+
const secondaryMapViews = s.secondaryMapViews.filter((pane) => pane.id !== id);
|
|
5571
|
+
if (secondaryMapViews.length === s.secondaryMapViews.length) return s;
|
|
5572
|
+
const { rows, cols } = fitGrid(secondaryMapViews.length + 1, s.mapLayout.cols);
|
|
5573
|
+
return {
|
|
5574
|
+
secondaryMapViews,
|
|
5575
|
+
mapLayout: {
|
|
5576
|
+
...s.mapLayout,
|
|
5577
|
+
rows,
|
|
5578
|
+
cols
|
|
5579
|
+
},
|
|
5580
|
+
isDirty: true
|
|
5581
|
+
};
|
|
5582
|
+
}),
|
|
5583
|
+
setBasemapStyleUrl: (url) => set((state) => ({
|
|
5584
|
+
...explicitBasemapChoice(state, url),
|
|
5585
|
+
preferences: preferencesForBasemap(state),
|
|
5586
|
+
isDirty: true
|
|
5587
|
+
})),
|
|
5588
|
+
applyPlanetaryBasemap: (basemap) => set((state) => ({
|
|
5589
|
+
...explicitBasemapChoice(state, basemap.styleUrl),
|
|
5590
|
+
preferences: preferencesForBasemap(state, basemap.ellipsoidId),
|
|
5591
|
+
isDirty: true
|
|
5592
|
+
})),
|
|
5593
|
+
restoreEarthBasemap: (styleUrl) => set((state) => ({
|
|
5594
|
+
...explicitBasemapChoice(state, styleUrl),
|
|
5595
|
+
preferences: preferencesForBasemap(state, DEFAULT_ELLIPSOID_ID),
|
|
5596
|
+
isDirty: true
|
|
5597
|
+
})),
|
|
5598
|
+
setBasemapVisible: (visible) => set({
|
|
5599
|
+
basemapVisible: visible,
|
|
5600
|
+
isDirty: true
|
|
5601
|
+
}),
|
|
5602
|
+
setBasemapOpacity: (opacity) => set({
|
|
5603
|
+
basemapOpacity: opacity,
|
|
5604
|
+
isDirty: true
|
|
5605
|
+
}),
|
|
5606
|
+
setBlankBackgroundColor: (color) => set({
|
|
5607
|
+
blankBackgroundColor: normalizeBlankBackgroundColor(color),
|
|
5608
|
+
isDirty: true
|
|
5609
|
+
}),
|
|
5610
|
+
setPreferences: (preferences) => set({
|
|
5611
|
+
preferences,
|
|
5612
|
+
isDirty: true
|
|
5613
|
+
}),
|
|
5614
|
+
setRegionalBasemapPolicy: (policy) => {
|
|
5615
|
+
const state = get();
|
|
5616
|
+
const hold = policy === "block" && state.regionalBasemapHold?.reason === "prompt" ? null : state.regionalBasemapHold;
|
|
5617
|
+
if (policy === "block" && getRegionalBasemapByStyleUrl(state.basemapStyleUrl)) {
|
|
5618
|
+
withoutHistory(() => set({
|
|
5619
|
+
regionalBasemapPolicy: policy,
|
|
5620
|
+
regionalBasemapHold: hold,
|
|
5621
|
+
basemapStyleUrl: DEFAULT_BASEMAP
|
|
5622
|
+
}));
|
|
5623
|
+
return;
|
|
5624
|
+
}
|
|
5625
|
+
set({
|
|
5626
|
+
regionalBasemapPolicy: policy,
|
|
5627
|
+
regionalBasemapHold: hold
|
|
5628
|
+
});
|
|
5629
|
+
},
|
|
5630
|
+
resolveRegionalBasemapHold: (load, options = {}) => {
|
|
5631
|
+
const state = get();
|
|
5632
|
+
const hold = state.regionalBasemapHold;
|
|
5633
|
+
if (!hold) return;
|
|
5634
|
+
const answered = hold.reason === "prompt" && state.regionalBasemapPolicy !== "block";
|
|
5635
|
+
const regionalBasemapAnswers = answered && options.remember !== false ? {
|
|
5636
|
+
...state.regionalBasemapAnswers,
|
|
5637
|
+
[hold.styleUrl]: load
|
|
5638
|
+
} : state.regionalBasemapAnswers;
|
|
5639
|
+
if (!(load && answered && state.basemapStyleUrl === "https://tiles.openfreemap.org/styles/liberty")) {
|
|
5640
|
+
set({
|
|
5641
|
+
regionalBasemapHold: null,
|
|
5642
|
+
regionalBasemapAnswers
|
|
5643
|
+
});
|
|
5644
|
+
return;
|
|
5645
|
+
}
|
|
5646
|
+
withoutHistory(() => set({
|
|
5647
|
+
basemapStyleUrl: hold.styleUrl,
|
|
5648
|
+
regionalBasemapHold: null,
|
|
5649
|
+
regionalBasemapAnswers,
|
|
5650
|
+
basemapChoiceGeneration: state.basemapChoiceGeneration + 1
|
|
5651
|
+
}));
|
|
5652
|
+
replaceHistoryBasemap(DEFAULT_BASEMAP, hold.styleUrl);
|
|
5653
|
+
}
|
|
5654
|
+
});
|
|
5655
|
+
// src/store/processing-slice.ts
|
|
5656
|
+
/**
|
|
5657
|
+
* Saved processing models (issue #344) and the Processing History log
|
|
5658
|
+
* (issue #1292). Both are saved with the project.
|
|
5659
|
+
*/
|
|
5660
|
+
const createProcessingSlice = (set) => ({
|
|
5661
|
+
models: [],
|
|
5662
|
+
processingHistory: [],
|
|
5663
|
+
obiaWorkbench: null,
|
|
5664
|
+
saveModel: (model) => set((s) => {
|
|
5665
|
+
return {
|
|
5666
|
+
models: s.models.some((m) => m.id === model.id) ? s.models.map((m) => m.id === model.id ? model : m) : [...s.models, model],
|
|
5667
|
+
isDirty: true
|
|
5668
|
+
};
|
|
5669
|
+
}),
|
|
5670
|
+
deleteModel: (id) => set((s) => ({
|
|
5671
|
+
models: s.models.filter((m) => m.id !== id),
|
|
5672
|
+
isDirty: true
|
|
5673
|
+
})),
|
|
5674
|
+
addProcessingRun: (run) => set((s) => {
|
|
5675
|
+
if (s.processingHistory.some((r) => r.id === run.id)) return s;
|
|
5676
|
+
return {
|
|
5677
|
+
processingHistory: [...s.processingHistory, run].slice(-100),
|
|
5678
|
+
isDirty: true
|
|
5679
|
+
};
|
|
5680
|
+
}),
|
|
5681
|
+
updateProcessingRun: (id, patch) => set((s) => {
|
|
5682
|
+
if (!s.processingHistory.some((r) => r.id === id)) return s;
|
|
5683
|
+
return {
|
|
5684
|
+
processingHistory: s.processingHistory.map((r) => r.id === id ? {
|
|
5685
|
+
...r,
|
|
5686
|
+
...patch,
|
|
5687
|
+
id: r.id
|
|
5688
|
+
} : r),
|
|
5689
|
+
isDirty: true
|
|
5690
|
+
};
|
|
5691
|
+
}),
|
|
5692
|
+
clearProcessingHistory: () => set((s) => s.processingHistory.length === 0 ? s : {
|
|
5693
|
+
processingHistory: [],
|
|
5694
|
+
isDirty: true
|
|
5695
|
+
}),
|
|
5696
|
+
setObiaWorkbench: (obiaWorkbench, markDirty = true) => set(markDirty ? {
|
|
5697
|
+
obiaWorkbench,
|
|
5698
|
+
isDirty: true
|
|
5699
|
+
} : { obiaWorkbench })
|
|
5700
|
+
});
|
|
5701
|
+
// src/store/project-content-slice.ts
|
|
5702
|
+
/**
|
|
5703
|
+
* Project content that sits beside the layers: review comments (issue #1518),
|
|
5704
|
+
* Dashboard widgets (issue #401) and the story map. All of it is saved with the
|
|
5705
|
+
* project; `comments` and `storymap` are also tracked by undo history.
|
|
5706
|
+
*/
|
|
5707
|
+
const createProjectContentSlice = (set) => ({
|
|
5708
|
+
storymap: null,
|
|
5709
|
+
widgets: [],
|
|
5710
|
+
dashboardColumns: 2,
|
|
5711
|
+
bookmarks: [],
|
|
5712
|
+
bookmarkGroups: [],
|
|
5713
|
+
comments: [],
|
|
5714
|
+
projectInteraction: null,
|
|
5715
|
+
setBookmarks: (bookmarks, bookmarkGroups) => set({
|
|
5716
|
+
bookmarks,
|
|
5717
|
+
bookmarkGroups,
|
|
5718
|
+
isDirty: true
|
|
5719
|
+
}),
|
|
5720
|
+
addComment: (comment) => set((s) => {
|
|
5721
|
+
if (s.comments.some((c) => c.id === comment.id)) return s;
|
|
5722
|
+
return {
|
|
5723
|
+
comments: [...s.comments, comment],
|
|
5724
|
+
isDirty: true
|
|
5725
|
+
};
|
|
5726
|
+
}),
|
|
5727
|
+
replyToComment: (commentId, reply) => set((s) => {
|
|
5728
|
+
let appended = false;
|
|
5729
|
+
const nextComments = s.comments.map((c) => {
|
|
5730
|
+
if (c.id !== commentId) return c;
|
|
5731
|
+
if (c.replies.some((r) => r.id === reply.id)) return c;
|
|
5732
|
+
appended = true;
|
|
5733
|
+
return {
|
|
5734
|
+
...c,
|
|
5735
|
+
replies: [...c.replies, reply]
|
|
5736
|
+
};
|
|
5737
|
+
});
|
|
5738
|
+
if (!appended) return s;
|
|
5739
|
+
return {
|
|
5740
|
+
comments: nextComments,
|
|
5741
|
+
isDirty: true
|
|
5742
|
+
};
|
|
5743
|
+
}),
|
|
5744
|
+
toggleResolveComment: (commentId, resolved) => set((s) => ({
|
|
5745
|
+
comments: s.comments.map((c) => c.id === commentId ? {
|
|
5746
|
+
...c,
|
|
5747
|
+
resolved: resolved !== void 0 ? resolved : !c.resolved
|
|
5748
|
+
} : c),
|
|
5749
|
+
isDirty: true
|
|
5750
|
+
})),
|
|
5751
|
+
deleteComment: (commentId) => set((s) => ({
|
|
5752
|
+
comments: s.comments.filter((c) => c.id !== commentId),
|
|
5753
|
+
isDirty: true
|
|
5754
|
+
})),
|
|
5755
|
+
setComments: (comments) => set({
|
|
5756
|
+
comments,
|
|
5757
|
+
isDirty: true
|
|
5758
|
+
}),
|
|
5759
|
+
addWidget: (widget) => set((s) => {
|
|
5760
|
+
if (s.widgets.some((w) => w.id === widget.id)) return s;
|
|
5761
|
+
return {
|
|
5762
|
+
widgets: [...s.widgets, widget],
|
|
5763
|
+
isDirty: true
|
|
5764
|
+
};
|
|
5765
|
+
}),
|
|
5766
|
+
updateWidget: (id, patch) => set((s) => {
|
|
5767
|
+
if (!s.widgets.some((w) => w.id === id)) return s;
|
|
5768
|
+
return {
|
|
5769
|
+
widgets: s.widgets.map((w) => w.id === id ? {
|
|
5770
|
+
...w,
|
|
5771
|
+
...patch,
|
|
5772
|
+
id: w.id
|
|
5773
|
+
} : w),
|
|
5774
|
+
isDirty: true
|
|
5775
|
+
};
|
|
5776
|
+
}),
|
|
5777
|
+
replaceWidget: (id, widget) => set((s) => {
|
|
5778
|
+
if (!s.widgets.some((w) => w.id === id)) return s;
|
|
5779
|
+
return {
|
|
5780
|
+
widgets: s.widgets.map((w) => w.id === id ? {
|
|
5781
|
+
...widget,
|
|
5782
|
+
id
|
|
5783
|
+
} : w),
|
|
5784
|
+
isDirty: true
|
|
5785
|
+
};
|
|
5786
|
+
}),
|
|
5787
|
+
removeWidget: (id) => set((s) => ({
|
|
5788
|
+
widgets: s.widgets.filter((w) => w.id !== id),
|
|
5789
|
+
isDirty: true
|
|
5790
|
+
})),
|
|
5791
|
+
moveWidget: (id, toIndex) => set((s) => {
|
|
5792
|
+
const from = s.widgets.findIndex((w) => w.id === id);
|
|
5793
|
+
if (from < 0) return s;
|
|
5794
|
+
const target = Math.max(0, Math.min(s.widgets.length - 1, toIndex));
|
|
5795
|
+
if (target === from) return s;
|
|
5796
|
+
const widgets = [...s.widgets];
|
|
5797
|
+
const [moved] = widgets.splice(from, 1);
|
|
5798
|
+
widgets.splice(target, 0, moved);
|
|
5799
|
+
return {
|
|
5800
|
+
widgets,
|
|
5801
|
+
isDirty: true
|
|
5802
|
+
};
|
|
5803
|
+
}),
|
|
5804
|
+
setDashboardColumns: (columns) => set((s) => {
|
|
5805
|
+
if (!Number.isFinite(columns)) return s;
|
|
5806
|
+
return {
|
|
5807
|
+
dashboardColumns: Math.max(1, Math.min(6, Math.trunc(columns))),
|
|
5808
|
+
isDirty: true
|
|
5809
|
+
};
|
|
5810
|
+
}),
|
|
5811
|
+
setStorymap: (storymap) => set({
|
|
5812
|
+
storymap,
|
|
5813
|
+
isDirty: true
|
|
5814
|
+
}),
|
|
5815
|
+
updateStorymapSettings: (patch) => set((s) => ({
|
|
5816
|
+
storymap: {
|
|
5817
|
+
...s.storymap ?? DEFAULT_STORY_MAP,
|
|
5818
|
+
...patch
|
|
5819
|
+
},
|
|
5820
|
+
isDirty: true
|
|
5821
|
+
})),
|
|
5822
|
+
addStoryChapter: (chapter, atIndex) => set((s) => {
|
|
5823
|
+
const base = s.storymap ?? DEFAULT_STORY_MAP;
|
|
5824
|
+
const chapters = [...base.chapters];
|
|
5825
|
+
const index = atIndex === void 0 ? chapters.length : Math.min(Math.max(atIndex, 0), chapters.length);
|
|
5826
|
+
chapters.splice(index, 0, chapter);
|
|
5827
|
+
return {
|
|
5828
|
+
storymap: {
|
|
5829
|
+
...base,
|
|
5830
|
+
chapters
|
|
5831
|
+
},
|
|
5832
|
+
isDirty: true
|
|
5833
|
+
};
|
|
5834
|
+
}),
|
|
5835
|
+
updateStoryChapter: (id, patch) => set((s) => {
|
|
5836
|
+
if (!s.storymap) return s;
|
|
5837
|
+
return {
|
|
5838
|
+
storymap: {
|
|
5839
|
+
...s.storymap,
|
|
5840
|
+
chapters: s.storymap.chapters.map((chapter) => chapter.id === id ? {
|
|
5841
|
+
...chapter,
|
|
5842
|
+
...patch
|
|
5843
|
+
} : chapter)
|
|
5844
|
+
},
|
|
5845
|
+
isDirty: true
|
|
5846
|
+
};
|
|
5847
|
+
}),
|
|
5848
|
+
removeStoryChapter: (id) => set((s) => {
|
|
5849
|
+
if (!s.storymap) return s;
|
|
5850
|
+
return {
|
|
5851
|
+
storymap: {
|
|
5852
|
+
...s.storymap,
|
|
5853
|
+
chapters: s.storymap.chapters.filter((chapter) => chapter.id !== id)
|
|
5854
|
+
},
|
|
5855
|
+
isDirty: true
|
|
5856
|
+
};
|
|
5857
|
+
}),
|
|
5858
|
+
moveStoryChapter: (id, targetIndex) => set((s) => {
|
|
5859
|
+
if (!s.storymap) return s;
|
|
5860
|
+
const current = s.storymap.chapters.findIndex((chapter) => chapter.id === id);
|
|
5861
|
+
if (current < 0) return s;
|
|
5862
|
+
const chapters = [...s.storymap.chapters];
|
|
5863
|
+
const [chapter] = chapters.splice(current, 1);
|
|
5864
|
+
if (!chapter) return s;
|
|
5865
|
+
const next = Math.min(Math.max(targetIndex, 0), chapters.length);
|
|
5866
|
+
chapters.splice(next, 0, chapter);
|
|
5867
|
+
if (chapters.every((item, i) => item.id === s.storymap?.chapters[i]?.id)) return s;
|
|
5868
|
+
return {
|
|
5869
|
+
storymap: {
|
|
5870
|
+
...s.storymap,
|
|
5871
|
+
chapters
|
|
5872
|
+
},
|
|
5873
|
+
isDirty: true
|
|
5874
|
+
};
|
|
5875
|
+
})
|
|
5876
|
+
});
|
|
6353
5877
|
// src/store/project-slice.ts
|
|
6354
5878
|
/**
|
|
6355
5879
|
* Project identity and lifecycle: name, path, dirty flag, recent projects, and
|
|
@@ -6382,6 +5906,7 @@ const createProjectSlice = (set, get) => ({
|
|
|
6382
5906
|
projectName: DEFAULT_PROJECT_NAME,
|
|
6383
5907
|
projectPath: null,
|
|
6384
5908
|
projectGeneration: 0,
|
|
5909
|
+
projectLoaded: false,
|
|
6385
5910
|
isDirty: false,
|
|
6386
5911
|
projectPlugins: null,
|
|
6387
5912
|
legend: { ...DEFAULT_LEGEND_CONFIG },
|
|
@@ -6420,11 +5945,15 @@ const createProjectSlice = (set, get) => ({
|
|
|
6420
5945
|
...options.layerGroups ? { layerGroups: options.layerGroups } : {}
|
|
6421
5946
|
};
|
|
6422
5947
|
const applied = applyProjectToStore(project);
|
|
5948
|
+
applied.basemapStyleUrl = gateRegionalBasemap(applied.basemapStyleUrl, get().regionalBasemapPolicy, DEFAULT_BASEMAP, true).styleUrl;
|
|
6423
5949
|
set((s) => ({
|
|
6424
5950
|
...applied,
|
|
5951
|
+
regionalBasemapHold: null,
|
|
5952
|
+
basemapChoiceGeneration: s.basemapChoiceGeneration + 1,
|
|
6425
5953
|
hoverTooltipsEnabled: true,
|
|
6426
5954
|
projectPath: null,
|
|
6427
5955
|
projectGeneration: s.projectGeneration + 1,
|
|
5956
|
+
projectLoaded: false,
|
|
6428
5957
|
isDirty: false,
|
|
6429
5958
|
selectedLayerId: null,
|
|
6430
5959
|
selectedFeatureId: null,
|
|
@@ -6458,13 +5987,23 @@ const createProjectSlice = (set, get) => ({
|
|
|
6458
5987
|
loadProject: (project, path = null, options = {}) => {
|
|
6459
5988
|
const applied = applyProjectToStore(project);
|
|
6460
5989
|
applied.layers = reapplyLayerJoins(applied.layers);
|
|
5990
|
+
const requestedBasemap = applied.basemapStyleUrl;
|
|
5991
|
+
const priorAnswer = get().regionalBasemapAnswers[requestedBasemap];
|
|
5992
|
+
const gated = gateRegionalBasemap(requestedBasemap, get().regionalBasemapPolicy, DEFAULT_BASEMAP, options.trustBasemap === true || priorAnswer === true);
|
|
5993
|
+
if (gated.held === "prompt" && priorAnswer === false) delete gated.held;
|
|
5994
|
+
applied.basemapStyleUrl = gated.styleUrl;
|
|
6461
5995
|
const presentStory = options.presenting ?? (applied.storymap?.chapters.length ?? 0) > 0;
|
|
6462
5996
|
const selectedLayerId = project.selectedLayerId === null ? null : typeof project.selectedLayerId === "string" && applied.layers.some((layer) => layer.id === project.selectedLayerId) ? project.selectedLayerId : applied.layers[0]?.id ?? null;
|
|
6463
5997
|
set((s) => ({
|
|
6464
5998
|
...applied,
|
|
5999
|
+
regionalBasemapHold: gated.held ? {
|
|
6000
|
+
styleUrl: requestedBasemap,
|
|
6001
|
+
reason: gated.held
|
|
6002
|
+
} : null,
|
|
6465
6003
|
hoverTooltipsEnabled: true,
|
|
6466
6004
|
projectPath: path,
|
|
6467
6005
|
projectGeneration: s.projectGeneration + 1,
|
|
6006
|
+
projectLoaded: true,
|
|
6468
6007
|
isDirty: false,
|
|
6469
6008
|
selectedLayerId,
|
|
6470
6009
|
selectedFeatureId: null,
|
|
@@ -9133,6 +8672,6 @@ function withJpegTablesPatch(module) {
|
|
|
9133
8672
|
openCog: (async (input) => patchJpegCogSource(await module.openCog(input)))
|
|
9134
8673
|
};
|
|
9135
8674
|
}
|
|
9136
|
-
export { ALL_APP_PRIVILEGES, ALL_DEPLOYMENT_CAPABILITIES, ARCGIS_PROJECTION_PRESETS, BLANK_BASEMAP, BLEND_MODES, BUILT_IN_STYLE_PRESETS, CARTOCIUDAD_MIN_INTERVAL_MS, CARTOCIUDAD_PUBLIC_HOST, CESIUM_BASEMAPS, CESIUM_BING_AERIAL_ASSET_ID, CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID, CESIUM_ION_QUICK_PICKS, CESIUM_ION_SOURCE_KIND, CESIUM_KML_SOURCE_KIND, CESIUM_OSM_BUILDINGS_ASSET_ID, CHINA_BASEMAPS, CZML_QUICK_PICKS, CZML_SOURCE_KIND, DEFAULT_BASEMAP, DEFAULT_BLEND_MODE, DEFAULT_COLLABORATION_STATE, DEFAULT_DASHBOARD_COLUMNS, DEFAULT_EDITOR_IDENTITY, DEFAULT_EDITOR_TRACKING_CONFIG, DEFAULT_ELLIPSOID_ID, DEFAULT_FORWARD_GEOCODE_ENDPOINT, DEFAULT_GEOCODING_PROVIDER_ID, DEFAULT_LAYER_GROUP_OPACITY, DEFAULT_LAYER_STYLE, DEFAULT_LEGEND_CONFIG, DEFAULT_MAP_GRID_LAYOUT, DEFAULT_PRIMARY_RENDERER, DEFAULT_PRINT_LAYOUT, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_PREFERENCES, DEFAULT_REVERSE_GEOCODE_ENDPOINT, DEFAULT_ROUTING_ENDPOINT, DEFAULT_STORY_CHAPTER_DURATION_SECONDS, DEFAULT_STORY_MAP, DEPLOYMENT_CAPABILITIES, DUCKDB_VECTOR_FEATURE_WARN_COUNT, DUCKDB_VECTOR_ROUTE_BYTES, EARTH_MEAN_RADIUS_METERS, EDITOR_IDENTITY_STORAGE_KEY, ELEVATION_REQUEST_TIMEOUT_MS, ELLIPSOIDS, EXPRESSION_FUNCTION_CATEGORIES, ElevationFetchError, FEET_PER_METER, FEET_PER_MILE, GEOCODE_ATTRIBUTE_PREFIX, GEOCODE_DISPLAY_NAME_KEY, GEOCODE_LAT_KEY, GEOCODE_LON_KEY, GEOCODE_PROVIDER_KEY, GEOCODE_SCORE_KEY, GEOCODE_STATUS_KEY, GEOCODING_PROVIDERS, GOOGLE_MAPS_API_KEY_HEADER, GeocoderServiceError, GeocoderUnsupportedError, IDENTIFY_ALL_LAYERS_ID, LABEL_NUMBER_LOCALES, LABEL_NUMBER_SAMPLE, LARGE_VECTOR_FEATURE_THRESHOLD, LAYERS_FILE_TYPE, LAYERS_FILE_VERSION, LAYER_LIBRARY_BUNDLE_TYPE, LAYER_LIBRARY_BUNDLE_VERSION, LAYER_PALETTE, LAYER_STYLES_FILE_TYPE, LAYER_STYLES_FILE_VERSION, LAYER_TYPES, MAP_CRS_CHANGE_EVENT, MAP_CRS_PRESETS, MAX_DASHBOARD_COLUMNS, MAX_DIAGRAM_FEATURES, MAX_DIAGRAM_SCAN_FEATURES, MAX_LAYERS_FILE_BYTES, MAX_LAYER_LIBRARY_ENTRIES, MAX_LAYER_LIBRARY_ENTRY_BYTES, MAX_MAP_GRID_DIM, MAX_POINTS_PER_REQUEST, MAX_PROCESSING_HISTORY, MAX_PROJECT_BOOKMARKS, MAX_REDACT_DEPTH, MAX_STORY_CHAPTER_DURATION_SECONDS, MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0, METERS_PER_FOOT, METERS_PER_MILE, METERS_PER_NAUTICAL_MILE, MIN_DASHBOARD_COLUMNS, MIN_DIAGRAM_SIZE, MIN_STORY_CHAPTER_DURATION_SECONDS, NETCDF_IMAGE_SOURCE_KIND, NOMINATIM_MIN_INTERVAL_MS, NOMINATIM_PUBLIC_HOST, NO_DEPLOYMENT_CAPABILITIES, NO_LAYERS, OPENFREEMAP_BASEMAPS, PHOTO_FULL_PROPERTY, PHOTO_PROPERTY, PLANETARY_BASEMAPS, PLANETARY_BASEMAP_GROUPS, PLANETARY_BASEMAP_SENTINEL_PREFIX, PLANET_SWITCHER_OPTIONS, POINTER_ELEVATION_CACHE_LIMIT, POINTER_ELEVATION_DEBOUNCE_MS, POPUP_IMAGE_HEIGHT_RANGE, POPUP_MAX_WIDTH_RANGE, PROJECT_CREDENTIAL_FIELDS, PROJECT_VERSION, PROTOMAPS_BASEMAPS, PUBLIC_GEOCODE_ROW_CAP, PUBLISHABLE_PLUGIN_SETTINGS, RAMP_STYLE_KEYS, RASTER_APPEARANCE_STATE_KEYS, REGIONAL_BASEMAPS, REGIONAL_BASEMAP_GROUPS, REGIONAL_BASEMAP_SENTINEL_PREFIX, ROLE_PRIVILEGES, RoutingRequestError, S3CorsError, S3_MAX_PRESIGN_SECONDS, SELECTION_MODES, SQL_QUERY_SOURCE_KIND, STAC_PROCESSING_EXTENSION, STAC_VERSION, STYLE_LIBRARY_BUNDLE_TYPE, STYLE_LIBRARY_BUNDLE_VERSION, SYMBOL_STYLE_KEYS, VECTOR_COLOR_RAMPS, activeLayerFilterExpression, activeQuickFilters, adaptBboxTemplateToCrs, addLayersToProject, allowsCredentialHeaders, appPrivilegeReason, applyCopiedLayerStyle, applyGroupEffects, applyJoinsToLayer, applyLayerJoins, applyLayerVirtualFields, applyMatchedSelection, applyProjectToStore, applySelectionMode, applyStoryLayerOpacity, arcgisLocatorInputField, asTileScheme, attributeFormFieldLabel, attributeLinkUrl, availableCesiumBasemap, basemapToCesiumImagery, batchDecodePolylines, bodyLengthToEarth, buildForwardGeocodeUrl, buildIsochroneRequest, buildLayerPanelUnits, buildLayerStacItem, buildLayerTree, buildMatrixRequest, buildReverseGeocodeUrl, buildRouteRequest, canRedoProjectRestore, canSaveLayerToLibrary, canUndoProjectRestore, captureLayerLibraryEntry, cascadeLayerJoinRefresh, cesiumIonAssetId, cesiumIonAssetKind, cesiumKmlSource, changedPreferenceCredentials, circleRadiusValue, clampLabelDecimals, clampLatitude, clearExternalNativePaintBridge, clearHistory, clearQuickFilterValues, coerceAttributeFormValue, collectDiagramData, collectTransitiveJoinSourceIds, compareSequenceValues, compileFeatureExpression, compileLayerFilters, compileQuickFilter, compileQuickFilters, computeGeoArrowBBox, controlRendersLayer, convertTiffYCbCrToRgb, copyableLayerStyleKind, createCesiumIonLayer, createCesiumKmlLayer, createCzmlLayer, createDefaultAppCapabilities, createDefaultMapView, createDefaultPrintLayout, createEmptyProject, createEqualIntervalBreaks, createGraduatedClassBreaks, createLayerLibraryEntryId, createPointerElevationResolver, createProjectLayerSerializationCache, createProjectTemplate, createQuantileBreaks, createSampleStoryMap, createStyleLayerEvaluator, createStyleLibraryEntryId, csvRowsToGeocodeRequests, currentEditorIdentity, czmlSource, darkenHex, datumShiftFor, decodePolyline, decodePolylineDetailed, deriveTileMatrixSet, detachProjectCopy, detectNonGeographicCoordinates, diagramAnchor, diagramPixelSize, diagramsSuppressedByPointRenderer, diffFeatureCollections, diffProjects, documentLocale, dominantGeometry, drawMarkerPath, earthAreaToBody, earthLengthToBody, editorTrackingFieldNames, effectiveLayerRenderState, effectiveVectorRules, encodePolyline, encodeS3Key, ensureEditorTrackingFields, environmentVariableAccount, evaluateMapExpression, excludeHiddenFieldsFromGeojson, excludeHiddenFieldsFromProject, explainS3ReadError, extractCopiedLayerStyle, extractLayerStyleEntries, extractLayersFileContent, extractStyleLibraryStyle, extrusionColorValue, extrusionHeightValue, featureCollectionToGeoArrow, featureSelectionId, fetchArcGISLocatorInfo, fetchElevations, findLayerStyleEntryIndex, formatCameraAltitude, formatExpressionPreviewValue, formatLabelNumber, formatLabelNumberSample, formatPixelValue, formatPopupValue, formatRoundNum, formatS3Uri, generatorCircleRadiusValue, generatorSizeRange, geoArrowToFeatureCollection, geoJSONToPolylineStr, geocodeForward, geocodeMatchToFeature, geocodeReverse, geocodeSuggest, geocoderMinIntervalMs, geocoderNeedsApiKey, geocoderSupportsReverse, geocoderSupportsSuggest, geocodingApiKeyAccount, geojsonHasZCoordinates, getActiveBodyRadiusRatio, getActiveEllipsoid, getActiveMeanRadiusMeters, getActiveSemiMajorAxisMeters, getArcgisApiKey, getAttributeFormField, getBuildEnvironment, getCesiumIonToken, getEllipsoid, getExternalNativePaintBridge, getGeocoderConfig, getGeocodingProvider, getGoogleMapsApiKey, getHistoryCoalesceMs, getMapCrsPreset, getMapboxAccessToken, getMaxHistoryFeatureCount, getPlanetaryBasemapById, getPlanetaryBasemapByStyleUrl, getProtomapsApiKey, getProtomapsStyleUrl, getRegionalBasemapById, getRegionalBasemapByStyleUrl, getRoundNum, getRoutingConfig, getRuntimeEnvironment, getS3UrlSigner, getSpatialExtensionPath, getVectorColorRamp, googleMapsApiKeyHeaderValue, groupMemberIndices, hasActiveLayerFilter, hasActiveQuickFilter, hasAppPrivilege, hasHeaderReferences, hasPathTraversal, hasRestorableLayerSource, hasSimpleStyleProperties, heatmapRampColors, horizontalBbox, hydrateProjectLayer, identifyAllIncludes, identifyStateWithoutLayers, inferFieldTypes, inferLayerCapabilities, initialLayerStyle, installCogTilerDatumShift, interpolateColors, interpolateHexColor, interpolateRampColors, intersectPrivileges, invertSelection, isAbbreviatedJpegCompression, isAbsoluteFilesystemPath, isAdoptedVectorAwaitingFeatures, isAllowedPluginManifestUrl, isAttributeFormFieldVisible, isCesiumIonLayer, isCesiumKmlLayer, isCesiumOnlyLayer, isCredentialFieldName, isCredentialUrlParam, isCredentialedS3Url, isCzmlLayer, isDefaultPrintLayout, isDeploymentCapability, isDiagramStyleEnabled, isDuckDBQueryLayer, isExternalNativeLayerRecord, isGeoArrowBatch, isGooglePhotorealisticTilesetUrl, isHeaderReferenceOnly, isHexColor, isInitialLayerStyle, isInlineImageValue, isInternalPopupField, isIpadDesktopUserAgent, isMaintainedEditorTrackingField, isMapClickClaimed, isPopupClickEnabled, isPopupHoverEnabled, isRasterMarkerSource, isReferencedLayer, isRegionalBasemapSentinel, isS3Uri, isSafePopupUrl, isSecretEnvironmentVariable, isSessionOnlyLayer, isStoryHiddenLayer, isStyleLibraryTargetLayer, isStyleSpecColor, isSvgMarkerResponse, isTileAddressError, isValidMetadataDate, isValidMetadataEmail, isValidMetadataUrl, isVectorColorExpression, isochroneResponseToFeatures, jsonValuesEqual, labelFieldTextField, layerGroupDepth, layerGroupMoveability, layerGroupSortability, layerJoinKey, layerLibraryConfigPatch, layerLibraryEntryNeedsLocalFile, layerLocalPath, layerPanelGroupHeaders, lineWidthValue, localFileName, lookupProjectCredential, mapZoomStepOutputs, matchFeaturesByExpression, matchLayerStyleEntry, matrixResponseToFeatures, meanRadiusMeters, mercatorMetersPerPixelAtZoom0, mergeAttributeFormConfigs, metadataDateToRfc3339, metersWidthExpression, nextDelayMs, nextLayerPaletteColor, nominatimResultToFeature, nominatimReverseResultToDisplay, nonEmptyRecord, normalizeAppPrivileges, normalizeArcGISLocatorUrl, normalizeArcgisWkid, normalizeBlankBackgroundColor, normalizeBookmarkGroups, normalizeBookmarks, normalizeCesiumBasemap, normalizeDashboardColumns, normalizeGeocodingLocator, normalizeGeocodingProviderId, normalizeGroupContiguity, normalizeHexColor, normalizeLayerCapabilities, normalizeLayerDescriptiveMetadata, normalizeLayerLibraryEntries, normalizeLayerStyleEntries, normalizeLayersFileContent, normalizeLegendConfig, normalizeLongitude, normalizeMapCrs, normalizeMapLayout, normalizeMapViewState, normalizeModelGraph, normalizeModels, normalizeObiaWorkbench, normalizePrimaryRenderer, normalizePrintLayoutConfig, normalizeProcessingHistory, normalizeProjectComments, normalizeProjectInteraction, normalizeSecondaryMapViews, normalizeStoryChapterDurationSeconds, normalizeStoryMap, normalizeStyleLibraryEntries, normalizeWidgets, overlayStoredEnvironmentVariables, overlayStoredGeocodingApiKeys, overlayStoredPreferenceCredentials, parseArcGISLocatorInfo, parseArcGISSuggestions, parseCesiumIonAssetId, parseContours, parseCzml, parseDeploymentCapabilities, parseHexColor, parseHexColorList, parseJsonExpression, parseLayerLibrary, parseLayerStylesFile, parseLayersFile, parseMapCrsEpsgCode, parseMetadataKeywords, parseProject, parseS3BucketList, parseS3Error, parseS3ListObjects, parseS3Url, parseStoryMapCsv, parseStoryMapJson, parseStyleLibrary, parseValueMapText, patchJpegCogSource, persistedThreeDTilesRequestHeaders, pickEditorIdentity, planLayerLibraryAdd, pluginOwnsPaint, polylineStrToGeoJSON, popupFieldLabel, portableWmsTileUrl, presignS3Url, previewValue, printLayoutConfigsEqual, projectFromStore, projectPathLabel, proportionalRadiusExpression, proportionalSizeRange, readStoredAuthorName, reapplyLayerJoins, redactConfigurationCredentials, redactCredentials, redactProjectCredentials, redactUrlCredentials, redo, registerMapClickClaim, registerProjectRestoreHistory, registerS3UrlSigner, removeTrailingJsonCommas, rendererAppliesOpacity, reorderLayerGroupInPanel, requestIsochrone, requestMatrix, requestNavigationRoute, requestRoute, resolveConfiguredPopupTitle, resolveEditorTrackingConfig, resolveGeocoderConfig, resolveHeaderReferences, resolveIdentifyTarget, resolveLabelNumberLocale, resolveLayerCapabilities, resolveLayerTileAddress, resolveMapGrid, resolvePopupBody, resolvePopupImageHeight, resolvePopupMaxWidth, resolvePopupRows, resolvePopupTitle, resolveProjectHeaderReferences, resolveReadableUrl, resolveRolePrivileges, resolveSvgSource, resolveThreeDTilesRequestHeaders, resolveTileAddress, rgbToHex, routeResponseToFeatures, rowCap, ruleBasedColorExpression, ruleBasedVisibilityFilter, s3ObjectHttpsUrl, sameCesiumImagery, sampleMapTerrain, sampleMapTerrainPoint, sampleRemoteElevations, sanitizeLayerStylePatch, scaleAltitudeToActiveBody, scaleDenomination, scaleSpan, scrubCommentsForRemovedLayers, scrubLegendForRemovedLayers, scrubPrintLayoutForLayers, scrubPrintLayoutForRemovedLayers, scrubWidgetsForRemovedLayers, selectLayerById, selectLayerIds, selectLayerSummaries, selectLayersWhen, selectedGeocodingLocator, serializeLayerLibrary, serializeLayerStylesFile, serializeLayersFile, serializeProject, serializeProjectWithLayerCache, serializeProjectWithLayerCacheAsync, serializeStoryMapCsv, serializeStoryMapJson, serializeStyleLibrary, setActiveEllipsoidId, setExternalNativePaintBridge, setGeocodingFetch, setGeocodingTokenProvider, setHistoryCoalesceMs, setMaxHistoryFeatureCount, setProjectCredentialLookup, setRegistryPublishableSettings, setStoredAuthorName, shouldThrottle, shouldUseTiledRendering, shouldZoomToNewLayers, simpleStyleNumberValue, sortLayerGroupInPanel, splitProjectCredentials, stacAssetMediaType, stampFeatureCollectionEditorTracking, stampFeatureEditorTracking, stampFeaturePropertiesEditorTracking, storyLayerOpacityFactor, storyLocationView, storyMapHasContent, storyVisibleLayers, stringifyPopupValue, stripGoogleMapsApiKeyHeader, stripJoinFields, stripVirtualFieldColumns, styleValue, subscribeProjectRestoreHistory, substituteExpressionVariables, supportsBridgedOpacity, transformGeojsonElevation, trimHistoryBySize, undo, unescapePolyline, uniqueImportedLayerName, unmatchedGeocodeFeature, unresolvedLayerLibraryJoins, unsignedSourceUrl, useAppCapability, useAppStore, useLayer, useLayerIds, useLayerSummaries, useLayersWhen, validateAttributeFormField, validateAttributeFormValues, validateLayerDescriptiveMetadata, validateMapExpression, valueMapLabelFor, valueMapToText, vectorCircleColorValue, vectorColorExpression, vectorFillColorValue, vectorFillOpacityValue, vectorLineColorValue, vectorOutlineColorValue, vectorStrokeWidthValue, visiblePopupFields, visiblePopupProperties, withDatumShift, withGeoKeysDatumShift, withJpegTablesPatch, withWktDatumShift, withoutHistory };
|
|
8675
|
+
export { ALL_APP_PRIVILEGES, ALL_DEPLOYMENT_CAPABILITIES, ARCGIS_PROJECTION_PRESETS, BLANK_BASEMAP, BLEND_MODES, BUILT_IN_STYLE_PRESETS, CARTOCIUDAD_MIN_INTERVAL_MS, CARTOCIUDAD_PUBLIC_HOST, CESIUM_BASEMAPS, CESIUM_BING_AERIAL_ASSET_ID, CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID, CESIUM_ION_QUICK_PICKS, CESIUM_ION_SOURCE_KIND, CESIUM_KML_SOURCE_KIND, CESIUM_OSM_BUILDINGS_ASSET_ID, CHINA_BASEMAPS, CZML_QUICK_PICKS, CZML_SOURCE_KIND, DEFAULT_BASEMAP, DEFAULT_BLEND_MODE, DEFAULT_COLLABORATION_STATE, DEFAULT_DASHBOARD_COLUMNS, DEFAULT_EDITOR_IDENTITY, DEFAULT_EDITOR_TRACKING_CONFIG, DEFAULT_ELLIPSOID_ID, DEFAULT_FORWARD_GEOCODE_ENDPOINT, DEFAULT_GEOCODING_PROVIDER_ID, DEFAULT_LAYER_GROUP_OPACITY, DEFAULT_LAYER_STYLE, DEFAULT_LEGEND_CONFIG, DEFAULT_MAP_GRID_LAYOUT, DEFAULT_PRIMARY_RENDERER, DEFAULT_PRINT_LAYOUT, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_PREFERENCES, DEFAULT_REVERSE_GEOCODE_ENDPOINT, DEFAULT_ROUTING_ENDPOINT, DEFAULT_STORY_CHAPTER_DURATION_SECONDS, DEFAULT_STORY_MAP, DEPLOYMENT_CAPABILITIES, DUCKDB_VECTOR_FEATURE_WARN_COUNT, DUCKDB_VECTOR_ROUTE_BYTES, EARTH_MEAN_RADIUS_METERS, EDITOR_IDENTITY_STORAGE_KEY, ELEVATION_REQUEST_TIMEOUT_MS, ELLIPSOIDS, EXPRESSION_FUNCTION_CATEGORIES, ElevationFetchError, FEET_PER_METER, FEET_PER_MILE, GEOCODE_ATTRIBUTE_PREFIX, GEOCODE_DISPLAY_NAME_KEY, GEOCODE_LAT_KEY, GEOCODE_LON_KEY, GEOCODE_PROVIDER_KEY, GEOCODE_SCORE_KEY, GEOCODE_STATUS_KEY, GEOCODING_PROVIDERS, GOOGLE_MAPS_API_KEY_HEADER, GeocoderServiceError, GeocoderUnsupportedError, IDENTIFY_ALL_LAYERS_ID, LABEL_NUMBER_LOCALES, LABEL_NUMBER_SAMPLE, LARGE_VECTOR_FEATURE_THRESHOLD, LAYERS_FILE_TYPE, LAYERS_FILE_VERSION, LAYER_LIBRARY_BUNDLE_TYPE, LAYER_LIBRARY_BUNDLE_VERSION, LAYER_PALETTE, LAYER_STYLES_FILE_TYPE, LAYER_STYLES_FILE_VERSION, LAYER_TYPES, MAP_CRS_CHANGE_EVENT, MAP_CRS_PRESETS, MAX_DASHBOARD_COLUMNS, MAX_DIAGRAM_FEATURES, MAX_DIAGRAM_SCAN_FEATURES, MAX_LAYERS_FILE_BYTES, MAX_LAYER_LIBRARY_ENTRIES, MAX_LAYER_LIBRARY_ENTRY_BYTES, MAX_MAP_GRID_DIM, MAX_POINTS_PER_REQUEST, MAX_PROCESSING_HISTORY, MAX_PROJECT_BOOKMARKS, MAX_REDACT_DEPTH, MAX_STORY_CHAPTER_DURATION_SECONDS, MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0, METERS_PER_FOOT, METERS_PER_MILE, METERS_PER_NAUTICAL_MILE, MIN_DASHBOARD_COLUMNS, MIN_DIAGRAM_SIZE, MIN_STORY_CHAPTER_DURATION_SECONDS, NETCDF_IMAGE_SOURCE_KIND, NOMINATIM_MIN_INTERVAL_MS, NOMINATIM_PUBLIC_HOST, NO_DEPLOYMENT_CAPABILITIES, NO_LAYERS, OPENFREEMAP_BASEMAPS, PHOTO_FULL_PROPERTY, PHOTO_PROPERTY, PLANETARY_BASEMAPS, PLANETARY_BASEMAP_GROUPS, PLANETARY_BASEMAP_SENTINEL_PREFIX, PLANET_SWITCHER_OPTIONS, POINTER_ELEVATION_CACHE_LIMIT, POINTER_ELEVATION_DEBOUNCE_MS, POPUP_IMAGE_HEIGHT_RANGE, POPUP_MAX_WIDTH_RANGE, PROJECT_CREDENTIAL_FIELDS, PROJECT_VERSION, PROTOMAPS_BASEMAPS, PUBLIC_GEOCODE_ROW_CAP, PUBLISHABLE_PLUGIN_SETTINGS, RAMP_STYLE_KEYS, RASTER_APPEARANCE_STATE_KEYS, REGIONAL_BASEMAPS, REGIONAL_BASEMAP_GROUPS, REGIONAL_BASEMAP_SENTINEL_PREFIX, ROLE_PRIVILEGES, RoutingRequestError, S3CorsError, S3_MAX_PRESIGN_SECONDS, SELECTION_MODES, SQL_QUERY_SOURCE_KIND, STAC_PROCESSING_EXTENSION, STAC_VERSION, STROKE_DASH_PRESETS, STYLE_LIBRARY_BUNDLE_TYPE, STYLE_LIBRARY_BUNDLE_VERSION, SYMBOL_STYLE_KEYS, VECTOR_COLOR_RAMPS, activeLayerFilterExpression, activeQuickFilters, adaptBboxTemplateToCrs, addLayersToProject, allowsCredentialHeaders, appPrivilegeReason, applyCopiedLayerStyle, applyGroupEffects, applyJoinsToLayer, applyLayerFilters, applyLayerJoins, applyLayerVirtualFields, applyMatchedSelection, applyProjectToStore, applySelectionMode, applyStoryLayerOpacity, arcgisLocatorInputField, asTileScheme, attributeFormFieldLabel, attributeLinkUrl, availableCesiumBasemap, basemapToCesiumImagery, batchDecodePolylines, blobRangeReader, bodyLengthToEarth, buildForwardGeocodeUrl, buildIsochroneRequest, buildLayerPanelUnits, buildLayerStacItem, buildLayerTree, buildMatrixRequest, buildReverseGeocodeUrl, buildRouteRequest, bytesRangeReader, canRedoProjectRestore, canSaveLayerToLibrary, canUndoProjectRestore, captureLayerLibraryEntry, cascadeLayerJoinRefresh, centralAngle, cesiumIonAssetId, cesiumIonAssetKind, cesiumKmlSource, changedPreferenceCredentials, circleRadiusValue, clampLabelDecimals, clampLatitude, clearExternalNativePaintBridge, clearHistory, clearQuickFilterValues, coerceAttributeFormValue, collectDiagramData, collectLevels, collectTransitiveJoinSourceIds, compareSequenceValues, compileFeatureExpression, compileLayerFilters, compileLevelFilter, compileQuickFilter, compileQuickFilters, computeGeoArrowBBox, controlRendersLayer, convertTiffYCbCrToRgb, copyableLayerStyleKind, createCesiumIonLayer, createCesiumKmlLayer, createCzmlLayer, createDefaultAppCapabilities, createDefaultMapView, createDefaultPrintLayout, createEmptyProject, createEqualIntervalBreaks, createGraduatedClassBreaks, createLayerLibraryEntryId, createPointerElevationResolver, createProjectLayerSerializationCache, createProjectTemplate, createQuantileBreaks, createSampleStoryMap, createStyleLayerEvaluator, createStyleLibraryEntryId, csvRowsToGeocodeRequests, currentEditorIdentity, currentLevelChoice, czmlSource, darkenHex, datumShiftFor, decodePolyline, decodePolylineDetailed, deriveTileMatrixSet, detachProjectCopy, detectNonGeographicCoordinates, diagramAnchor, diagramPixelSize, diagramsSuppressedByPointRenderer, diffFeatureCollections, diffProjects, documentLocale, dominantGeometry, drawMarkerPath, earthAreaToBody, earthLengthToBody, editorTrackingFieldNames, effectiveLayerRenderState, effectiveVectorRules, encodePolyline, encodeS3Key, ensureEditorTrackingFields, environmentVariableAccount, evaluateMapExpression, excludeHiddenFieldsFromGeojson, excludeHiddenFieldsFromProject, explainS3ReadError, extractCopiedLayerStyle, extractLayerStyleEntries, extractLayersFileContent, extractStyleLibraryStyle, extrusionBaseValue, extrusionColorValue, extrusionFloorHeightValue, extrusionHeightValue, featureCollectionToGeoArrow, featureLevel, featureSelectionId, fetchArcGISLocatorInfo, fetchElevations, findLayerStyleEntryIndex, formatCameraAltitude, formatExpressionPreviewValue, formatLabelNumber, formatLabelNumberSample, formatPixelValue, formatPopupValue, formatRoundNum, formatS3Uri, gateRegionalBasemap, generatorCircleRadiusValue, generatorSizeRange, geoArrowToFeatureCollection, geoJSONToPolylineStr, geocodeForward, geocodeMatchToFeature, geocodeReverse, geocodeSuggest, geocoderMinIntervalMs, geocoderNeedsApiKey, geocoderSupportsReverse, geocoderSupportsSuggest, geocodingApiKeyAccount, geodesicDistance, geodesicLength, geodesicRadiusBounds, geodesicRingArea, geojsonHasZCoordinates, getActiveBodyRadiusRatio, getActiveEllipsoid, getActiveMeanRadiusMeters, getActiveSemiMajorAxisMeters, getArcgisApiKey, getAttributeFormField, getBuildEnvironment, getCesiumIonToken, getEllipsoid, getExternalNativePaintBridge, getGeocoderConfig, getGeocodingProvider, getGoogleMapsApiKey, getHistoryCoalesceMs, getMapCrsPreset, getMapboxAccessToken, getMaxHistoryFeatureCount, getPlanetaryBasemapById, getPlanetaryBasemapByStyleUrl, getProtomapsApiKey, getProtomapsStyleUrl, getRegionalBasemapById, getRegionalBasemapByStyleUrl, getRoundNum, getRoutingConfig, getRuntimeEnvironment, getS3UrlSigner, getSpatialExtensionPath, getVectorColorRamp, googleMapsApiKeyHeaderValue, groupMemberIndices, guessLevelField, hasActiveLayerFilter, hasActiveQuickFilter, hasAppPrivilege, hasHeaderReferences, hasPathTraversal, hasRestorableLayerSource, hasSimpleStyleProperties, heatmapRampColors, horizontalBbox, hydrateProjectLayer, identifyAllIncludes, identifyStateWithoutLayers, inferFieldTypes, inferLayerCapabilities, initialLayerStyle, installCogTilerDatumShift, interpolateColors, interpolateHexColor, interpolateRampColors, intersectPrivileges, invertSelection, isAbbreviatedJpegCompression, isAbsoluteFilesystemPath, isAdoptedVectorAwaitingFeatures, isAllowedPluginManifestUrl, isAttributeFormFieldVisible, isCesiumIonLayer, isCesiumKmlLayer, isCesiumOnlyLayer, isCredentialFieldName, isCredentialUrlParam, isCredentialedS3Url, isCzmlLayer, isDefaultPrintLayout, isDeploymentCapability, isDiagramStyleEnabled, isDuckDBQueryLayer, isExternalNativeLayerRecord, isGeoArrowBatch, isGooglePhotorealisticTilesetUrl, isHeaderReferenceOnly, isHexColor, isImdfArchiveEntries, isImdfZipBlob, isImdfZipRange, isInitialLayerStyle, isInlineImageValue, isInternalPopupField, isIpadDesktopUserAgent, isLevelFilterActive, isLonLat, isMacOsZipMetadata, isMaintainedEditorTrackingField, isMapClickClaimed, isPopupClickEnabled, isPopupHoverEnabled, isRasterMarkerSource, isReferencedLayer, isRegionalBasemapSentinel, isS3Uri, isSafePopupUrl, isSecretEnvironmentVariable, isSessionOnlyLayer, isStoryHiddenLayer, isStrokeDashPattern, isStyleLibraryTargetLayer, isStyleSpecColor, isSvgMarkerResponse, isTileAddressError, isValidMetadataDate, isValidMetadataEmail, isValidMetadataUrl, isVectorColorExpression, isochroneResponseToFeatures, jsonValuesEqual, labelFieldTextField, latLonBoxArea, layerGroupDepth, layerGroupMoveability, layerGroupSortability, layerJoinKey, layerLibraryConfigPatch, layerLibraryEntryNeedsLocalFile, layerLocalPath, layerPanelGroupHeaders, levelValueMatches, lineWidthValue, listZipEntryPaths, localFileName, lookupProjectCredential, mapCrsPreferencePatch, mapZoomStepOutputs, matchFeaturesByExpression, matchLayerStyleEntry, matchingLevelValues, matrixResponseToFeatures, meanRadiusMeters, mercatorMetersPerPixelAtZoom0, mergeAttributeFormConfigs, mergeLevels, metadataDateToRfc3339, metersWidthExpression, nextDelayMs, nextLayerPaletteColor, nominatimResultToFeature, nominatimReverseResultToDisplay, nonEmptyRecord, normalizeAppPrivileges, normalizeArcGISLocatorUrl, normalizeArcgisWkid, normalizeBlankBackgroundColor, normalizeBookmarkGroups, normalizeBookmarks, normalizeCesiumBasemap, normalizeDashboardColumns, normalizeGeocodingLocator, normalizeGeocodingProviderId, normalizeGroupContiguity, normalizeHexColor, normalizeLayerCapabilities, normalizeLayerDescriptiveMetadata, normalizeLayerLibraryEntries, normalizeLayerStyleEntries, normalizeLayersFileContent, normalizeLegendConfig, normalizeLongitude, normalizeMapCrs, normalizeMapLayout, normalizeMapViewState, normalizeModelGraph, normalizeModels, normalizeObiaWorkbench, normalizePrimaryRenderer, normalizePrintLayoutConfig, normalizeProcessingHistory, normalizeProjectComments, normalizeProjectInteraction, normalizeSecondaryMapViews, normalizeStoryChapterDurationSeconds, normalizeStoryMap, normalizeStyleLibraryEntries, normalizeWidgets, overlayStoredEnvironmentVariables, overlayStoredGeocodingApiKeys, overlayStoredPreferenceCredentials, parseArcGISLocatorInfo, parseArcGISSuggestions, parseCesiumIonAssetId, parseContours, parseCzml, parseDeploymentCapabilities, parseHexColor, parseHexColorList, parseJsonExpression, parseLayerLibrary, parseLayerStylesFile, parseLayersFile, parseLevelValue, parseMapCrsEpsgCode, parseMetadataKeywords, parseProject, parseS3BucketList, parseS3Error, parseS3ListObjects, parseS3Url, parseStoryMapCsv, parseStoryMapJson, parseStyleLibrary, parseValueMapText, patchJpegCogSource, persistedThreeDTilesRequestHeaders, pickEditorIdentity, planLayerLibraryAdd, pluginOwnsPaint, polylineStrToGeoJSON, popupFieldLabel, portableWmsTileUrl, presignS3Url, previewValue, printLayoutConfigsEqual, probeImdfZipUrl, projectFromStore, projectPathLabel, proportionalRadiusExpression, proportionalSizeRange, readSimpleStyleNumberLookup, readStoredAuthorName, readsSimpleStyleProperties, reapplyLayerJoins, redactConfigurationCredentials, redactCredentials, redactProjectCredentials, redactUrlCredentials, redo, registerMapClickClaim, registerProjectRestoreHistory, registerS3UrlSigner, removeTrailingJsonCommas, rendererAppliesOpacity, reorderLayerGroupInPanel, requestIsochrone, requestMatrix, requestNavigationRoute, requestRoute, resolveConfiguredPopupTitle, resolveEditorTrackingConfig, resolveGeocoderConfig, resolveHeaderReferences, resolveIdentifyTarget, resolveLabelNumberLocale, resolveLayerCapabilities, resolveLayerTileAddress, resolveLevelLinks, resolveMapGrid, resolvePopupBody, resolvePopupImageHeight, resolvePopupMaxWidth, resolvePopupRows, resolvePopupTitle, resolveProjectHeaderReferences, resolveReadableUrl, resolveRolePrivileges, resolveSvgSource, resolveThreeDTilesRequestHeaders, resolveTileAddress, rgbToHex, routeResponseToFeatures, rowCap, ruleBasedColorExpression, ruleBasedVisibilityFilter, s3ObjectHttpsUrl, sameCesiumImagery, sameLevelLink, sampleMapTerrain, sampleMapTerrainPoint, sampleRemoteElevations, sanitizeLayerStylePatch, scaleAltitudeToActiveBody, scaleDenomination, scaleSpan, scrubCommentsForRemovedLayers, scrubLegendForRemovedLayers, scrubPrintLayoutForLayers, scrubPrintLayoutForRemovedLayers, scrubWidgetsForRemovedLayers, selectLayerById, selectLayerIds, selectLayerSummaries, selectLayersWhen, selectLevel, selectedGeocodingLocator, serializeLayerLibrary, serializeLayerStylesFile, serializeLayersFile, serializeProject, serializeProjectWithLayerCache, serializeProjectWithLayerCacheAsync, serializeStoryMapCsv, serializeStoryMapJson, serializeStyleLibrary, setActiveEllipsoidId, setExternalNativePaintBridge, setGeocodingFetch, setGeocodingTokenProvider, setHistoryCoalesceMs, setMaxHistoryFeatureCount, setProjectCredentialLookup, setRegistryPublishableSettings, setStoredAuthorName, shouldThrottle, shouldUseTiledRendering, shouldZoomToNewLayers, simpleStyleNumberValue, sortLayerGroupInPanel, splitProjectCredentials, stacAssetMediaType, stampFeatureCollectionEditorTracking, stampFeatureEditorTracking, stampFeaturePropertiesEditorTracking, storyLayerOpacityFactor, storyLocationView, storyMapHasContent, storyVisibleLayers, stringifyPopupValue, stripGoogleMapsApiKeyHeader, stripJoinFields, stripVirtualFieldColumns, strokeBlurValue, strokeDashPresetName, strokeDashValue, strokeOpacityPaintValue, strokeOpacityValue, styleValue, subscribeProjectRestoreHistory, substituteExpressionVariables, supportsBridgedOpacity, transformGeojsonElevation, trimHistoryBySize, undo, unescapePolyline, uniqueImportedLayerName, unmatchedGeocodeFeature, unresolvedLayerLibraryJoins, unsignedSourceUrl, useAppCapability, useAppStore, useLayer, useLayerIds, useLayerSummaries, useLayersWhen, validateAttributeFormField, validateAttributeFormValues, validateLayerDescriptiveMetadata, validateMapExpression, valueMapLabelFor, valueMapToText, vectorCircleColorValue, vectorColorExpression, vectorFillColorValue, vectorFillOpacityValue, vectorLineColorValue, vectorOutlineColorValue, vectorStrokeWidthValue, visiblePopupFields, visiblePopupProperties, withDatumShift, withGeoKeysDatumShift, withJpegTablesPatch, withWktDatumShift, withinGeodesicDistance, withoutHistory };
|
|
9137
8676
|
|
|
9138
8677
|
//# sourceMappingURL=index.mjs.map
|