diff --git a/packages/client/package-lock.json b/packages/client/package-lock.json index 1c0350f13..a9d497c7c 100644 --- a/packages/client/package-lock.json +++ b/packages/client/package-lock.json @@ -28,9 +28,11 @@ "@percy/cli": "^1.15.0", "@percy/cypress": "^3.1.2", "@popperjs/core": "^2.11.6", + "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-menubar": "^1.0.3", + "@radix-ui/react-scroll-area": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.6", "@sentry/react": "^6.17.1", "@sentry/tracing": "^6.17.1", @@ -5997,6 +5999,14 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@radix-ui/number": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", + "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", @@ -6005,6 +6015,37 @@ "@babel/runtime": "^7.13.10" } }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.2.tgz", + "integrity": "sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collapsible": "1.0.3", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", @@ -6028,6 +6069,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", + "integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", @@ -6446,6 +6517,37 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.4.tgz", + "integrity": "sha512-OIClwBkwPG+FKvC4OMTRaa/3cfD069nkKFFL/TQzRzaO42Ce5ivKU9VMKgT7UU6UIkjcQqKBrDOIzWtPGw6e6w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -46945,6 +47047,14 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==" }, + "@radix-ui/number": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", + "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, "@radix-ui/primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", @@ -46953,6 +47063,23 @@ "@babel/runtime": "^7.13.10" } }, + "@radix-ui/react-accordion": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.2.tgz", + "integrity": "sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collapsible": "1.0.3", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + } + }, "@radix-ui/react-arrow": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", @@ -46962,6 +47089,22 @@ "@radix-ui/react-primitive": "1.0.3" } }, + "@radix-ui/react-collapsible": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", + "integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + } + }, "@radix-ui/react-collection": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", @@ -47176,6 +47319,23 @@ "@radix-ui/react-use-controllable-state": "1.0.1" } }, + "@radix-ui/react-scroll-area": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.4.tgz", + "integrity": "sha512-OIClwBkwPG+FKvC4OMTRaa/3cfD069nkKFFL/TQzRzaO42Ce5ivKU9VMKgT7UU6UIkjcQqKBrDOIzWtPGw6e6w==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + } + }, "@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", diff --git a/packages/client/package.json b/packages/client/package.json index de1ef9143..e3740fe55 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -23,11 +23,14 @@ "@percy/cli": "^1.15.0", "@percy/cypress": "^3.1.2", "@popperjs/core": "^2.11.6", + "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-menubar": "^1.0.3", + "@radix-ui/react-scroll-area": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.6", "@seasketch/map-tile-cache-calculator": "^1.0.0", + "@seasketch/mapbox-gl-esri-sources": "0.9.0", "@seasketch/vector-data-source": "^1.0.0", "@sentry/react": "^6.17.1", "@sentry/tracing": "^6.17.1", @@ -150,7 +153,6 @@ "mapbox-gl-draw-rectangle-mode": "^1.0.4", "mapbox-gl-esri-feature-layers": "^1.0.0", "mapbox-gl-esri-sources": "git+https://git@github.com/underbluewaters/mapbox-gl-esri-sources.git", - "@seasketch/mapbox-gl-esri-sources": "0.9.0", "md5": "^2.3.0", "mnemonist": "^0.39.2", "mustache": "^4.1.0", diff --git a/packages/client/src/admin/data/arcgis/Accordion.css b/packages/client/src/admin/data/arcgis/Accordion.css new file mode 100644 index 000000000..c7644ec31 --- /dev/null +++ b/packages/client/src/admin/data/arcgis/Accordion.css @@ -0,0 +1,3 @@ +.AccordionTrigger[data-state="open"] > .AccordionChevron { + transform: rotate(180deg); +} diff --git a/packages/client/src/admin/data/arcgis/ArcGISBrowser.tsx b/packages/client/src/admin/data/arcgis/ArcGISBrowser.tsx index 2a1cf397f..10fe76366 100644 --- a/packages/client/src/admin/data/arcgis/ArcGISBrowser.tsx +++ b/packages/client/src/admin/data/arcgis/ArcGISBrowser.tsx @@ -1,755 +1,758 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; -import { - CatalogItem, - extentToLatLngBounds, - NormalizedArcGISServerLocation, - useArcGISServiceSettings, - useMapServerInfo, - LayerInfo, - MapServerCatalogInfo, - ArcGISServiceSettings, - VectorSublayerSettings, -} from "./arcgis"; -import { Map } from "mapbox-gl"; -import ArcGISSearchPage from "./ArcGISSearchPage"; -import { - ArcGISBrowserColumn, - ArcGISBrowserColumnProps, -} from "./ArcGISBrowserColumn"; -import Spinner from "../../../components/Spinner"; -import MapboxMap from "../../../components/MapboxMap"; -import OutgoingLinkIcon from "../../../components/OutgoingLinkIcon"; -import ArcGISServiceMetadata from "./ArcGISServiceMetadata"; -import SegmentControl from "../../../components/SegmentControl"; -import SettingsIcon from "../../../components/SettingsIcon"; -import DynamicMapServerSettingsForm from "./DynamicMapServerSettingsForm"; -import { FeatureLayerSettings } from "./FeatureLayerSettings"; -import { - MapContext, - useMapContext, -} from "../../../dataLayers/MapContextManager"; -import Button from "../../../components/Button"; -// import ImportVectorLayersModal from "./ImportVectorLayersModal"; -import ExcludeLayerToggle, { - ExcludeAddIcon, - ExcludeIcon, -} from "./ExcludeLayerToggle"; -import { - DataLayerDetailsFragment, - DataSourceDetailsFragment, - DataSourceTypes, - RenderUnderType, - SpriteDetailsFragment, - useGetBasemapsQuery, -} from "../../../generated/graphql"; -import bytes from "bytes"; -import { useParams } from "react-router-dom"; -import { useTranslation, Trans } from "react-i18next"; -import MiniBasemapSelector from "../MiniBasemapSelector"; - export default function ArcGISBrowser() { - const [server, setServer] = useState<{ - location: NormalizedArcGISServerLocation; - version: string; - }>(); - const [columns, setColumns] = useState([]); - const [map, setMap] = useState(null); - const [selectedMapServer, setSelectedMapServer] = useState(); - const mapServerInfo = useMapServerInfo(selectedMapServer); - const [selectedFeatureLayer, setSelectedFeatureLayer] = useState(); - const serviceColumnRef = useRef(null); - const mapContext = useMapContext({ - preferencesKey: "arcgis-browser", - cacheSize: bytes("500mb"), - }); - // TODO: do something about this - // const [treeData, setTreeData] = useState([]); - const [serviceSettings, setServiceSettings] = - useArcGISServiceSettings(selectedMapServer); - const { slug } = useParams<{ slug: string }>(); - const { t } = useTranslation("admin"); - const basemapsData = useGetBasemapsQuery({ - variables: { - slug, - }, - }); - const [modalOpen, setModalOpen] = useState(false); - const serviceData = mapServerInfo.data; + return
; +} +// import React, { useCallback, useEffect, useRef, useState } from "react"; +// import { +// CatalogItem, +// extentToLatLngBounds, +// NormalizedArcGISServerLocation, +// useArcGISServiceSettings, +// useMapServerInfo, +// LayerInfo, +// MapServerCatalogInfo, +// ArcGISServiceSettings, +// VectorSublayerSettings, +// } from "./arcgis"; +// import { Map } from "mapbox-gl"; +// import ArcGISSearchPage from "./ArcGISSearchPage"; +// import { +// ArcGISBrowserColumn, +// ArcGISBrowserColumnProps, +// } from "./ArcGISBrowserColumn"; +// import Spinner from "../../../components/Spinner"; +// import MapboxMap from "../../../components/MapboxMap"; +// import OutgoingLinkIcon from "../../../components/OutgoingLinkIcon"; +// import ArcGISServiceMetadata from "./ArcGISServiceMetadata"; +// import SegmentControl from "../../../components/SegmentControl"; +// import SettingsIcon from "../../../components/SettingsIcon"; +// import DynamicMapServerSettingsForm from "./DynamicMapServerSettingsForm"; +// import { FeatureLayerSettings } from "./FeatureLayerSettings"; +// import { +// MapContext, +// useMapContext, +// } from "../../../dataLayers/MapContextManager"; +// import Button from "../../../components/Button"; +// // import ImportVectorLayersModal from "./ImportVectorLayersModal"; +// import ExcludeLayerToggle, { +// ExcludeAddIcon, +// ExcludeIcon, +// } from "./ExcludeLayerToggle"; +// import { +// DataLayerDetailsFragment, +// DataSourceDetailsFragment, +// DataSourceTypes, +// RenderUnderType, +// SpriteDetailsFragment, +// useGetBasemapsQuery, +// } from "../../../generated/graphql"; +// import bytes from "bytes"; +// import { useParams } from "react-router-dom"; +// import { useTranslation, Trans } from "react-i18next"; +// import MiniBasemapSelector from "../MiniBasemapSelector"; - // Update map extent when showing new services - useEffect(() => { - if (serviceData && map) { - const extent = - serviceData.mapServerInfo.fullExtent || - serviceData.mapServerInfo.initialExtent; - if (extent) { - const bounds = extentToLatLngBounds(extent); - if (bounds) { - map.fitBounds(bounds, { duration: 0, padding: 40 }); - } - } - } - }, [serviceData, map]); +// export default function ArcGISBrowser() { +// const [server, setServer] = useState<{ +// location: NormalizedArcGISServerLocation; +// version: string; +// }>(); +// const [columns, setColumns] = useState([]); +// const [map, setMap] = useState(null); +// const [selectedMapServer, setSelectedMapServer] = useState(); +// const mapServerInfo = useMapServerInfo(selectedMapServer); +// const [selectedFeatureLayer, setSelectedFeatureLayer] = useState(); +// const serviceColumnRef = useRef(null); +// const mapContext = useMapContext({ +// preferencesKey: "arcgis-browser", +// cacheSize: bytes("500mb"), +// }); +// // TODO: do something about this +// // const [treeData, setTreeData] = useState([]); +// const [serviceSettings, setServiceSettings] = +// useArcGISServiceSettings(selectedMapServer); +// const { slug } = useParams<{ slug: string }>(); +// const { t } = useTranslation("admin"); +// const basemapsData = useGetBasemapsQuery({ +// variables: { +// slug, +// }, +// }); +// const [modalOpen, setModalOpen] = useState(false); +// const serviceData = mapServerInfo.data; - // TODO: replace with useMapEssentials - // useEffect(() => { - // if (basemapsData.data && mapContext.manager) { - // mapContext.manager.setBasemaps( - // basemapsData.data.projectBySlug!.basemaps! as ClientBasemap[] - // ); - // } - // }, [basemapsData.data, mapContext.manager]); +// // Update map extent when showing new services +// useEffect(() => { +// if (serviceData && map) { +// const extent = +// serviceData.mapServerInfo.fullExtent || +// serviceData.mapServerInfo.initialExtent; +// if (extent) { +// const bounds = extentToLatLngBounds(extent); +// if (bounds) { +// map.fitBounds(bounds, { duration: 0, padding: 40 }); +// } +// } +// } +// }, [serviceData, map]); - // TODO: - // Update sources and layers whenever settings change - // useEffect(() => { - // if (serviceSettings && serviceData && mapContext.manager) { - // setTreeData( - // updateDisabledState( - // serviceSettings?.sourceType || "arcgis-dynamic-mapservice", - // treeData, - // serviceData.layerInfo - // ) - // ); - // const sources: ClientDataSource[] = []; - // const layers: ClientDataLayer[] = []; - // if (serviceSettings.sourceType === "arcgis-dynamic-mapservice") { - // sources.push( - // dynamicServiceSourceFromSettings(serviceData, serviceSettings) - // ); - // } else if (serviceSettings.sourceType === "arcgis-vector-source") { - // for (const layer of serviceData.layerInfo) { - // const settings = serviceSettings.vectorSublayerSettings.find( - // (s) => s.sublayer === layer.id - // ); - // sources.push(vectorSourceFromSettings(layer, settings!)); - // } - // } +// // TODO: replace with useMapEssentials +// // useEffect(() => { +// // if (basemapsData.data && mapContext.manager) { +// // mapContext.manager.setBasemaps( +// // basemapsData.data.projectBySlug!.basemaps! as ClientBasemap[] +// // ); +// // } +// // }, [basemapsData.data, mapContext.manager]); - // for (const layer of serviceData.layerInfo) { - // if (serviceSettings.sourceType === "arcgis-dynamic-mapservice") { - // layers.push({ - // id: layer.generatedId, - // sublayer: layer.id.toString(), - // dataSourceId: serviceData.mapServerInfo.generatedId, - // renderUnder: serviceSettings.renderUnder || RenderUnderType.Labels, - // zIndex: layer.id, - // }); - // } else if (layer.type !== "Raster Layer") { - // const vectorSettings = serviceSettings.vectorSublayerSettings.find( - // (v) => v.sublayer === layer.id - // ); - // layers.push(vectorLayerFromSettings(layer, vectorSettings)); - // } - // } - // // TODO: - // // mapContext.manager.reset(sources, layers); - // } - // }, [serviceData, serviceSettings, mapContext.manager]); +// // TODO: +// // Update sources and layers whenever settings change +// // useEffect(() => { +// // if (serviceSettings && serviceData && mapContext.manager) { +// // setTreeData( +// // updateDisabledState( +// // serviceSettings?.sourceType || "arcgis-dynamic-mapservice", +// // treeData, +// // serviceData.layerInfo +// // ) +// // ); +// // const sources: ClientDataSource[] = []; +// // const layers: ClientDataLayer[] = []; +// // if (serviceSettings.sourceType === "arcgis-dynamic-mapservice") { +// // sources.push( +// // dynamicServiceSourceFromSettings(serviceData, serviceSettings) +// // ); +// // } else if (serviceSettings.sourceType === "arcgis-vector-source") { +// // for (const layer of serviceData.layerInfo) { +// // const settings = serviceSettings.vectorSublayerSettings.find( +// // (s) => s.sublayer === layer.id +// // ); +// // sources.push(vectorSourceFromSettings(layer, settings!)); +// // } +// // } - // useEffect(() => { - // if ( - // serviceData && - // mapContext.manager && - // serviceSettings && - // serviceSettings.sourceType === "arcgis-dynamic-mapservice" - // ) { - // mapContext.manager.updateArcGISDynamicMapServiceSource( - // dynamicServiceSourceFromSettings(serviceData, serviceSettings) - // ); - // } - // }, [ - // serviceSettings?.enableHighDpi, - // serviceSettings?.imageFormat, - // serviceSettings?.renderUnder, - // ]); +// // for (const layer of serviceData.layerInfo) { +// // if (serviceSettings.sourceType === "arcgis-dynamic-mapservice") { +// // layers.push({ +// // id: layer.generatedId, +// // sublayer: layer.id.toString(), +// // dataSourceId: serviceData.mapServerInfo.generatedId, +// // renderUnder: serviceSettings.renderUnder || RenderUnderType.Labels, +// // zIndex: layer.id, +// // }); +// // } else if (layer.type !== "Raster Layer") { +// // const vectorSettings = serviceSettings.vectorSublayerSettings.find( +// // (v) => v.sublayer === layer.id +// // ); +// // layers.push(vectorLayerFromSettings(layer, vectorSettings)); +// // } +// // } +// // // TODO: +// // // mapContext.manager.reset(sources, layers); +// // } +// // }, [serviceData, serviceSettings, mapContext.manager]); - // TODO: - // useEffect(() => { - // if (serviceData && mapContext.manager && map) { - // const data = treeDataFromLayerList(serviceData.layerInfo); - // setTreeData( - // updateDisabledState( - // serviceSettings?.sourceType || "arcgis-dynamic-mapservice", - // data, - // serviceData.layerInfo - // ) - // ); - // // Collect visible layers *only* if they are under toggled groups/folders - // const collectIds = (ids: string[], node: ClientTableOfContentsItem) => { - // const layerInfo = serviceData.layerInfo.find( - // (lyr) => lyr.generatedId === node.id - // ); - // if (layerInfo?.defaultVisibility === true || node === data[0]) { - // if (node.children) { - // for (const child of node.children) { - // collectIds(ids, child); - // } - // } else { - // if (!node.isFolder) { - // if (layerInfo?.defaultVisibility === true) { - // ids.push(node.id.toString()); - // } - // } - // } - // } else { - // // Don't descend into un-toggled folders - // } - // return ids; - // }; - // const collectedVisibleLayers = collectIds([], data[0]); - // setTimeout(() => { - // mapContext.manager!.setVisibleLayers(collectedVisibleLayers); - // }, 50); - // } - // }, [serviceData, mapContext.manager]); +// // useEffect(() => { +// // if ( +// // serviceData && +// // mapContext.manager && +// // serviceSettings && +// // serviceSettings.sourceType === "arcgis-dynamic-mapservice" +// // ) { +// // mapContext.manager.updateArcGISDynamicMapServiceSource( +// // dynamicServiceSourceFromSettings(serviceData, serviceSettings) +// // ); +// // } +// // }, [ +// // serviceSettings?.enableHighDpi, +// // serviceSettings?.imageFormat, +// // serviceSettings?.renderUnder, +// // ]); - const featureLayerSettingsRef = useRef(null); - const allFeatureLayerIds = (mapServerInfo.data?.layerInfo || []) - .filter((l) => l.type === "Feature Layer") - .map((l) => l.generatedId); - const allLayerIds = (mapServerInfo.data?.layerInfo || []) - .filter((l) => l.type !== "Group Layer") - .map((l) => l.generatedId); +// // TODO: +// // useEffect(() => { +// // if (serviceData && mapContext.manager && map) { +// // const data = treeDataFromLayerList(serviceData.layerInfo); +// // setTreeData( +// // updateDisabledState( +// // serviceSettings?.sourceType || "arcgis-dynamic-mapservice", +// // data, +// // serviceData.layerInfo +// // ) +// // ); +// // // Collect visible layers *only* if they are under toggled groups/folders +// // const collectIds = (ids: string[], node: ClientTableOfContentsItem) => { +// // const layerInfo = serviceData.layerInfo.find( +// // (lyr) => lyr.generatedId === node.id +// // ); +// // if (layerInfo?.defaultVisibility === true || node === data[0]) { +// // if (node.children) { +// // for (const child of node.children) { +// // collectIds(ids, child); +// // } +// // } else { +// // if (!node.isFolder) { +// // if (layerInfo?.defaultVisibility === true) { +// // ids.push(node.id.toString()); +// // } +// // } +// // } +// // } else { +// // // Don't descend into un-toggled folders +// // } +// // return ids; +// // }; +// // const collectedVisibleLayers = collectIds([], data[0]); +// // setTimeout(() => { +// // mapContext.manager!.setVisibleLayers(collectedVisibleLayers); +// // }, 50); +// // } +// // }, [serviceData, mapContext.manager]); - const settingsButton = (path: HTMLElement[]) => { - for (const el of path.slice(0, 3)) { - if ( - el.tagName === "BUTTON" && - el.getAttribute("data-type") === "settings" - ) { - return true; - } - } - return false; - }; +// const featureLayerSettingsRef = useRef(null); +// const allFeatureLayerIds = (mapServerInfo.data?.layerInfo || []) +// .filter((l) => l.type === "Feature Layer") +// .map((l) => l.generatedId); +// const allLayerIds = (mapServerInfo.data?.layerInfo || []) +// .filter((l) => l.type !== "Group Layer") +// .map((l) => l.generatedId); - const colorPicker = (path: HTMLElement[]) => { - for (const el of path.slice(0, 5)) { - try { - if (el.className.indexOf("colorpicker-body") !== -1) { - return true; - } - } catch (e) { - return false; - } - } - return false; - }; +// const settingsButton = (path: HTMLElement[]) => { +// for (const el of path.slice(0, 3)) { +// if ( +// el.tagName === "BUTTON" && +// el.getAttribute("data-type") === "settings" +// ) { +// return true; +// } +// } +// return false; +// }; - const handleDocumentClick = useCallback( - (e) => { - if ( - selectedFeatureLayer && - e.path.indexOf(featureLayerSettingsRef.current) === -1 && - e.target.tagName !== "INPUT" && - !settingsButton(e.path) && - !colorPicker(e.path) - ) { - setSelectedFeatureLayer(undefined); - } - }, - [selectedFeatureLayer] - ); +// const colorPicker = (path: HTMLElement[]) => { +// for (const el of path.slice(0, 5)) { +// try { +// if (el.className.indexOf("colorpicker-body") !== -1) { +// return true; +// } +// } catch (e) { +// return false; +// } +// } +// return false; +// }; - const columnContainer = useRef(null); +// const handleDocumentClick = useCallback( +// (e) => { +// if ( +// selectedFeatureLayer && +// e.path.indexOf(featureLayerSettingsRef.current) === -1 && +// e.target.tagName !== "INPUT" && +// !settingsButton(e.path) && +// !colorPicker(e.path) +// ) { +// setSelectedFeatureLayer(undefined); +// } +// }, +// [selectedFeatureLayer] +// ); - useEffect(() => { - if (columnContainer.current) { - columnContainer.current.scrollLeft = 10000; - } - }, [selectedFeatureLayer, selectedMapServer, columns]); +// const columnContainer = useRef(null); - useEffect(() => { - if (selectedFeatureLayer) { - document.addEventListener("click", handleDocumentClick); - } +// useEffect(() => { +// if (columnContainer.current) { +// columnContainer.current.scrollLeft = 10000; +// } +// }, [selectedFeatureLayer, selectedMapServer, columns]); - return () => { - document.removeEventListener("click", handleDocumentClick); - }; - }, [selectedFeatureLayer]); +// useEffect(() => { +// if (selectedFeatureLayer) { +// document.addEventListener("click", handleDocumentClick); +// } - // Add new catalog column or service column on selection - const onCatalogItemSelection = ( - item: CatalogItem, - column: ArcGISBrowserColumnProps - ) => { - if (item.type === "Folder") { - const index = columns.indexOf(column); - setColumns([...columns.slice(0, index + 1), item]); - setSelectedMapServer(undefined); - setSelectedFeatureLayer(undefined); - } else if (item.type === "MapServer") { - setSelectedMapServer(item.url); - setSelectedFeatureLayer(undefined); - } else if (item.type === "FeatureServer") { - setSelectedMapServer(item.url); - setSelectedFeatureLayer(undefined); - } else { - throw new Error("Unsupported type " + item.type); - } - }; +// return () => { +// document.removeEventListener("click", handleDocumentClick); +// }; +// }, [selectedFeatureLayer]); - if (!server) { - return ( - { - setServer(e); - setColumns([ - { - name: "Root", - type: "Root", - url: e.location.servicesRoot, - }, - ]); - }} - /> - ); - } else { - return ( - <> - -
- { - setMap(map); - }} - /> - {/* TODO: */} - {/* */} -
- {server.location.baseUrl} - - ArcGIS Version {server.version} - -
-
- {columns.map((props, i) => ( - onCatalogItemSelection(item, props)} - /> - ))} - {selectedMapServer && mapServerInfo.loading && ( -
- -
- )} - {selectedMapServer && serviceData && serviceSettings && ( -
-
-

- {serviceData.mapServerInfo.documentInfo?.Title || - serviceData.mapServerInfo.mapName} - - - -

-
- -
-
- - setServiceSettings({ - ...serviceSettings, - sourceType: - segment === "Image Source" - ? "arcgis-dynamic-mapservice" - : "arcgis-vector-source", - }) - } - /> -
- {serviceSettings.sourceType === - "arcgis-dynamic-mapservice" && ( -

- - Image sources display data as full-screen images, - typical of how most web mapping portals work. Each - time the user pans or zooms the map a new image will - be requested and displayed with the requested layers. - -

- )} +// // Add new catalog column or service column on selection +// const onCatalogItemSelection = ( +// item: CatalogItem, +// column: ArcGISBrowserColumnProps +// ) => { +// if (item.type === "Folder") { +// const index = columns.indexOf(column); +// setColumns([...columns.slice(0, index + 1), item]); +// setSelectedMapServer(undefined); +// setSelectedFeatureLayer(undefined); +// } else if (item.type === "MapServer") { +// setSelectedMapServer(item.url); +// setSelectedFeatureLayer(undefined); +// } else if (item.type === "FeatureServer") { +// setSelectedMapServer(item.url); +// setSelectedFeatureLayer(undefined); +// } else { +// throw new Error("Unsupported type " + item.type); +// } +// }; - {serviceSettings.sourceType === "arcgis-vector-source" && ( -

- - When using vector sources, SeaSketch loads actual - geometry data and uses the user's browser to render - it. This can result in a much faster, sharper, and - more interactive map but takes a little more work to - configure. Each layer can be styled and configured - independently - - { - // eslint-disable-next-line - `(click ` - } - - ). -

- )} -
- - ); - } +// if (!server) { +// return ( +// { +// setServer(e); +// setColumns([ +// { +// name: "Root", +// type: "Root", +// url: e.location.servicesRoot, +// }, +// ]); +// }} +// /> +// ); +// } else { +// return ( +// <> +// +//
+// { +// setMap(map); +// }} +// /> +// {/* TODO: */} +// {/* */} +//
+// {server.location.baseUrl} +// +// ArcGIS Version {server.version} +// +//
+//
+// {columns.map((props, i) => ( +// onCatalogItemSelection(item, props)} +// /> +// ))} +// {selectedMapServer && mapServerInfo.loading && ( +//
+// +//
+// )} +// {selectedMapServer && serviceData && serviceSettings && ( +//
+//
+//

+// {serviceData.mapServerInfo.documentInfo?.Title || +// serviceData.mapServerInfo.mapName} +// +// +// +//

+//
+// +//
+//
+// +// setServiceSettings({ +// ...serviceSettings, +// sourceType: +// segment === "Image Source" +// ? "arcgis-dynamic-mapservice" +// : "arcgis-vector-source", +// }) +// } +// /> +//
+// {serviceSettings.sourceType === +// "arcgis-dynamic-mapservice" && ( +//

+// +// Image sources display data as full-screen images, +// typical of how most web mapping portals work. Each +// time the user pans or zooms the map a new image will +// be requested and displayed with the requested layers. +// +//

+// )} - buttons.push( - { - let excluded = [ - ...serviceSettings.excludedSublayers, - ]; - if ( - serviceSettings.excludedSublayers.indexOf( - node.id.toString() - ) !== -1 - ) { - // already excluded - excluded = excluded.filter( - (id) => id !== node.id - ); - } else { - excluded.push(node.id.toString()); - } - mapContext.manager?.hideLayers([ - node.id.toString(), - ]); - setServiceSettings({ - ...serviceSettings, - excludedSublayers: excluded, - }); - }} - /> - ); - } - return buttons; - }} - /> */} -
- {serviceSettings.sourceType === - "arcgis-dynamic-mapservice" && ( - - )} - {serviceSettings.sourceType === "arcgis-vector-source" && ( -
-
-

{t("Import Layers")}

-

- - Before importing vector sources, SeaSketch will - these vector sources for compatability and file - size issues. - -

-
-
- )} -
-
- )} - {selectedFeatureLayer && serviceSettings && ( -
- { - setServiceSettings({ ...settings }); - // const layerSettings = settings.vectorSublayerSettings.find( - // (s) => s.sublayer === selectedFeatureLayer.id - // ); - // const source = vectorSourceFromSettings( - // selectedFeatureLayer, - // layerSettings - // ); - // if ( - // source.type === DataSourceTypes.ArcgisDynamicMapserver - // ) { - // mapContext.manager!.updateArcGISDynamicMapServiceSource( - // source - // ); - // } - // mapContext.manager!.updateLayer( - // vectorLayerFromSettings( - // selectedFeatureLayer, - // layerSettings - // ) - // ); - }} - /> -
- )} -
- {/* - l.type !== "Raster Layer" && - serviceSettings?.excludedSublayers.indexOf(l.generatedId) === - -1 - )} - settings={serviceSettings} - open={modalOpen} - onRequestClose={() => setModalOpen(false)} - mapServerInfo={mapServerInfo.data?.mapServerInfo!} - /> */} -
- - - ); - } -} +// {serviceSettings.sourceType === "arcgis-vector-source" && ( +//

+// +// When using vector sources, SeaSketch loads actual +// geometry data and uses the user's browser to render +// it. This can result in a much faster, sharper, and +// more interactive map but takes a little more work to +// configure. Each layer can be styled and configured +// independently +// +// { +// // eslint-disable-next-line +// `(click ` +// } +// +// ). +//

+// )} +//
+// +// ); +// } -function dynamicServiceSourceFromSettings( - serviceData: { - mapServerInfo: MapServerCatalogInfo; - layerInfo: LayerInfo[]; - }, - serviceSettings: ArcGISServiceSettings -): Partial { - return { - // TODO: fix id assignment - id: 1, // || serviceData.mapServerInfo.generatedId, - type: DataSourceTypes.ArcgisDynamicMapserver, - url: serviceData.mapServerInfo.url, - useDevicePixelRatio: serviceSettings.enableHighDpi, - queryParameters: { - format: serviceSettings.imageFormat, - transparent: "true", - }, - // interactivitySettings: [], - supportsDynamicLayers: serviceData.mapServerInfo.supportsDynamicLayers, - }; -} +// buttons.push( +// { +// let excluded = [ +// ...serviceSettings.excludedSublayers, +// ]; +// if ( +// serviceSettings.excludedSublayers.indexOf( +// node.id.toString() +// ) !== -1 +// ) { +// // already excluded +// excluded = excluded.filter( +// (id) => id !== node.id +// ); +// } else { +// excluded.push(node.id.toString()); +// } +// mapContext.manager?.hideLayers([ +// node.id.toString(), +// ]); +// setServiceSettings({ +// ...serviceSettings, +// excludedSublayers: excluded, +// }); +// }} +// /> +// ); +// } +// return buttons; +// }} +// /> */} +//
+// {serviceSettings.sourceType === +// "arcgis-dynamic-mapservice" && ( +// +// )} +// {serviceSettings.sourceType === "arcgis-vector-source" && ( +//
+//
+//

{t("Import Layers")}

+//

+// +// Before importing vector sources, SeaSketch will +// these vector sources for compatability and file +// size issues. +// +//

+//
+//
+// )} +//
+//
+// )} +// {selectedFeatureLayer && serviceSettings && ( +//
+// { +// setServiceSettings({ ...settings }); +// // const layerSettings = settings.vectorSublayerSettings.find( +// // (s) => s.sublayer === selectedFeatureLayer.id +// // ); +// // const source = vectorSourceFromSettings( +// // selectedFeatureLayer, +// // layerSettings +// // ); +// // if ( +// // source.type === DataSourceTypes.ArcgisDynamicMapserver +// // ) { +// // mapContext.manager!.updateArcGISDynamicMapServiceSource( +// // source +// // ); +// // } +// // mapContext.manager!.updateLayer( +// // vectorLayerFromSettings( +// // selectedFeatureLayer, +// // layerSettings +// // ) +// // ); +// }} +// /> +//
+// )} +//
+// {/* +// l.type !== "Raster Layer" && +// serviceSettings?.excludedSublayers.indexOf(l.generatedId) === +// -1 +// )} +// settings={serviceSettings} +// open={modalOpen} +// onRequestClose={() => setModalOpen(false)} +// mapServerInfo={mapServerInfo.data?.mapServerInfo!} +// /> */} +//
+//
+// +// ); +// } +// } -function vectorSourceFromSettings( - layer: LayerInfo, - settings?: VectorSublayerSettings -): Partial { - return { - id: 1, //layer.generatedId, - type: DataSourceTypes.ArcgisVector, - url: layer.url, - // TODO: add imageSets back - // imageSets: layer.imageList ? layer.imageList.toJSON() : [], - // bytesLimit: settings?.ignoreByteLimit ? undefined : 5000000, - queryParameters: { - outFields: settings?.outFields, - geometryPrecision: settings?.geometryPrecision, - }, - // interactivitySettings: [], - supportsDynamicLayers: false, - }; -} +// function dynamicServiceSourceFromSettings( +// serviceData: { +// mapServerInfo: MapServerCatalogInfo; +// layerInfo: LayerInfo[]; +// }, +// serviceSettings: ArcGISServiceSettings +// ): Partial { +// return { +// // TODO: fix id assignment +// id: 1, // || serviceData.mapServerInfo.generatedId, +// type: DataSourceTypes.ArcgisDynamicMapserver, +// url: serviceData.mapServerInfo.url, +// useDevicePixelRatio: serviceSettings.enableHighDpi, +// queryParameters: { +// format: serviceSettings.imageFormat, +// transparent: "true", +// }, +// // interactivitySettings: [], +// supportsDynamicLayers: serviceData.mapServerInfo.supportsDynamicLayers, +// }; +// } -function vectorLayerFromSettings( - layer: LayerInfo, - settings?: VectorSublayerSettings -): Partial { - const imageSetJSON = layer.imageList ? layer.imageList.toJSON() : []; - let sprites: SpriteDetailsFragment[] = []; - if (imageSetJSON.length) { - for (const imageSet of imageSetJSON) { - sprites.push({ - id: 1, //imageSet.id, - // @ts-ignore - spriteImages: imageSet.images.map((i: any) => ({ - height: i.height, - width: i.width, - dataUri: i.dataURI, - pixelRatio: i.pixelRatio, - })), - }); - } - } - return { - id: 1, //layer.generatedId, - // dataSourceId: layer.generatedId, - renderUnder: settings?.renderUnder || RenderUnderType.Labels, - mapboxGlStyles: settings?.mapboxLayers || layer.mapboxLayers, - sprites: sprites.length ? sprites : undefined, - zIndex: layer.id, - }; -} +// function vectorSourceFromSettings( +// layer: LayerInfo, +// settings?: VectorSublayerSettings +// ): Partial { +// return { +// id: 1, //layer.generatedId, +// type: DataSourceTypes.ArcgisVector, +// url: layer.url, +// // TODO: add imageSets back +// // imageSets: layer.imageList ? layer.imageList.toJSON() : [], +// // bytesLimit: settings?.ignoreByteLimit ? undefined : 5000000, +// queryParameters: { +// outFields: settings?.outFields, +// geometryPrecision: settings?.geometryPrecision, +// }, +// // interactivitySettings: [], +// supportsDynamicLayers: false, +// }; +// } -// function updateDisabledState( -// sourceType: "arcgis-dynamic-mapservice" | "arcgis-vector-source", -// treeData: ClientTableOfContentsItem[], -// layers: LayerInfo[] -// ) { -// const updateChildren = (node: ClientTableOfContentsItem) => { -// if (node.children) { -// for (const child of node.children) { -// updateChildren(child); -// } +// function vectorLayerFromSettings( +// layer: LayerInfo, +// settings?: VectorSublayerSettings +// ): Partial { +// const imageSetJSON = layer.imageList ? layer.imageList.toJSON() : []; +// let sprites: SpriteDetailsFragment[] = []; +// if (imageSetJSON.length) { +// for (const imageSet of imageSetJSON) { +// sprites.push({ +// id: 1, //imageSet.id, +// // @ts-ignore +// spriteImages: imageSet.images.map((i: any) => ({ +// height: i.height, +// width: i.width, +// dataUri: i.dataURI, +// pixelRatio: i.pixelRatio, +// })), +// }); // } -// if (!node.isFolder) { -// const layer = layers.find((l) => l.generatedId === node.id); -// if ( -// sourceType === "arcgis-vector-source" && -// layer?.type === "Raster Layer" -// ) { -// node.disabled = true; -// } else { -// node.disabled = false; -// } -// } -// }; -// if (treeData.length) { -// updateChildren(treeData[0]); // } -// return [...treeData]; +// return { +// id: 1, //layer.generatedId, +// // dataSourceId: layer.generatedId, +// renderUnder: settings?.renderUnder || RenderUnderType.Labels, +// mapboxGlStyles: settings?.mapboxLayers || layer.mapboxLayers, +// sprites: sprites.length ? sprites : undefined, +// zIndex: layer.id, +// }; // } + +// // function updateDisabledState( +// // sourceType: "arcgis-dynamic-mapservice" | "arcgis-vector-source", +// // treeData: ClientTableOfContentsItem[], +// // layers: LayerInfo[] +// // ) { +// // const updateChildren = (node: ClientTableOfContentsItem) => { +// // if (node.children) { +// // for (const child of node.children) { +// // updateChildren(child); +// // } +// // } +// // if (!node.isFolder) { +// // const layer = layers.find((l) => l.generatedId === node.id); +// // if ( +// // sourceType === "arcgis-vector-source" && +// // layer?.type === "Raster Layer" +// // ) { +// // node.disabled = true; +// // } else { +// // node.disabled = false; +// // } +// // } +// // }; +// // if (treeData.length) { +// // updateChildren(treeData[0]); +// // } +// // return [...treeData]; +// // } diff --git a/packages/client/src/admin/data/arcgis/ArcGISBrowserColumn.tsx b/packages/client/src/admin/data/arcgis/ArcGISBrowserColumn.tsx index ac09a1cc5..3e2e14939 100644 --- a/packages/client/src/admin/data/arcgis/ArcGISBrowserColumn.tsx +++ b/packages/client/src/admin/data/arcgis/ArcGISBrowserColumn.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import Spinner from "../../../components/Spinner"; import { useCatalogItems, CatalogItem } from "./arcgis"; +import { ArcGISRESTServiceRequestManager } from "@seasketch/mapbox-gl-esri-sources"; export interface ArcGISBrowserColumnProps { url: string; @@ -15,10 +16,14 @@ export interface ArcGISBrowserColumnProps { | "GeocodeServer"; onSelection?: (item: CatalogItem) => void; leading?: boolean; + requestManager: ArcGISRESTServiceRequestManager; } export function ArcGISBrowserColumn(props: ArcGISBrowserColumnProps) { - const { catalogInfo, error, loading } = useCatalogItems(props.url); + const { catalogInfo, error, loading } = useCatalogItems( + props.url, + props.requestManager + ); const [selectedItem, setSelectedItem] = useState(); const updateSelection = (item: CatalogItem) => { setSelectedItem(item); diff --git a/packages/client/src/admin/data/arcgis/ArcGISCartLegend.tsx b/packages/client/src/admin/data/arcgis/ArcGISCartLegend.tsx new file mode 100644 index 000000000..084bf96a6 --- /dev/null +++ b/packages/client/src/admin/data/arcgis/ArcGISCartLegend.tsx @@ -0,0 +1,195 @@ +/* eslint-disable i18next/no-literal-string */ +import { + CaretDownIcon, + EyeClosedIcon, + EyeOpenIcon, +} from "@radix-ui/react-icons"; +import { + DataTableOfContentsItem, + FolderTableOfContentsItem, + LegendItem, +} from "@seasketch/mapbox-gl-esri-sources"; +import * as Accordion from "@radix-ui/react-accordion"; +import Spinner from "../../../components/Spinner"; +require("./Accordion.css"); + +export default function ArcGISCartLegend({ + items, + className, + loading, + visibleLayerIds, + toggleLayer, +}: { + className?: string; + items: (FolderTableOfContentsItem | DataTableOfContentsItem)[]; + loading?: boolean; + visibleLayerIds?: string[]; + toggleLayer?: (id: string) => void; +}) { + function onChangeVisibility(id: string) { + if (toggleLayer) { + return () => { + toggleLayer(id); + }; + } else { + return undefined; + } + } + if (items.length === 0) { + return null; + } else { + return ( +
+ + + + +

+ Legend + {loading && } +

+ + +
+
+ + {/*
*/} +
    + {items.map((item) => { + const visible = visibleLayerIds + ? visibleLayerIds.includes(item.id) + : item.defaultVisibility; + if (item.type === "folder") { + return ( +
  • + {item.label} +
  • + ); + } else { + if (!item.legend) { + return ( +
  • + {item.label} +
  • + ); + } else if (item.legend && item.legend.length === 1) { + const legendItem = item.legend[0]; + return ( +
  • + + {item.label} + +
  • + ); + } else if (item.legend && item.legend.length > 1) { + return ( +
  • +
    + + {item.label} + + +
    +
      + {item.legend.map((legendItem) => { + return ( +
    • + + + {legendItem.label} + +
    • + ); + })} +
    +
  • + ); + } else { + return null; + } + } + })} +
+ + + +
+ ); + } +} + +function LegendImage({ + item, + className, +}: { + item: LegendItem; + className?: string; +}) { + return ( + 1 ? window.devicePixelRatio / 1.5 : 1) + } + height={ + (item.imageHeight || 20) / + (window.devicePixelRatio > 1 ? window.devicePixelRatio / 1.5 : 1) + } + /> + ); +} + +function Toggle({ + visible, + onChange, + className, +}: { + visible: boolean; + onChange?: () => void; + className?: string; +}) { + return ( + + ); +} diff --git a/packages/client/src/admin/data/arcgis/ArcGISCartModal.tsx b/packages/client/src/admin/data/arcgis/ArcGISCartModal.tsx index 7932864e6..552a467df 100644 --- a/packages/client/src/admin/data/arcgis/ArcGISCartModal.tsx +++ b/packages/client/src/admin/data/arcgis/ArcGISCartModal.tsx @@ -5,26 +5,27 @@ import ArcGISSearchPage from "./ArcGISSearchPage"; import { CatalogItem, NormalizedArcGISServerLocation, - extentToLatLngBounds, + useCatalogItemDetails, useCatalogItems, - useMapServerInfo, } from "./arcgis"; import { LngLatBounds, LngLatBoundsLike, Map } from "mapbox-gl"; import { SearchIcon } from "@heroicons/react/outline"; import Skeleton from "../../../components/Skeleton"; -import { ArrowLeftIcon, MinusIcon, PlusIcon } from "@radix-ui/react-icons"; +import { ArrowLeftIcon } from "@radix-ui/react-icons"; import { FolderIcon } from "@heroicons/react/solid"; import Spinner from "../../../components/Spinner"; -import FeatureService from "mapbox-gl-arcgis-featureserver"; import Button from "../../../components/Button"; import { ArcGISDynamicMapService, ArcGISRESTServiceRequestManager, CustomGLSource, ArcGISTiledMapService, + DataTableOfContentsItem, + FolderTableOfContentsItem, } from "@seasketch/mapbox-gl-esri-sources"; import { Feature } from "geojson"; import bbox from "@turf/bbox"; +import ArcGISCartLegend from "./ArcGISCartLegend"; const requestManager = new ArcGISRESTServiceRequestManager(); @@ -53,19 +54,23 @@ export default function ArcGISCartModal({ ); const [map, setMap] = useState(null); const [showLayerList, setShowLayerList] = useState(false); - const [dynamicMapService, setDynamicMapService] = - useState(null); - - // const mapServerInfo = useMapServerInfo(location?.baseUrl); const { catalogInfo, error, loading } = useCatalogItems( location?.servicesRoot ? selectedFolder ? location.servicesRoot + "/" + selectedFolder.name : location.servicesRoot - : "" + : "", + requestManager ); const [selectedLayerIds, setSelectedLayerIds] = useState([]); + const [sourceLoading, setSourceLoading] = useState(false); + + const [tableOfContentsItems, setTableOfContentsItems] = useState< + (FolderTableOfContentsItem | DataTableOfContentsItem)[] + >([]); + + const [visibleLayers, setVisibleLayers] = useState([]); useEffect(() => { if (mapDiv) { @@ -83,32 +88,50 @@ export default function ArcGISCartModal({ // m.fitBounds(mapBounds as LngLatBounds, { padding: 10, animate: false }); m.resize(); setMap(m); - m.on("dataloading", (e) => { - setMapIsLoadingData(true); - setTimeout(() => { - for (const id in m.getStyle().sources || {}) { - if (!m.isSourceLoaded(id)) { - setMapIsLoadingData(true); - return; - } - } - setMapIsLoadingData(false); - }, 1000); - }); - m.on("data", (e) => { - for (const id in m.getStyle().sources || {}) { - if (!m.isSourceLoaded(id)) { - setMapIsLoadingData(true); - return; + }); + } + }, [mapDiv, setSourceLoading]); + const [customSources, setCustomSources] = useState[]>([]); + + useEffect(() => { + if (map) { + const dataLoadingHandler = () => { + let anyLoading = false; + if (customSources.length > 0) { + for (const source of customSources) { + if (source.loading) { + anyLoading = true; + break; } } - setMapIsLoadingData(false); - }); - }); + } + if (anyLoading) { + setSourceLoading(true); + setTimeout(() => { + dataLoadingHandler(); + }, 200); + } else { + setSourceLoading(false); + } + }; + map.on("dataloading", dataLoadingHandler); + map.on("data", dataLoadingHandler); + map.on("moveend", dataLoadingHandler); + + return () => { + map.off("dataloading", dataLoadingHandler); + map.off("data", dataLoadingHandler); + map.off("moveend", dataLoadingHandler); + }; } - }, [mapDiv]); + }, [map, setSourceLoading, customSources]); - const mapServerInfo = useMapServerInfo(selection?.url); + const catalogItemDetailsQuery = useCatalogItemDetails( + requestManager, + selection?.type === "FeatureServer" || selection?.type === "MapServer" + ? selection.url + : undefined + ); const [search, setSearch] = useState(""); @@ -121,163 +144,151 @@ export default function ArcGISCartModal({ i.name.toLowerCase().includes(search.toLowerCase())) ); - const [mapIsLoadingData, setMapIsLoadingData] = useState(false); - - const [customSources, setCustomSources] = useState[]>([]); - useEffect(() => { - if (mapServerInfo.data && map && selection) { - const bounds = extentToLatLngBounds( - mapServerInfo.data.mapServerInfo.fullExtent - ) as LngLatBoundsLike | undefined; - if (bounds) { - map.fitBounds(bounds, { padding: 10 }); - } - if (selection.type === "MapServer") { - const layersToSelect = mapServerInfo.data.layerInfo - .filter((l) => l.defaultVisibility && l.type !== "Group Layer") - .map((l) => l.id.toString()); - setSelectedLayerIds(layersToSelect); - if (mapServerInfo.data.mapServerInfo.tileInfo?.rows) { - const tileSource = new ArcGISTiledMapService(requestManager, { - url: selection.url, - supportHighDpiDisplays: true, - }); - // @ts-ignore - window.map = map; - tileSource.getLegend().then((legend) => { - console.log("legend", legend); - }); - tileSource.getComputedMetadata().then((metadata) => { - console.log("metadata", metadata); - }); - setCustomSources([tileSource]); - tileSource.addToMap(map).then(() => { - tileSource.getLayers().then((layers) => { - console.log("add layers", layers); - for (const layer of layers) { - map.addLayer(layer); - } - }); - }); - return () => { - setCustomSources([]); - tileSource.removeFromMap(map); - }; - } else { - const useTiles = false; - const sourceId = `${selection.name}-raster-source`; - const source = new ArcGISDynamicMapService( - map, - sourceId, - selection.url, - { - supportsDynamicLayers: - mapServerInfo.data.mapServerInfo.supportsDynamicLayers, - useDevicePixelRatio: true, - layers: layersToSelect.map((l) => ({ - sublayer: parseInt(l), - opacity: 1, - })), - useTiles, + setSourceLoading(false); + setTableOfContentsItems([]); + if (catalogItemDetailsQuery.data && map && selection) { + const { type } = catalogItemDetailsQuery.data; + setSourceLoading(true); + if (type === "MapServer" && catalogItemDetailsQuery.data.tiled) { + const tileSource = new ArcGISTiledMapService(requestManager, { + url: selection.url, + supportHighDpiDisplays: true, + }); + tileSource + .getComputedMetadata() + .then(({ bounds, tableOfContentsItems }) => { + setTableOfContentsItems(tableOfContentsItems); + setVisibleLayers( + tableOfContentsItems + .filter( + (item) => + item.type === "data" && item.defaultVisibility === true + ) + .map((item) => item.id) + ); + if (bounds && bounds[0]) { + map.fitBounds(bounds, { padding: 10 }); } - ); - const layerId = `${selection.name}-dynamic-layer`; - - map.addLayer({ - id: layerId, - type: "raster", - source: sourceId, - paint: { - "raster-fade-duration": useTiles ? 100 : 0, - }, - }); - setDynamicMapService(source); - return () => { - setDynamicMapService(null); - map.removeLayer(layerId); - map.removeSource(sourceId); - }; - } - } else if (selection.type === "FeatureServer") { - const featureLayers = mapServerInfo.data.layerInfo.filter( - (l) => l.type === "Feature Layer" - ); - const sources: { [sourceId: string]: FeatureService } = {}; - // @ts-ignore - window.map = map; - for (const layer of featureLayers) { - if (layer.defaultVisibility === false) { - continue; - } - const sourceId = `${selection.name}-${layer.id}`; - const source = new FeatureService(sourceId, map, { - url: layer.url, - precision: 6, - simplifyFactor: 0.3, - setAttributionFromService: false, - minZoom: 0, }); - - // const source = new ArcGISVectorSource(map, sourceId, layer.url, { - // supportsPagination: true, - // bytesLimit: 10000000, // 10 mb - // }); - sources[sourceId] = source; - const imageList = layer.imageList; - if (layer.mapboxLayers) { - if (imageList) { - imageList.addToMap(map); - } - for (const glLayer of [...layer.mapboxLayers].reverse()) { - console.log("adding layer", glLayer, sourceId); - // @ts-ignore - map.addLayer({ - ...glLayer, - source: sourceId, - }); + setCustomSources([tileSource]); + tileSource.addToMap(map).then(() => { + tileSource.getGLStyleLayers().then((layers) => { + for (const layer of layers) { + map.addLayer(layer); } - } - } + }); + }); return () => { - const layers = map.getStyle().layers || []; - for (const sourceId in sources) { + setCustomSources([]); + tileSource.destroy(); + }; + } else if (type === "MapServer") { + const dynamicSource = new ArcGISDynamicMapService(requestManager, { + url: selection.url, + supportHighDpiDisplays: true, + tileSize: 512, + useTiles: false, + }); + dynamicSource.getComputedMetadata().then((data) => { + const { bounds, tableOfContentsItems } = data; + setTableOfContentsItems(tableOfContentsItems); + setVisibleLayers( + tableOfContentsItems + .filter( + (item) => + item.type === "data" && item.defaultVisibility === true + ) + .map((item) => item.id) + ); + if (bounds && bounds[0]) { + map.fitBounds(bounds, { padding: 10 }); + } + }); + setCustomSources([dynamicSource]); + dynamicSource.addToMap(map).then(() => { + dynamicSource.getGLStyleLayers().then((layers) => { for (const layer of layers) { - // @ts-ignore - console.log("removing", layer.source, sourceId); - // @ts-ignore - if (layer.source === sourceId) { - map.removeLayer(layer.id); - } + map.addLayer(layer); } - // source.destroy(); - sources[sourceId].destroySource(); - } + }); + }); + return () => { + setCustomSources([]); + dynamicSource.destroy(); }; } + return; + // } else if (selection.type === "FeatureServer") { + // const featureLayers = mapServerInfo.data.layerInfo.filter( + // (l) => l.type === "Feature Layer" + // ); + // const sources: { [sourceId: string]: FeatureService } = {}; + // // @ts-ignore + // window.map = map; + // for (const layer of featureLayers) { + // if (layer.defaultVisibility === false) { + // continue; + // } + // const sourceId = `${selection.name}-${layer.id}`; + // const source = new FeatureService(sourceId, map, { + // url: layer.url, + // precision: 6, + // simplifyFactor: 0.3, + // setAttributionFromService: false, + // minZoom: 0, + // }); + + // // const source = new ArcGISVectorSource(map, sourceId, layer.url, { + // // supportsPagination: true, + // // bytesLimit: 10000000, // 10 mb + // // }); + // sources[sourceId] = source; + // const imageList = layer.imageList; + // if (layer.mapboxLayers) { + // if (imageList) { + // imageList.addToMap(map); + // } + // for (const glLayer of [...layer.mapboxLayers].reverse()) { + // console.log("adding layer", glLayer, sourceId); + // // @ts-ignore + // map.addLayer({ + // ...glLayer, + // source: sourceId, + // }); + // } + // } + // } + // return () => { + // const layers = map.getStyle().layers || []; + // for (const sourceId in sources) { + // for (const layer of layers) { + // // @ts-ignore + // console.log("removing", layer.source, sourceId); + // // @ts-ignore + // if (layer.source === sourceId) { + // map.removeLayer(layer.id); + // } + // } + // // source.destroy(); + // sources[sourceId].destroySource(); + // } + // }; + // } } - }, [mapServerInfo.data, map]); + }, [catalogItemDetailsQuery.data, map]); useEffect(() => { - if (selectedLayerIds && map && dynamicMapService) { - dynamicMapService.updateLayers( - selectedLayerIds.sort().map((l) => ({ - sublayer: parseInt(l), + if (customSources.length === 1) { + const source = customSources[0]; + source.updateLayers( + visibleLayers.map((id) => ({ + id: id.toString(), opacity: 1, })) ); - if (selectedLayerIds.length === 0) { - map.removeLayer("dynamic-layer"); - } else if (!map.getLayer("dynamic-layer")) { - map.addLayer({ - id: "dynamic-layer", - type: "raster", - source: "dynamic", - }); - } - // } } - }, [selectedLayerIds, map, dynamicMapService]); + }, [visibleLayers, customSources]); return createPortal( <> @@ -386,11 +397,11 @@ export default function ArcGISCartModal({ height={133 / 3} loading="lazy" alt="thumbnail map" - className="bg-gray-100 flex-none" + className="bg-gray-100 flex-none rounded-sm" /> {selection && selection === item && - mapServerInfo.loading && ( + catalogItemDetailsQuery.loading && (
@@ -401,99 +412,21 @@ export default function ArcGISCartModal({ ))}
- {map && - mapServerInfo.data?.layerInfo && - // @ts-ignore - !mapServerInfo.data.mapServerInfo.tileInfo?.format && - (showLayerList ? ( -
-
- - - - {mapIsLoadingData ? ( - - ) : ( -
- )} -
- {mapServerInfo.data.layerInfo - .filter((l) => l.type !== "Group Layer") - .map((layer) => ( -
- -
- ))} -
- ) : ( - - ))} + { + setVisibleLayers((prev) => { + if (prev.includes(id)) { + return prev.filter((i) => i !== id); + } else { + return [...prev, id]; + } + }); + }} + />
diff --git a/packages/client/src/admin/data/arcgis/arcgis.ts b/packages/client/src/admin/data/arcgis/arcgis.ts index 9810c35d6..fc397b402 100644 --- a/packages/client/src/admin/data/arcgis/arcgis.ts +++ b/packages/client/src/admin/data/arcgis/arcgis.ts @@ -3,8 +3,10 @@ import { Layer } from "mapbox-gl"; import { useContext, useEffect, useState } from "react"; import { Symbol } from "arcgis-rest-api"; import { + ArcGISRESTServiceRequestManager, ImageList, styleForFeatureLayer, + MapServiceMetadata, } from "@seasketch/mapbox-gl-esri-sources"; import { v4 as uuid } from "uuid"; import bboxPolygon from "@turf/bbox-polygon"; @@ -34,6 +36,7 @@ import { customAlphabet } from "nanoid"; import { default as axios } from "axios"; import { MapContext } from "../../../dataLayers/MapContextManager"; import { MutationFunctionOptions } from "@apollo/client"; +import { metadata } from "../../../editor/config"; // import { ArcGISVectorSourceCacheEvent } from "../../../dataLayers/ArcGISVectorSourceCache"; const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; @@ -351,6 +354,60 @@ export function useMapServerInfo(location: string | undefined) { }; } +export interface MapServerCatalogItemDetails { + type: "MapServer"; + tiled: boolean; + metadata: MapServiceMetadata; +} + +export interface FeatureServerCatalogItemDetails { + type: "FeatureServer"; + metadata: any; +} + +export function useCatalogItemDetails( + requestManager: ArcGISRESTServiceRequestManager, + url?: string +) { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(); + const [data, setData] = useState< + MapServerCatalogItemDetails | FeatureServerCatalogItemDetails | null + >(); + + useEffect(() => { + setLoading(true); + setError(undefined); + setData(null); + const AC = new AbortController(); + if (url && /MapServer/.test(url)) { + requestManager + .getMapServiceMetadata(url, { signal: AC.signal }) + .then((data) => { + if (!AC.signal.aborted) { + setError(undefined); + setLoading(false); + setData({ + type: "MapServer", + tiled: !!data.serviceMetadata.singleFusedMapCache, + metadata: data.serviceMetadata, + }); + } + }) + .catch((e) => { + if (e.cancelled) { + // do nothing + } else { + setError(e.message); + } + }); + return () => AC.abort(); + } + }, [url]); + + return { data, loading, error }; +} + export function metersToDegrees(x: number, y: number): [number, number] { var lon = (x * 180) / 20037508.34; var lat = @@ -397,63 +454,10 @@ export interface CatalogItem { url: string; } -function cachedResponseIsExpired(response: Response) { - const cacheControlHeader = response.headers.get("Cache-Control"); - if (cacheControlHeader) { - const expires = /expires=(.*)/i.exec(cacheControlHeader); - if (expires) { - const expiration = new Date(expires[1]); - if (new Date().getTime() > expiration.getTime()) { - return true; - } else { - return false; - } - } - } - return false; -} - -async function fetchWithTTL( - url: string, - ttl: number, - options?: RequestInit - // @ts-ignore -): Promise { - const cache = await caches.open("seasketch-arcgis-browser"); - if (!options?.signal?.aborted) { - const request = new Request(url, options); - if (options?.signal?.aborted) { - Promise.reject("aborted"); - } - let cachedResponse = await cache.match(request); - if (cachedResponse && cachedResponseIsExpired(cachedResponse)) { - cache.delete(request); - cachedResponse = undefined; - } - if (cachedResponse) { - return cachedResponse; - } else { - const response = await fetch(url, options); - if (!options?.signal?.aborted) { - const headers = new Headers(response.headers); - headers.set( - "Cache-Control", - `Expires=${new Date(new Date().getTime() + 60000 * 2).toUTCString()}` - ); - const copy = response.clone(); - const clone = new Response(copy.body, { - headers, - status: response.status, - statusText: response.statusText, - }); - cache.put(url, clone); - } - return response; - } - } -} - -export function useCatalogItems(location: string) { +export function useCatalogItems( + location: string, + requestManager: ArcGISRESTServiceRequestManager +) { const [loading, setLoading] = useState(true); const [error, setError] = useState(); const [catalogInfo, setCatalogInfo] = useState(); @@ -462,15 +466,17 @@ export function useCatalogItems(location: string) { setCatalogInfo([]); setLoading(true); setError(undefined); + if (!location) { + return; + } let abortController = new AbortController(); - fetchWithTTL(location + "?f=json", 5, { - signal: abortController.signal, - // cache: "force-cache", - }).then(async (r) => { - const data = await r.json(); - if (!abortController.signal.aborted) { - setError(undefined); - setLoading(false); + requestManager + .getCatalogItems(location, { signal: abortController.signal }) + .then((data) => { + if (!abortController.signal.aborted) { + setError(undefined); + setLoading(false); + } const info = [ ...(data.folders || []).map((name: string) => ({ name, @@ -486,17 +492,29 @@ export function useCatalogItems(location: string) { } let name = item.name; if (/\//.test(item.name)) { - name = item.name.split("/").slice(-1); + name = item.name.split("/").slice(-1)[0]; } return { name, url, - type: item.type, + type: item.type as + | "Folder" + | "GPServer" + | "MapServer" + | "FeatureServer" + | "GeometryServer" + | "GeocodeServer", }; }); setCatalogInfo(info); - } - }); + }) + .catch((e) => { + if ("cancelled" in e) { + // do nothing + } else { + setError(e.message); + } + }); return () => abortController.abort(); }, [location]); diff --git a/packages/mapbox-gl-esri-sources/dist/bundle.js b/packages/mapbox-gl-esri-sources/dist/bundle.js index d0f5d5fb8..270476f09 100644 --- a/packages/mapbox-gl-esri-sources/dist/bundle.js +++ b/packages/mapbox-gl-esri-sources/dist/bundle.js @@ -1,1674 +1,1886 @@ var MapBoxGLEsriSources = (function (exports) { - 'use strict'; + 'use strict'; - const blankDataUri = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; - class ArcGISDynamicMapService { - constructor(map, id, baseUrl, options) { - this.supportDevicePixelRatio = true; - this.supportsDynamicLayers = false; - this._loading = true; - this.useTiles = false; - this.tileSize = 256; - this.updateSource = () => { - this._loading = true; - if (this.useTiles || this.source.type === "raster") { - this.source.setTiles([this.getUrl()]); - } - else { - const bounds = this.map.getBounds(); - this.source.updateImage({ - url: this.getUrl(), - coordinates: [ - [bounds.getNorthWest().lng, bounds.getNorthWest().lat], - [bounds.getNorthEast().lng, bounds.getNorthEast().lat], - [bounds.getSouthEast().lng, bounds.getSouthEast().lat], - [bounds.getSouthWest().lng, bounds.getSouthWest().lat], - ], - }); - } - }; - this.debouncedUpdateSource = () => { - if (this.debounceTimeout) { - clearTimeout(this.debounceTimeout); - } - this.debounceTimeout = setTimeout(() => { - delete this.debounceTimeout; - this.updateSource(); - }, 5); - }; - this.id = id; - this.baseUrl = baseUrl; - this.url = new URL(this.baseUrl + "/export"); - this.url.searchParams.set("f", "image"); - this.map = map; - if (!(options === null || options === void 0 ? void 0 : options.useTiles)) { - this.map.on("moveend", this.updateSource); - } - this.layers = options === null || options === void 0 ? void 0 : options.layers; - this.useTiles = (options === null || options === void 0 ? void 0 : options.useTiles) || false; - this.tileSize = (options === null || options === void 0 ? void 0 : options.tileSize) || 256; - this.queryParameters = { - transparent: "true", - ...((options === null || options === void 0 ? void 0 : options.queryParameters) || {}), - }; - if (options && "useDevicePixelRatio" in options) { - this.supportDevicePixelRatio = !!options.useDevicePixelRatio; - } - matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`).addListener(() => { - if (this.supportDevicePixelRatio) { - this.updateSource(); - } - }); - this.supportsDynamicLayers = (options === null || options === void 0 ? void 0 : options.supportsDynamicLayers) || false; - const bounds = this.map.getBounds(); - if (this.useTiles) { - this.map.addSource(this.id, { - type: "raster", - tiles: [this.getUrl()], - tileSize: this.tileSize, - }); - } - else { - this.map.addSource(this.id, { - type: "image", - url: this.getUrl(), - coordinates: [ - [bounds.getWest(), bounds.getNorth()], - [bounds.getEast(), bounds.getNorth()], - [bounds.getEast(), bounds.getSouth()], - [bounds.getWest(), bounds.getSouth()], - ], - }); - } - this.source = this.map.getSource(this.id); - this.map.on("data", (event) => { - if (event.sourceId === this.id && - event.dataType === "source" && - event.sourceDataType === "content") { - this._loading = false; - } - }); - this.map.on("error", (event) => { - if (event.sourceId && event.sourceId === this.id) { - this._loading = false; - } - }); - } - destroy() { - this.map.off("moveend", this.updateSource); - this.map.off("data", this.updateSource); - this.map.off("error", this.updateSource); - } - getUrl() { - const bounds = this.map.getBounds(); - let bbox = [ - lon2meters(bounds.getWest()), - lat2meters(bounds.getSouth()), - lon2meters(bounds.getEast()), - lat2meters(bounds.getNorth()), - ]; - const groundResolution = getGroundResolution(this.map.getZoom() + - (this.supportDevicePixelRatio ? window.devicePixelRatio - 1 : 0)); - const width = Math.round((bbox[2] - bbox[0]) / groundResolution); - const height = Math.round((bbox[3] - bbox[1]) / groundResolution); - this.url.searchParams.set("format", "png"); - this.url.searchParams.set("size", [width, height].join(",")); - if (this.supportDevicePixelRatio) { - switch (window.devicePixelRatio) { - case 1: - this.url.searchParams.set("dpi", "96"); - break; - case 2: - this.url.searchParams.set("dpi", "220"); - break; - case 3: - this.url.searchParams.set("dpi", "390"); - break; - default: - this.url.searchParams.set("dpi", - (window.devicePixelRatio * 96 * 1.22).toString()); - break; - } - } - else { - this.url.searchParams.set("dpi", "96"); - } - this.url.searchParams.set("imageSR", "102100"); - this.url.searchParams.set("bboxSR", "102100"); - if (Math.abs(bbox[0]) > 20037508.34 || Math.abs(bbox[2]) > 20037508.34) { - const centralMeridian = bounds.getCenter().lng; - if (this.supportDevicePixelRatio && window.devicePixelRatio > 1) { - bbox[0] = -(width * groundResolution) / (window.devicePixelRatio * 2); - bbox[2] = (width * groundResolution) / (window.devicePixelRatio * 2); - } - else { - bbox[0] = -(width * groundResolution) / 2; - bbox[2] = (width * groundResolution) / 2; - } - const sr = JSON.stringify({ - wkt: `PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",${centralMeridian}],PARAMETER["Standard_Parallel_1",0.0],PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0]]`, - }); - this.url.searchParams.set("imageSR", sr); - this.url.searchParams.set("bboxSR", sr); - } - if (Array.isArray(this.layers)) { - if (this.layers.length === 0) { - return blankDataUri; - } - else { - this.url.searchParams.set("layers", `show:${this.layers.map((lyr) => lyr.sublayer).join(",")}`); - } - } - this.url.searchParams.set("bbox", bbox.join(",")); - this.url.searchParams.delete("dynamicLayers"); - let layersInOrder = true; - let hasOpacityUpdates = false; - if (this.supportsDynamicLayers && this.layers) { - for (var i = 0; i < this.layers.length; i++) { - if (this.layers[i - 1] && - this.layers[i].sublayer < this.layers[i - 1].sublayer) { - layersInOrder = false; - } - const opacity = this.layers[i].opacity; - if (opacity !== undefined && opacity < 1) { - hasOpacityUpdates = true; - } - } - } - if (this.layers && (!layersInOrder || hasOpacityUpdates)) { - const dynamicLayers = this.layers.map((lyr) => { - return { - id: lyr.sublayer, - source: { - mapLayerId: lyr.sublayer, - type: "mapLayer", - }, - drawingInfo: { - transparency: lyr.opacity !== undefined ? 100 - lyr.opacity * 100 : 0, - }, - }; - }); - this.url.searchParams.set("dynamicLayers", JSON.stringify(dynamicLayers)); - } - for (const key in this.queryParameters) { - this.url.searchParams.set(key, this.queryParameters[key].toString()); - } - if (this.useTiles) { - this.url.searchParams.set("bbox", `seasketch-replace-me`); - if (this.supportDevicePixelRatio && window.devicePixelRatio > 1) { - const size = this.tileSize * window.devicePixelRatio; - this.url.searchParams.set("size", [size, size].join(",")); - } - else { - this.url.searchParams.set("size", [this.tileSize, this.tileSize].join(",")); - } - } - return this.url - .toString() - .replace("seasketch-replace-me", "{bbox-epsg-3857}"); - } - get loading() { - return this._loading; - } - updateLayers(layers) { - if (JSON.stringify(layers) !== JSON.stringify(this.layers)) { - this.layers = layers; - this.debouncedUpdateSource(); - } - } - updateQueryParameters(queryParameters) { - if (JSON.stringify(this.queryParameters) !== JSON.stringify(queryParameters)) { - this.queryParameters = queryParameters; - this.debouncedUpdateSource(); - } - } - updateUseDevicePixelRatio(enable) { - if (enable !== this.supportDevicePixelRatio) { - this.supportDevicePixelRatio = enable; - this.debouncedUpdateSource(); - } - } - } - function lat2meters(lat) { - var y = Math.log(Math.tan(((90 + lat) * Math.PI) / 360)) / (Math.PI / 180); - return (y * 20037508.34) / 180; - } - function lon2meters(lon) { - return (lon * 20037508.34) / 180; - } - function getGroundResolution(level) { - let groundResolution = resolutions[level]; - if (!groundResolution) { - groundResolution = (2 * Math.PI * 6378137) / (256 * 2 ** (level + 1)); - resolutions[level] = groundResolution; - } - return groundResolution; + var getRandomValues; + var rnds8 = new Uint8Array(16); + function rng() { + if (!getRandomValues) { + getRandomValues = typeof crypto !== 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto) || typeof msCrypto !== 'undefined' && typeof msCrypto.getRandomValues === 'function' && msCrypto.getRandomValues.bind(msCrypto); + if (!getRandomValues) { + throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported'); + } } - const resolutions = {}; + return getRandomValues(rnds8); + } - class ArcGISVectorSource { - constructor(map, id, url, options) { - var _a; - this.data = { - type: "FeatureCollection", - features: [], - }; - this.outFields = "*"; - this.supportsPagination = true; - this._loading = true; - this._id = id; - this.baseUrl = url; - this.options = options; - this.map = map; - this.map.addSource(this._id, { - data: this.data, - type: "geojson", - }); - if (options && - "supportsPagination" in options && - options["supportsPagination"] === false) { - this.supportsPagination = false; - } - if (options && options.outFields) { - this.outFields = options.outFields; - } - this.source = this.map.getSource(this._id); - let hadError = false; - const onError = (e) => { - hadError = true; - this._loading = false; - this.map.fire("error", { - source: this.source, - sourceId: this._id, - error: e, - }); - }; - this.map.fire("dataloading", { - source: this.source, - sourceId: this._id, - dataType: "source", - isSourceLoaded: false, - sourceDataType: "content", - }); - fetchFeatureLayerData(this.baseUrl, this.outFields, onError, (_a = this.options) === null || _a === void 0 ? void 0 : _a.geometryPrecision, null, null, false, 1000, options === null || options === void 0 ? void 0 : options.bytesLimit) - .then((fc) => { - this._loading = false; - if (!hadError) { - this.source.setData(fc); - } - }) - .catch(onError); - } - get loading() { - return this._loading; - } - get id() { - return this._id; - } - destroy() { - this.map.removeSource(this._id); - } - } - async function fetchFeatureLayerData(url, outFields, onError, geometryPrecision = 6, abortController = null, onPageReceived = null, disablePagination = false, pageSize = 1000, bytesLimit) { - const featureCollection = { - type: "FeatureCollection", - features: [], - }; - const params = new URLSearchParams({ - inSR: "4326", - outSR: "4326", - where: "1>0", - outFields, - returnGeometry: "true", - geometryPrecision: geometryPrecision.toString(), - returnIdsOnly: "false", - f: "geojson", - }); - await fetchData(url, params, featureCollection, onError, abortController || new AbortController(), onPageReceived, disablePagination, pageSize, bytesLimit); - return featureCollection; - } - async function fetchData(baseUrl, params, featureCollection, onError, abortController, onPageReceived, disablePagination = false, pageSize = 1000, bytesLimit, bytesReceived, objectIdFieldName, expectedFeatureCount) { - bytesReceived = bytesReceived || 0; - new TextDecoder("utf-8"); - params.set("returnIdsOnly", "false"); - if (featureCollection.features.length > 0) { - params.delete("where"); - params.delete("resultOffset"); - params.delete("resultRecordCount"); - params.set("orderByFields", objectIdFieldName); - const lastFeature = featureCollection.features[featureCollection.features.length - 1]; - params.set("where", `${objectIdFieldName}>${lastFeature.id}`); - } - const response = await fetch(`${baseUrl}/query?${params.toString()}`, { - mode: "cors", - signal: abortController.signal, - }); - const str = await response.text(); - bytesReceived += byteLength(str); - if (bytesLimit && bytesReceived >= bytesLimit) { - const e = new Error(`Exceeded bytesLimit. ${bytesReceived} >= ${bytesLimit}`); - return onError(e); - } - const fc = JSON.parse(str); - if (fc.error) { - return onError(new Error(fc.error.message)); - } - else { - featureCollection.features.push(...fc.features); - if (fc.exceededTransferLimit) { - if (!objectIdFieldName) { - params.set("returnIdsOnly", "true"); - try { - const r = await fetch(`${baseUrl}/query?${params.toString()}`, { - mode: "cors", - signal: abortController.signal, - }); - const featureIds = featureCollection.features.map((f) => f.id); - const objectIdParameters = await r.json(); - expectedFeatureCount = objectIdParameters.objectIds.length; - objectIdFieldName = objectIdParameters.objectIdFieldName; - } - catch (e) { - return onError(e); - } - } - if (onPageReceived) { - onPageReceived(bytesReceived, featureCollection.features.length, expectedFeatureCount); - } - await fetchData(baseUrl, params, featureCollection, onError, abortController, onPageReceived, disablePagination, pageSize, bytesLimit, bytesReceived, objectIdFieldName, expectedFeatureCount); - } - } - return bytesReceived; - } - function byteLength(str) { - var s = str.length; - for (var i = str.length - 1; i >= 0; i--) { - var code = str.charCodeAt(i); - if (code > 0x7f && code <= 0x7ff) - s++; - else if (code > 0x7ff && code <= 0xffff) - s += 2; - if (code >= 0xdc00 && code <= 0xdfff) - i--; - } - return s; - } + var REGEX = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; - class ArcGISRESTServiceRequestManager { - constructor(options) { - this.inFlightRequests = {}; - caches - .open((options === null || options === void 0 ? void 0 : options.cacheKey) || "seasketch-arcgis-rest-services") - .then((cache) => { - this.cache = cache; - }); - } - async getMapServiceMetadata(url, credentials) { - if (!/rest\/services/.test(url)) { - throw new Error("Invalid ArcGIS REST Service URL"); - } - if (!/MapServer/.test(url)) { - throw new Error("Invalid MapServer URL"); - } - url = url.replace(/\/$/, ""); - url = url.replace(/\?.*$/, ""); - const params = new URLSearchParams(); - params.set("f", "json"); - if (credentials) { - const token = await this.getToken(url.replace(/rest\/services\/.*/, "/rest/services/"), credentials); - params.set("token", token); - } - const requestUrl = `${url}?${params.toString()}`; - const serviceMetadata = await this.fetch(requestUrl); - const layers = await this.fetch(`${url}/layers?${params.toString()}`); - if (layers.error) { - throw new Error(layers.error.message); - } - return { serviceMetadata, layers }; - } - async getToken(url, credentials) { - throw new Error("Not implemented"); - } - async fetch(url) { - if (url in this.inFlightRequests) { - return this.inFlightRequests[url].then((json) => json); - } - const cache = await this.cache; - if (!cache) { - throw new Error("Cache not initialized"); - } - this.inFlightRequests[url] = fetchWithTTL(url, 60 * 300, cache); - return new Promise((resolve, reject) => { - this.inFlightRequests[url] - .then((json) => { - if (json["error"]) { - reject(new Error(json["error"].message)); - } - else { - resolve(json); - } - }) - .catch(reject) - .finally(() => { - delete this.inFlightRequests[url]; - }); - }); - } - async getLegendMetadata(url, credentials) { - if (!/rest\/services/.test(url)) { - throw new Error("Invalid ArcGIS REST Service URL"); - } - if (!/MapServer/.test(url)) { - throw new Error("Invalid MapServer URL"); - } - url = url.replace(/\/$/, ""); - url = url.replace(/\?.*$/, ""); - const params = new URLSearchParams(); - params.set("f", "json"); - if (credentials) { - const token = await this.getToken(url.replace(/rest\/services\/.*/, "/rest/services/"), credentials); - params.set("token", token); - } - const requestUrl = `${url}/legend?${params.toString()}`; - const response = await this.fetch(requestUrl); - return response; - } - } - function cachedResponseIsExpired(response) { - const cacheControlHeader = response.headers.get("Cache-Control"); - if (cacheControlHeader) { - const expires = /expires=(.*)/i.exec(cacheControlHeader); - if (expires) { - const expiration = new Date(expires[1]); - if (new Date().getTime() > expiration.getTime()) { - return true; - } - else { - return false; - } - } - } - return false; - } - async function fetchWithTTL(url, ttl, cache, options - ) { - var _a, _b, _c; - if (!((_a = options === null || options === void 0 ? void 0 : options.signal) === null || _a === void 0 ? void 0 : _a.aborted)) { - const request = new Request(url, options); - if ((_b = options === null || options === void 0 ? void 0 : options.signal) === null || _b === void 0 ? void 0 : _b.aborted) { - Promise.reject("aborted"); - } - let cachedResponse = await cache.match(request); - if (cachedResponse && cachedResponseIsExpired(cachedResponse)) { - cache.delete(request); - cachedResponse = undefined; - } - if (cachedResponse) { - return cachedResponse.json(); - } - else { - const response = await fetch(url, options); - if (!((_c = options === null || options === void 0 ? void 0 : options.signal) === null || _c === void 0 ? void 0 : _c.aborted)) { - const headers = new Headers(response.headers); - headers.set("Cache-Control", `Expires=${new Date(new Date().getTime() + 1000 * ttl).toUTCString()}`); - const copy = response.clone(); - const clone = new Response(copy.body, { - headers, - status: response.status, - statusText: response.statusText, - }); - cache.put(url, clone); - } - return await response.json(); - } - } + function validate(uuid) { + return typeof uuid === 'string' && REGEX.test(uuid); + } + + var byteToHex = []; + for (var i = 0; i < 256; ++i) { + byteToHex.push((i + 0x100).toString(16).substr(1)); + } + function stringify(arr) { + var offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; + var uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); + if (!validate(uuid)) { + throw TypeError('Stringified UUID is invalid'); } + return uuid; + } - var getRandomValues; - var rnds8 = new Uint8Array(16); - function rng() { - if (!getRandomValues) { - getRandomValues = typeof crypto !== 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto) || typeof msCrypto !== 'undefined' && typeof msCrypto.getRandomValues === 'function' && msCrypto.getRandomValues.bind(msCrypto); - if (!getRandomValues) { - throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported'); - } + function v4(options, buf, offset) { + options = options || {}; + var rnds = options.random || (options.rng || rng)(); + rnds[6] = rnds[6] & 0x0f | 0x40; + rnds[8] = rnds[8] & 0x3f | 0x80; + if (buf) { + offset = offset || 0; + for (var i = 0; i < 16; ++i) { + buf[offset + i] = rnds[i]; } - return getRandomValues(rnds8); + return buf; } + return stringify(rnds); + } - var REGEX = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; - - function validate(uuid) { - return typeof uuid === 'string' && REGEX.test(uuid); - } + function replaceSource(sourceId, map, sourceData) { + var _a; + const existingSource = map.getSource(sourceId); + if (!existingSource) { + throw new Error("Source does not exist"); + } + if (existingSource.type !== sourceData.type) { + throw new Error("Source type mismatch"); + } + const allLayers = map.getStyle().layers || []; + const relatedLayers = allLayers.filter((l) => { + return "source" in l && l.source === sourceId; + }); + relatedLayers.reverse(); + const idx = allLayers.indexOf(relatedLayers[0]); + let before = ((_a = allLayers[idx + 1]) === null || _a === void 0 ? void 0 : _a.id) || undefined; + for (const layer of relatedLayers) { + map.removeLayer(layer.id); + } + map.removeSource(sourceId); + map.addSource(sourceId, sourceData); + for (const layer of relatedLayers) { + map.addLayer(layer, before); + before = layer.id; + } + } + function metersToDegrees(x, y) { + var lon = (x * 180) / 20037508.34; + var lat = (Math.atan(Math.exp((y * Math.PI) / 20037508.34)) * 360) / Math.PI - 90; + return [lon, lat]; + } + async function extentToLatLngBounds(extent) { + if (extent) { + const wkid = normalizeSpatialReference(extent.spatialReference); + if (wkid === 4326) { + return [extent.xmin, extent.ymin, extent.xmax, extent.ymax]; + } + else if (wkid === 3857 || wkid === 102100) { + return [ + ...metersToDegrees(extent.xmin, extent.ymin), + ...metersToDegrees(extent.xmax, extent.ymax), + ]; + } + else { + try { + const projected = await projectExtent(extent); + return [projected.xmin, projected.ymin, projected.xmax, projected.ymax]; + } + catch (e) { + console.error(e); + return; + } + } + } + } + function normalizeSpatialReference(sr) { + const wkid = "latestWkid" in sr ? sr.latestWkid : "wkid" in sr ? sr.wkid : -1; + if (typeof wkid === "string") { + if (/WGS\s*84/.test(wkid)) { + return 4326; + } + else { + return -1; + } + } + else { + return wkid || -1; + } + } + async function projectExtent(extent) { + const endpoint = "https://tasks.arcgisonline.com/arcgis/rest/services/Geometry/GeometryServer/project"; + const params = new URLSearchParams({ + geometries: JSON.stringify({ + geometryType: "esriGeometryEnvelope", + geometries: [extent], + }), + inSR: `${extent.spatialReference.wkid}`, + outSR: "4326", + f: "json", + }); + const response = await fetch(`${endpoint}?${params.toString()}`); + const data = await response.json(); + const projected = data.geometries[0]; + if (projected) { + return projected; + } + else { + throw new Error("Failed to reproject"); + } + } + function contentOrFalse(str) { + if (str && str.length > 0) { + return str; + } + else { + return false; + } + } + function pickDescription(info, layer) { + var _a, _b; + return (contentOrFalse(layer === null || layer === void 0 ? void 0 : layer.description) || + contentOrFalse(info.description) || + contentOrFalse((_a = info.documentInfo) === null || _a === void 0 ? void 0 : _a.Subject) || + contentOrFalse((_b = info.documentInfo) === null || _b === void 0 ? void 0 : _b.Comments)); + } + function generateMetadataForLayer(url, mapServerInfo, layer) { + var _a, _b, _c, _d; + const attribution = contentOrFalse(layer.copyrightText) || + contentOrFalse(mapServerInfo.copyrightText) || + contentOrFalse((_a = mapServerInfo.documentInfo) === null || _a === void 0 ? void 0 : _a.Author); + const description = pickDescription(mapServerInfo, layer); + let keywords = ((_b = mapServerInfo.documentInfo) === null || _b === void 0 ? void 0 : _b.Keywords) && + ((_c = mapServerInfo.documentInfo) === null || _c === void 0 ? void 0 : _c.Keywords.length) + ? (_d = mapServerInfo.documentInfo) === null || _d === void 0 ? void 0 : _d.Keywords.split(",") + : []; + return { + type: "doc", + content: [ + { + type: "heading", + attrs: { level: 1 }, + content: [{ type: "text", text: layer.name }], + }, + ...(description + ? [ + { + type: "paragraph", + content: [ + { + type: "text", + text: description, + }, + ], + }, + ] + : []), + ...(attribution + ? [ + { type: "paragraph" }, + { + type: "heading", + attrs: { level: 3 }, + content: [{ type: "text", text: "Attribution" }], + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: attribution, + }, + ], + }, + ] + : []), + ...(keywords && keywords.length + ? [ + { type: "paragraph" }, + { + type: "heading", + attrs: { level: 3 }, + content: [ + { + type: "text", + text: "Keywords", + }, + ], + }, + { + type: "bullet_list", + marks: [], + attrs: {}, + content: keywords.map((word) => ({ + type: "list_item", + content: [ + { + type: "paragraph", + content: [{ type: "text", text: word }], + }, + ], + })), + }, + ] + : []), + { type: "paragraph" }, + { + type: "paragraph", + content: [ + { + type: "text", + marks: [ + { + type: "link", + attrs: { + href: url, + title: "ArcGIS Server", + }, + }, + ], + text: url, + }, + ], + }, + ], + }; + } + function makeLegend(data, layerId) { + const legendLayer = data.layers.find((l) => l.layerId === layerId); + if (legendLayer) { + return legendLayer.legend.map((legend) => { + return { + id: legend.url, + label: legend.label && legend.label.length > 0 + ? legend.label + : legendLayer.legend.length === 1 + ? legendLayer.layerName + : "", + imageUrl: (legend === null || legend === void 0 ? void 0 : legend.imageData) + ? `data:${legend.contentType};base64,${legend.imageData}` + : blankDataUri, + imageWidth: 20, + imageHeight: 20, + }; + }); + } + else { + return undefined; + } + } - var byteToHex = []; - for (var i = 0; i < 256; ++i) { - byteToHex.push((i + 0x100).toString(16).substr(1)); - } - function stringify(arr) { - var offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - var uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); - if (!validate(uuid)) { - throw TypeError('Stringified UUID is invalid'); + const blankDataUri = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + class ArcGISDynamicMapService { + constructor(requestManager, options) { + this.supportsDynamicLayers = false; + this._loading = true; + this.respondToResolutionChange = () => { + if (this.options.supportHighDpiDisplays) { + this.updateSource(); + } + if (this.resolution) { + matchMedia(this.resolution).removeListener(this.respondToResolutionChange); + } + this.resolution = `(resolution: ${window.devicePixelRatio}dppx)`; + matchMedia(this.resolution).addListener(this.respondToResolutionChange); + }; + this.onMapData = (event) => { + if (event.sourceId && event.sourceId === this.sourceId) { + this._loading = false; + } + }; + this.onMapError = (event) => { + if (event.sourceId === this.sourceId && + event.dataType === "source" && + event.sourceDataType === "content") { + this._loading = false; + } + }; + this.updateSource = () => { + var _a; + this._loading = true; + const source = (_a = this.map) === null || _a === void 0 ? void 0 : _a.getSource(this.sourceId); + if (source && this.map) { + if (source.type === "raster") { + source.setTiles([this.getUrl()]); + } + else if (source.type === "image") { + const bounds = this.map.getBounds(); + source.updateImage({ + url: this.getUrl(), + coordinates: [ + [bounds.getNorthWest().lng, bounds.getNorthWest().lat], + [bounds.getNorthEast().lng, bounds.getNorthEast().lat], + [bounds.getSouthEast().lng, bounds.getSouthEast().lat], + [bounds.getSouthWest().lng, bounds.getSouthWest().lat], + ], + }); + } + else ; + } + }; + this.debouncedUpdateSource = () => { + if (this.debounceTimeout) { + clearTimeout(this.debounceTimeout); + } + this.debounceTimeout = setTimeout(() => { + delete this.debounceTimeout; + this.updateSource(); + }, 5); + }; + this.options = options; + this.requestManager = requestManager; + this.sourceId = (options === null || options === void 0 ? void 0 : options.sourceId) || v4(); + options.url = options.url.replace(/\/$/, ""); + if (!/rest\/services/.test(options.url) || !/MapServer/.test(options.url)) { + throw new Error("Invalid ArcGIS REST Service URL"); + } + this.resolution = `(resolution: ${window.devicePixelRatio}dppx)`; + matchMedia(this.resolution).addListener(this.respondToResolutionChange); } - return uuid; - } + getMetadata() { + if (this.serviceMetadata && this.layerMetadata) { + return Promise.resolve({ + serviceMetadata: this.serviceMetadata, + layers: this.layerMetadata, + }); + } + else { + return this.requestManager + .getMapServiceMetadata(this.options.url, { + credentials: this.options.credentials, + }) + .then(({ serviceMetadata, layers }) => { + this.serviceMetadata = serviceMetadata; + this.layerMetadata = layers; + this.supportsDynamicLayers = serviceMetadata.supportsDynamicLayers; + return { serviceMetadata, layers }; + }); + } + } + async getComputedMetadata() { + var _a, _b; + const { serviceMetadata, layers } = await this.getMetadata(); + const { bounds, minzoom, maxzoom, attribution } = await this.getComputedProperties(); + const results = /\/.+\/MapServer/.exec(this.options.url); + let label = results ? results[0] : false; + if (!label) { + if ((_b = (_a = this.layerMetadata) === null || _a === void 0 ? void 0 : _a.layers) === null || _b === void 0 ? void 0 : _b[0]) { + label = this.layerMetadata.layers[0].name; + } + } + const legendData = await this.requestManager.getLegendMetadata(this.options.url); + const hiddenIds = new Set(); + for (const layer of layers.layers) { + if (!layer.defaultVisibility) { + hiddenIds.add(layer.id); + } + else { + if (layer.parentLayer) { + if (hiddenIds.has(layer.parentLayer.id)) { + hiddenIds.add(layer.id); + } + else { + const parent = layers.layers.find((l) => { var _a; return l.id === ((_a = layer.parentLayer) === null || _a === void 0 ? void 0 : _a.id); }); + if (parent && !parent.defaultVisibility) { + hiddenIds.add(layer.id); + hiddenIds.add(parent.id); + } + } + } + } + } + return { + bounds: bounds || undefined, + minzoom, + maxzoom, + attribution, + tableOfContentsItems: layers.layers.map((lyr) => { + legendData.layers.find((l) => l.layerId === lyr.id); + const isFolder = lyr.type === "Group Layer"; + if (isFolder) { + return { + type: "folder", + id: lyr.id.toString(), + label: lyr.name, + defaultVisibility: hiddenIds.has(lyr.id) + ? false + : lyr.defaultVisibility, + parentId: lyr.parentLayer + ? lyr.parentLayer.id.toString() + : undefined, + }; + } + else { + return { + type: "data", + id: lyr.id.toString(), + label: lyr.name, + defaultVisibility: hiddenIds.has(lyr.id) + ? false + : lyr.defaultVisibility, + metadata: generateMetadataForLayer(this.options.url + "/" + lyr.id, this.serviceMetadata, lyr), + parentId: lyr.parentLayer + ? lyr.parentLayer.id.toString() + : undefined, + legend: makeLegend(legendData, lyr.id), + }; + } + }), + supportsDynamicRendering: { + layerOpacity: this.supportsDynamicLayers, + layerOrder: true, + layerVisibility: true, + }, + }; + } + async getComputedProperties() { + var _a, _b; + const { serviceMetadata, layers } = await this.getMetadata(); + const levels = ((_a = serviceMetadata.tileInfo) === null || _a === void 0 ? void 0 : _a.lods.map((l) => l.level)) || []; + const attribution = contentOrFalse(layers.layers[0].copyrightText) || + contentOrFalse(serviceMetadata.copyrightText) || + contentOrFalse((_b = serviceMetadata.documentInfo) === null || _b === void 0 ? void 0 : _b.Author) || + undefined; + const minzoom = Math.min(...levels); + const maxzoom = Math.max(...levels); + return { + minzoom, + maxzoom, + bounds: await extentToLatLngBounds(serviceMetadata.fullExtent), + attribution, + }; + } + async addToMap(map) { + var _a; + const { attribution, bounds } = await this.getComputedProperties(); + this.map = map; + if (!((_a = this.options) === null || _a === void 0 ? void 0 : _a.useTiles)) { + this.map.on("moveend", this.updateSource); + this.map.on("data", this.onMapData); + this.map.on("error", this.onMapError); + } + if (this.options.useTiles) { + this.map.addSource(this.sourceId, { + type: "raster", + tiles: [this.getUrl()], + tileSize: this.options.tileSize || 256, + bounds: bounds, + attribution, + }); + } + else { + const bounds = this.map.getBounds(); + this.map.addSource(this.sourceId, { + type: "image", + url: this.getUrl(), + coordinates: [ + [bounds.getWest(), bounds.getNorth()], + [bounds.getEast(), bounds.getNorth()], + [bounds.getEast(), bounds.getSouth()], + [bounds.getWest(), bounds.getSouth()], + ], + }); + } + return this.sourceId; + } + removeFromMap(map) { + if (map.getSource(this.sourceId)) { + const layers = map.getStyle().layers || []; + for (const layer of layers) { + if ("source" in layer && layer.source === this.sourceId) { + map.removeLayer(layer.id); + } + } + map.off("moveend", this.updateSource); + map.off("data", this.onMapData); + map.off("error", this.onMapError); + map.removeSource(this.sourceId); + this.map = undefined; + } + } + destroy() { + matchMedia(this.resolution).removeListener(this.respondToResolutionChange); + if (this.map) { + this.removeFromMap(this.map); + } + } + getUrl() { + if (!this.map) { + throw new Error("Map not set"); + } + let url = new URL(this.options.url + "/export"); + url.searchParams.set("f", "image"); + url.searchParams.set("transparent", "true"); + const bounds = this.map.getBounds(); + let bbox = [ + lon2meters(bounds.getWest()), + lat2meters(bounds.getSouth()), + lon2meters(bounds.getEast()), + lat2meters(bounds.getNorth()), + ]; + const groundResolution = getGroundResolution(this.map.getZoom() + + (this.options.supportHighDpiDisplays ? window.devicePixelRatio - 1 : 0)); + const width = Math.round((bbox[2] - bbox[0]) / groundResolution); + const height = Math.round((bbox[3] - bbox[1]) / groundResolution); + url.searchParams.set("format", "png"); + url.searchParams.set("size", [width, height].join(",")); + if (this.options.supportHighDpiDisplays) { + switch (window.devicePixelRatio) { + case 1: + url.searchParams.set("dpi", "96"); + break; + case 2: + url.searchParams.set("dpi", "220"); + break; + case 3: + url.searchParams.set("dpi", "390"); + break; + default: + url.searchParams.set("dpi", + (window.devicePixelRatio * 96 * 1.22).toString()); + break; + } + } + else { + url.searchParams.set("dpi", "96"); + } + url.searchParams.set("imageSR", "102100"); + url.searchParams.set("bboxSR", "102100"); + if (Math.abs(bbox[0]) > 20037508.34 || Math.abs(bbox[2]) > 20037508.34) { + const centralMeridian = bounds.getCenter().lng; + if (this.options.supportHighDpiDisplays && window.devicePixelRatio > 1) { + bbox[0] = -(width * groundResolution) / (window.devicePixelRatio * 2); + bbox[2] = (width * groundResolution) / (window.devicePixelRatio * 2); + } + else { + bbox[0] = -(width * groundResolution) / 2; + bbox[2] = (width * groundResolution) / 2; + } + const sr = JSON.stringify({ + wkt: `PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",${centralMeridian}],PARAMETER["Standard_Parallel_1",0.0],PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0]]`, + }); + url.searchParams.set("imageSR", sr); + url.searchParams.set("bboxSR", sr); + } + if (Array.isArray(this.layers)) { + if (this.layers.length === 0) { + return blankDataUri; + } + else { + url.searchParams.set("layers", `show:${this.layers.map((lyr) => lyr.id).join(",")}`); + } + } + url.searchParams.set("bbox", bbox.join(",")); + url.searchParams.delete("dynamicLayers"); + let layersInOrder = true; + let hasOpacityUpdates = false; + if (this.supportsDynamicLayers && this.layers) { + for (var i = 0; i < this.layers.length; i++) { + if (this.layers[i - 1] && + parseInt(this.layers[i].id) < parseInt(this.layers[i - 1].id)) { + layersInOrder = false; + } + const opacity = this.layers[i].opacity; + if (opacity !== undefined && opacity < 1) { + hasOpacityUpdates = true; + } + } + } + if (this.layers && (!layersInOrder || hasOpacityUpdates)) { + const dynamicLayers = this.layers.map((lyr) => { + return { + id: lyr.id, + source: { + mapLayerId: lyr.id, + type: "mapLayer", + }, + drawingInfo: { + transparency: lyr.opacity !== undefined ? 100 - lyr.opacity * 100 : 0, + }, + }; + }); + url.searchParams.set("dynamicLayers", JSON.stringify(dynamicLayers)); + } + for (const key in this.options.queryParameters) { + url.searchParams.set(key, this.options.queryParameters[key].toString()); + } + const tileSize = this.options.tileSize || 256; + if (this.options.useTiles) { + url.searchParams.set("bbox", `seasketch-replace-me`); + if (this.options.supportHighDpiDisplays && window.devicePixelRatio > 1) { + const size = tileSize * window.devicePixelRatio; + url.searchParams.set("size", [size, size].join(",")); + } + else { + url.searchParams.set("size", [tileSize, tileSize].join(",")); + } + } + return url.toString().replace("seasketch-replace-me", "{bbox-epsg-3857}"); + } + get loading() { + var _a; + const source = (_a = this.map) === null || _a === void 0 ? void 0 : _a.getSource(this.sourceId); + if (source && source.type === "raster") { + return this.map.isSourceLoaded(this.sourceId) === false; + } + else { + return this._loading; + } + } + updateLayers(layers) { + if (JSON.stringify(layers) !== JSON.stringify(this.layers)) { + this.layers = layers; + this.debouncedUpdateSource(); + } + } + updateQueryParameters(queryParameters) { + if (JSON.stringify(this.options.queryParameters) !== + JSON.stringify(queryParameters)) { + this.options.queryParameters = queryParameters; + this.debouncedUpdateSource(); + } + } + updateUseDevicePixelRatio(enable) { + if (enable !== this.options.supportHighDpiDisplays) { + this.options.supportHighDpiDisplays = enable; + this.debouncedUpdateSource(); + } + } + async getGLStyleLayers() { + return [ + { + id: v4(), + type: "raster", + source: this.sourceId, + paint: { + "raster-fade-duration": this.options.useTiles ? 300 : 0, + }, + }, + ]; + } + } + function lat2meters(lat) { + var y = Math.log(Math.tan(((90 + lat) * Math.PI) / 360)) / (Math.PI / 180); + return (y * 20037508.34) / 180; + } + function lon2meters(lon) { + return (lon * 20037508.34) / 180; + } + function getGroundResolution(level) { + let groundResolution = resolutions[level]; + if (!groundResolution) { + groundResolution = (2 * Math.PI * 6378137) / (256 * 2 ** (level + 1)); + resolutions[level] = groundResolution; + } + return groundResolution; + } + const resolutions = {}; - function v4(options, buf, offset) { - options = options || {}; - var rnds = options.random || (options.rng || rng)(); - rnds[6] = rnds[6] & 0x0f | 0x40; - rnds[8] = rnds[8] & 0x3f | 0x80; - if (buf) { - offset = offset || 0; - for (var i = 0; i < 16; ++i) { - buf[offset + i] = rnds[i]; - } - return buf; - } - return stringify(rnds); - } + class ArcGISVectorSource { + constructor(map, id, url, options) { + var _a; + this.data = { + type: "FeatureCollection", + features: [], + }; + this.outFields = "*"; + this.supportsPagination = true; + this._loading = true; + this._id = id; + this.baseUrl = url; + this.options = options; + this.map = map; + this.map.addSource(this._id, { + data: this.data, + type: "geojson", + }); + if (options && + "supportsPagination" in options && + options["supportsPagination"] === false) { + this.supportsPagination = false; + } + if (options && options.outFields) { + this.outFields = options.outFields; + } + this.source = this.map.getSource(this._id); + let hadError = false; + const onError = (e) => { + hadError = true; + this._loading = false; + this.map.fire("error", { + source: this.source, + sourceId: this._id, + error: e, + }); + }; + this.map.fire("dataloading", { + source: this.source, + sourceId: this._id, + dataType: "source", + isSourceLoaded: false, + sourceDataType: "content", + }); + fetchFeatureLayerData(this.baseUrl, this.outFields, onError, (_a = this.options) === null || _a === void 0 ? void 0 : _a.geometryPrecision, null, null, false, 1000, options === null || options === void 0 ? void 0 : options.bytesLimit) + .then((fc) => { + this._loading = false; + if (!hadError) { + this.source.setData(fc); + } + }) + .catch(onError); + } + get loading() { + return this._loading; + } + get id() { + return this._id; + } + destroy() { + this.map.removeSource(this._id); + } + } + async function fetchFeatureLayerData(url, outFields, onError, geometryPrecision = 6, abortController = null, onPageReceived = null, disablePagination = false, pageSize = 1000, bytesLimit) { + const featureCollection = { + type: "FeatureCollection", + features: [], + }; + const params = new URLSearchParams({ + inSR: "4326", + outSR: "4326", + where: "1>0", + outFields, + returnGeometry: "true", + geometryPrecision: geometryPrecision.toString(), + returnIdsOnly: "false", + f: "geojson", + }); + await fetchData(url, params, featureCollection, onError, abortController || new AbortController(), onPageReceived, disablePagination, pageSize, bytesLimit); + return featureCollection; + } + async function fetchData(baseUrl, params, featureCollection, onError, abortController, onPageReceived, disablePagination = false, pageSize = 1000, bytesLimit, bytesReceived, objectIdFieldName, expectedFeatureCount) { + bytesReceived = bytesReceived || 0; + new TextDecoder("utf-8"); + params.set("returnIdsOnly", "false"); + if (featureCollection.features.length > 0) { + params.delete("where"); + params.delete("resultOffset"); + params.delete("resultRecordCount"); + params.set("orderByFields", objectIdFieldName); + const lastFeature = featureCollection.features[featureCollection.features.length - 1]; + params.set("where", `${objectIdFieldName}>${lastFeature.id}`); + } + const response = await fetch(`${baseUrl}/query?${params.toString()}`, { + mode: "cors", + signal: abortController.signal, + }); + const str = await response.text(); + bytesReceived += byteLength(str); + if (bytesLimit && bytesReceived >= bytesLimit) { + const e = new Error(`Exceeded bytesLimit. ${bytesReceived} >= ${bytesLimit}`); + return onError(e); + } + const fc = JSON.parse(str); + if (fc.error) { + return onError(new Error(fc.error.message)); + } + else { + featureCollection.features.push(...fc.features); + if (fc.exceededTransferLimit) { + if (!objectIdFieldName) { + params.set("returnIdsOnly", "true"); + try { + const r = await fetch(`${baseUrl}/query?${params.toString()}`, { + mode: "cors", + signal: abortController.signal, + }); + const featureIds = featureCollection.features.map((f) => f.id); + const objectIdParameters = await r.json(); + expectedFeatureCount = objectIdParameters.objectIds.length; + objectIdFieldName = objectIdParameters.objectIdFieldName; + } + catch (e) { + return onError(e); + } + } + if (onPageReceived) { + onPageReceived(bytesReceived, featureCollection.features.length, expectedFeatureCount); + } + await fetchData(baseUrl, params, featureCollection, onError, abortController, onPageReceived, disablePagination, pageSize, bytesLimit, bytesReceived, objectIdFieldName, expectedFeatureCount); + } + } + return bytesReceived; + } + function byteLength(str) { + var s = str.length; + for (var i = str.length - 1; i >= 0; i--) { + var code = str.charCodeAt(i); + if (code > 0x7f && code <= 0x7ff) + s++; + else if (code > 0x7ff && code <= 0xffff) + s += 2; + if (code >= 0xdc00 && code <= 0xdfff) + i--; + } + return s; + } - function replaceSource(sourceId, map, sourceData) { - var _a; - const existingSource = map.getSource(sourceId); - if (!existingSource) { - throw new Error("Source does not exist"); - } - if (existingSource.type !== sourceData.type) { - throw new Error("Source type mismatch"); - } - const allLayers = map.getStyle().layers || []; - const relatedLayers = allLayers.filter((l) => { - return "source" in l && l.source === sourceId; - }); - relatedLayers.reverse(); - const idx = allLayers.indexOf(relatedLayers[0]); - let before = ((_a = allLayers[idx + 1]) === null || _a === void 0 ? void 0 : _a.id) || undefined; - for (const layer of relatedLayers) { - map.removeLayer(layer.id); - } - map.removeSource(sourceId); - map.addSource(sourceId, sourceData); - for (const layer of relatedLayers) { - map.addLayer(layer, before); - before = layer.id; - } - } - function metersToDegrees(x, y) { - var lon = (x * 180) / 20037508.34; - var lat = (Math.atan(Math.exp((y * Math.PI) / 20037508.34)) * 360) / Math.PI - 90; - return [lon, lat]; - } - async function extentToLatLngBounds(extent) { - if (extent) { - const wkid = normalizeSpatialReference(extent.spatialReference); - if (wkid === 4326) { - return [extent.xmin, extent.ymin, extent.xmax, extent.ymax]; - } - else if (wkid === 3857 || wkid === 102100) { - return [ - ...metersToDegrees(extent.xmin, extent.ymin), - ...metersToDegrees(extent.xmax, extent.ymax), - ]; - } - else { - try { - const projected = await projectExtent(extent); - console.log("projected", projected, extent); - return [projected.xmin, projected.ymin, projected.xmax, projected.ymax]; - } - catch (e) { - console.error(e); - return; - } - } - } - } - function normalizeSpatialReference(sr) { - const wkid = "latestWkid" in sr ? sr.latestWkid : "wkid" in sr ? sr.wkid : -1; - if (typeof wkid === "string") { - if (/WGS\s*84/.test(wkid)) { - return 4326; - } - else { - return -1; - } - } - else { - return wkid || -1; - } - } - async function projectExtent(extent) { - const endpoint = "https://tasks.arcgisonline.com/arcgis/rest/services/Geometry/GeometryServer/project"; - const params = new URLSearchParams({ - geometries: JSON.stringify({ - geometryType: "esriGeometryEnvelope", - geometries: [extent], - }), - inSR: `${extent.spatialReference.wkid}`, - outSR: "4326", - f: "json", - }); - const response = await fetch(`${endpoint}?${params.toString()}`); - const data = await response.json(); - const projected = data.geometries[0]; - if (projected) { - return projected; - } - else { - throw new Error("Failed to reproject"); - } - } - function contentOrFalse(str) { - if (str && str.length > 0) { - return str; - } - else { - return false; - } - } - function pickDescription(info, layer) { - var _a, _b; - return (contentOrFalse(layer === null || layer === void 0 ? void 0 : layer.description) || - contentOrFalse(info.description) || - contentOrFalse((_a = info.documentInfo) === null || _a === void 0 ? void 0 : _a.Subject) || - contentOrFalse((_b = info.documentInfo) === null || _b === void 0 ? void 0 : _b.Comments)); - } - function generateMetadataForLayer(url, mapServerInfo, layer) { - var _a, _b, _c, _d; - const attribution = contentOrFalse(layer.copyrightText) || - contentOrFalse(mapServerInfo.copyrightText) || - contentOrFalse((_a = mapServerInfo.documentInfo) === null || _a === void 0 ? void 0 : _a.Author); - const description = pickDescription(mapServerInfo, layer); - let keywords = ((_b = mapServerInfo.documentInfo) === null || _b === void 0 ? void 0 : _b.Keywords) && - ((_c = mapServerInfo.documentInfo) === null || _c === void 0 ? void 0 : _c.Keywords.length) - ? (_d = mapServerInfo.documentInfo) === null || _d === void 0 ? void 0 : _d.Keywords.split(",") - : []; - return { - type: "doc", - content: [ - { - type: "heading", - attrs: { level: 1 }, - content: [{ type: "text", text: layer.name }], - }, - ...(description - ? [ - { - type: "paragraph", - content: [ - { - type: "text", - text: description, - }, - ], - }, - ] - : []), - ...(attribution - ? [ - { type: "paragraph" }, - { - type: "heading", - attrs: { level: 3 }, - content: [{ type: "text", text: "Attribution" }], - }, - { - type: "paragraph", - content: [ - { - type: "text", - text: attribution, - }, - ], - }, - ] - : []), - ...(keywords && keywords.length - ? [ - { type: "paragraph" }, - { - type: "heading", - attrs: { level: 3 }, - content: [ - { - type: "text", - text: "Keywords", - }, - ], - }, - { - type: "bullet_list", - marks: [], - attrs: {}, - content: keywords.map((word) => ({ - type: "list_item", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: word }], - }, - ], - })), - }, - ] - : []), - { type: "paragraph" }, - { - type: "paragraph", - content: [ - { - type: "text", - marks: [ - { - type: "link", - attrs: { - href: url, - title: "ArcGIS Server", - }, - }, - ], - text: url, - }, - ], - }, - ], - }; - } + class ArcGISRESTServiceRequestManager { + constructor(options) { + this.inFlightRequests = {}; + caches + .open((options === null || options === void 0 ? void 0 : options.cacheKey) || "seasketch-arcgis-rest-services") + .then((cache) => { + this.cache = cache; + }); + } + async getMapServiceMetadata(url, options) { + if (!/rest\/services/.test(url)) { + throw new Error("Invalid ArcGIS REST Service URL"); + } + if (!/MapServer/.test(url)) { + throw new Error("Invalid MapServer URL"); + } + url = url.replace(/\/$/, ""); + url = url.replace(/\?.*$/, ""); + const params = new URLSearchParams(); + params.set("f", "json"); + if (options === null || options === void 0 ? void 0 : options.credentials) { + const token = await this.getToken(url.replace(/rest\/services\/.*/, "/rest/services/"), options.credentials); + params.set("token", token); + } + const requestUrl = `${url}?${params.toString()}`; + const serviceMetadata = await this.fetch(requestUrl, options === null || options === void 0 ? void 0 : options.signal); + const layers = await this.fetch(`${url}/layers?${params.toString()}`); + if (layers.error) { + throw new Error(layers.error.message); + } + return { serviceMetadata, layers }; + } + async getCatalogItems(url, options) { + if (!/rest\/services/.test(url)) { + throw new Error("Invalid ArcGIS REST Service URL"); + } + url = url.replace(/\/$/, ""); + url = url.replace(/\?.*$/, ""); + const params = new URLSearchParams(); + params.set("f", "json"); + if (options === null || options === void 0 ? void 0 : options.credentials) { + const token = await this.getToken(url.replace(/rest\/services\/.*/, "/rest/services/"), options.credentials); + params.set("token", token); + } + const requestUrl = `${url}?${params.toString()}`; + const response = await this.fetch(requestUrl, options === null || options === void 0 ? void 0 : options.signal); + return response; + } + async getToken(url, credentials) { + throw new Error("Not implemented"); + } + async fetch(url, signal) { + if (url in this.inFlightRequests) { + return this.inFlightRequests[url].then((json) => json); + } + const cache = await this.cache; + if (!cache) { + throw new Error("Cache not initialized"); + } + this.inFlightRequests[url] = fetchWithTTL(url, 60 * 300, cache, { signal }); + return new Promise((resolve, reject) => { + this.inFlightRequests[url] + .then((json) => { + if (json["error"]) { + reject(new Error(json["error"].message)); + } + else { + resolve(json); + } + }) + .catch(reject) + .finally(() => { + delete this.inFlightRequests[url]; + }); + }); + } + async getLegendMetadata(url, credentials) { + if (!/rest\/services/.test(url)) { + throw new Error("Invalid ArcGIS REST Service URL"); + } + if (!/MapServer/.test(url)) { + throw new Error("Invalid MapServer URL"); + } + url = url.replace(/\/$/, ""); + url = url.replace(/\?.*$/, ""); + const params = new URLSearchParams(); + params.set("f", "json"); + if (credentials) { + const token = await this.getToken(url.replace(/rest\/services\/.*/, "/rest/services/"), credentials); + params.set("token", token); + } + const requestUrl = `${url}/legend?${params.toString()}`; + const response = await this.fetch(requestUrl); + return response; + } + } + function cachedResponseIsExpired(response) { + const cacheControlHeader = response.headers.get("Cache-Control"); + if (cacheControlHeader) { + const expires = /expires=(.*)/i.exec(cacheControlHeader); + if (expires) { + const expiration = new Date(expires[1]); + if (new Date().getTime() > expiration.getTime()) { + return true; + } + else { + return false; + } + } + } + return false; + } + async function fetchWithTTL(url, ttl, cache, options + ) { + var _a, _b, _c; + if (!((_a = options === null || options === void 0 ? void 0 : options.signal) === null || _a === void 0 ? void 0 : _a.aborted)) { + const request = new Request(url, options); + if ((_b = options === null || options === void 0 ? void 0 : options.signal) === null || _b === void 0 ? void 0 : _b.aborted) { + Promise.reject("aborted"); + } + let cachedResponse = await cache.match(request); + if (cachedResponse && cachedResponseIsExpired(cachedResponse)) { + cache.delete(request); + cachedResponse = undefined; + } + if (cachedResponse) { + return cachedResponse.json(); + } + else { + const response = await fetch(url, options); + if (!((_c = options === null || options === void 0 ? void 0 : options.signal) === null || _c === void 0 ? void 0 : _c.aborted)) { + const headers = new Headers(response.headers); + headers.set("Cache-Control", `Expires=${new Date(new Date().getTime() + 1000 * ttl).toUTCString()}`); + const copy = response.clone(); + const clone = new Response(copy.body, { + headers, + status: response.status, + statusText: response.statusText, + }); + cache.put(url, clone); + } + return await response.json(); + } + } + } - class ArcGISTiledMapService { - constructor(requestManager, options) { - options.url = options.url.replace(/\/$/, ""); - if (!/rest\/services/.test(options.url) || !/MapServer/.test(options.url)) { - throw new Error("Invalid ArcGIS REST Service URL"); - } - this.requestManager = requestManager; - this.sourceId = options.sourceId || v4(); - this.options = options; - } - getMetadata() { - if (this.serviceMetadata && this.layerMetadata) { - return Promise.resolve({ - serviceMetadata: this.serviceMetadata, - layers: this.layerMetadata, - }); - } - else { - return this.requestManager - .getMapServiceMetadata(this.options.url, this.options.credentials) - .then(({ serviceMetadata, layers }) => { - this.serviceMetadata = serviceMetadata; - this.layerMetadata = layers; - return { serviceMetadata, layers }; - }); - } - } - async getComputedMetadata() { - await this.getMetadata(); - const { bounds, minzoom, maxzoom, tileSize, attribution } = await this.getComputedProperties(); - return { - bounds: bounds || undefined, - minzoom, - maxzoom, - attribution, - metadata: generateMetadataForLayer(this.options.url, this.serviceMetadata, this.layerMetadata.layers[0]), - }; - } - get loading() { - var _a, _b; - return Boolean(((_a = this.map) === null || _a === void 0 ? void 0 : _a.getSource(this.sourceId)) && - ((_b = this.map) === null || _b === void 0 ? void 0 : _b.isSourceLoaded(this.sourceId)) === false); - } - async getLegend() { - const data = await this.requestManager.getLegendMetadata(this.options.url); - return data.layers[0].legend.map((l) => ({ - id: l.url, - label: l.label, - imageUrl: `data:${l.contentType};base64,${l.imageData}`, - })); - } - async getComputedProperties() { - var _a, _b, _c; - const { serviceMetadata, layers } = await this.getMetadata(); - const levels = ((_a = serviceMetadata.tileInfo) === null || _a === void 0 ? void 0 : _a.lods.map((l) => l.level)) || []; - const attribution = contentOrFalse(layers.layers[0].copyrightText) || - contentOrFalse(serviceMetadata.copyrightText) || - contentOrFalse((_b = serviceMetadata.documentInfo) === null || _b === void 0 ? void 0 : _b.Author) || - undefined; - const minzoom = Math.min(...levels); - const maxzoom = Math.max(...levels); - if (!((_c = serviceMetadata.tileInfo) === null || _c === void 0 ? void 0 : _c.rows)) { - throw new Error("Invalid tile info"); - } - return { - minzoom, - maxzoom, - bounds: await extentToLatLngBounds(serviceMetadata.fullExtent), - tileSize: serviceMetadata.tileInfo.rows, - attribution, - }; - } - async addToMap(map) { - this.map = map; - const { minzoom, maxzoom, bounds, tileSize, attribution } = await this.getComputedProperties(); - const sourceData = { - type: "raster", - tiles: [`${this.options.url}/tile/{z}/{y}/{x}`], - tileSize: this.options.supportHighDpiDisplays - ? tileSize / window.devicePixelRatio - : tileSize, - minzoom, - maxzoom, - attribution, - ...(bounds ? { bounds } : {}), - }; - console.log("add to map", sourceData); - if (this.map.getSource(this.sourceId)) { - replaceSource(this.sourceId, this.map, sourceData); - } - else { - this.map.addSource(this.sourceId, sourceData); - } - return this.sourceId; - } - async getLayers() { - return [ - { - type: "raster", - source: this.sourceId, - id: v4(), - paint: { - "raster-fade-duration": 300, - }, - }, - ]; - } - removeFromMap(map, removeLayers) { - if (map.getSource(this.sourceId)) { - const layers = map.getStyle().layers || []; - for (const layer of layers) { - if ("source" in layer && layer.source === this.sourceId) { - map.removeLayer(layer.id); - } - } - map.removeSource(this.sourceId); - this.map = undefined; - } - } - async getSupportsDynamicRendering() { - return { - layerOrder: false, - layerOpacity: false, - }; - } - destroy() { - if (this.map) { - this.removeFromMap(this.map); - } - } - } + class ArcGISTiledMapService { + constructor(requestManager, options) { + options.url = options.url.replace(/\/$/, ""); + if (!/rest\/services/.test(options.url) || !/MapServer/.test(options.url)) { + throw new Error("Invalid ArcGIS REST Service URL"); + } + this.requestManager = requestManager; + this.sourceId = options.sourceId || v4(); + this.options = options; + } + getMetadata() { + if (this.serviceMetadata && this.layerMetadata) { + return Promise.resolve({ + serviceMetadata: this.serviceMetadata, + layers: this.layerMetadata, + }); + } + else { + return this.requestManager + .getMapServiceMetadata(this.options.url, { + credentials: this.options.credentials, + }) + .then(({ serviceMetadata, layers }) => { + this.serviceMetadata = serviceMetadata; + this.layerMetadata = layers; + return { serviceMetadata, layers }; + }); + } + } + async getComputedMetadata() { + var _a, _b; + await this.getMetadata(); + const { bounds, minzoom, maxzoom, tileSize, attribution } = await this.getComputedProperties(); + const legendData = await this.requestManager.getLegendMetadata(this.options.url); + const results = /\/([^/]+)\/MapServer/.exec(this.options.url); + let label = results && results.length >= 1 ? results[1] : false; + if (!label) { + if ((_b = (_a = this.layerMetadata) === null || _a === void 0 ? void 0 : _a.layers) === null || _b === void 0 ? void 0 : _b[0]) { + label = this.layerMetadata.layers[0].name; + } + } + return { + bounds: bounds || undefined, + minzoom, + maxzoom, + attribution, + tableOfContentsItems: [ + { + type: "data", + id: this.sourceId, + label: label || "Layer", + defaultVisibility: true, + metadata: generateMetadataForLayer(this.options.url, this.serviceMetadata, this.layerMetadata.layers[0]), + legend: makeLegend(legendData, legendData.layers[0].layerId), + }, + ], + supportsDynamicRendering: { + layerOrder: false, + layerOpacity: false, + layerVisibility: false, + }, + }; + } + get loading() { + var _a, _b; + return Boolean(((_a = this.map) === null || _a === void 0 ? void 0 : _a.getSource(this.sourceId)) && + ((_b = this.map) === null || _b === void 0 ? void 0 : _b.isSourceLoaded(this.sourceId)) === false); + } + async getComputedProperties() { + var _a, _b, _c; + const { serviceMetadata, layers } = await this.getMetadata(); + const levels = ((_a = serviceMetadata.tileInfo) === null || _a === void 0 ? void 0 : _a.lods.map((l) => l.level)) || []; + const attribution = contentOrFalse(layers.layers[0].copyrightText) || + contentOrFalse(serviceMetadata.copyrightText) || + contentOrFalse((_b = serviceMetadata.documentInfo) === null || _b === void 0 ? void 0 : _b.Author) || + undefined; + const minzoom = Math.min(...levels); + const maxzoom = Math.max(...levels); + if (!((_c = serviceMetadata.tileInfo) === null || _c === void 0 ? void 0 : _c.rows)) { + throw new Error("Invalid tile info"); + } + return { + minzoom, + maxzoom, + bounds: await extentToLatLngBounds(serviceMetadata.fullExtent), + tileSize: serviceMetadata.tileInfo.rows, + attribution, + }; + } + async addToMap(map) { + this.map = map; + const { minzoom, maxzoom, bounds, tileSize, attribution } = await this.getComputedProperties(); + const sourceData = { + type: "raster", + tiles: [`${this.options.url}/tile/{z}/{y}/{x}`], + tileSize: this.options.supportHighDpiDisplays + ? tileSize / window.devicePixelRatio + : tileSize, + minzoom, + maxzoom, + attribution, + ...(bounds ? { bounds } : {}), + }; + if (this.map.getSource(this.sourceId)) { + replaceSource(this.sourceId, this.map, sourceData); + } + else { + this.map.addSource(this.sourceId, sourceData); + } + return this.sourceId; + } + async getGLStyleLayers() { + return [ + { + type: "raster", + source: this.sourceId, + id: v4(), + paint: { + "raster-fade-duration": 300, + }, + }, + ]; + } + removeFromMap(map) { + if (map.getSource(this.sourceId)) { + const layers = map.getStyle().layers || []; + for (const layer of layers) { + if ("source" in layer && layer.source === this.sourceId) { + map.removeLayer(layer.id); + } + } + map.removeSource(this.sourceId); + this.map = undefined; + } + } + destroy() { + if (this.map) { + this.removeFromMap(this.map); + } + } + updateLayers(layers) { } + } - function generateId() { - return v4(); - } - function createCanvas(w, h) { - const canvas = document.createElement("canvas"); - canvas.setAttribute("width", w.toString()); - canvas.setAttribute("height", h.toString()); - return canvas; - } - const rgba = (color) => { - color = color || [0, 0, 0, 0]; - return `rgba(${color[0]},${color[1]},${color[2]},${color[3] / 255})`; - }; - const colorAndOpacity = (color) => { - color = color || [0, 0, 0, 0]; - return { - color: `rgb(${color[0]},${color[1]},${color[2]})`, - opacity: color[3] / 255, - }; - }; - const ptToPx = (pt) => Math.round(pt * 1.33); - const ANCHORS = { - esriServerPointLabelPlacementAboveCenter: "bottom", - esriServerPointLabelPlacementAboveLeft: "bottom-right", - esriServerPointLabelPlacementAboveRight: "bottom-left", - esriServerPointLabelPlacementBelowCenter: "top", - esriServerPointLabelPlacementBelowLeft: "top-right", - esriServerPointLabelPlacementBelowRight: "top-left", - esriServerPointLabelPlacementCenterCenter: "center", - esriServerPointLabelPlacementCenterLeft: "right", - esriServerPointLabelPlacementCenterRight: "left", - esriServerLinePlacementAboveAlong: "bottom", - esriServerLinePlacementAboveBefore: "bottom-left", - esriServerLinePlacementAboveStart: "bottom-left", - esriServerLinePlacementAboveEnd: "bottom-right", - esriServerLinePlacementBelowAfter: "top-right", - esriServerLinePlacementBelowAlong: "top", - esriServerLinePlacementBelowBefore: "top-left", - esriServerLinePlacementBelowStart: "top-left", - esriServerLinePlacementBelowEnd: "top-right", - esriServerLinePlacementCenterAfter: "right", - esriServerLinePlacementCenterAlong: "center", - esriServerLinePlacementCenterBefore: "center-left", - esriServerLinePlacementCenterStart: "center-left", - esriServerLinePlacementCenterEnd: "center-right", - esriServerPolygonPlacementAlwaysHorizontal: "center", - }; - const toTextAnchor = (labelPlacement) => ANCHORS[labelPlacement] || "center"; + function generateId() { + return v4(); + } + function createCanvas(w, h) { + const canvas = document.createElement("canvas"); + canvas.setAttribute("width", w.toString()); + canvas.setAttribute("height", h.toString()); + return canvas; + } + const rgba = (color) => { + color = color || [0, 0, 0, 0]; + return `rgba(${color[0]},${color[1]},${color[2]},${color[3] / 255})`; + }; + const colorAndOpacity = (color) => { + color = color || [0, 0, 0, 0]; + return { + color: `rgb(${color[0]},${color[1]},${color[2]})`, + opacity: color[3] / 255, + }; + }; + const ptToPx = (pt) => Math.round(pt * 1.33); + const ANCHORS = { + esriServerPointLabelPlacementAboveCenter: "bottom", + esriServerPointLabelPlacementAboveLeft: "bottom-right", + esriServerPointLabelPlacementAboveRight: "bottom-left", + esriServerPointLabelPlacementBelowCenter: "top", + esriServerPointLabelPlacementBelowLeft: "top-right", + esriServerPointLabelPlacementBelowRight: "top-left", + esriServerPointLabelPlacementCenterCenter: "center", + esriServerPointLabelPlacementCenterLeft: "right", + esriServerPointLabelPlacementCenterRight: "left", + esriServerLinePlacementAboveAlong: "bottom", + esriServerLinePlacementAboveBefore: "bottom-left", + esriServerLinePlacementAboveStart: "bottom-left", + esriServerLinePlacementAboveEnd: "bottom-right", + esriServerLinePlacementBelowAfter: "top-right", + esriServerLinePlacementBelowAlong: "top", + esriServerLinePlacementBelowBefore: "top-left", + esriServerLinePlacementBelowStart: "top-left", + esriServerLinePlacementBelowEnd: "top-right", + esriServerLinePlacementCenterAfter: "right", + esriServerLinePlacementCenterAlong: "center", + esriServerLinePlacementCenterBefore: "center-left", + esriServerLinePlacementCenterStart: "center-left", + esriServerLinePlacementCenterEnd: "center-right", + esriServerPolygonPlacementAlwaysHorizontal: "center", + }; + const toTextAnchor = (labelPlacement) => ANCHORS[labelPlacement] || "center"; - const patterns = { - esriSLSDash: (strokeWidth) => [2, 0.5], - esriSLSDashDot: (strokeWidth) => [3, 1, 1, 1], - esriSLSDashDotDot: (strokeWidth) => [3, 1, 1, 1, 1, 1], - esriSLSNull: () => [0, 10], - esriSLSDot: (strokeWidth) => [1, 1], - }; + const patterns = { + esriSLSDash: (strokeWidth) => [2, 0.5], + esriSLSDashDot: (strokeWidth) => [3, 1, 1, 1], + esriSLSDashDotDot: (strokeWidth) => [3, 1, 1, 1, 1, 1], + esriSLSNull: () => [0, 10], + esriSLSDot: (strokeWidth) => [1, 1], + }; - var esriSLS = (symbol, sourceId) => { - const { color, opacity } = colorAndOpacity(symbol.color); - let strokeWidth = ptToPx(symbol.width || 1); - if (strokeWidth === -1) { - strokeWidth = 1; - } - const style = symbol.style || "esriSLSSolid"; - const layer = { - id: generateId(), - type: "line", - paint: { - "line-color": color, - "line-opacity": opacity, - "line-width": strokeWidth, - }, - layout: {}, - source: sourceId, - }; - if (style !== "esriSLSSolid") { - layer.paint["line-dasharray"] = patterns[style](strokeWidth); - } - return [layer]; - }; + var esriSLS = (symbol, sourceId) => { + const { color, opacity } = colorAndOpacity(symbol.color); + let strokeWidth = ptToPx(symbol.width || 1); + if (strokeWidth === -1) { + strokeWidth = 1; + } + const style = symbol.style || "esriSLSSolid"; + const layer = { + id: generateId(), + type: "line", + paint: { + "line-color": color, + "line-opacity": opacity, + "line-width": strokeWidth, + }, + layout: {}, + source: sourceId, + }; + if (style !== "esriSLSSolid") { + layer.paint["line-dasharray"] = patterns[style](strokeWidth); + } + return [layer]; + }; - var esriSFS = (symbol, sourceId, imageList) => { - const layers = []; - let useFillOutlineColor = symbol.outline && - ptToPx(symbol.outline.width || 1) === 1 && - symbol.outline.style === "esriSLSSolid"; - switch (symbol.style) { - case "esriSFSSolid": - if (symbol.color && symbol.color[3] === 0) { - useFillOutlineColor = false; - } - else { - layers.push({ - id: generateId(), - type: "fill", - source: sourceId, - paint: { - "fill-color": rgba(symbol.color), - ...(useFillOutlineColor - ? { "fill-outline-color": rgba(symbol.outline.color) } - : {}), - }, - }); - } - break; - case "esriSFSNull": - break; - case "esriSFSBackwardDiagonal": - case "esriSFSCross": - case "esriSFSDiagonalCross": - case "esriSFSForwardDiagonal": - case "esriSFSHorizontal": - case "esriSFSVertical": - const imageId = imageList.addEsriSFS(symbol); - layers.push({ - id: generateId(), - source: sourceId, - type: "fill", - paint: { - "fill-pattern": imageId, - ...(useFillOutlineColor - ? { "fill-outline-color": rgba(symbol.outline.color) } - : {}), - }, - }); - break; - default: - throw new Error(`Unknown fill style ${symbol.style}`); - } - if (symbol.outline && !useFillOutlineColor) { - let outline = esriSLS(symbol.outline, sourceId); - layers.push(...outline); - } - return layers; - }; + var esriSFS = (symbol, sourceId, imageList) => { + const layers = []; + let useFillOutlineColor = symbol.outline && + ptToPx(symbol.outline.width || 1) === 1 && + symbol.outline.style === "esriSLSSolid"; + switch (symbol.style) { + case "esriSFSSolid": + if (symbol.color && symbol.color[3] === 0) { + useFillOutlineColor = false; + } + else { + layers.push({ + id: generateId(), + type: "fill", + source: sourceId, + paint: { + "fill-color": rgba(symbol.color), + ...(useFillOutlineColor + ? { "fill-outline-color": rgba(symbol.outline.color) } + : {}), + }, + }); + } + break; + case "esriSFSNull": + break; + case "esriSFSBackwardDiagonal": + case "esriSFSCross": + case "esriSFSDiagonalCross": + case "esriSFSForwardDiagonal": + case "esriSFSHorizontal": + case "esriSFSVertical": + const imageId = imageList.addEsriSFS(symbol); + layers.push({ + id: generateId(), + source: sourceId, + type: "fill", + paint: { + "fill-pattern": imageId, + ...(useFillOutlineColor + ? { "fill-outline-color": rgba(symbol.outline.color) } + : {}), + }, + }); + break; + default: + throw new Error(`Unknown fill style ${symbol.style}`); + } + if (symbol.outline && !useFillOutlineColor) { + let outline = esriSLS(symbol.outline, sourceId); + layers.push(...outline); + } + return layers; + }; - var esriPMS = (symbol, sourceId, imageList, serviceBaseUrl, sublayer, legendIndex) => { - const imageId = imageList.addEsriPMS(symbol, serviceBaseUrl, sublayer, legendIndex); - return [ - { - id: generateId(), - source: sourceId, - type: "symbol", - paint: {}, - layout: { - "icon-allow-overlap": true, - "icon-rotate": symbol.angle || 0, - "icon-offset": [symbol.xoffset || 0, symbol.yoffset || 0], - "icon-image": imageId, - }, - }, - ]; - }; + var esriPMS = (symbol, sourceId, imageList, serviceBaseUrl, sublayer, legendIndex) => { + const imageId = imageList.addEsriPMS(symbol, serviceBaseUrl, sublayer, legendIndex); + return [ + { + id: generateId(), + source: sourceId, + type: "symbol", + paint: {}, + layout: { + "icon-allow-overlap": true, + "icon-rotate": symbol.angle || 0, + "icon-offset": [symbol.xoffset || 0, symbol.yoffset || 0], + "icon-image": imageId, + }, + }, + ]; + }; - var esriSMS = (symbol, sourceId, imageList) => { - const imageId = imageList.addEsriSMS(symbol); - return [ - { - id: generateId(), - type: "symbol", - source: sourceId, - paint: {}, - layout: { - "icon-allow-overlap": true, - "icon-rotate": symbol.angle, - "icon-offset": [symbol.xoffset || 0, symbol.yoffset || 0], - "icon-image": imageId, - "icon-size": 1, - }, - }, - ]; - }; + var esriSMS = (symbol, sourceId, imageList) => { + const imageId = imageList.addEsriSMS(symbol); + return [ + { + id: generateId(), + type: "symbol", + source: sourceId, + paint: {}, + layout: { + "icon-allow-overlap": true, + "icon-rotate": symbol.angle, + "icon-offset": [symbol.xoffset || 0, symbol.yoffset || 0], + "icon-image": imageId, + "icon-size": 1, + }, + }, + ]; + }; - var esriPFS = (symbol, sourceId, imageList) => { - const imageId = imageList.addEsriPFS(symbol); - const layers = [ - { - id: generateId(), - source: sourceId, - type: "fill", - paint: { - "fill-pattern": imageId, - }, - layout: {}, - }, - ]; - if ("outline" in symbol) { - let outline = esriSLS(symbol.outline, sourceId); - layers.push(...outline); - } - return layers; - }; + var esriPFS = (symbol, sourceId, imageList) => { + const imageId = imageList.addEsriPFS(symbol); + const layers = [ + { + id: generateId(), + source: sourceId, + type: "fill", + paint: { + "fill-pattern": imageId, + }, + layout: {}, + }, + ]; + if ("outline" in symbol) { + let outline = esriSLS(symbol.outline, sourceId); + layers.push(...outline); + } + return layers; + }; - function symbolToLayers(symbol, sourceId, imageList, serviceBaseUrl, sublayer, legendIndex) { - var layers; - switch (symbol.type) { - case "esriSFS": - layers = esriSFS(symbol, sourceId, imageList); - break; - case "esriPFS": - layers = esriPFS(symbol, sourceId, imageList); - break; - case "esriSLS": - layers = esriSLS(symbol, sourceId); - break; - case "esriPMS": - layers = esriPMS(symbol, sourceId, imageList, serviceBaseUrl, sublayer, legendIndex); - break; - case "esriSMS": - layers = esriSMS(symbol, sourceId, imageList); - break; - default: - throw new Error(`Unknown symbol type ${symbol.type}`); - } - return layers; - } + function symbolToLayers(symbol, sourceId, imageList, serviceBaseUrl, sublayer, legendIndex) { + var layers; + switch (symbol.type) { + case "esriSFS": + layers = esriSFS(symbol, sourceId, imageList); + break; + case "esriPFS": + layers = esriPFS(symbol, sourceId, imageList); + break; + case "esriSLS": + layers = esriSLS(symbol, sourceId); + break; + case "esriPMS": + layers = esriPMS(symbol, sourceId, imageList, serviceBaseUrl, sublayer, legendIndex); + break; + case "esriSMS": + layers = esriSMS(symbol, sourceId, imageList); + break; + default: + throw new Error(`Unknown symbol type ${symbol.type}`); + } + return layers; + } - function drawSMS (symbol, pixelRatio) { - var _a, _b; - const size = ptToPx(symbol.size || 13); - const scale = 2 ** (pixelRatio - 1); - const width = (size + 1 * 2 + (((_a = symbol.outline) === null || _a === void 0 ? void 0 : _a.width) || 0) * 2) * scale; - const height = width; - let canvas = createCanvas(width, height); - var ctx = canvas.getContext("2d"); - ctx.lineWidth = - ptToPx(!!symbol.outline ? symbol.outline.width || 1 : 1) * scale; - ctx.strokeStyle = !!symbol.outline - ? rgba((_b = symbol.outline) === null || _b === void 0 ? void 0 : _b.color) - : rgba(symbol.color); - ctx.fillStyle = rgba(symbol.color); - switch (symbol.style) { - case "esriSMSCircle": - ctx.beginPath(); - var x = width / 2; - var y = height / 2; - var diameter = size * scale; - var radius = Math.round((diameter + ctx.lineWidth) / 2); - ctx.arc(x, y, radius, 0, Math.PI * 2, true); - ctx.fill(); - ctx.stroke(); - break; - case "esriSMSCross": - var w = size * scale; - ctx.lineWidth = Math.round(w / 4); - ctx.strokeStyle = rgba(symbol.color); - ctx.moveTo(width / 2, (height - w) / 2); - ctx.lineTo(width / 2, height - (height - w) / 2); - ctx.moveTo((width - w) / 2, height / 2); - ctx.lineTo(width - (width - w) / 2, height / 2); - ctx.stroke(); - ctx.fill(); - break; - case "esriSMSX": - var w = size * scale; - ctx.translate(width / 2, height / 2); - ctx.rotate((45 * Math.PI) / 180); - ctx.translate(-width / 2, -height / 2); - ctx.moveTo(width / 2, (height - w) / 2); - ctx.lineTo(width / 2, height - (height - w) / 2); - ctx.moveTo((width - w) / 2, height / 2); - ctx.lineTo(width - (width - w) / 2, height / 2); - ctx.stroke(); - ctx.fill(); - ctx.setTransform(1, 0, 0, 1, 0, 0); - break; - case "esriSMSDiamond": - var w = size * scale; - var h = w; - var x = width / 2 - w / 2; - var y = height / 2 - h / 2; - ctx.translate(x + w / 2, y + h / 2); - ctx.rotate((45 * Math.PI) / 180); - ctx.fillRect(-w / 2, -h / 2, w, h); - ctx.strokeRect(-w / 2, -h / 2, w, h); - break; - case "esriSMSSquare": - var w = size * scale; - var h = w; - var x = width / 2 - w / 2; - var y = height / 2 - h / 2; - ctx.fillRect(x, y, w, h); - ctx.strokeRect(x, y, w, h); - break; - case "esriSMSTriangle": - ctx.beginPath(); - var w = size * scale; - var h = w; - var midpoint = width / 2; - var x1 = midpoint; - var y1 = (height - width) / 2; - var x2 = width - (width - width) / 2; - var y2 = height - (height - width) / 2; - var x3 = (width - width) / 2; - var y3 = height - (height - width) / 2; - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.lineTo(x3, y3); - ctx.lineTo(x1, y1); - ctx.fill(); - ctx.stroke(); - break; - default: - throw new Error(`Unknown symbol type ${symbol.style}`); - } - return { width, height, data: canvas.toDataURL() }; - } + function drawSMS (symbol, pixelRatio) { + var _a, _b; + const size = ptToPx(symbol.size || 13); + const scale = 2 ** (pixelRatio - 1); + const width = (size + 1 * 2 + (((_a = symbol.outline) === null || _a === void 0 ? void 0 : _a.width) || 0) * 2) * scale; + const height = width; + let canvas = createCanvas(width, height); + var ctx = canvas.getContext("2d"); + ctx.lineWidth = + ptToPx(!!symbol.outline ? symbol.outline.width || 1 : 1) * scale; + ctx.strokeStyle = !!symbol.outline + ? rgba((_b = symbol.outline) === null || _b === void 0 ? void 0 : _b.color) + : rgba(symbol.color); + ctx.fillStyle = rgba(symbol.color); + switch (symbol.style) { + case "esriSMSCircle": + ctx.beginPath(); + var x = width / 2; + var y = height / 2; + var diameter = size * scale; + var radius = Math.round((diameter + ctx.lineWidth) / 2); + ctx.arc(x, y, radius, 0, Math.PI * 2, true); + ctx.fill(); + ctx.stroke(); + break; + case "esriSMSCross": + var w = size * scale; + ctx.lineWidth = Math.round(w / 4); + ctx.strokeStyle = rgba(symbol.color); + ctx.moveTo(width / 2, (height - w) / 2); + ctx.lineTo(width / 2, height - (height - w) / 2); + ctx.moveTo((width - w) / 2, height / 2); + ctx.lineTo(width - (width - w) / 2, height / 2); + ctx.stroke(); + ctx.fill(); + break; + case "esriSMSX": + var w = size * scale; + ctx.translate(width / 2, height / 2); + ctx.rotate((45 * Math.PI) / 180); + ctx.translate(-width / 2, -height / 2); + ctx.moveTo(width / 2, (height - w) / 2); + ctx.lineTo(width / 2, height - (height - w) / 2); + ctx.moveTo((width - w) / 2, height / 2); + ctx.lineTo(width - (width - w) / 2, height / 2); + ctx.stroke(); + ctx.fill(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + break; + case "esriSMSDiamond": + var w = size * scale; + var h = w; + var x = width / 2 - w / 2; + var y = height / 2 - h / 2; + ctx.translate(x + w / 2, y + h / 2); + ctx.rotate((45 * Math.PI) / 180); + ctx.fillRect(-w / 2, -h / 2, w, h); + ctx.strokeRect(-w / 2, -h / 2, w, h); + break; + case "esriSMSSquare": + var w = size * scale; + var h = w; + var x = width / 2 - w / 2; + var y = height / 2 - h / 2; + ctx.fillRect(x, y, w, h); + ctx.strokeRect(x, y, w, h); + break; + case "esriSMSTriangle": + ctx.beginPath(); + var w = size * scale; + var h = w; + var midpoint = width / 2; + var x1 = midpoint; + var y1 = (height - width) / 2; + var x2 = width - (width - width) / 2; + var y2 = height - (height - width) / 2; + var x3 = (width - width) / 2; + var y3 = height - (height - width) / 2; + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.lineTo(x3, y3); + ctx.lineTo(x1, y1); + ctx.fill(); + ctx.stroke(); + break; + default: + throw new Error(`Unknown symbol type ${symbol.style}`); + } + return { width, height, data: canvas.toDataURL() }; + } - var fillPatterns = { - esriSFSVertical: (strokeStyle = "#000000") => { - var canvas = createCanvas(16, 16); - var ctx = canvas.getContext("2d"); - ctx.strokeStyle = strokeStyle || "#000000"; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(8, 0); - ctx.lineTo(8, 16); - ctx.stroke(); - return ctx.createPattern(canvas, "repeat"); - }, - esriSFSHorizontal: (strokeStyle = "#000000") => { - var canvas = createCanvas(16, 16); - var ctx = canvas.getContext("2d"); - ctx.strokeStyle = strokeStyle || "#000000"; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(0, 8); - ctx.lineTo(16, 8); - ctx.stroke(); - return ctx.createPattern(canvas, "repeat"); - }, - esriSFSBackwardDiagonal: (strokeStyle = "#000000") => { - var canvas = createCanvas(16, 16); - var ctx = canvas.getContext("2d"); - ctx.strokeStyle = strokeStyle; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(0, 8); - ctx.lineTo(8, 0); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(0, 24); - ctx.lineTo(24, 0); - ctx.stroke(); - return ctx.createPattern(canvas, "repeat"); - }, - esriSFSForwardDiagonal: (strokeStyle = "#000000") => { - var canvas = createCanvas(16, 16); - var ctx = canvas.getContext("2d"); - ctx.strokeStyle = strokeStyle; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(0, 8); - ctx.lineTo(8, 16); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(8, 0); - ctx.lineTo(16, 8); - ctx.stroke(); - return ctx.createPattern(canvas, "repeat"); - }, - esriSFSCross: (strokeStyle = "#000000") => { - var canvas = createCanvas(16, 16); - var ctx = canvas.getContext("2d"); - ctx.strokeStyle = strokeStyle; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(0, 8); - ctx.lineTo(16, 8); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(8, 0); - ctx.lineTo(8, 16); - ctx.stroke(); - return ctx.createPattern(canvas, "repeat"); - }, - esriSFSDiagonalCross: (strokeStyle = "#000000") => { - var canvas = createCanvas(16, 16); - var ctx = canvas.getContext("2d"); - ctx.strokeStyle = strokeStyle; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(0, 8); - ctx.lineTo(8, 16); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(8, 0); - ctx.lineTo(16, 8); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(0, 8); - ctx.lineTo(8, 0); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(0, 24); - ctx.lineTo(24, 0); - ctx.stroke(); - return ctx.createPattern(canvas, "repeat"); - }, - }; + var fillPatterns = { + esriSFSVertical: (strokeStyle = "#000000") => { + var canvas = createCanvas(16, 16); + var ctx = canvas.getContext("2d"); + ctx.strokeStyle = strokeStyle || "#000000"; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(8, 0); + ctx.lineTo(8, 16); + ctx.stroke(); + return ctx.createPattern(canvas, "repeat"); + }, + esriSFSHorizontal: (strokeStyle = "#000000") => { + var canvas = createCanvas(16, 16); + var ctx = canvas.getContext("2d"); + ctx.strokeStyle = strokeStyle || "#000000"; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(0, 8); + ctx.lineTo(16, 8); + ctx.stroke(); + return ctx.createPattern(canvas, "repeat"); + }, + esriSFSBackwardDiagonal: (strokeStyle = "#000000") => { + var canvas = createCanvas(16, 16); + var ctx = canvas.getContext("2d"); + ctx.strokeStyle = strokeStyle; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(0, 8); + ctx.lineTo(8, 0); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(0, 24); + ctx.lineTo(24, 0); + ctx.stroke(); + return ctx.createPattern(canvas, "repeat"); + }, + esriSFSForwardDiagonal: (strokeStyle = "#000000") => { + var canvas = createCanvas(16, 16); + var ctx = canvas.getContext("2d"); + ctx.strokeStyle = strokeStyle; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(0, 8); + ctx.lineTo(8, 16); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(8, 0); + ctx.lineTo(16, 8); + ctx.stroke(); + return ctx.createPattern(canvas, "repeat"); + }, + esriSFSCross: (strokeStyle = "#000000") => { + var canvas = createCanvas(16, 16); + var ctx = canvas.getContext("2d"); + ctx.strokeStyle = strokeStyle; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(0, 8); + ctx.lineTo(16, 8); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(8, 0); + ctx.lineTo(8, 16); + ctx.stroke(); + return ctx.createPattern(canvas, "repeat"); + }, + esriSFSDiagonalCross: (strokeStyle = "#000000") => { + var canvas = createCanvas(16, 16); + var ctx = canvas.getContext("2d"); + ctx.strokeStyle = strokeStyle; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(0, 8); + ctx.lineTo(8, 16); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(8, 0); + ctx.lineTo(16, 8); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(0, 8); + ctx.lineTo(8, 0); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(0, 24); + ctx.lineTo(24, 0); + ctx.stroke(); + return ctx.createPattern(canvas, "repeat"); + }, + }; - class ImageList { - constructor(arcGISVersion, imageSets) { - this.imageSets = []; - this.supportsHighDPILegends = false; - if (arcGISVersion && arcGISVersion >= 10.6) { - this.supportsHighDPILegends = true; - } - if (imageSets) { - this.imageSets = imageSets; - } - } - toJSON() { - return this.imageSets; - } - addEsriPFS(symbol) { - const imageid = v4(); - this.imageSets.push({ - id: imageid, - images: [ - { - pixelRatio: 1, - dataURI: `data:${symbol.contentType};base64,${symbol.imageData}`, - width: ptToPx(symbol.width), - height: ptToPx(symbol.height), - }, - ], - }); - return imageid; - } - addEsriPMS(symbol, serviceBaseUrl, sublayer, legendIndex) { - const imageid = v4(); - if (this.supportsHighDPILegends) { - this.imageSets.push(new Promise(async (resolve) => { - const imageSet = { - id: imageid, - images: [ - { - pixelRatio: 1, - dataURI: `data:${symbol.contentType};base64,${symbol.imageData}`, - width: ptToPx(symbol.width), - height: ptToPx(symbol.height), - }, - ], - }; - if (/MapServer/.test(serviceBaseUrl)) { - const legend2x = await fetchLegendImage(serviceBaseUrl, sublayer, legendIndex, 2); - const legend3x = await fetchLegendImage(serviceBaseUrl, sublayer, legendIndex, 3); - imageSet.images.push(legend2x, legend3x); - } - resolve(imageSet); - })); - } - else { - this.imageSets.push({ - id: imageid, - images: [ - { - pixelRatio: 1, - dataURI: `data:${symbol.contentType};base64,${symbol.imageData}`, - width: ptToPx(symbol.width), - height: ptToPx(symbol.height), - }, - ], - }); - } - return imageid; - } - addEsriSMS(symbol) { - const imageid = v4(); - const images = [1, 2, 3].map((pixelRatio) => { - const marker = drawSMS(symbol, pixelRatio); - return { - dataURI: marker.data, - pixelRatio, - width: marker.width, - height: marker.height, - }; - }); - this.imageSets.push({ - id: imageid, - images: images, - }); - return imageid; - } - addEsriSFS(symbol) { - const imageId = v4(); - const pattern = fillPatterns[symbol.style](rgba(symbol.color)); - this.imageSets.push({ - id: imageId, - images: [ - createFillImage(pattern, 1), - createFillImage(pattern, 2), - createFillImage(pattern, 3), - ], - }); - return imageId; - } - addToMap(map) { - return Promise.all(this.imageSets.map(async (imageSet) => { - if (imageSet instanceof Promise) { - imageSet = await imageSet; - } - let imageData = imageSet.images[0]; - if (imageSet.images.length > 1) { - imageData = - imageSet.images.find((i) => i.pixelRatio === Math.round(window.devicePixelRatio)) || imageData; - } - const image = await createImage(imageData.width, imageData.height, imageData.dataURI); - map.addImage(imageSet.id, image, { - pixelRatio: imageData.pixelRatio, - }); - })); - } - removeFromMap(map) { - return Promise.all(this.imageSets.map(async (imageSet) => { - if (imageSet instanceof Promise) { - imageSet = await imageSet; - } - map.removeImage(imageSet.id); - })); - } - } - async function createImage(width, height, dataURI) { - return new Promise((resolve) => { - const image = new Image(width, height); - image.src = dataURI; - image.onload = () => { - resolve(image); - }; - }); - } - function createFillImage(pattern, pixelRatio) { - const size = 4 * 2 ** pixelRatio; - const canvas = document.createElement("canvas"); - canvas.setAttribute("width", size.toString()); - canvas.setAttribute("height", size.toString()); - const ctx = canvas.getContext("2d"); - ctx.fillStyle = pattern; - ctx.beginPath(); - ctx.moveTo(0, 0); - ctx.lineTo(0, size); - ctx.lineTo(size, size); - ctx.lineTo(size, 0); - ctx.closePath(); - ctx.fill(); - return { - pixelRatio, - dataURI: canvas.toDataURL(), - width: size, - height: size, - }; - } - const cache = {}; - async function fetchLegendImage(serviceRoot, sublayer, legendIndex, pixelRatio) { - const legendData = await fetchLegendData(serviceRoot, pixelRatio); - console.log("legendData", serviceRoot, legendData); - const sublayerData = legendData.layers.find((lyr) => lyr.layerId === sublayer); - const legendItem = sublayerData.legend[legendIndex]; - return { - dataURI: `data:${legendItem.contentType};base64,${legendItem.imageData}`, - pixelRatio, - width: legendItem.width, - height: legendItem.height, - }; - } - async function fetchLegendData(serviceRoot, pixelRatio) { - const dpi = pixelRatio === 2 ? 192 : 384; - if (!cache[serviceRoot]) { - cache[serviceRoot] = {}; - } - if (!cache[serviceRoot][pixelRatio]) { - cache[serviceRoot][pixelRatio] = fetch(`${serviceRoot}/legend?f=json&dpi=${dpi}`).then((r) => r.json()); - } - return cache[serviceRoot][pixelRatio]; - } + class ImageList { + constructor(arcGISVersion, imageSets) { + this.imageSets = []; + this.supportsHighDPILegends = false; + if (arcGISVersion && arcGISVersion >= 10.6) { + this.supportsHighDPILegends = true; + } + if (imageSets) { + this.imageSets = imageSets; + } + } + toJSON() { + return this.imageSets; + } + addEsriPFS(symbol) { + const imageid = v4(); + this.imageSets.push({ + id: imageid, + images: [ + { + pixelRatio: 1, + dataURI: `data:${symbol.contentType};base64,${symbol.imageData}`, + width: ptToPx(symbol.width), + height: ptToPx(symbol.height), + }, + ], + }); + return imageid; + } + addEsriPMS(symbol, serviceBaseUrl, sublayer, legendIndex) { + const imageid = v4(); + if (this.supportsHighDPILegends) { + this.imageSets.push(new Promise(async (resolve) => { + const imageSet = { + id: imageid, + images: [ + { + pixelRatio: 1, + dataURI: `data:${symbol.contentType};base64,${symbol.imageData}`, + width: ptToPx(symbol.width), + height: ptToPx(symbol.height), + }, + ], + }; + if (/MapServer/.test(serviceBaseUrl)) { + const legend2x = await fetchLegendImage(serviceBaseUrl, sublayer, legendIndex, 2); + const legend3x = await fetchLegendImage(serviceBaseUrl, sublayer, legendIndex, 3); + imageSet.images.push(legend2x, legend3x); + } + resolve(imageSet); + })); + } + else { + this.imageSets.push({ + id: imageid, + images: [ + { + pixelRatio: 1, + dataURI: `data:${symbol.contentType};base64,${symbol.imageData}`, + width: ptToPx(symbol.width), + height: ptToPx(symbol.height), + }, + ], + }); + } + return imageid; + } + addEsriSMS(symbol) { + const imageid = v4(); + const images = [1, 2, 3].map((pixelRatio) => { + const marker = drawSMS(symbol, pixelRatio); + return { + dataURI: marker.data, + pixelRatio, + width: marker.width, + height: marker.height, + }; + }); + this.imageSets.push({ + id: imageid, + images: images, + }); + return imageid; + } + addEsriSFS(symbol) { + const imageId = v4(); + const pattern = fillPatterns[symbol.style](rgba(symbol.color)); + this.imageSets.push({ + id: imageId, + images: [ + createFillImage(pattern, 1), + createFillImage(pattern, 2), + createFillImage(pattern, 3), + ], + }); + return imageId; + } + addToMap(map) { + return Promise.all(this.imageSets.map(async (imageSet) => { + if (imageSet instanceof Promise) { + imageSet = await imageSet; + } + let imageData = imageSet.images[0]; + if (imageSet.images.length > 1) { + imageData = + imageSet.images.find((i) => i.pixelRatio === Math.round(window.devicePixelRatio)) || imageData; + } + const image = await createImage(imageData.width, imageData.height, imageData.dataURI); + map.addImage(imageSet.id, image, { + pixelRatio: imageData.pixelRatio, + }); + })); + } + removeFromMap(map) { + return Promise.all(this.imageSets.map(async (imageSet) => { + if (imageSet instanceof Promise) { + imageSet = await imageSet; + } + map.removeImage(imageSet.id); + })); + } + } + async function createImage(width, height, dataURI) { + return new Promise((resolve) => { + const image = new Image(width, height); + image.src = dataURI; + image.onload = () => { + resolve(image); + }; + }); + } + function createFillImage(pattern, pixelRatio) { + const size = 4 * 2 ** pixelRatio; + const canvas = document.createElement("canvas"); + canvas.setAttribute("width", size.toString()); + canvas.setAttribute("height", size.toString()); + const ctx = canvas.getContext("2d"); + ctx.fillStyle = pattern; + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, size); + ctx.lineTo(size, size); + ctx.lineTo(size, 0); + ctx.closePath(); + ctx.fill(); + return { + pixelRatio, + dataURI: canvas.toDataURL(), + width: size, + height: size, + }; + } + const cache = {}; + async function fetchLegendImage(serviceRoot, sublayer, legendIndex, pixelRatio) { + const legendData = await fetchLegendData(serviceRoot, pixelRatio); + console.log("legendData", serviceRoot, legendData); + const sublayerData = legendData.layers.find((lyr) => lyr.layerId === sublayer); + const legendItem = sublayerData.legend[legendIndex]; + return { + dataURI: `data:${legendItem.contentType};base64,${legendItem.imageData}`, + pixelRatio, + width: legendItem.width, + height: legendItem.height, + }; + } + async function fetchLegendData(serviceRoot, pixelRatio) { + const dpi = pixelRatio === 2 ? 192 : 384; + if (!cache[serviceRoot]) { + cache[serviceRoot] = {}; + } + if (!cache[serviceRoot][pixelRatio]) { + cache[serviceRoot][pixelRatio] = fetch(`${serviceRoot}/legend?f=json&dpi=${dpi}`).then((r) => r.json()); + } + return cache[serviceRoot][pixelRatio]; + } - var esriTS = (labelingInfo, geometryType, fieldNames) => { - return { - id: generateId(), - type: "symbol", - layout: { - "text-field": toExpression(labelingInfo.labelExpression, fieldNames), - "text-anchor": toTextAnchor(labelingInfo.labelPlacement), - "text-size": ptToPx(labelingInfo.symbol.font.size || 13), - "symbol-placement": geometryType === "line" ? "line" : "point", - "text-max-angle": 20, - }, - paint: { - "text-color": rgba(labelingInfo.symbol.color), - "text-halo-width": ptToPx(labelingInfo.symbol.haloSize || 0), - "text-halo-color": rgba(labelingInfo.symbol.haloColor || [255, 255, 255, 255]), - "text-halo-blur": ptToPx(labelingInfo.symbol.haloSize || 0) * 0.5, - }, - }; - }; - function toExpression(labelExpression, fieldNames) { - const fields = (labelExpression.match(/\[\w+\]/g) || []) - .map((val) => val.replace(/[\[\]]/g, "")) - .map((val) => fieldNames.find((name) => name.toLowerCase() === val.toLowerCase())); - const strings = labelExpression.split(/\[\w+\]/g); - const expression = ["format"]; - while (strings.length) { - expression.push(strings.shift()); - const field = fields.shift(); - if (field) { - expression.push(["get", field]); - } - } - return expression; - } + var esriTS = (labelingInfo, geometryType, fieldNames) => { + return { + id: generateId(), + type: "symbol", + layout: { + "text-field": toExpression(labelingInfo.labelExpression, fieldNames), + "text-anchor": toTextAnchor(labelingInfo.labelPlacement), + "text-size": ptToPx(labelingInfo.symbol.font.size || 13), + "symbol-placement": geometryType === "line" ? "line" : "point", + "text-max-angle": 20, + }, + paint: { + "text-color": rgba(labelingInfo.symbol.color), + "text-halo-width": ptToPx(labelingInfo.symbol.haloSize || 0), + "text-halo-color": rgba(labelingInfo.symbol.haloColor || [255, 255, 255, 255]), + "text-halo-blur": ptToPx(labelingInfo.symbol.haloSize || 0) * 0.5, + }, + }; + }; + function toExpression(labelExpression, fieldNames) { + const fields = (labelExpression.match(/\[\w+\]/g) || []) + .map((val) => val.replace(/[\[\]]/g, "")) + .map((val) => fieldNames.find((name) => name.toLowerCase() === val.toLowerCase())); + const strings = labelExpression.split(/\[\w+\]/g); + const expression = ["format"]; + while (strings.length) { + expression.push(strings.shift()); + const field = fields.shift(); + if (field) { + expression.push(["get", field]); + } + } + return expression; + } - async function styleForFeatureLayer(serviceBaseUrl, sublayer, sourceId, serviceMetadata) { - console.log({ serviceMetadata }); - serviceBaseUrl = serviceBaseUrl.replace(/\/$/, ""); - const url = `${serviceBaseUrl}/${sublayer}`; - serviceMetadata = - serviceMetadata || (await fetch(url + "?f=json").then((r) => r.json())); - const renderer = serviceMetadata.drawingInfo.renderer; - let layers = []; - const imageList = new ImageList(serviceMetadata.currentVersion); - let legendItemIndex = 0; - switch (renderer.type) { - case "uniqueValue": { - const fields = [renderer.field1]; - if (renderer.field2) { - fields.push(renderer.field2); - if (renderer.field3) { - fields.push(renderer.field3); - } - } - const filters = []; - const field = renderer.field1; - legendItemIndex = renderer.defaultSymbol ? 1 : 0; - const fieldTypes = fields.map((f) => { - const fieldRecord = serviceMetadata.fields.find((r) => r.name === f); - return FIELD_TYPES[fieldRecord === null || fieldRecord === void 0 ? void 0 : fieldRecord.type] || "string"; - }); - for (const info of renderer.uniqueValueInfos) { - const values = normalizeValuesForFieldTypes(info.value, renderer.fieldDelimiter, fieldTypes); - layers.push(...symbolToLayers(info.symbol, sourceId, imageList, serviceBaseUrl, sublayer, legendItemIndex++).map((lyr) => { - if (fields.length === 1) { - lyr.filter = ["==", field, values[0]]; - filters.push(lyr.filter); - } - else { - lyr.filter = [ - "all", - ...fields.map((field) => [ - "==", - field, - values[fields.indexOf(field)], - ]), - ]; - filters.push(lyr.filter); - } - return lyr; - })); - } - if (renderer.defaultSymbol && renderer.defaultSymbol.type) { - layers.push(...symbolToLayers(renderer.defaultSymbol, sourceId, imageList, serviceBaseUrl, sublayer, 0).map((lyr) => { - lyr.filter = ["none", ...filters]; - return lyr; - })); - } - break; - } - case "classBreaks": - if (renderer.backgroundFillSymbol) { - layers.push(...symbolToLayers(renderer.backgroundFillSymbol, sourceId, imageList, serviceBaseUrl, sublayer, 0)); - } - const field = renderer.field; - const filters = []; - legendItemIndex = renderer.classBreakInfos.length - 1; - let minValue = 0; - const minMaxValues = renderer.classBreakInfos.map((b) => { - const values = [b.classMinValue || minValue, b.classMaxValue]; - minValue = values[1]; - return values; - }); - for (const info of [...renderer.classBreakInfos].reverse()) { - layers.push(...symbolToLayers(info.symbol, sourceId, imageList, serviceBaseUrl, sublayer, legendItemIndex--).map((lyr) => { - const [min, max] = minMaxValues[renderer.classBreakInfos.indexOf(info)]; - if (renderer.classBreakInfos.indexOf(info) === 0) { - lyr.filter = ["all", ["<=", field, max]]; - } - else { - lyr.filter = ["all", [">", field, min], ["<=", field, max]]; - } - filters.push(lyr.filter); - return lyr; - })); - } - if (renderer.defaultSymbol && renderer.defaultSymbol.type) { - const defaultLayers = await symbolToLayers(renderer.defaultSymbol, sourceId, imageList, serviceBaseUrl, sublayer, 0); - for (const index in defaultLayers) { - defaultLayers[index].filter = ["none", filters]; - } - layers.push(...defaultLayers); - } - break; - default: - layers = symbolToLayers(renderer.symbol, sourceId, imageList, serviceBaseUrl, sublayer, 0); - break; - } - if (serviceMetadata.drawingInfo.labelingInfo) { - for (const info of serviceMetadata.drawingInfo.labelingInfo) { - if (info.labelExpression) { - const layer = esriTS(info, serviceMetadata.geometryType, serviceMetadata.fields.map((f) => f.name)); - layer.source = sourceId; - layer.id = generateId(); - layers.push(layer); - } - } - } - return { - imageList, - layers, - }; - } - function normalizeValuesForFieldTypes(value, delimiter, fieldTypes) { - const values = value.split(delimiter); - return values.map((v, i) => { - if (fieldTypes[i] === "string") { - return v; - } - else if (fieldTypes[i] === "integer") { - return parseInt(v); - } - else if (fieldTypes[i] === "float") { - return parseFloat(v); - } - }); - } - const FIELD_TYPES = { - esriFieldTypeSmallInteger: "integer", - esriFieldTypeInteger: "integer", - esriFieldTypeSingle: "float", - esriFieldTypeDouble: "float", - esriFieldTypeString: "string", - esriFieldTypeDate: "string", - esriFieldTypeOID: "integer", - esriFieldTypeGeometry: "string", - esriFieldTypeBlob: "string", - esriFieldTypeRaster: "string", - esriFieldTypeGUID: "string", - esriFieldTypeGlobalID: "string", - esriFieldTypeXML: "string", - }; + async function styleForFeatureLayer(serviceBaseUrl, sublayer, sourceId, serviceMetadata) { + console.log({ serviceMetadata }); + serviceBaseUrl = serviceBaseUrl.replace(/\/$/, ""); + const url = `${serviceBaseUrl}/${sublayer}`; + serviceMetadata = + serviceMetadata || (await fetch(url + "?f=json").then((r) => r.json())); + const renderer = serviceMetadata.drawingInfo.renderer; + let layers = []; + const imageList = new ImageList(serviceMetadata.currentVersion); + let legendItemIndex = 0; + switch (renderer.type) { + case "uniqueValue": { + const fields = [renderer.field1]; + if (renderer.field2) { + fields.push(renderer.field2); + if (renderer.field3) { + fields.push(renderer.field3); + } + } + const filters = []; + const field = renderer.field1; + legendItemIndex = renderer.defaultSymbol ? 1 : 0; + const fieldTypes = fields.map((f) => { + const fieldRecord = serviceMetadata.fields.find((r) => r.name === f); + return FIELD_TYPES[fieldRecord === null || fieldRecord === void 0 ? void 0 : fieldRecord.type] || "string"; + }); + for (const info of renderer.uniqueValueInfos) { + const values = normalizeValuesForFieldTypes(info.value, renderer.fieldDelimiter, fieldTypes); + layers.push(...symbolToLayers(info.symbol, sourceId, imageList, serviceBaseUrl, sublayer, legendItemIndex++).map((lyr) => { + if (fields.length === 1) { + lyr.filter = ["==", field, values[0]]; + filters.push(lyr.filter); + } + else { + lyr.filter = [ + "all", + ...fields.map((field) => [ + "==", + field, + values[fields.indexOf(field)], + ]), + ]; + filters.push(lyr.filter); + } + return lyr; + })); + } + if (renderer.defaultSymbol && renderer.defaultSymbol.type) { + layers.push(...symbolToLayers(renderer.defaultSymbol, sourceId, imageList, serviceBaseUrl, sublayer, 0).map((lyr) => { + lyr.filter = ["none", ...filters]; + return lyr; + })); + } + break; + } + case "classBreaks": + if (renderer.backgroundFillSymbol) { + layers.push(...symbolToLayers(renderer.backgroundFillSymbol, sourceId, imageList, serviceBaseUrl, sublayer, 0)); + } + const field = renderer.field; + const filters = []; + legendItemIndex = renderer.classBreakInfos.length - 1; + let minValue = 0; + const minMaxValues = renderer.classBreakInfos.map((b) => { + const values = [b.classMinValue || minValue, b.classMaxValue]; + minValue = values[1]; + return values; + }); + for (const info of [...renderer.classBreakInfos].reverse()) { + layers.push(...symbolToLayers(info.symbol, sourceId, imageList, serviceBaseUrl, sublayer, legendItemIndex--).map((lyr) => { + const [min, max] = minMaxValues[renderer.classBreakInfos.indexOf(info)]; + if (renderer.classBreakInfos.indexOf(info) === 0) { + lyr.filter = ["all", ["<=", field, max]]; + } + else { + lyr.filter = ["all", [">", field, min], ["<=", field, max]]; + } + filters.push(lyr.filter); + return lyr; + })); + } + if (renderer.defaultSymbol && renderer.defaultSymbol.type) { + const defaultLayers = await symbolToLayers(renderer.defaultSymbol, sourceId, imageList, serviceBaseUrl, sublayer, 0); + for (const index in defaultLayers) { + defaultLayers[index].filter = ["none", filters]; + } + layers.push(...defaultLayers); + } + break; + default: + layers = symbolToLayers(renderer.symbol, sourceId, imageList, serviceBaseUrl, sublayer, 0); + break; + } + if (serviceMetadata.drawingInfo.labelingInfo) { + for (const info of serviceMetadata.drawingInfo.labelingInfo) { + if (info.labelExpression) { + const layer = esriTS(info, serviceMetadata.geometryType, serviceMetadata.fields.map((f) => f.name)); + layer.source = sourceId; + layer.id = generateId(); + layers.push(layer); + } + } + } + return { + imageList, + layers, + }; + } + function normalizeValuesForFieldTypes(value, delimiter, fieldTypes) { + const values = value.split(delimiter); + return values.map((v, i) => { + if (fieldTypes[i] === "string") { + return v; + } + else if (fieldTypes[i] === "integer") { + return parseInt(v); + } + else if (fieldTypes[i] === "float") { + return parseFloat(v); + } + }); + } + const FIELD_TYPES = { + esriFieldTypeSmallInteger: "integer", + esriFieldTypeInteger: "integer", + esriFieldTypeSingle: "float", + esriFieldTypeDouble: "float", + esriFieldTypeString: "string", + esriFieldTypeDate: "string", + esriFieldTypeOID: "integer", + esriFieldTypeGeometry: "string", + esriFieldTypeBlob: "string", + esriFieldTypeRaster: "string", + esriFieldTypeGUID: "string", + esriFieldTypeGlobalID: "string", + esriFieldTypeXML: "string", + }; - exports.ArcGISDynamicMapService = ArcGISDynamicMapService; - exports.ArcGISRESTServiceRequestManager = ArcGISRESTServiceRequestManager; - exports.ArcGISTiledMapService = ArcGISTiledMapService; - exports.ArcGISVectorSource = ArcGISVectorSource; - exports.ImageList = ImageList; - exports.styleForFeatureLayer = styleForFeatureLayer; + exports.ArcGISDynamicMapService = ArcGISDynamicMapService; + exports.ArcGISRESTServiceRequestManager = ArcGISRESTServiceRequestManager; + exports.ArcGISTiledMapService = ArcGISTiledMapService; + exports.ArcGISVectorSource = ArcGISVectorSource; + exports.ImageList = ImageList; + exports.styleForFeatureLayer = styleForFeatureLayer; - Object.defineProperty(exports, '__esModule', { value: true }); + Object.defineProperty(exports, '__esModule', { value: true }); - return exports; + return exports; })({}); diff --git a/packages/mapbox-gl-esri-sources/dist/index.d.ts b/packages/mapbox-gl-esri-sources/dist/index.d.ts index 1a927fb53..c85be8eed 100644 --- a/packages/mapbox-gl-esri-sources/dist/index.d.ts +++ b/packages/mapbox-gl-esri-sources/dist/index.d.ts @@ -1,8 +1,9 @@ import { ArcGISDynamicMapService, ArcGISDynamicMapServiceOptions } from "./src/ArcGISDynamicMapService"; import { ArcGISVectorSource, ArcGISVectorSourceOptions } from "./src/ArcGISVectorSource"; import { ArcGISRESTServiceRequestManager } from "./src/ArcGISRESTServiceRequestManager"; -import { ArcGISTiledMapService, ArcGISTiledMapServiceOptions } from "./src/ArcGISTiledMapService"; -export { CustomGLSource, CustomGLSourceOptions, DynamicRenderingSupportOptions, LegendItem, SingleImageLegend, } from "./src/CustomGLSource"; -export { ArcGISDynamicMapService, ArcGISVectorSource, ArcGISDynamicMapServiceOptions, ArcGISVectorSourceOptions, ArcGISRESTServiceRequestManager, ArcGISTiledMapService, ArcGISTiledMapServiceOptions, }; +export { ArcGISTiledMapService, ArcGISTiledMapServiceOptions, } from "./src/ArcGISTiledMapService"; +export { MapServiceMetadata } from "./src/ServiceMetadata"; +export { CustomGLSource, CustomGLSourceOptions, DynamicRenderingSupportOptions, LegendItem, SingleImageLegend, DataTableOfContentsItem, FolderTableOfContentsItem, } from "./src/CustomGLSource"; +export { ArcGISDynamicMapService, ArcGISVectorSource, ArcGISDynamicMapServiceOptions, ArcGISVectorSourceOptions, ArcGISRESTServiceRequestManager, }; export { default as styleForFeatureLayer } from "./src/styleForFeatureLayer"; export { ImageList } from "./src/ImageList"; diff --git a/packages/mapbox-gl-esri-sources/dist/index.js b/packages/mapbox-gl-esri-sources/dist/index.js index 76e42493c..7c71311ce 100644 --- a/packages/mapbox-gl-esri-sources/dist/index.js +++ b/packages/mapbox-gl-esri-sources/dist/index.js @@ -1,7 +1,7 @@ import { ArcGISDynamicMapService, } from "./src/ArcGISDynamicMapService"; import { ArcGISVectorSource, } from "./src/ArcGISVectorSource"; import { ArcGISRESTServiceRequestManager } from "./src/ArcGISRESTServiceRequestManager"; -import { ArcGISTiledMapService, } from "./src/ArcGISTiledMapService"; -export { ArcGISDynamicMapService, ArcGISVectorSource, ArcGISRESTServiceRequestManager, ArcGISTiledMapService, }; +export { ArcGISTiledMapService, } from "./src/ArcGISTiledMapService"; +export { ArcGISDynamicMapService, ArcGISVectorSource, ArcGISRESTServiceRequestManager, }; export { default as styleForFeatureLayer } from "./src/styleForFeatureLayer"; export { ImageList } from "./src/ImageList"; diff --git a/packages/mapbox-gl-esri-sources/dist/src/ArcGISDynamicMapService.d.ts b/packages/mapbox-gl-esri-sources/dist/src/ArcGISDynamicMapService.d.ts index 367a7e4a4..8183d9f4e 100644 --- a/packages/mapbox-gl-esri-sources/dist/src/ArcGISDynamicMapService.d.ts +++ b/packages/mapbox-gl-esri-sources/dist/src/ArcGISDynamicMapService.d.ts @@ -1,29 +1,24 @@ -import { Map } from "mapbox-gl"; -export interface SublayerState { - /** 0-based sublayer index */ - sublayer: number; - /** sublayer opacity from 0.0 - 1.0 */ - opacity?: number; -} -export interface ArcGISDynamicMapServiceOptions { +import { Map, AnyLayer } from "mapbox-gl"; +import { ComputedMetadata, CustomGLSource, CustomGLSourceOptions, LegendItem, OrderedLayerSettings } from "./CustomGLSource"; +import { ArcGISRESTServiceRequestManager } from "./ArcGISRESTServiceRequestManager"; +/** @hidden */ +export declare const blankDataUri = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; +export interface ArcGISDynamicMapServiceOptions extends CustomGLSourceOptions { + /** + * URL for the service. Should end in /MapServer + */ + url: string; /** * Fetch larger images for high-resolution devices. * @default true * */ - useDevicePixelRatio?: boolean; + supportHighDpiDisplays?: boolean; /** * List of sublayers to display, in order. Order will be respected only if - * `supportsDynamicLayers` is true. If left undefined the service will be + * `supportsDynamicRendering` is true. If left undefined the service will be * with the default layers. * */ - layers?: SublayerState[]; - /** - * Set true if the Map Service supports [dynamic layers](https://developers.arcgis.com/rest/services-reference/export-map.htm#GUID-E781BA37-0260-485E-BB21-CA9B85206AAE) - * , in which case sublayer order and opacity can be specified. If set false - * any order and opacity settings will be ignored. - * @default false - * */ - supportsDynamicLayers?: boolean; + layers?: OrderedLayerSettings; /** * All query parameters will be added to each MapServer export request, * overriding any settings made by this library. Useful for specifying image @@ -38,82 +33,56 @@ export interface ArcGISDynamicMapServiceOptions { */ useTiles?: boolean; /** - * Tile size in pixels. Only used if `useTiles` is true. - * @default 256 - * */ + * 256 or 512 would be most appropriate. default is 256 + */ tileSize?: number; + credentials?: { + username: string; + password: string; + }; } -/** - * Add an Esri Dynamic Map Service as an image source to a MapBox GL JS map, and - * use the included methods to update visible sublayers, set layer order and - * opacity, support high-dpi screens, and transparently deal with issues related - * to crossing the central meridian. - * - * ```typescript - * import { ArcGISDynamicMapService } from "mapbox-gl-esri-sources"; - * - * // ... setup your map - * - * const populatedPlaces = new ArcGISDynamicMapService( - * map, - * "populated-places-source", - * "https://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServer", { - * supportsDynamicLayers: true, - * sublayers: [ - * { sublayer: 0, opacity: 1 }, - * { sublayer: 1, opacity: 1 }, - * { sublayer: 2, opacity: 0.5 }, - * ], - * queryParameters: { - * format: 'png32' - * } - * } - * }); - * - * // Don't forget to add a layer to reference your source - * map.addLayer({ - * id: "ags-layer", - * type: "raster", - * source: populatedPlaces.id, - * paint: { - * "raster-fade-duration": 0, - * "raster-opacity": 0.9 - * }, - * }); - * - * // turn off the third sublayer and update opacity - * populatedPlaces.updateLayers([ - * { sublayer: 0, opacity: 0.5 }, - * { sublayer: 1, opacity: 1 }, - * ]); - * - * // disable high-dpi screen support - * populatedPlaces.updateUseDevicePixelRatio(false); - * ``` - * @class ArcGISDynamicMapService - */ -export declare class ArcGISDynamicMapService { +export declare class ArcGISDynamicMapService implements CustomGLSource { /** Source id used in the map style */ - id: string; - private baseUrl; - private url; - private map; + sourceId: string; + private map?; + private requestManager; + private serviceMetadata?; + private layerMetadata?; + private options; private layers?; - private queryParameters; - private source; - private supportDevicePixelRatio; private supportsDynamicLayers; private debounceTimeout?; private _loading; - private useTiles; - private tileSize; + private resolution?; /** - * @param {Map} map MapBox GL JS Map instance - * @param {string} id ID to be used when adding refering to this source from layers + * @param {string} sourceId ID to be used when adding refering to this source from layers * @param {string} baseUrl Location of the service. Should end in /MapServer * @param {ArcGISDynamicMapServiceOptions} [options] */ - constructor(map: Map, id: string, baseUrl: string, options?: ArcGISDynamicMapServiceOptions); + constructor(requestManager: ArcGISRESTServiceRequestManager, options: ArcGISDynamicMapServiceOptions); + private respondToResolutionChange; + /** + * Use ArcGISRESTServiceRequestManager to fetch metadata for the service, + * caching it on the instance for reuse. + */ + private getMetadata; + /** + * Returns computed metadata for the service, including bounds, minzoom, maxzoom, and attribution. + * @returns ComputedMetadata + * @throws Error if metadata is not available + * @throws Error if tileInfo is not available + * */ + getComputedMetadata(): Promise; + /** + * Private method used as the basis for getComputedMetadata and also used + * when generating the source data for addToMap. + * @returns Computed properties for the service, including bounds, minzoom, maxzoom, and attribution. + */ + private getComputedProperties; + private onMapData; + private onMapError; + addToMap(map: Map): Promise; + removeFromMap(map: Map): void; /** * Clears all map event listeners setup by this instance. */ @@ -140,7 +109,7 @@ export declare class ArcGISDynamicMapService { * optional `opacity` props. * */ - updateLayers(layers: SublayerState[]): void; + updateLayers(layers: OrderedLayerSettings): void; /** * Update query params sent with each export request and re-render the map. A * list of supported parameters can be found in the [Esri REST API docs](https://developers.arcgis.com/rest/services-reference/export-map.htm#GUID-C93E8957-99FD-473B-B0E1-68EA315EBD98). @@ -167,4 +136,5 @@ export declare class ArcGISDynamicMapService { * @param enable */ updateUseDevicePixelRatio(enable: boolean): void; + getGLStyleLayers(): Promise; } diff --git a/packages/mapbox-gl-esri-sources/dist/src/ArcGISDynamicMapService.js b/packages/mapbox-gl-esri-sources/dist/src/ArcGISDynamicMapService.js index cfc88d4de..0f7c9e893 100644 --- a/packages/mapbox-gl-esri-sources/dist/src/ArcGISDynamicMapService.js +++ b/packages/mapbox-gl-esri-sources/dist/src/ArcGISDynamicMapService.js @@ -1,85 +1,63 @@ +import { v4 as uuid } from "uuid"; +import { contentOrFalse, extentToLatLngBounds, generateMetadataForLayer, makeLegend, } from "./utils"; /** @hidden */ -const blankDataUri = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; -/** - * Add an Esri Dynamic Map Service as an image source to a MapBox GL JS map, and - * use the included methods to update visible sublayers, set layer order and - * opacity, support high-dpi screens, and transparently deal with issues related - * to crossing the central meridian. - * - * ```typescript - * import { ArcGISDynamicMapService } from "mapbox-gl-esri-sources"; - * - * // ... setup your map - * - * const populatedPlaces = new ArcGISDynamicMapService( - * map, - * "populated-places-source", - * "https://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServer", { - * supportsDynamicLayers: true, - * sublayers: [ - * { sublayer: 0, opacity: 1 }, - * { sublayer: 1, opacity: 1 }, - * { sublayer: 2, opacity: 0.5 }, - * ], - * queryParameters: { - * format: 'png32' - * } - * } - * }); - * - * // Don't forget to add a layer to reference your source - * map.addLayer({ - * id: "ags-layer", - * type: "raster", - * source: populatedPlaces.id, - * paint: { - * "raster-fade-duration": 0, - * "raster-opacity": 0.9 - * }, - * }); - * - * // turn off the third sublayer and update opacity - * populatedPlaces.updateLayers([ - * { sublayer: 0, opacity: 0.5 }, - * { sublayer: 1, opacity: 1 }, - * ]); - * - * // disable high-dpi screen support - * populatedPlaces.updateUseDevicePixelRatio(false); - * ``` - * @class ArcGISDynamicMapService - */ +export const blankDataUri = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; export class ArcGISDynamicMapService { // TODO: fetch metadata and calculate minzoom, maxzoom, and bounds /** - * @param {Map} map MapBox GL JS Map instance - * @param {string} id ID to be used when adding refering to this source from layers + * @param {string} sourceId ID to be used when adding refering to this source from layers * @param {string} baseUrl Location of the service. Should end in /MapServer * @param {ArcGISDynamicMapServiceOptions} [options] */ - constructor(map, id, baseUrl, options) { - this.supportDevicePixelRatio = true; + constructor(requestManager, options) { this.supportsDynamicLayers = false; this._loading = true; - this.useTiles = false; - this.tileSize = 256; + this.respondToResolutionChange = () => { + if (this.options.supportHighDpiDisplays) { + this.updateSource(); + } + if (this.resolution) { + matchMedia(this.resolution).removeListener(this.respondToResolutionChange); + } + this.resolution = `(resolution: ${window.devicePixelRatio}dppx)`; + matchMedia(this.resolution).addListener(this.respondToResolutionChange); + }; + this.onMapData = (event) => { + if (event.sourceId && event.sourceId === this.sourceId) { + this._loading = false; + } + }; + this.onMapError = (event) => { + if (event.sourceId === this.sourceId && + event.dataType === "source" && + event.sourceDataType === "content") { + this._loading = false; + } + }; this.updateSource = () => { + var _a; this._loading = true; - if (this.useTiles || this.source.type === "raster") { - // @ts-ignore - setTiles is in fact a valid method - this.source.setTiles([this.getUrl()]); - } - else { - const bounds = this.map.getBounds(); - this.source.updateImage({ - url: this.getUrl(), - coordinates: [ - [bounds.getNorthWest().lng, bounds.getNorthWest().lat], - [bounds.getNorthEast().lng, bounds.getNorthEast().lat], - [bounds.getSouthEast().lng, bounds.getSouthEast().lat], - [bounds.getSouthWest().lng, bounds.getSouthWest().lat], - ], - }); + const source = (_a = this.map) === null || _a === void 0 ? void 0 : _a.getSource(this.sourceId); + if (source && this.map) { + if (source.type === "raster") { + // @ts-ignore - setTiles is in fact a valid method + source.setTiles([this.getUrl()]); + } + else if (source.type === "image") { + const bounds = this.map.getBounds(); + source.updateImage({ + url: this.getUrl(), + coordinates: [ + [bounds.getNorthWest().lng, bounds.getNorthWest().lat], + [bounds.getNorthEast().lng, bounds.getNorthEast().lat], + [bounds.getSouthEast().lng, bounds.getSouthEast().lat], + [bounds.getSouthWest().lng, bounds.getSouthWest().lat], + ], + }); + } + else { + // do nothing, source isn't added + } } }; this.debouncedUpdateSource = () => { @@ -91,40 +69,172 @@ export class ArcGISDynamicMapService { this.updateSource(); }, 5); }; - this.id = id; - this.baseUrl = baseUrl; - this.url = new URL(this.baseUrl + "/export"); - this.url.searchParams.set("f", "image"); - this.map = map; - if (!(options === null || options === void 0 ? void 0 : options.useTiles)) { - this.map.on("moveend", this.updateSource); + this.options = options; + this.requestManager = requestManager; + this.sourceId = (options === null || options === void 0 ? void 0 : options.sourceId) || uuid(); + // remove trailing slash if present + options.url = options.url.replace(/\/$/, ""); + if (!/rest\/services/.test(options.url) || !/MapServer/.test(options.url)) { + throw new Error("Invalid ArcGIS REST Service URL"); } - this.layers = options === null || options === void 0 ? void 0 : options.layers; - this.useTiles = (options === null || options === void 0 ? void 0 : options.useTiles) || false; - this.tileSize = (options === null || options === void 0 ? void 0 : options.tileSize) || 256; - this.queryParameters = { - transparent: "true", - ...((options === null || options === void 0 ? void 0 : options.queryParameters) || {}), - }; - if (options && "useDevicePixelRatio" in options) { - this.supportDevicePixelRatio = !!options.useDevicePixelRatio; + this.resolution = `(resolution: ${window.devicePixelRatio}dppx)`; + matchMedia(this.resolution).addListener(this.respondToResolutionChange); + } + /** + * Use ArcGISRESTServiceRequestManager to fetch metadata for the service, + * caching it on the instance for reuse. + */ + getMetadata() { + if (this.serviceMetadata && this.layerMetadata) { + return Promise.resolve({ + serviceMetadata: this.serviceMetadata, + layers: this.layerMetadata, + }); } - matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`).addListener(() => { - if (this.supportDevicePixelRatio) { - this.updateSource(); + else { + return this.requestManager + .getMapServiceMetadata(this.options.url, { + credentials: this.options.credentials, + }) + .then(({ serviceMetadata, layers }) => { + this.serviceMetadata = serviceMetadata; + this.layerMetadata = layers; + this.supportsDynamicLayers = serviceMetadata.supportsDynamicLayers; + return { serviceMetadata, layers }; + }); + } + } + /** + * Returns computed metadata for the service, including bounds, minzoom, maxzoom, and attribution. + * @returns ComputedMetadata + * @throws Error if metadata is not available + * @throws Error if tileInfo is not available + * */ + async getComputedMetadata() { + var _a, _b; + const { serviceMetadata, layers } = await this.getMetadata(); + const { bounds, minzoom, maxzoom, attribution } = await this.getComputedProperties(); + const results = /\/.+\/MapServer/.exec(this.options.url); + let label = results ? results[0] : false; + if (!label) { + if ((_b = (_a = this.layerMetadata) === null || _a === void 0 ? void 0 : _a.layers) === null || _b === void 0 ? void 0 : _b[0]) { + label = this.layerMetadata.layers[0].name; } - }); - this.supportsDynamicLayers = (options === null || options === void 0 ? void 0 : options.supportsDynamicLayers) || false; - const bounds = this.map.getBounds(); - if (this.useTiles) { - this.map.addSource(this.id, { + } + const legendData = await this.requestManager.getLegendMetadata(this.options.url); + // find hidden layers + // Not as simple as just reading the defaultVisibility property, because + // if a parent layer is hidden, all children are hidden as well. Folders can + // be nested arbitrarily deep. + const hiddenIds = new Set(); + for (const layer of layers.layers) { + if (!layer.defaultVisibility) { + hiddenIds.add(layer.id); + } + else { + // check if parents are hidden + if (layer.parentLayer) { + if (hiddenIds.has(layer.parentLayer.id)) { + hiddenIds.add(layer.id); + } + else { + // may not be added yet + const parent = layers.layers.find((l) => { var _a; return l.id === ((_a = layer.parentLayer) === null || _a === void 0 ? void 0 : _a.id); }); + if (parent && !parent.defaultVisibility) { + hiddenIds.add(layer.id); + hiddenIds.add(parent.id); + } + } + } + } + } + return { + bounds: bounds || undefined, + minzoom, + maxzoom, + attribution, + tableOfContentsItems: layers.layers.map((lyr) => { + const legendLayer = legendData.layers.find((l) => l.layerId === lyr.id); + const isFolder = lyr.type === "Group Layer"; + if (isFolder) { + return { + type: "folder", + id: lyr.id.toString(), + label: lyr.name, + defaultVisibility: hiddenIds.has(lyr.id) + ? false + : lyr.defaultVisibility, + parentId: lyr.parentLayer + ? lyr.parentLayer.id.toString() + : undefined, + }; + } + else { + return { + type: "data", + id: lyr.id.toString(), + label: lyr.name, + defaultVisibility: hiddenIds.has(lyr.id) + ? false + : lyr.defaultVisibility, + metadata: generateMetadataForLayer(this.options.url + "/" + lyr.id, this.serviceMetadata, lyr), + parentId: lyr.parentLayer + ? lyr.parentLayer.id.toString() + : undefined, + legend: makeLegend(legendData, lyr.id), + }; + } + }), + supportsDynamicRendering: { + layerOpacity: this.supportsDynamicLayers, + layerOrder: true, + layerVisibility: true, + }, + }; + } + /** + * Private method used as the basis for getComputedMetadata and also used + * when generating the source data for addToMap. + * @returns Computed properties for the service, including bounds, minzoom, maxzoom, and attribution. + */ + async getComputedProperties() { + var _a, _b; + const { serviceMetadata, layers } = await this.getMetadata(); + const levels = ((_a = serviceMetadata.tileInfo) === null || _a === void 0 ? void 0 : _a.lods.map((l) => l.level)) || []; + const attribution = contentOrFalse(layers.layers[0].copyrightText) || + contentOrFalse(serviceMetadata.copyrightText) || + contentOrFalse((_b = serviceMetadata.documentInfo) === null || _b === void 0 ? void 0 : _b.Author) || + undefined; + const minzoom = Math.min(...levels); + const maxzoom = Math.max(...levels); + return { + minzoom, + maxzoom, + bounds: await extentToLatLngBounds(serviceMetadata.fullExtent), + attribution, + }; + } + async addToMap(map) { + var _a; + const { attribution, bounds } = await this.getComputedProperties(); + this.map = map; + if (!((_a = this.options) === null || _a === void 0 ? void 0 : _a.useTiles)) { + this.map.on("moveend", this.updateSource); + this.map.on("data", this.onMapData); + this.map.on("error", this.onMapError); + } + if (this.options.useTiles) { + this.map.addSource(this.sourceId, { type: "raster", tiles: [this.getUrl()], - tileSize: this.tileSize, + tileSize: this.options.tileSize || 256, + bounds: bounds, + attribution, }); } else { - this.map.addSource(this.id, { + const bounds = this.map.getBounds(); + this.map.addSource(this.sourceId, { type: "image", url: this.getUrl(), coordinates: [ @@ -135,29 +245,39 @@ export class ArcGISDynamicMapService { ], }); } - this.source = this.map.getSource(this.id); - this.map.on("data", (event) => { - if (event.sourceId === this.id && - event.dataType === "source" && - event.sourceDataType === "content") { - this._loading = false; - } - }); - this.map.on("error", (event) => { - if (event.sourceId && event.sourceId === this.id) { - this._loading = false; + return this.sourceId; + } + removeFromMap(map) { + if (map.getSource(this.sourceId)) { + const layers = map.getStyle().layers || []; + for (const layer of layers) { + if ("source" in layer && layer.source === this.sourceId) { + map.removeLayer(layer.id); + } } - }); + map.off("moveend", this.updateSource); + map.off("data", this.onMapData); + map.off("error", this.onMapError); + map.removeSource(this.sourceId); + this.map = undefined; + } } /** * Clears all map event listeners setup by this instance. */ destroy() { - this.map.off("moveend", this.updateSource); - this.map.off("data", this.updateSource); - this.map.off("error", this.updateSource); + matchMedia(this.resolution).removeListener(this.respondToResolutionChange); + if (this.map) { + this.removeFromMap(this.map); + } } getUrl() { + if (!this.map) { + throw new Error("Map not set"); + } + let url = new URL(this.options.url + "/export"); + url.searchParams.set("f", "image"); + url.searchParams.set("transparent", "true"); const bounds = this.map.getBounds(); // create bbox in web mercator let bbox = [ @@ -167,39 +287,39 @@ export class ArcGISDynamicMapService { lat2meters(bounds.getNorth()), ]; const groundResolution = getGroundResolution(this.map.getZoom() + - (this.supportDevicePixelRatio ? window.devicePixelRatio - 1 : 0)); + (this.options.supportHighDpiDisplays ? window.devicePixelRatio - 1 : 0)); // Width and height can't be based on container width if the map is rotated const width = Math.round((bbox[2] - bbox[0]) / groundResolution); const height = Math.round((bbox[3] - bbox[1]) / groundResolution); - this.url.searchParams.set("format", "png"); - this.url.searchParams.set("size", [width, height].join(",")); - if (this.supportDevicePixelRatio) { + url.searchParams.set("format", "png"); + url.searchParams.set("size", [width, height].join(",")); + if (this.options.supportHighDpiDisplays) { switch (window.devicePixelRatio) { case 1: // standard pixelRatio looks best at 96 - this.url.searchParams.set("dpi", "96"); + url.searchParams.set("dpi", "96"); break; case 2: // for higher pixelRatios, esri's software seems to like the dpi // bumped up somewhat higher than a simple formula would suggest - this.url.searchParams.set("dpi", "220"); + url.searchParams.set("dpi", "220"); break; case 3: - this.url.searchParams.set("dpi", "390"); + url.searchParams.set("dpi", "390"); break; default: - this.url.searchParams.set("dpi", + url.searchParams.set("dpi", // Bumping pixel ratio a bit. see above (window.devicePixelRatio * 96 * 1.22).toString()); break; } } else { - this.url.searchParams.set("dpi", "96"); + url.searchParams.set("dpi", "96"); } // Default to epsg:3857 - this.url.searchParams.set("imageSR", "102100"); - this.url.searchParams.set("bboxSR", "102100"); + url.searchParams.set("imageSR", "102100"); + url.searchParams.set("bboxSR", "102100"); // If the map extent crosses the meridian, we need to create a new // projection and map the x coordinates to that space. The Esri JS API // exhibits this same behavior. Solution was inspired by: @@ -207,7 +327,7 @@ export class ArcGISDynamicMapService { // * https://gist.github.com/perrygeo/4478844 if (Math.abs(bbox[0]) > 20037508.34 || Math.abs(bbox[2]) > 20037508.34) { const centralMeridian = bounds.getCenter().lng; - if (this.supportDevicePixelRatio && window.devicePixelRatio > 1) { + if (this.options.supportHighDpiDisplays && window.devicePixelRatio > 1) { bbox[0] = -(width * groundResolution) / (window.devicePixelRatio * 2); bbox[2] = (width * groundResolution) / (window.devicePixelRatio * 2); } @@ -218,25 +338,25 @@ export class ArcGISDynamicMapService { const sr = JSON.stringify({ wkt: `PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",${centralMeridian}],PARAMETER["Standard_Parallel_1",0.0],PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0]]`, }); - this.url.searchParams.set("imageSR", sr); - this.url.searchParams.set("bboxSR", sr); + url.searchParams.set("imageSR", sr); + url.searchParams.set("bboxSR", sr); } if (Array.isArray(this.layers)) { if (this.layers.length === 0) { return blankDataUri; } else { - this.url.searchParams.set("layers", `show:${this.layers.map((lyr) => lyr.sublayer).join(",")}`); + url.searchParams.set("layers", `show:${this.layers.map((lyr) => lyr.id).join(",")}`); } } - this.url.searchParams.set("bbox", bbox.join(",")); - this.url.searchParams.delete("dynamicLayers"); + url.searchParams.set("bbox", bbox.join(",")); + url.searchParams.delete("dynamicLayers"); let layersInOrder = true; let hasOpacityUpdates = false; if (this.supportsDynamicLayers && this.layers) { for (var i = 0; i < this.layers.length; i++) { if (this.layers[i - 1] && - this.layers[i].sublayer < this.layers[i - 1].sublayer) { + parseInt(this.layers[i].id) < parseInt(this.layers[i - 1].id)) { layersInOrder = false; } const opacity = this.layers[i].opacity; @@ -249,9 +369,9 @@ export class ArcGISDynamicMapService { // need to provide renderInfo const dynamicLayers = this.layers.map((lyr) => { return { - id: lyr.sublayer, + id: lyr.id, source: { - mapLayerId: lyr.sublayer, + mapLayerId: lyr.id, type: "mapLayer", }, drawingInfo: { @@ -259,28 +379,34 @@ export class ArcGISDynamicMapService { }, }; }); - this.url.searchParams.set("dynamicLayers", JSON.stringify(dynamicLayers)); + url.searchParams.set("dynamicLayers", JSON.stringify(dynamicLayers)); } - for (const key in this.queryParameters) { - this.url.searchParams.set(key, this.queryParameters[key].toString()); + for (const key in this.options.queryParameters) { + url.searchParams.set(key, this.options.queryParameters[key].toString()); } - if (this.useTiles) { - this.url.searchParams.set("bbox", `seasketch-replace-me`); - if (this.supportDevicePixelRatio && window.devicePixelRatio > 1) { - const size = this.tileSize * window.devicePixelRatio; - this.url.searchParams.set("size", [size, size].join(",")); + const tileSize = this.options.tileSize || 256; + if (this.options.useTiles) { + url.searchParams.set("bbox", `seasketch-replace-me`); + if (this.options.supportHighDpiDisplays && window.devicePixelRatio > 1) { + const size = tileSize * window.devicePixelRatio; + url.searchParams.set("size", [size, size].join(",")); } else { - this.url.searchParams.set("size", [this.tileSize, this.tileSize].join(",")); + url.searchParams.set("size", [tileSize, tileSize].join(",")); } } - return this.url - .toString() - .replace("seasketch-replace-me", "{bbox-epsg-3857}"); + return url.toString().replace("seasketch-replace-me", "{bbox-epsg-3857}"); } /** Whether a source image is currently being fetched over the network */ get loading() { - return this._loading; + var _a; + const source = (_a = this.map) === null || _a === void 0 ? void 0 : _a.getSource(this.sourceId); + if (source && source.type === "raster") { + return this.map.isSourceLoaded(this.sourceId) === false; + } + else { + return this._loading; + } } /** * Update the list of sublayers and re-render the the map. If @@ -324,8 +450,9 @@ export class ArcGISDynamicMapService { */ updateQueryParameters(queryParameters) { // do a deep comparison of layers to detect whether there are any changes - if (JSON.stringify(this.queryParameters) !== JSON.stringify(queryParameters)) { - this.queryParameters = queryParameters; + if (JSON.stringify(this.options.queryParameters) !== + JSON.stringify(queryParameters)) { + this.options.queryParameters = queryParameters; this.debouncedUpdateSource(); } } @@ -336,11 +463,23 @@ export class ArcGISDynamicMapService { * @param enable */ updateUseDevicePixelRatio(enable) { - if (enable !== this.supportDevicePixelRatio) { - this.supportDevicePixelRatio = enable; + if (enable !== this.options.supportHighDpiDisplays) { + this.options.supportHighDpiDisplays = enable; this.debouncedUpdateSource(); } } + async getGLStyleLayers() { + return [ + { + id: uuid(), + type: "raster", + source: this.sourceId, + paint: { + "raster-fade-duration": this.options.useTiles ? 300 : 0, + }, + }, + ]; + } } /** @hidden */ function lat2meters(lat) { diff --git a/packages/mapbox-gl-esri-sources/index.ts b/packages/mapbox-gl-esri-sources/index.ts index 7d5769ac7..4fef39f09 100644 --- a/packages/mapbox-gl-esri-sources/index.ts +++ b/packages/mapbox-gl-esri-sources/index.ts @@ -8,16 +8,19 @@ import { fetchFeatureLayerData, } from "./src/ArcGISVectorSource"; import { ArcGISRESTServiceRequestManager } from "./src/ArcGISRESTServiceRequestManager"; -import { +export { ArcGISTiledMapService, ArcGISTiledMapServiceOptions, } from "./src/ArcGISTiledMapService"; +export { MapServiceMetadata } from "./src/ServiceMetadata"; export { CustomGLSource, CustomGLSourceOptions, DynamicRenderingSupportOptions, LegendItem, SingleImageLegend, + DataTableOfContentsItem, + FolderTableOfContentsItem, } from "./src/CustomGLSource"; export { ArcGISDynamicMapService, @@ -25,8 +28,6 @@ export { ArcGISDynamicMapServiceOptions, ArcGISVectorSourceOptions, ArcGISRESTServiceRequestManager, - ArcGISTiledMapService, - ArcGISTiledMapServiceOptions, }; export { default as styleForFeatureLayer } from "./src/styleForFeatureLayer"; export { ImageList } from "./src/ImageList"; diff --git a/packages/mapbox-gl-esri-sources/src/ArcGISDynamicMapService.ts b/packages/mapbox-gl-esri-sources/src/ArcGISDynamicMapService.ts index e6eb27281..19545f35c 100644 --- a/packages/mapbox-gl-esri-sources/src/ArcGISDynamicMapService.ts +++ b/packages/mapbox-gl-esri-sources/src/ArcGISDynamicMapService.ts @@ -1,41 +1,42 @@ +import { Map, ImageSource, RasterSource, AnyLayer } from "mapbox-gl"; import { - Map, - LngLatBounds, - AnySourceImpl, - ImageSource, - RasterSource, -} from "mapbox-gl"; + ComputedMetadata, + CustomGLSource, + CustomGLSourceOptions, + DynamicRenderingSupportOptions, + LegendItem, + OrderedLayerSettings, +} from "./CustomGLSource"; +import { v4 as uuid } from "uuid"; +import { ArcGISRESTServiceRequestManager } from "./ArcGISRESTServiceRequestManager"; +import { LayersMetadata, MapServiceMetadata } from "./ServiceMetadata"; +import { + contentOrFalse, + extentToLatLngBounds, + generateMetadataForLayer, + makeLegend, +} from "./utils"; /** @hidden */ -const blankDataUri = +export const blankDataUri = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; -export interface SublayerState { - /** 0-based sublayer index */ - sublayer: number; - /** sublayer opacity from 0.0 - 1.0 */ - opacity?: number; -} - -export interface ArcGISDynamicMapServiceOptions { +export interface ArcGISDynamicMapServiceOptions extends CustomGLSourceOptions { + /** + * URL for the service. Should end in /MapServer + */ + url: string; /** * Fetch larger images for high-resolution devices. * @default true * */ - useDevicePixelRatio?: boolean; + supportHighDpiDisplays?: boolean; /** * List of sublayers to display, in order. Order will be respected only if - * `supportsDynamicLayers` is true. If left undefined the service will be + * `supportsDynamicRendering` is true. If left undefined the service will be * with the default layers. * */ - layers?: SublayerState[]; - /** - * Set true if the Map Service supports [dynamic layers](https://developers.arcgis.com/rest/services-reference/export-map.htm#GUID-E781BA37-0260-485E-BB21-CA9B85206AAE) - * , in which case sublayer order and opacity can be specified. If set false - * any order and opacity settings will be ignored. - * @default false - * */ - supportsDynamicLayers?: boolean; + layers?: OrderedLayerSettings; /** * All query parameters will be added to each MapServer export request, * overriding any settings made by this library. Useful for specifying image @@ -50,127 +51,243 @@ export interface ArcGISDynamicMapServiceOptions { */ useTiles?: boolean; /** - * Tile size in pixels. Only used if `useTiles` is true. - * @default 256 - * */ + * 256 or 512 would be most appropriate. default is 256 + */ tileSize?: number; + credentials?: { username: string; password: string }; } -/** - * Add an Esri Dynamic Map Service as an image source to a MapBox GL JS map, and - * use the included methods to update visible sublayers, set layer order and - * opacity, support high-dpi screens, and transparently deal with issues related - * to crossing the central meridian. - * - * ```typescript - * import { ArcGISDynamicMapService } from "mapbox-gl-esri-sources"; - * - * // ... setup your map - * - * const populatedPlaces = new ArcGISDynamicMapService( - * map, - * "populated-places-source", - * "https://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServer", { - * supportsDynamicLayers: true, - * sublayers: [ - * { sublayer: 0, opacity: 1 }, - * { sublayer: 1, opacity: 1 }, - * { sublayer: 2, opacity: 0.5 }, - * ], - * queryParameters: { - * format: 'png32' - * } - * } - * }); - * - * // Don't forget to add a layer to reference your source - * map.addLayer({ - * id: "ags-layer", - * type: "raster", - * source: populatedPlaces.id, - * paint: { - * "raster-fade-duration": 0, - * "raster-opacity": 0.9 - * }, - * }); - * - * // turn off the third sublayer and update opacity - * populatedPlaces.updateLayers([ - * { sublayer: 0, opacity: 0.5 }, - * { sublayer: 1, opacity: 1 }, - * ]); - * - * // disable high-dpi screen support - * populatedPlaces.updateUseDevicePixelRatio(false); - * ``` - * @class ArcGISDynamicMapService - */ -export class ArcGISDynamicMapService { +export class ArcGISDynamicMapService + implements CustomGLSource +{ /** Source id used in the map style */ - id: string; - private baseUrl: string; - private url: URL; - private map: Map; - private layers?: SublayerState[]; - private queryParameters: { [queryString: string]: number | string }; - private source: ImageSource | RasterSource; - private supportDevicePixelRatio: boolean = true; + sourceId: string; + private map?: Map; + private requestManager: ArcGISRESTServiceRequestManager; + private serviceMetadata?: MapServiceMetadata; + private layerMetadata?: LayersMetadata; + private options: ArcGISDynamicMapServiceOptions; + + private layers?: OrderedLayerSettings; private supportsDynamicLayers = false; private debounceTimeout?: NodeJS.Timeout; private _loading = true; - private useTiles = false; - private tileSize = 256; + private resolution?: string; // TODO: fetch metadata and calculate minzoom, maxzoom, and bounds /** - * @param {Map} map MapBox GL JS Map instance - * @param {string} id ID to be used when adding refering to this source from layers + * @param {string} sourceId ID to be used when adding refering to this source from layers * @param {string} baseUrl Location of the service. Should end in /MapServer * @param {ArcGISDynamicMapServiceOptions} [options] */ constructor( - map: Map, - id: string, - baseUrl: string, - options?: ArcGISDynamicMapServiceOptions + requestManager: ArcGISRESTServiceRequestManager, + options: ArcGISDynamicMapServiceOptions ) { - this.id = id; - this.baseUrl = baseUrl; - this.url = new URL(this.baseUrl + "/export"); - this.url.searchParams.set("f", "image"); - this.map = map; - if (!options?.useTiles) { - this.map.on("moveend", this.updateSource); + this.options = options; + this.requestManager = requestManager; + this.sourceId = options?.sourceId || uuid(); + // remove trailing slash if present + options.url = options.url.replace(/\/$/, ""); + if (!/rest\/services/.test(options.url) || !/MapServer/.test(options.url)) { + throw new Error("Invalid ArcGIS REST Service URL"); } - this.layers = options?.layers; - this.useTiles = options?.useTiles || false; - this.tileSize = options?.tileSize || 256; - this.queryParameters = { - transparent: "true", - ...(options?.queryParameters || {}), - }; - if (options && "useDevicePixelRatio" in options) { - this.supportDevicePixelRatio = !!options.useDevicePixelRatio; + this.resolution = `(resolution: ${window.devicePixelRatio}dppx)`; + matchMedia(this.resolution).addListener(this.respondToResolutionChange); + } + + private respondToResolutionChange = () => { + if (this.options.supportHighDpiDisplays) { + this.updateSource(); + } + if (this.resolution) { + matchMedia(this.resolution).removeListener( + this.respondToResolutionChange + ); } + this.resolution = `(resolution: ${window.devicePixelRatio}dppx)`; + matchMedia(this.resolution).addListener(this.respondToResolutionChange); + }; - matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`).addListener( - () => { - if (this.supportDevicePixelRatio) { - this.updateSource(); - } + /** + * Use ArcGISRESTServiceRequestManager to fetch metadata for the service, + * caching it on the instance for reuse. + */ + private getMetadata() { + if (this.serviceMetadata && this.layerMetadata) { + return Promise.resolve({ + serviceMetadata: this.serviceMetadata, + layers: this.layerMetadata, + }); + } else { + return this.requestManager + .getMapServiceMetadata(this.options.url, { + credentials: this.options.credentials, + }) + .then(({ serviceMetadata, layers }) => { + this.serviceMetadata = serviceMetadata; + this.layerMetadata = layers; + this.supportsDynamicLayers = serviceMetadata.supportsDynamicLayers; + return { serviceMetadata, layers }; + }); + } + } + + /** + * Returns computed metadata for the service, including bounds, minzoom, maxzoom, and attribution. + * @returns ComputedMetadata + * @throws Error if metadata is not available + * @throws Error if tileInfo is not available + * */ + async getComputedMetadata(): Promise { + const { serviceMetadata, layers } = await this.getMetadata(); + const { bounds, minzoom, maxzoom, attribution } = + await this.getComputedProperties(); + + const results = /\/.+\/MapServer/.exec(this.options.url); + let label = results ? results[0] : false; + if (!label) { + if (this.layerMetadata?.layers?.[0]) { + label = this.layerMetadata.layers[0].name; } + } + const legendData = await this.requestManager.getLegendMetadata( + this.options.url ); - this.supportsDynamicLayers = options?.supportsDynamicLayers || false; - const bounds = this.map.getBounds(); - if (this.useTiles) { - this.map.addSource(this.id, { + // find hidden layers + // Not as simple as just reading the defaultVisibility property, because + // if a parent layer is hidden, all children are hidden as well. Folders can + // be nested arbitrarily deep. + const hiddenIds = new Set(); + for (const layer of layers.layers) { + if (!layer.defaultVisibility) { + hiddenIds.add(layer.id); + } else { + // check if parents are hidden + if (layer.parentLayer) { + if (hiddenIds.has(layer.parentLayer.id)) { + hiddenIds.add(layer.id); + } else { + // may not be added yet + const parent = layers.layers.find( + (l) => l.id === layer.parentLayer?.id + ); + if (parent && !parent.defaultVisibility) { + hiddenIds.add(layer.id); + hiddenIds.add(parent.id); + } + } + } + } + } + + return { + bounds: bounds || undefined, + minzoom, + maxzoom, + attribution, + tableOfContentsItems: layers.layers.map((lyr) => { + const legendLayer = legendData.layers.find((l) => l.layerId === lyr.id); + const isFolder = lyr.type === "Group Layer"; + if (isFolder) { + return { + type: "folder", + id: lyr.id.toString(), + label: lyr.name, + defaultVisibility: hiddenIds.has(lyr.id) + ? false + : lyr.defaultVisibility, + parentId: lyr.parentLayer + ? lyr.parentLayer.id.toString() + : undefined, + }; + } else { + return { + type: "data", + id: lyr.id.toString(), + label: lyr.name, + defaultVisibility: hiddenIds.has(lyr.id) + ? false + : lyr.defaultVisibility, + metadata: generateMetadataForLayer( + this.options.url + "/" + lyr.id, + this.serviceMetadata!, + lyr + ), + parentId: lyr.parentLayer + ? lyr.parentLayer.id.toString() + : undefined, + legend: makeLegend(legendData, lyr.id), + }; + } + }), + supportsDynamicRendering: { + layerOpacity: this.supportsDynamicLayers, + layerOrder: true, + layerVisibility: true, + }, + }; + } + + /** + * Private method used as the basis for getComputedMetadata and also used + * when generating the source data for addToMap. + * @returns Computed properties for the service, including bounds, minzoom, maxzoom, and attribution. + */ + private async getComputedProperties() { + const { serviceMetadata, layers } = await this.getMetadata(); + const levels = serviceMetadata.tileInfo?.lods.map((l) => l.level) || []; + const attribution = + contentOrFalse(layers.layers[0].copyrightText) || + contentOrFalse(serviceMetadata.copyrightText) || + contentOrFalse(serviceMetadata.documentInfo?.Author) || + undefined; + const minzoom = Math.min(...levels); + const maxzoom = Math.max(...levels); + return { + minzoom, + maxzoom, + bounds: await extentToLatLngBounds(serviceMetadata.fullExtent), + attribution, + }; + } + + private onMapData = (event: mapboxgl.MapDataEvent & mapboxgl.EventData) => { + if (event.sourceId && event.sourceId === this.sourceId) { + this._loading = false; + } + }; + + private onMapError = (event: mapboxgl.ErrorEvent & mapboxgl.EventData) => { + if ( + event.sourceId === this.sourceId && + event.dataType === "source" && + event.sourceDataType === "content" + ) { + this._loading = false; + } + }; + + async addToMap(map: Map) { + const { attribution, bounds } = await this.getComputedProperties(); + this.map = map; + if (!this.options?.useTiles) { + this.map.on("moveend", this.updateSource); + this.map.on("data", this.onMapData); + this.map.on("error", this.onMapError); + } + if (this.options.useTiles) { + this.map.addSource(this.sourceId, { type: "raster", tiles: [this.getUrl()], - tileSize: this.tileSize, + tileSize: this.options.tileSize || 256, + bounds: bounds as [number, number, number, number] | undefined, + attribution, }); } else { - this.map.addSource(this.id, { + const bounds = this.map.getBounds(); + this.map.addSource(this.sourceId, { type: "image", url: this.getUrl(), coordinates: [ @@ -181,33 +298,42 @@ export class ArcGISDynamicMapService { ], }); } - this.source = this.map.getSource(this.id) as ImageSource; - this.map.on("data", (event) => { - if ( - event.sourceId === this.id && - event.dataType === "source" && - event.sourceDataType === "content" - ) { - this._loading = false; - } - }); - this.map.on("error", (event) => { - if (event.sourceId && event.sourceId === this.id) { - this._loading = false; + return this.sourceId; + } + + removeFromMap(map: Map) { + if (map.getSource(this.sourceId)) { + const layers = map.getStyle().layers || []; + for (const layer of layers) { + if ("source" in layer && layer.source === this.sourceId) { + map.removeLayer(layer.id); + } } - }); + map.off("moveend", this.updateSource); + map.off("data", this.onMapData); + map.off("error", this.onMapError); + map.removeSource(this.sourceId); + this.map = undefined; + } } /** * Clears all map event listeners setup by this instance. */ destroy() { - this.map.off("moveend", this.updateSource); - this.map.off("data", this.updateSource); - this.map.off("error", this.updateSource); + matchMedia(this.resolution!).removeListener(this.respondToResolutionChange); + if (this.map) { + this.removeFromMap(this.map); + } } private getUrl() { + if (!this.map) { + throw new Error("Map not set"); + } + let url = new URL(this.options.url + "/export"); + url.searchParams.set("f", "image"); + url.searchParams.set("transparent", "true"); const bounds = this.map.getBounds(); // create bbox in web mercator let bbox = [ @@ -218,30 +344,30 @@ export class ArcGISDynamicMapService { ]; const groundResolution = getGroundResolution( this.map.getZoom() + - (this.supportDevicePixelRatio ? window.devicePixelRatio - 1 : 0) + (this.options.supportHighDpiDisplays ? window.devicePixelRatio - 1 : 0) ); // Width and height can't be based on container width if the map is rotated const width = Math.round((bbox[2] - bbox[0]) / groundResolution); const height = Math.round((bbox[3] - bbox[1]) / groundResolution); - this.url.searchParams.set("format", "png"); - this.url.searchParams.set("size", [width, height].join(",")); - if (this.supportDevicePixelRatio) { + url.searchParams.set("format", "png"); + url.searchParams.set("size", [width, height].join(",")); + if (this.options.supportHighDpiDisplays) { switch (window.devicePixelRatio) { case 1: // standard pixelRatio looks best at 96 - this.url.searchParams.set("dpi", "96"); + url.searchParams.set("dpi", "96"); break; case 2: // for higher pixelRatios, esri's software seems to like the dpi // bumped up somewhat higher than a simple formula would suggest - this.url.searchParams.set("dpi", "220"); + url.searchParams.set("dpi", "220"); break; case 3: - this.url.searchParams.set("dpi", "390"); + url.searchParams.set("dpi", "390"); break; default: - this.url.searchParams.set( + url.searchParams.set( "dpi", // Bumping pixel ratio a bit. see above (window.devicePixelRatio * 96 * 1.22).toString() @@ -249,11 +375,11 @@ export class ArcGISDynamicMapService { break; } } else { - this.url.searchParams.set("dpi", "96"); + url.searchParams.set("dpi", "96"); } // Default to epsg:3857 - this.url.searchParams.set("imageSR", "102100"); - this.url.searchParams.set("bboxSR", "102100"); + url.searchParams.set("imageSR", "102100"); + url.searchParams.set("bboxSR", "102100"); // If the map extent crosses the meridian, we need to create a new // projection and map the x coordinates to that space. The Esri JS API // exhibits this same behavior. Solution was inspired by: @@ -261,7 +387,7 @@ export class ArcGISDynamicMapService { // * https://gist.github.com/perrygeo/4478844 if (Math.abs(bbox[0]) > 20037508.34 || Math.abs(bbox[2]) > 20037508.34) { const centralMeridian = bounds.getCenter().lng; - if (this.supportDevicePixelRatio && window.devicePixelRatio > 1) { + if (this.options.supportHighDpiDisplays && window.devicePixelRatio > 1) { bbox[0] = -(width * groundResolution) / (window.devicePixelRatio * 2); bbox[2] = (width * groundResolution) / (window.devicePixelRatio * 2); } else { @@ -271,31 +397,31 @@ export class ArcGISDynamicMapService { const sr = JSON.stringify({ wkt: `PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",${centralMeridian}],PARAMETER["Standard_Parallel_1",0.0],PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0]]`, }); - this.url.searchParams.set("imageSR", sr); - this.url.searchParams.set("bboxSR", sr); + url.searchParams.set("imageSR", sr); + url.searchParams.set("bboxSR", sr); } if (Array.isArray(this.layers)) { if (this.layers.length === 0) { return blankDataUri; } else { - this.url.searchParams.set( + url.searchParams.set( "layers", - `show:${this.layers.map((lyr) => lyr.sublayer).join(",")}` + `show:${this.layers.map((lyr) => lyr.id).join(",")}` ); } } - this.url.searchParams.set("bbox", bbox.join(",")); + url.searchParams.set("bbox", bbox.join(",")); - this.url.searchParams.delete("dynamicLayers"); + url.searchParams.delete("dynamicLayers"); let layersInOrder = true; let hasOpacityUpdates = false; if (this.supportsDynamicLayers && this.layers) { for (var i = 0; i < this.layers.length; i++) { if ( this.layers[i - 1] && - this.layers[i].sublayer < this.layers[i - 1].sublayer + parseInt(this.layers[i].id) < parseInt(this.layers[i - 1].id) ) { layersInOrder = false; } @@ -309,9 +435,9 @@ export class ArcGISDynamicMapService { // need to provide renderInfo const dynamicLayers = this.layers.map((lyr) => { return { - id: lyr.sublayer, + id: lyr.id, source: { - mapLayerId: lyr.sublayer, + mapLayerId: lyr.id, type: "mapLayer", }, drawingInfo: { @@ -320,49 +446,55 @@ export class ArcGISDynamicMapService { }, }; }); - this.url.searchParams.set("dynamicLayers", JSON.stringify(dynamicLayers)); + url.searchParams.set("dynamicLayers", JSON.stringify(dynamicLayers)); } - for (const key in this.queryParameters) { - this.url.searchParams.set(key, this.queryParameters[key].toString()); + for (const key in this.options.queryParameters) { + url.searchParams.set(key, this.options.queryParameters[key].toString()); } - if (this.useTiles) { - this.url.searchParams.set("bbox", `seasketch-replace-me`); - if (this.supportDevicePixelRatio && window.devicePixelRatio > 1) { - const size = this.tileSize * window.devicePixelRatio; - this.url.searchParams.set("size", [size, size].join(",")); + const tileSize = this.options.tileSize || 256; + if (this.options.useTiles) { + url.searchParams.set("bbox", `seasketch-replace-me`); + if (this.options.supportHighDpiDisplays && window.devicePixelRatio > 1) { + const size = tileSize * window.devicePixelRatio; + url.searchParams.set("size", [size, size].join(",")); } else { - this.url.searchParams.set( - "size", - [this.tileSize, this.tileSize].join(",") - ); + url.searchParams.set("size", [tileSize, tileSize].join(",")); } } - return this.url - .toString() - .replace("seasketch-replace-me", "{bbox-epsg-3857}"); + return url.toString().replace("seasketch-replace-me", "{bbox-epsg-3857}"); } /** Whether a source image is currently being fetched over the network */ get loading(): boolean { - return this._loading; + const source = this.map?.getSource(this.sourceId); + if (source && source.type === "raster") { + return this.map!.isSourceLoaded(this.sourceId) === false; + } else { + return this._loading; + } } private updateSource = () => { this._loading = true; - if (this.useTiles || this.source.type === "raster") { - // @ts-ignore - setTiles is in fact a valid method - this.source.setTiles([this.getUrl()]); - } else { - const bounds = this.map.getBounds(); - this.source.updateImage({ - url: this.getUrl(), - coordinates: [ - [bounds.getNorthWest().lng, bounds.getNorthWest().lat], - [bounds.getNorthEast().lng, bounds.getNorthEast().lat], - [bounds.getSouthEast().lng, bounds.getSouthEast().lat], - [bounds.getSouthWest().lng, bounds.getSouthWest().lat], - ], - }); + const source = this.map?.getSource(this.sourceId); + if (source && this.map) { + if (source.type === "raster") { + // @ts-ignore - setTiles is in fact a valid method + source.setTiles([this.getUrl()]); + } else if (source.type === "image") { + const bounds = this.map.getBounds(); + source.updateImage({ + url: this.getUrl(), + coordinates: [ + [bounds.getNorthWest().lng, bounds.getNorthWest().lat], + [bounds.getNorthEast().lng, bounds.getNorthEast().lat], + [bounds.getSouthEast().lng, bounds.getSouthEast().lat], + [bounds.getSouthWest().lng, bounds.getSouthWest().lat], + ], + }); + } else { + // do nothing, source isn't added + } } }; @@ -393,7 +525,7 @@ export class ArcGISDynamicMapService { * optional `opacity` props. * */ - updateLayers(layers: SublayerState[]) { + updateLayers(layers: OrderedLayerSettings) { // do a deep comparison of layers to detect whether there are any changes if (JSON.stringify(layers) !== JSON.stringify(this.layers)) { this.layers = layers; @@ -422,9 +554,10 @@ export class ArcGISDynamicMapService { }) { // do a deep comparison of layers to detect whether there are any changes if ( - JSON.stringify(this.queryParameters) !== JSON.stringify(queryParameters) + JSON.stringify(this.options.queryParameters) !== + JSON.stringify(queryParameters) ) { - this.queryParameters = queryParameters; + this.options.queryParameters = queryParameters; this.debouncedUpdateSource(); } } @@ -436,11 +569,24 @@ export class ArcGISDynamicMapService { * @param enable */ updateUseDevicePixelRatio(enable: boolean) { - if (enable !== this.supportDevicePixelRatio) { - this.supportDevicePixelRatio = enable; + if (enable !== this.options.supportHighDpiDisplays) { + this.options.supportHighDpiDisplays = enable; this.debouncedUpdateSource(); } } + + async getGLStyleLayers(): Promise { + return [ + { + id: uuid(), + type: "raster", + source: this.sourceId, + paint: { + "raster-fade-duration": this.options.useTiles ? 300 : 0, + }, + }, + ] as AnyLayer[]; + } } /** @hidden */ diff --git a/packages/mapbox-gl-esri-sources/src/ArcGISRESTServiceRequestManager.ts b/packages/mapbox-gl-esri-sources/src/ArcGISRESTServiceRequestManager.ts index d45d29df6..976891537 100644 --- a/packages/mapbox-gl-esri-sources/src/ArcGISRESTServiceRequestManager.ts +++ b/packages/mapbox-gl-esri-sources/src/ArcGISRESTServiceRequestManager.ts @@ -4,6 +4,24 @@ import { MapServiceMetadata, } from "./ServiceMetadata"; +interface UserCredentials { + username: string; + password: string; +} + +interface FetchOptions { + signal?: AbortSignal; + credentials?: UserCredentials; +} + +export interface MapServiceLegendMetadata { + layers: { + layerId: number; + layerName: string; + layerType: "Feature Layer" | "Raster Layer"; + legend: LayerLegendData[]; + }[]; +} export class ArcGISRESTServiceRequestManager { private cache?: Cache; @@ -17,10 +35,7 @@ export class ArcGISRESTServiceRequestManager { }); } - async getMapServiceMetadata( - url: string, - credentials?: { username: string; password: string } - ) { + async getMapServiceMetadata(url: string, options: FetchOptions) { if (!/rest\/services/.test(url)) { throw new Error("Invalid ArcGIS REST Service URL"); } @@ -33,16 +48,19 @@ export class ArcGISRESTServiceRequestManager { url = url.replace(/\?.*$/, ""); const params = new URLSearchParams(); params.set("f", "json"); - if (credentials) { + if (options?.credentials) { const token = await this.getToken( url.replace(/rest\/services\/.*/, "/rest/services/"), - credentials + options.credentials ); params.set("token", token); } const requestUrl = `${url}?${params.toString()}`; - const serviceMetadata = await this.fetch(requestUrl); + const serviceMetadata = await this.fetch( + requestUrl, + options?.signal + ); const layers = await this.fetch( `${url}/layers?${params.toString()}` ); @@ -52,6 +70,43 @@ export class ArcGISRESTServiceRequestManager { return { serviceMetadata, layers }; } + async getCatalogItems(url: string, options?: FetchOptions) { + if (!/rest\/services/.test(url)) { + throw new Error("Invalid ArcGIS REST Service URL"); + } + // remove trailing slash if present + url = url.replace(/\/$/, ""); + // remove url params if present + url = url.replace(/\?.*$/, ""); + const params = new URLSearchParams(); + params.set("f", "json"); + if (options?.credentials) { + const token = await this.getToken( + url.replace(/rest\/services\/.*/, "/rest/services/"), + options.credentials + ); + params.set("token", token); + } + + const requestUrl = `${url}?${params.toString()}`; + const response = await this.fetch<{ + currentVersion: number; + folders: string[]; + services: { + name: string; + type: + | "MapServer" + | "FeatureServer" + | "GPServer" + | "GeometryServer" + | "ImageServer" + | "GeocodeServer" + | string; + }[]; + }>(requestUrl, options?.signal); + return response; + } + async getToken( url: string, credentials: { username: string; password: string } @@ -61,7 +116,7 @@ export class ArcGISRESTServiceRequestManager { private inFlightRequests: { [url: string]: Promise } = {}; - private async fetch(url: string) { + private async fetch(url: string, signal?: AbortSignal) { if (url in this.inFlightRequests) { return this.inFlightRequests[url].then((json) => json as T); } @@ -69,7 +124,7 @@ export class ArcGISRESTServiceRequestManager { if (!cache) { throw new Error("Cache not initialized"); } - this.inFlightRequests[url] = fetchWithTTL(url, 60 * 300, cache); + this.inFlightRequests[url] = fetchWithTTL(url, 60 * 300, cache, { signal }); return new Promise((resolve, reject) => { this.inFlightRequests[url] .then((json) => { @@ -111,14 +166,7 @@ export class ArcGISRESTServiceRequestManager { } const requestUrl = `${url}/legend?${params.toString()}`; - const response = await this.fetch<{ - layers: { - layerId: number; - layerName: string; - layerType: "Feature Layer" | "Raster Layer"; - legend: LayerLegendData[]; - }[]; - }>(requestUrl); + const response = await this.fetch(requestUrl); return response; } } diff --git a/packages/mapbox-gl-esri-sources/src/ArcGISTiledMapService.ts b/packages/mapbox-gl-esri-sources/src/ArcGISTiledMapService.ts index 01fa01522..a76108b20 100644 --- a/packages/mapbox-gl-esri-sources/src/ArcGISTiledMapService.ts +++ b/packages/mapbox-gl-esri-sources/src/ArcGISTiledMapService.ts @@ -5,6 +5,7 @@ import { CustomGLSource, CustomGLSourceOptions, LegendItem, + OrderedLayerSettings, } from "./CustomGLSource"; import { v4 as uuid } from "uuid"; import { LayersMetadata, MapServiceMetadata } from "./ServiceMetadata"; @@ -12,8 +13,10 @@ import { contentOrFalse, extentToLatLngBounds, generateMetadataForLayer, + makeLegend, replaceSource, } from "./utils"; +import { blankDataUri } from "./ArcGISDynamicMapService"; export interface ArcGISTiledMapServiceOptions extends CustomGLSourceOptions { url: string; @@ -68,7 +71,9 @@ export class ArcGISTiledMapService }); } else { return this.requestManager - .getMapServiceMetadata(this.options.url, this.options.credentials) + .getMapServiceMetadata(this.options.url, { + credentials: this.options.credentials, + }) .then(({ serviceMetadata, layers }) => { this.serviceMetadata = serviceMetadata; this.layerMetadata = layers; @@ -87,17 +92,41 @@ export class ArcGISTiledMapService const { serviceMetadata, layers } = await this.getMetadata(); const { bounds, minzoom, maxzoom, tileSize, attribution } = await this.getComputedProperties(); + const legendData = await this.requestManager.getLegendMetadata( + this.options.url + ); + const results = /\/([^/]+)\/MapServer/.exec(this.options.url); + let label = results && results.length >= 1 ? results[1] : false; + if (!label) { + if (this.layerMetadata?.layers?.[0]) { + label = this.layerMetadata.layers[0].name; + } + } return { bounds: bounds || undefined, minzoom, maxzoom, attribution, - metadata: generateMetadataForLayer( - this.options.url, - this.serviceMetadata!, - this.layerMetadata!.layers[0] - ), + tableOfContentsItems: [ + { + type: "data", + id: this.sourceId, + label: label || "Layer", + defaultVisibility: true, + metadata: generateMetadataForLayer( + this.options.url, + this.serviceMetadata!, + this.layerMetadata!.layers[0] + ), + legend: makeLegend(legendData, legendData.layers[0].layerId), + }, + ], + supportsDynamicRendering: { + layerOrder: false, + layerOpacity: false, + layerVisibility: false, + }, }; } @@ -112,15 +141,6 @@ export class ArcGISTiledMapService ); } - async getLegend(): Promise { - const data = await this.requestManager.getLegendMetadata(this.options.url); - return data.layers[0].legend.map((l) => ({ - id: l.url, - label: l.label, - imageUrl: `data:${l.contentType};base64,${l.imageData}`, - })); - } - /** * Private method used as the basis for getComputedMetadata and also used * when generating the source data for addToMap. @@ -168,7 +188,6 @@ export class ArcGISTiledMapService attribution, ...(bounds ? { bounds } : {}), } as RasterSource; - console.log("add to map", sourceData); // It's possible that the map has changed since we started fetching metadata if (this.map.getSource(this.sourceId)) { replaceSource(this.sourceId, this.map, sourceData); @@ -182,7 +201,7 @@ export class ArcGISTiledMapService * Returns a raster layer for the source. * @returns RasterLayer[] */ - async getLayers() { + async getGLStyleLayers() { return [ { type: "raster", @@ -200,7 +219,7 @@ export class ArcGISTiledMapService * source, they will also be removed. * @param map Mapbox GL JS Map */ - removeFromMap(map: Map, removeLayers?: boolean) { + removeFromMap(map: Map) { if (map.getSource(this.sourceId)) { const layers = map.getStyle().layers || []; for (const layer of layers) { @@ -213,20 +232,6 @@ export class ArcGISTiledMapService } } - /** - * Returns an object with booleans indicating whether the source supports - * dynamic rendering of layer order and opacity. Always returns false for - * this service type. - * @returns DynamicRenderingSupportOptions - * @throws Error if metadata is not available - */ - async getSupportsDynamicRendering() { - return { - layerOrder: false, - layerOpacity: false, - }; - } - /** * Removes the source from the map and removes any event listeners */ @@ -235,4 +240,7 @@ export class ArcGISTiledMapService this.removeFromMap(this.map); } } + + /** noop */ + updateLayers(layers: OrderedLayerSettings) {} } diff --git a/packages/mapbox-gl-esri-sources/src/CustomGLSource.ts b/packages/mapbox-gl-esri-sources/src/CustomGLSource.ts index 1a1657b5f..54b8934ce 100644 --- a/packages/mapbox-gl-esri-sources/src/CustomGLSource.ts +++ b/packages/mapbox-gl-esri-sources/src/CustomGLSource.ts @@ -1,18 +1,26 @@ -import { AnyLayer, AnySourceData, Map } from "mapbox-gl"; -import { ArcGISRESTServiceRequestManager } from "./ArcGISRESTServiceRequestManager"; +import { AnyLayer, Map } from "mapbox-gl"; export interface CustomGLSourceOptions { /** Optional. If not provided a uuid will be used. */ sourceId?: string; } +/** + * LegendItems are assumed to be static and need not be refreshed after updating + * layer visibility. + */ export interface LegendItem { /** Use for jsx key, no more */ id: string; label: string; imageUrl: string; + imageWidth?: number; + imageHeight?: number; } +/** + * SingleImage legends should be updated whenever layer visibility changes + */ export interface SingleImageLegend { url: string; } @@ -20,21 +28,46 @@ export interface SingleImageLegend { export interface DynamicRenderingSupportOptions { layerOrder: boolean; layerOpacity: boolean; + layerVisibility: boolean; +} + +export interface FolderTableOfContentsItem { + type: "folder"; + id: string; + label: string; + defaultVisibility: boolean; + parentId?: string; } +export interface DataTableOfContentsItem { + type: "data"; + id: string; + label: string; + defaultVisibility: boolean; + /** Metadata as prosemirror document */ + metadata: { + type: string; + content: ({ type: string } & any)[]; + }; + legend?: LegendItem[]; + parentId?: string; +} export interface ComputedMetadata { /** xmin, ymin, xmax, ymax */ bounds?: [number, number, number, number]; minzoom: number; maxzoom: number; attribution?: string; - /** Metadata as prosemirror document */ - metadata: { - type: string; - content: ({ type: string } & any)[]; - }; + tableOfContentsItems: (FolderTableOfContentsItem | DataTableOfContentsItem)[]; + supportsDynamicRendering: DynamicRenderingSupportOptions; } +export interface LayerSettings { + id: string; + opacity: number; +} + +export type OrderedLayerSettings = LayerSettings[]; /** * CustomGLSources are used to add custom data sources to a Mapbox GL JS map. * Used to support ArcGIS, WMS, (and other?) sources using SeaSketch's @@ -69,19 +102,15 @@ export interface CustomGLSource< * @throws If the source is not on the map */ removeFromMap(map: Map): void; - // /** - // * Get metadata for the source. Requests should be de-duped and cached. - // * @returns Metadata for the source - // */ - // getMetadata(): Promise; - /** - * Whether the source supports dynamic rendering. If true, clients can use - * the updateLayers method to update source data. - */ - getSupportsDynamicRendering(): Promise; /** Removes the source from the map and removes any event listeners */ destroy(): void; - getLegend(): Promise; - getLayers(): Promise; + /** + * If provided, this source uses a single image to represent all sublayers in + * the legend. It will need to be updated with a new image each time rendering + * options are changed. + * */ + getLegend?(): Promise; + getGLStyleLayers(): Promise; getComputedMetadata(): Promise; + updateLayers(layers: OrderedLayerSettings): void; } diff --git a/packages/mapbox-gl-esri-sources/src/ServiceMetadata.d.ts b/packages/mapbox-gl-esri-sources/src/ServiceMetadata.d.ts index ccf36cd21..9dd0b73c2 100644 --- a/packages/mapbox-gl-esri-sources/src/ServiceMetadata.d.ts +++ b/packages/mapbox-gl-esri-sources/src/ServiceMetadata.d.ts @@ -76,11 +76,11 @@ export interface DetailedLayerMetadata { currentVersion: number; id: number; name: string; - type: "Feature Layer" | "Raster Layer"; + type: "Feature Layer" | "Raster Layer" | "Group Layer"; description: string; geometryType: esriGeometryType; copyrightText: string; - parentLayer: null | number; + parentLayer: null | { id: number; name: string }; sublayers: { id: number; name: string }[]; minScale: number; maxScale: number; diff --git a/packages/mapbox-gl-esri-sources/src/utils.ts b/packages/mapbox-gl-esri-sources/src/utils.ts index e98a6f1c7..9476474f3 100644 --- a/packages/mapbox-gl-esri-sources/src/utils.ts +++ b/packages/mapbox-gl-esri-sources/src/utils.ts @@ -5,6 +5,8 @@ import { MapServiceMetadata, } from "./ServiceMetadata"; import { SpatialReference } from "arcgis-rest-api"; +import { MapServiceLegendMetadata } from "./ArcGISRESTServiceRequestManager"; +import { blankDataUri } from "./ArcGISDynamicMapService"; /** * Replaced an existing source, preserving layers and their order by temporarily @@ -78,7 +80,6 @@ export async function extentToLatLngBounds( } else { try { const projected = await projectExtent(extent); - console.log("projected", projected, extent); return [projected.xmin, projected.ymin, projected.xmax, projected.ymax]; } catch (e) { console.error(e); @@ -258,3 +259,28 @@ export function generateMetadataForLayer( ], }; } + +export function makeLegend(data: MapServiceLegendMetadata, layerId: number) { + const legendLayer = data.layers.find((l) => l.layerId === layerId); + if (legendLayer) { + return legendLayer.legend.map((legend) => { + return { + id: legend.url, + label: + legend.label && legend.label.length > 0 + ? legend.label + : legendLayer.legend.length === 1 + ? legendLayer.layerName + : "", + imageUrl: legend?.imageData + ? `data:${legend.contentType};base64,${legend.imageData}` + : blankDataUri, + imageWidth: 20, + imageHeight: 20, + }; + }); + } else { + return undefined; + // throw new Error(`Legend for layerId=${layerId} not found`); + } +}