diff --git a/packages/client/src/admin/data/TableOfContentsEditor.tsx b/packages/client/src/admin/data/TableOfContentsEditor.tsx index c3215789..4c11b6e5 100644 --- a/packages/client/src/admin/data/TableOfContentsEditor.tsx +++ b/packages/client/src/admin/data/TableOfContentsEditor.tsx @@ -151,6 +151,7 @@ export default function TableOfContentsEditor() { ) { contextMenuOptions.push({ id: "zoom-to", + disabled: !item.bounds && !checkedItems.includes(item.stableId), label: t("Zoom to bounds"), onClick: async () => { let bounds: [number, number, number, number] | undefined; @@ -270,6 +271,7 @@ export default function TableOfContentsEditor() { t, tocQuery, layersAndSources.data, + checkedItems, ] ); diff --git a/packages/client/src/admin/data/arcgis/ArcGISCartModal.tsx b/packages/client/src/admin/data/arcgis/ArcGISCartModal.tsx index 2d049f9b..7c841acb 100644 --- a/packages/client/src/admin/data/arcgis/ArcGISCartModal.tsx +++ b/packages/client/src/admin/data/arcgis/ArcGISCartModal.tsx @@ -576,6 +576,7 @@ export default function ArcGISCartModal({ source as ArcGISFeatureLayerSource ).getFetchStrategy(); + console.log("fetch strategy", fetchStrategy); sources.push({ id: layer.id, type: ArcgisSourceType.ArcgisVector, diff --git a/packages/client/src/components/ContextMenuDropdown.tsx b/packages/client/src/components/ContextMenuDropdown.tsx index 4089cac2..a14cab3f 100644 --- a/packages/client/src/components/ContextMenuDropdown.tsx +++ b/packages/client/src/components/ContextMenuDropdown.tsx @@ -94,7 +94,11 @@ export default function ContextMenuDropdown({ onClick={onOptionClick(props)} className={classNames( "group", - "group-hover:bg-gray-100 group-hover:text-gray-900 text-gray-700", + `${ + props.disabled + ? "" + : "group-hover:bg-gray-100 group-hover:text-gray-900" + } text-gray-700`, "block px-4 py-2 text-sm w-full text-left", disabled ? "pointer-events-none opacity-50" : "" )} diff --git a/packages/client/src/dataLayers/MapContextManager.ts b/packages/client/src/dataLayers/MapContextManager.ts index 709465f9..fa51be16 100644 --- a/packages/client/src/dataLayers/MapContextManager.ts +++ b/packages/client/src/dataLayers/MapContextManager.ts @@ -784,7 +784,12 @@ class MapContextManager extends EventEmitter { async updateStyle() { if (this.map && this.internalState.ready) { + this.updateStyleInfiniteLoopDetector++; this.updateStyleInfiniteLoopDetector = 0; + if (this.updateStyleInfiniteLoopDetector > 10) { + this.updateStyleInfiniteLoopDetector = 0; + throw new Error("Infinite loop"); + } const { style, sprites } = await this.getComputedStyle(() => { this.debouncedUpdateStyle(); }); @@ -808,10 +813,6 @@ class MapContextManager extends EventEmitter { customSource.removeEventListeners(this.map); this.customSources[id].listenersAdded = false; } - // update sublayers - if (visible && sublayers !== undefined) { - customSource.updateLayers(sublayers); - } } this.pruneInactiveCustomSources(); this.setState((prev) => ({ ...prev, styleHash })); @@ -1315,10 +1316,8 @@ class MapContextManager extends EventEmitter { // add style if ready const { customSource, visible, listenersAdded } = this.customSources[source.id]; - + // Adding the source is skipped until later when sublayers are setup if (customSource.ready) { - baseStyle.sources[source.id.toString()] = - await customSource.getGLSource(); const styleData = await customSource.getGLStyleLayers(); if (styleData.imageList && this.map) { styleData.imageList.addToMap(this.map); @@ -1396,6 +1395,18 @@ class MapContextManager extends EventEmitter { for (const id in this.customSources) { if (!insertedCustomSourceIds.includes(parseInt(id))) { this.customSources[id].visible = false; + } else { + const { customSource, sublayers } = this.customSources[id]; + if (customSource.ready) { + // Update sublayers first so that sources that rely on a dynamically + // updated raster url can be initialized with proper data. + if (sublayers) { + customSource.updateLayers(sublayers); + } + const glSource = await customSource.getGLSource(this.map); + console.log(glSource); + baseStyle.sources[id] = glSource; + } } } diff --git a/packages/client/src/projects/OverlayLayers.tsx b/packages/client/src/projects/OverlayLayers.tsx index bff40207..014840b8 100644 --- a/packages/client/src/projects/OverlayLayers.tsx +++ b/packages/client/src/projects/OverlayLayers.tsx @@ -6,7 +6,10 @@ import { OverlayFragment, TableOfContentsItem, } from "../generated/graphql"; -import { MapContext } from "../dataLayers/MapContextManager"; +import { + MapContext, + sourceTypeIsCustomGLSource, +} from "../dataLayers/MapContextManager"; import TreeView, { TreeItem, useOverlayState } from "../components/TreeView"; import { DropdownDividerProps } from "../components/ContextMenuDropdown"; import { DropdownOption } from "../components/DropdownButton"; @@ -47,7 +50,8 @@ export default function OverlayLayers({ { id: "zoom-to", label: t("Zoom to bounds"), - onClick: () => { + disabled: !item.bounds && !checkedItems.includes(item.stableId), + onClick: async () => { let bounds: [number, number, number, number] | undefined; if (item.isFolder) { bounds = createBoundsRecursive(item, items); @@ -56,6 +60,22 @@ export default function OverlayLayers({ bounds = item.bounds.map((coord: string) => parseFloat(coord) ) as [number, number, number, number]; + } else { + const layer = layers?.find((l) => l.id === item.dataLayerId); + if (layer && layer.dataSourceId) { + const source = sources?.find( + (s) => s.id === layer.dataSourceId + ); + if (source && sourceTypeIsCustomGLSource(source.type)) { + const customSource = + mapContext.manager?.getCustomGLSource(source.id); + const metadata = + await customSource?.getComputedMetadata(); + if (metadata?.bounds) { + bounds = metadata.bounds; + } + } + } } } if ( @@ -89,7 +109,15 @@ export default function OverlayLayers({ return []; } }, - [items, mapContext.manager?.map, t, mapContext.manager] + [ + items, + mapContext.manager?.map, + t, + mapContext.manager, + layers, + sources, + checkedItems, + ] ); return ( diff --git a/packages/mapbox-gl-esri-sources/dist/bundle.js b/packages/mapbox-gl-esri-sources/dist/bundle.js index 096f8d22..e09573fd 100644 --- a/packages/mapbox-gl-esri-sources/dist/bundle.js +++ b/packages/mapbox-gl-esri-sources/dist/bundle.js @@ -785,44 +785,21 @@ var MapBoxGLEsriSources = (function () { source.setTiles([this.getUrl()]); } else if (source.type === "image") { - const bounds = this.map.getBounds(); - const 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 url = this.getUrl(); - if ( - source.url === url) { + const coordinates = this.getCoordinates(this.map); + const url = this.getUrl(this.map); + const currentUrl = source.url; + if (currentUrl === url) { + console.log("skipping, urls match", currentUrl, url); return; } - if (source.image) { - source.updateImage({ - url, - coordinates, - }); - } - else { - let tries = 0; - const updater = () => { - var _a; - const source = (_a = this.map) === null || _a === void 0 ? void 0 : _a.getSource(this.sourceId); - if (source && this.getUrl() === url) { - if (source.image && source.type === "image") { - source.updateImage({ - url, - coordinates, - }); - } - else if (tries < 10) { - tries++; - setTimeout(updater, 50); - } - } - }; - setTimeout(updater, 50); + console.log("updating image", url, source); + if (source.url === url) { + return; } + source.updateImage({ + url, + coordinates, + }); } else ; } @@ -964,46 +941,57 @@ var MapBoxGLEsriSources = (function () { attribution, }; } - async getGLSource() { + async getGLSource(map) { let { attribution, bounds } = await this.getComputedProperties(); - bounds = bounds || [-90, -180, 90, 180]; + bounds = bounds || [-89, -179, 89, 179]; if (this.options.useTiles) { return { type: "raster", - tiles: [this.getUrl()], + tiles: [this.getUrl(map)], tileSize: this.options.tileSize || 256, bounds: bounds, attribution, }; } else { - let coordinates = [ - [bounds[0], bounds[1]], - [bounds[2], bounds[1]], - [bounds[2], bounds[3]], - [bounds[0], bounds[3]], - ]; - if (this.map) { - const bounds = this.map.getBounds(); - 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 coordinates = this.getCoordinates(map); + const url = this.getUrl(map); + console.log("initializing with this url", url); return { type: "image", - url: this.getUrl(), + url, coordinates, }; } } + getCoordinates(map) { + const bounds = map.getBounds(); + const coordinates = [ + [ + Math.max(bounds.getNorthWest().lng, -179), + Math.min(bounds.getNorthWest().lat, 89), + ], + [ + Math.min(bounds.getNorthEast().lng, 179), + Math.min(bounds.getNorthEast().lat, 89), + ], + [ + Math.min(bounds.getSouthEast().lng, 179), + Math.max(bounds.getSouthEast().lat, -89), + ], + [ + Math.max(bounds.getSouthWest().lng, -179), + Math.max(bounds.getSouthWest().lat, -89), + ], + ]; + console.log(coordinates.join(",")); + return coordinates; + } async addToMap(map) { if (!map) { throw new Error("Map not provided to addToMap"); } - const sourceData = await this.getGLSource(); + const sourceData = await this.getGLSource(map); map.addSource(this.sourceId, sourceData); this.addEventListeners(map); return this.sourceId; @@ -1022,7 +1010,6 @@ var MapBoxGLEsriSources = (function () { map.on("moveend", this.updateSource); map.on("data", this.onMapData); map.on("error", this.onMapError); - this.updateSource(); } } } @@ -1057,21 +1044,23 @@ var MapBoxGLEsriSources = (function () { this.removeFromMap(this.map); } } - getUrl() { - if (!this.map) { + getUrl(map) { + var _a; + map = this.map || map; + if (!map) { return exports.blankDataUri; } - const bounds = this.map.getBounds(); let url = new URL(this.options.url + "/export"); url.searchParams.set("f", "image"); url.searchParams.set("transparent", "true"); + const coordinates = this.getCoordinates(map); let bbox = [ - lon2meters(bounds.getWest()), - lat2meters(bounds.getSouth()), - lon2meters(bounds.getEast()), - lat2meters(bounds.getNorth()), + lon2meters(coordinates[0][0]), + lat2meters(coordinates[2][1]), + lon2meters(coordinates[2][0]), + lat2meters(coordinates[0][1]), ]; - const groundResolution = getGroundResolution(this.map.getZoom() + + const groundResolution = getGroundResolution(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); @@ -1100,7 +1089,7 @@ var MapBoxGLEsriSources = (function () { 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; + const centralMeridian = (_a = this.map) === null || _a === void 0 ? void 0 : _a.getCenter().lng; if (this.options.supportHighDpiDisplays && window.devicePixelRatio > 1) { bbox[0] = -(width * groundResolution) / (window.devicePixelRatio * 2); bbox[2] = (width * groundResolution) / (window.devicePixelRatio * 2); @@ -1181,6 +1170,7 @@ var MapBoxGLEsriSources = (function () { } } updateLayers(layers) { + console.log("update layers", layers); if (JSON.stringify(layers) !== JSON.stringify(this.layers)) { this.layers = layers; this.debouncedUpdateSource(); @@ -1948,7 +1938,7 @@ var MapBoxGLEsriSources = (function () { var fetchData_1 = createCommonjsModule(function (module, exports) { Object.defineProperty(exports, "__esModule", { value: true }); - exports.fetchFeatureLayerData = exports.fetchFeatureCollection = void 0; + exports.urlForRawGeoJSONData = exports.fetchFeatureLayerData = exports.fetchFeatureCollection = void 0; function fetchFeatureCollection(url, geometryPrecision = 6, outFields = "*", bytesLimit = 1000000 * 100, abortController = null, disablePagination = false) { return new Promise((resolve, reject) => { fetchFeatureLayerData(url, outFields, reject, geometryPrecision, abortController, null, disablePagination, undefined, bytesLimit) @@ -1976,6 +1966,21 @@ var MapBoxGLEsriSources = (function () { return featureCollection; } exports.fetchFeatureLayerData = fetchFeatureLayerData; + function urlForRawGeoJSONData(baseUrl, outFields = "*", geometryPrecision = 6, queryOptions) { + const params = new URLSearchParams({ + inSR: "4326", + outSR: "4326", + where: "1>0", + outFields, + returnGeometry: "true", + geometryPrecision: geometryPrecision.toString(), + returnIdsOnly: "false", + f: "geojson", + ...(queryOptions || {}), + }); + return `${baseUrl}/query?${params.toString()}`; + } + exports.urlForRawGeoJSONData = urlForRawGeoJSONData; async function fetchData(baseUrl, params, featureCollection, onError, abortController, onPageReceived, disablePagination = false, pageSize = 1000, bytesLimit, bytesReceived, objectIdFieldName, expectedFeatureCount) { var _a; bytesReceived = bytesReceived || 0; @@ -2005,6 +2010,9 @@ var MapBoxGLEsriSources = (function () { else { featureCollection.features.push(...fc.features); if (fc.exceededTransferLimit || ((_a = fc.properties) === null || _a === void 0 ? void 0 : _a.exceededTransferLimit)) { + if (disablePagination) { + throw new Error("Exceeded transfer limit. Pagination disabled."); + } if (!objectIdFieldName) { params.set("returnIdsOnly", "true"); try { @@ -3945,7 +3953,14 @@ var MapBoxGLEsriSources = (function () { } } get loading() { - return this._loading; + var _a, _b; + if (this.options.fetchStrategy === "raw") { + 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); + } + else { + return this._loading; + } } async getGLStyleLayers() { if (this._glStylePromise) { @@ -3967,14 +3982,23 @@ var MapBoxGLEsriSources = (function () { } async getGLSource() { const { attribution } = await this.getComputedProperties(); - return { - type: "geojson", - data: this.featureData || { - type: "FeatureCollection", - features: this.featureData || [], - }, - attribution: attribution ? attribution : "", - }; + if (this.options.fetchStrategy === "raw") { + return { + type: "geojson", + data: (0, fetchData.urlForRawGeoJSONData)(this.options.url, "*", 6), + attribution: attribution ? attribution : "", + }; + } + else { + return { + type: "geojson", + data: this.featureData || { + type: "FeatureCollection", + features: this.featureData || [], + }, + attribution: attribution ? attribution : "", + }; + } } addEventListeners(map) { if (this.map && this.map === map) { @@ -3984,6 +4008,9 @@ var MapBoxGLEsriSources = (function () { this.removeEventListeners(map); } this.map = map; + if (this.options.fetchStrategy === "raw") { + return; + } this.QuantizedVectorRequestManager = (0, QuantizedVectorRequestManager.getOrCreateQuantizedVectorRequestManager)(map); this._loading = this.featureData ? false : true; @@ -3993,9 +4020,12 @@ var MapBoxGLEsriSources = (function () { } removeEventListeners(map) { var _a; + delete this.map; + if (this.options.fetchStrategy === "raw") { + return; + } (_a = this.QuantizedVectorRequestManager) === null || _a === void 0 ? void 0 : _a.off("update"); delete this.QuantizedVectorRequestManager; - delete this.map; } async addToMap(map) { const source = await this.getGLSource(); @@ -4037,8 +4067,9 @@ var MapBoxGLEsriSources = (function () { this.rawFeaturesHaveBeenFetched = true; } catch (e) { + console.log("caught error", e); let shouldFireError = true; - if (("message" in e && /bytesLimit/.test(e.message)) || + if (("message" in e && /limit/i.test(e.message)) || ((_d = (_c = this.abortController) === null || _c === void 0 ? void 0 : _c.signal) === null || _d === void 0 ? void 0 : _d.reason) === "timeout") { this.exceededBytesLimit = true; if (this.options.fetchStrategy === "auto") { @@ -4231,7 +4262,7 @@ var MapBoxGLEsriSources = (function () { } async getFetchStrategy() { if (this.options.fetchStrategy === "auto") { - if (this.featureData) { + if (this.rawFeaturesHaveBeenFetched) { return "raw"; } else if (this.options.fetchStrategy === "auto" && !this.error) { 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 fe80255c..60b972f5 100644 --- a/packages/mapbox-gl-esri-sources/dist/src/ArcGISDynamicMapService.d.ts +++ b/packages/mapbox-gl-esri-sources/dist/src/ArcGISDynamicMapService.d.ts @@ -81,7 +81,8 @@ export declare class ArcGISDynamicMapService implements CustomGLSource; + getGLSource(map: Map): Promise; + private getCoordinates; addToMap(map: Map): Promise; addEventListeners(map: Map): void; removeEventListeners(map: Map): void; diff --git a/packages/mapbox-gl-esri-sources/dist/src/ArcGISDynamicMapService.js b/packages/mapbox-gl-esri-sources/dist/src/ArcGISDynamicMapService.js index e946cd39..57e9df86 100644 --- a/packages/mapbox-gl-esri-sources/dist/src/ArcGISDynamicMapService.js +++ b/packages/mapbox-gl-esri-sources/dist/src/ArcGISDynamicMapService.js @@ -49,47 +49,24 @@ class ArcGISDynamicMapService { source.setTiles([this.getUrl()]); } else if (source.type === "image") { - const bounds = this.map.getBounds(); - const 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 url = this.getUrl(); - if ( + const coordinates = this.getCoordinates(this.map); + const url = this.getUrl(this.map); // @ts-ignore - source.url === url) { + const currentUrl = source.url; + if (currentUrl === url) { + console.log("skipping, urls match", currentUrl, url); return; } // @ts-ignore Using a private member here - if (source.image) { - source.updateImage({ - url, - coordinates, - }); - } - else { - let tries = 0; - const updater = () => { - var _a; - const source = (_a = this.map) === null || _a === void 0 ? void 0 : _a.getSource(this.sourceId); - if (source && this.getUrl() === url) { - // @ts-ignore - if (source.image && source.type === "image") { - source.updateImage({ - url, - coordinates, - }); - } - else if (tries < 10) { - tries++; - setTimeout(updater, 50); - } - } - }; - setTimeout(updater, 50); + console.log("updating image", url, source); + // @ts-ignore + if (source.url === url) { + return; } + source.updateImage({ + url, + coordinates, + }); } else { // do nothing, source isn't added @@ -255,47 +232,62 @@ class ArcGISDynamicMapService { attribution, }; } - async getGLSource() { + async getGLSource(map) { let { attribution, bounds } = await this.getComputedProperties(); - bounds = bounds || [-90, -180, 90, 180]; + bounds = bounds || [-89, -179, 89, 179]; if (this.options.useTiles) { return { type: "raster", - tiles: [this.getUrl()], + tiles: [this.getUrl(map)], tileSize: this.options.tileSize || 256, bounds: bounds, attribution, }; } else { - let coordinates = [ - [bounds[0], bounds[1]], - [bounds[2], bounds[1]], - [bounds[2], bounds[3]], - [bounds[0], bounds[3]], - ]; - if (this.map) { - const bounds = this.map.getBounds(); - 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 coordinates = this.getCoordinates(map); // return a blank image until map event listeners are setup + const url = this.getUrl(map); + console.log("initializing with this url", url); return { type: "image", - url: this.getUrl(), + url, coordinates, }; } } + getCoordinates(map) { + const bounds = map.getBounds(); + // bbox's rubbing up against max extents appear to cause exceptions + // to be repeatedly thrown in mapbox-gl in globe projection + // TODO: this might be better solved by limiting image to the max + // bounds of the dataset returned by the service + const coordinates = [ + [ + Math.max(bounds.getNorthWest().lng, -179), + Math.min(bounds.getNorthWest().lat, 89), + ], + [ + Math.min(bounds.getNorthEast().lng, 179), + Math.min(bounds.getNorthEast().lat, 89), + ], + [ + Math.min(bounds.getSouthEast().lng, 179), + Math.max(bounds.getSouthEast().lat, -89), + ], + [ + Math.max(bounds.getSouthWest().lng, -179), + Math.max(bounds.getSouthWest().lat, -89), + ], + ]; + console.log(coordinates.join(",")); + return coordinates; + } async addToMap(map) { if (!map) { throw new Error("Map not provided to addToMap"); } - const sourceData = await this.getGLSource(); + const sourceData = await this.getGLSource(map); map.addSource(this.sourceId, sourceData); this.addEventListeners(map); return this.sourceId; @@ -316,7 +308,7 @@ class ArcGISDynamicMapService { map.on("error", this.onMapError); // Source is added as a blank image at first. Initialize it with // proper bounds and image - this.updateSource(); + // this.updateSource(); } } } @@ -354,22 +346,24 @@ class ArcGISDynamicMapService { this.removeFromMap(this.map); } } - getUrl() { - if (!this.map) { + getUrl(map) { + var _a; + map = this.map || map; + if (!map) { return exports.blankDataUri; } - const bounds = this.map.getBounds(); let url = new URL(this.options.url + "/export"); url.searchParams.set("f", "image"); url.searchParams.set("transparent", "true"); // create bbox in web mercator + const coordinates = this.getCoordinates(map); let bbox = [ - lon2meters(bounds.getWest()), - lat2meters(bounds.getSouth()), - lon2meters(bounds.getEast()), - lat2meters(bounds.getNorth()), + lon2meters(coordinates[0][0]), + lat2meters(coordinates[2][1]), + lon2meters(coordinates[2][0]), + lat2meters(coordinates[0][1]), ]; - const groundResolution = getGroundResolution(this.map.getZoom() + + const groundResolution = getGroundResolution(map.getZoom() + (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); @@ -409,7 +403,7 @@ class ArcGISDynamicMapService { // * https://github.com/Esri/esri-leaflet/issues/672#issuecomment-160691149 // * https://gist.github.com/perrygeo/4478844 if (Math.abs(bbox[0]) > 20037508.34 || Math.abs(bbox[2]) > 20037508.34) { - const centralMeridian = bounds.getCenter().lng; + const centralMeridian = (_a = this.map) === null || _a === void 0 ? void 0 : _a.getCenter().lng; if (this.options.supportHighDpiDisplays && window.devicePixelRatio > 1) { bbox[0] = -(width * groundResolution) / (window.devicePixelRatio * 2); bbox[2] = (width * groundResolution) / (window.devicePixelRatio * 2); @@ -509,6 +503,7 @@ class ArcGISDynamicMapService { * */ updateLayers(layers) { + console.log("update layers", layers); // do a deep comparison of layers to detect whether there are any changes if (JSON.stringify(layers) !== JSON.stringify(this.layers)) { this.layers = layers; diff --git a/packages/mapbox-gl-esri-sources/src/ArcGISDynamicMapService.ts b/packages/mapbox-gl-esri-sources/src/ArcGISDynamicMapService.ts index 9a13aa63..c3750ce6 100644 --- a/packages/mapbox-gl-esri-sources/src/ArcGISDynamicMapService.ts +++ b/packages/mapbox-gl-esri-sources/src/ArcGISDynamicMapService.ts @@ -291,47 +291,63 @@ export class ArcGISDynamicMapService } }; - async getGLSource(): Promise { + async getGLSource(map: Map): Promise { let { attribution, bounds } = await this.getComputedProperties(); - bounds = bounds || [-90, -180, 90, 180]; + bounds = bounds || [-89, -179, 89, 179]; if (this.options.useTiles) { return { type: "raster", - tiles: [this.getUrl()], + tiles: [this.getUrl(map)], tileSize: this.options.tileSize || 256, bounds: bounds as [number, number, number, number] | undefined, attribution, }; } else { - let coordinates = [ - [bounds[0], bounds[1]], - [bounds[2], bounds[1]], - [bounds[2], bounds[3]], - [bounds[0], bounds[3]], - ]; - if (this.map) { - const bounds = this.map.getBounds(); - 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 coordinates = this.getCoordinates(map); // return a blank image until map event listeners are setup + const url = this.getUrl(map); + console.log("initializing with this url", url); return { type: "image", - url: this.getUrl(), + url, coordinates, } as ImageSourceRaw; } } + private getCoordinates(map: Map) { + const bounds = map.getBounds(); + // bbox's rubbing up against max extents appear to cause exceptions + // to be repeatedly thrown in mapbox-gl in globe projection + // TODO: this might be better solved by limiting image to the max + // bounds of the dataset returned by the service + const coordinates = [ + [ + Math.max(bounds.getNorthWest().lng, -179), + Math.min(bounds.getNorthWest().lat, 89), + ], + [ + Math.min(bounds.getNorthEast().lng, 179), + Math.min(bounds.getNorthEast().lat, 89), + ], + [ + Math.min(bounds.getSouthEast().lng, 179), + Math.max(bounds.getSouthEast().lat, -89), + ], + [ + Math.max(bounds.getSouthWest().lng, -179), + Math.max(bounds.getSouthWest().lat, -89), + ], + ]; + console.log(coordinates.join(",")); + return coordinates; + } + async addToMap(map: Map) { if (!map) { throw new Error("Map not provided to addToMap"); } - const sourceData = await this.getGLSource(); + const sourceData = await this.getGLSource(map); map.addSource(this.sourceId, sourceData); this.addEventListeners(map); return this.sourceId; @@ -352,7 +368,7 @@ export class ArcGISDynamicMapService map.on("error", this.onMapError); // Source is added as a blank image at first. Initialize it with // proper bounds and image - this.updateSource(); + // this.updateSource(); } } } @@ -393,23 +409,24 @@ export class ArcGISDynamicMapService } } - private getUrl() { - if (!this.map) { + private getUrl(map?: Map) { + map = this.map || map; + if (!map) { return blankDataUri; } - const bounds = this.map.getBounds(); let url = new URL(this.options.url + "/export"); url.searchParams.set("f", "image"); url.searchParams.set("transparent", "true"); // create bbox in web mercator + const coordinates = this.getCoordinates(map); let bbox = [ - lon2meters(bounds.getWest()), - lat2meters(bounds.getSouth()), - lon2meters(bounds.getEast()), - lat2meters(bounds.getNorth()), + lon2meters(coordinates[0][0]), + lat2meters(coordinates[2][1]), + lon2meters(coordinates[2][0]), + lat2meters(coordinates[0][1]), ]; const groundResolution = getGroundResolution( - this.map.getZoom() + + map.getZoom() + (this.options.supportHighDpiDisplays ? window.devicePixelRatio - 1 : 0) ); // Width and height can't be based on container width if the map is rotated @@ -452,7 +469,7 @@ export class ArcGISDynamicMapService // * https://github.com/Esri/esri-leaflet/issues/672#issuecomment-160691149 // * https://gist.github.com/perrygeo/4478844 if (Math.abs(bbox[0]) > 20037508.34 || Math.abs(bbox[2]) > 20037508.34) { - const centralMeridian = bounds.getCenter().lng; + const centralMeridian = this.map?.getCenter().lng; if (this.options.supportHighDpiDisplays && window.devicePixelRatio > 1) { bbox[0] = -(width * groundResolution) / (window.devicePixelRatio * 2); bbox[2] = (width * groundResolution) / (window.devicePixelRatio * 2); @@ -548,45 +565,25 @@ export class ArcGISDynamicMapService // @ts-ignore - setTiles is in fact a valid method source.setTiles([this.getUrl()]); } else if (source.type === "image") { - const bounds = this.map.getBounds(); - const 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 url = this.getUrl(); - if ( - // @ts-ignore - source.url === url - ) { + const coordinates = this.getCoordinates(this.map); + + const url = this.getUrl(this.map); + // @ts-ignore + const currentUrl = source.url; + if (currentUrl === url) { + console.log("skipping, urls match", currentUrl, url); return; } // @ts-ignore Using a private member here - if (source.image) { - source.updateImage({ - url, - coordinates, - }); - } else { - let tries = 0; - const updater = () => { - const source = this.map?.getSource(this.sourceId); - if (source && this.getUrl() === url) { - // @ts-ignore - if (source.image && source.type === "image") { - source.updateImage({ - url, - coordinates, - }); - } else if (tries < 10) { - tries++; - setTimeout(updater, 50); - } - } - }; - setTimeout(updater, 50); + console.log("updating image", url, source); + // @ts-ignore + if (source.url === url) { + return; } + source.updateImage({ + url, + coordinates, + }); } else { // do nothing, source isn't added } @@ -621,6 +618,7 @@ export class ArcGISDynamicMapService * */ updateLayers(layers: OrderedLayerSettings) { + console.log("update layers", layers); // do a deep comparison of layers to detect whether there are any changes if (JSON.stringify(layers) !== JSON.stringify(this.layers)) { this.layers = layers; diff --git a/packages/mapbox-gl-esri-sources/src/ArcGISFeatureLayerSource.ts b/packages/mapbox-gl-esri-sources/src/ArcGISFeatureLayerSource.ts index 0c225964..4d26c748 100644 --- a/packages/mapbox-gl-esri-sources/src/ArcGISFeatureLayerSource.ts +++ b/packages/mapbox-gl-esri-sources/src/ArcGISFeatureLayerSource.ts @@ -24,7 +24,7 @@ import { generateMetadataForLayer, } from "./utils"; import { FeatureCollection } from "geojson"; -import { fetchFeatureCollection } from "./fetchData"; +import { fetchFeatureCollection, urlForRawGeoJSONData } from "./fetchData"; import { QuantizedVectorRequestManager, getOrCreateQuantizedVectorRequestManager, @@ -101,6 +101,7 @@ export default class ArcGISFeatureLayerSource this.type = "ArcGISFeatureLayer"; this.sourceId = options.sourceId || uuid(); this.options = options; + // options.fetchStrategy = "raw"; this.requestManager = requestManager; this.url = this.options.url; // remove trailing slash if present @@ -242,7 +243,14 @@ export default class ArcGISFeatureLayerSource } get loading() { - return this._loading; + if (this.options.fetchStrategy === "raw") { + return Boolean( + this.map?.getSource(this.sourceId) && + this.map?.isSourceLoaded(this.sourceId) === false + ); + } else { + return this._loading; + } } private _glStylePromise?: Promise<{ layers: Layer[]; imageList: ImageList }>; @@ -273,14 +281,22 @@ export default class ArcGISFeatureLayerSource async getGLSource() { const { attribution } = await this.getComputedProperties(); - return { - type: "geojson", - data: this.featureData || { - type: "FeatureCollection", - features: this.featureData || [], - }, - attribution: attribution ? attribution : "", - } as GeoJSONSourceRaw; + if (this.options.fetchStrategy === "raw") { + return { + type: "geojson", + data: urlForRawGeoJSONData(this.options.url, "*", 6), + attribution: attribution ? attribution : "", + } as GeoJSONSourceRaw; + } else { + return { + type: "geojson", + data: this.featureData || { + type: "FeatureCollection", + features: this.featureData || [], + }, + attribution: attribution ? attribution : "", + } as GeoJSONSourceRaw; + } } addEventListeners(map: Map) { @@ -290,6 +306,9 @@ export default class ArcGISFeatureLayerSource this.removeEventListeners(map); } this.map = map; + if (this.options.fetchStrategy === "raw") { + return; + } this.QuantizedVectorRequestManager = getOrCreateQuantizedVectorRequestManager(map); this._loading = this.featureData ? false : true; @@ -299,9 +318,12 @@ export default class ArcGISFeatureLayerSource } removeEventListeners(map: Map) { + delete this.map; + if (this.options.fetchStrategy === "raw") { + return; + } this.QuantizedVectorRequestManager?.off("update"); delete this.QuantizedVectorRequestManager; - delete this.map; } async addToMap(map: Map) { @@ -356,9 +378,10 @@ export default class ArcGISFeatureLayerSource this._loading = false; this.rawFeaturesHaveBeenFetched = true; } catch (e) { + console.log("caught error", e); let shouldFireError = true; if ( - ("message" in (e as any) && /bytesLimit/.test((e as any).message)) || + ("message" in (e as any) && /limit/i.test((e as any).message)) || this.abortController?.signal?.reason === "timeout" ) { this.exceededBytesLimit = true; @@ -588,7 +611,7 @@ export default class ArcGISFeatureLayerSource async getFetchStrategy() { if (this.options.fetchStrategy === "auto") { - if (this.featureData) { + if (this.rawFeaturesHaveBeenFetched) { return "raw"; } else if (this.options.fetchStrategy === "auto" && !this.error) { // wait to finish loading then determine strategy diff --git a/packages/mapbox-gl-esri-sources/src/CustomGLSource.ts b/packages/mapbox-gl-esri-sources/src/CustomGLSource.ts index 7f84b91d..5e188e2a 100644 --- a/packages/mapbox-gl-esri-sources/src/CustomGLSource.ts +++ b/packages/mapbox-gl-esri-sources/src/CustomGLSource.ts @@ -124,8 +124,9 @@ export interface CustomGLSource< * options are changed. * */ getLegend?(): Promise; - getGLSource(): Promise; + getGLSource(map: Map): Promise; getGLStyleLayers(): Promise<{ layers: Layer[]; imageList?: ImageList }>; + // handling on the server now getComputedMetadata(): Promise; updateLayers(layers: OrderedLayerSettings): void; /** diff --git a/packages/mapbox-gl-esri-sources/src/fetchData.ts b/packages/mapbox-gl-esri-sources/src/fetchData.ts index 5c7c7326..4c4fc18c 100644 --- a/packages/mapbox-gl-esri-sources/src/fetchData.ts +++ b/packages/mapbox-gl-esri-sources/src/fetchData.ts @@ -70,6 +70,26 @@ export async function fetchFeatureLayerData( return featureCollection; } +export function urlForRawGeoJSONData( + baseUrl: string, + outFields = "*", + geometryPrecision = 6, + queryOptions?: { [key: string]: string } +) { + const params = new URLSearchParams({ + inSR: "4326", + outSR: "4326", + where: "1>0", + outFields, + returnGeometry: "true", + geometryPrecision: geometryPrecision.toString(), + returnIdsOnly: "false", + f: "geojson", + ...(queryOptions || {}), + }); + return `${baseUrl}/query?${params.toString()}`; +} + async function fetchData( baseUrl: string, params: URLSearchParams, @@ -133,6 +153,9 @@ async function fetchData( } else { featureCollection.features.push(...fc.features); if (fc.exceededTransferLimit || fc.properties?.exceededTransferLimit) { + if (disablePagination) { + throw new Error("Exceeded transfer limit. Pagination disabled."); + } if (!objectIdFieldName) { // Fetch objectIds to do manual paging params.set("returnIdsOnly", "true");