UNPKG

esri-leaflet-vector

Version:

Esri vector basemap and vector tile layer plugin for Leaflet.

402 lines (348 loc) 12.4 kB
import { latLng, latLngBounds } from "leaflet"; import { request, Support, Util } from "esri-leaflet"; /* utility to establish a URL for the basemap styles API used primarily by VectorBasemapLayer.js */ export function getBasemapStyleUrl(style, apikey) { if (style.includes("/")) { throw new Error( `${style} is a v2 style enumeration. Set version:2 to request this style`, ); } let url = `https://basemaps-api.arcgis.com/arcgis/rest/services/styles/${style}?type=style`; if (apikey) { url = `${url}&token=${apikey}`; } return url; } /** * Utility to establish a URL for the basemap styles API v2 * * @param {string} style * @param {string} token * @param {Object} [options] Optional list of options: language, worldview, or places. * @returns {string} the URL */ export function getBasemapStyleV2Url(style, token, options) { if (style.startsWith("osm/")) { console.log( "L.esri.Vector.vectorBasemapLayer: All 'osm/*' styles are retired are no longer receiving updates and were last updated in 2024. Please use 'open/*' styles instead.", ); } if (style.includes(":")) { throw new Error( `${style} is a v1 style enumeration. Set version:1 to request this style`, ); } let url = options.baseUrl || "https://basemapstyles-api.arcgis.com/arcgis/rest/services/styles/v2/styles/"; if ( !( style.startsWith("open/") || style.startsWith("osm/") || style.startsWith("arcgis/") ) && style.length === 32 ) { // style is an itemID url = `${url}items/${style}`; if (options.language) { throw new Error( "The 'language' parameter is not supported for custom basemap styles", ); } } else { url = url + style; } if (!token) { throw new Error("A token is required to access basemap styles."); } url = `${url}?token=${token}`; if (options.language) { url = `${url}&language=${options.language}`; } if (options.worldview) { url = `${url}&worldview=${options.worldview}`; } if (options.places) { url = `${url}&places=${options.places}`; } return url; } /* utilities to communicate with custom user styles via an ITEM ID or SERVICE URL used primarily by VectorTileLayer.js */ export function loadStyle(idOrUrl, options, callback) { const httpRegex = /^https?:\/\//; const serviceRegex = /\/VectorTileServer\/?$/; if (httpRegex.test(idOrUrl) && serviceRegex.test(idOrUrl)) { const serviceUrl = idOrUrl; loadStyleFromService(serviceUrl, options, callback); } else { const itemId = idOrUrl; loadStyleFromItem(itemId, options, callback); } } export function loadService(serviceUrl, options, callback) { const params = options.token ? { token: options.token } : {}; request(serviceUrl, params, callback); } function loadItem(itemId, options, callback) { const params = options.token ? { token: options.token } : {}; const url = `${options.portalUrl}/sharing/rest/content/items/${itemId}`; request(url, params, callback); } function loadStyleFromItem(itemId, options, callback) { const itemStyleUrl = toCdnUrl( `${options.portalUrl}/sharing/rest/content/items/${ itemId }/resources/styles/root.json`, ); loadStyleFromUrl(itemStyleUrl, options, (error, style) => { if (error) { loadItem(itemId, options, (error, item) => { if (error) { callback(error); return; } loadStyleFromService(item.url, options, callback); }); } else { loadItem(itemId, options, (error, item) => { if (error) { callback(error); return; } loadService(item.url, options, (error, service) => { callback(error, style, itemStyleUrl, service, item.url); }); }); } }); } function loadStyleFromService(serviceUrl, options, callback) { loadService(serviceUrl, options, (error, service) => { if (error) { callback(error); return; } let sanitizedServiceUrl = serviceUrl; // a trailing "/" may create invalid paths if (serviceUrl.charAt(serviceUrl.length - 1) === "/") { sanitizedServiceUrl = serviceUrl.slice(0, serviceUrl.length - 1); } let defaultStylesUrl; // inadvertently inserting more than 1 adjacent "/" may create invalid paths if (service.defaultStyles.charAt(0) === "/") { defaultStylesUrl = `${sanitizedServiceUrl + service.defaultStyles}/root.json`; } else { defaultStylesUrl = `${sanitizedServiceUrl}/${service.defaultStyles}/root.json`; } loadStyleFromUrl(defaultStylesUrl, options, (error, style) => { if (error) { callback(error); return; } callback(null, style, defaultStylesUrl, service, serviceUrl); }); }); } function loadStyleFromUrl(styleUrl, options, callback) { const params = options.token ? { token: options.token } : {}; request(styleUrl, params, callback); } function isSameTLD(url1, url2) { return new URL(url1).hostname === new URL(url2).hostname; } /** * Converts an ArcGIS Online URL to a CDN URL to reduce latency and server load. This will not * convert a ArcGIS Enterprise URL since they will be hosting their own resources. * * Borrowed from the JS API. */ function toCdnUrl(url) { if (!url) { return url || null; } let outUrl = url; if (outUrl) { outUrl = normalizeArcGISOnlineOrgDomain(outUrl); outUrl = outUrl.replace( /^https?:\/\/www\.arcgis\.com/, "https://cdn.arcgis.com", ); outUrl = outUrl.replace( /^https?:\/\/devext\.arcgis\.com/, "https://cdndev.arcgis.com", ); outUrl = outUrl.replace( /^https?:\/\/qaext\.arcgis\.com/, "https://cdnqa.arcgis.com", ); } return outUrl; } /** * Replaces the AGOL org domains with non-org domains. * * Borrowed from the JS API. */ function normalizeArcGISOnlineOrgDomain(url) { const prdOrg = /^https?:\/\/(?:cdn|[a-z\d-]+\.maps)\.arcgis\.com/i; // https://cdn.arcgis.com or https://x.maps.arcgis.com const devextOrg = /^https?:\/\/(?:cdndev|[a-z\d-]+\.mapsdevext)\.arcgis\.com/i; // https://cdndev.arcgis.com or https://x.mapsdevext.arcgis.com const qaOrg = /^https?:\/\/(?:cdnqa|[a-z\d-]+\.mapsqa)\.arcgis\.com/i; // https://cdnqa.arcgis.com or https://x.mapsqa.arcgis.com // replace AGOL org domains with non-org domains if (prdOrg.test(url)) { url = url.replace(prdOrg, "https://www.arcgis.com"); } else if (devextOrg.test(url)) { url = url.replace(devextOrg, "https://devext.arcgis.com"); } else if (qaOrg.test(url)) { url = url.replace(qaOrg, "https://qaext.arcgis.com"); } return url; } export function formatStyle(style, styleUrl, metadata, token) { // transforms style object in place and also returns it // modify each source in style.sources const sourcesKeys = Object.keys(style.sources); for (let sourceIndex = 0; sourceIndex < sourcesKeys.length; sourceIndex++) { const source = style.sources[sourcesKeys[sourceIndex]]; // if a relative path is referenced, the default style can be found in a standard location if (source.url.indexOf("http") === -1) { source.url = styleUrl.replace("/resources/styles/root.json", ""); } // a trailing "/" may create invalid paths if (source.url.charAt(source.url.length - 1) === "/") { source.url = source.url.slice(0, source.url.length - 1); } // add tiles property if missing if (!source.tiles) { // right now ArcGIS Pro published vector services have a slightly different signature // the '/' is needed in the URL string concatenation below for source.tiles if (metadata.tiles && metadata.tiles[0].charAt(0) !== "/") { metadata.tiles[0] = `/${metadata.tiles[0]}`; } source.tiles = [source.url + metadata.tiles[0]]; } // some VectorTileServer endpoints may default to returning f=html, // specify f=json to account for that behavior source.url += "?f=json"; // add the token to the source url and tiles properties as a query param source.url += token ? `&token=${token}` : ""; source.tiles[0] += token ? `?token=${token}` : ""; // add minzoom and maxzoom to each source based on the service metadata // prefer minLOD/maxLOD if it exists since that is the level that tiles are cooked too // MapLibre will overzoom for LODs that are not cooked source.minzoom = metadata.minLOD || metadata.tileInfo.lods[0].level; source.maxzoom = metadata.maxLOD || metadata.tileInfo.lods[metadata.tileInfo.lods.length - 1].level; } // add the attribution and copyrightText properties to the last source in style.sources based on the service metadata const lastSource = style.sources[sourcesKeys[sourcesKeys.length - 1]]; lastSource.attribution = metadata.copyrightText || ""; lastSource.copyrightText = metadata.copyrightText || ""; // if any layer in style.layers has a layout.text-font property (it will be any array of strings) remove all items in the array after the first for (let layerIndex = 0; layerIndex < style.layers.length; layerIndex++) { const layer = style.layers[layerIndex]; if ( layer.layout && layer.layout["text-font"] && layer.layout["text-font"].length > 1 ) { layer.layout["text-font"] = [layer.layout["text-font"][0]]; } } if (style.sprite && style.sprite.indexOf("http") === -1) { // resolve absolute URL for style.sprite style.sprite = styleUrl.replace( "styles/root.json", style.sprite.replace("../", ""), ); } // Convert the style.glyphs and style.sprite URLs to CDN URLs if possable if (style.glyphs) { style.glyphs = toCdnUrl(style.glyphs); } if (style.sprite) { style.sprite = toCdnUrl(style.sprite); } // a trailing "/" may create invalid paths if (style.sprite && token) { // add the token to the style.sprite property as a query param, only if same domain (for token security) if (isSameTLD(styleUrl, style.sprite)) { style.sprite += `?token=${token}`; } else { console.warn( "Passing a token but sprite URL is not on same base URL, so you must pass the token manually.", ); } } if (style.glyphs && style.glyphs.indexOf("http") === -1) { // resolve absolute URL for style.glyphs style.glyphs = styleUrl.replace( "styles/root.json", style.glyphs.replace("../", ""), ); } if (style.glyphs && token) { // add the token to the style.glyphs property as a query param if (isSameTLD(styleUrl, style.glyphs)) { style.glyphs += `?token=${token}`; } else { console.warn( "Passing a token but glyph URL is not on same base URL, so you must pass the token manually.", ); } } return style; } /* utility to assist with dynamic attribution data used primarily by VectorBasemapLayer.js */ export function getAttributionData(url, map) { if (Support.cors) { request(url, {}, (error, attributions) => { if (error) { return; } map._esriAttributions = map._esriAttributions || []; for (let c = 0; c < attributions.contributors.length; c++) { const contributor = attributions.contributors[c]; for (let i = 0; i < contributor.coverageAreas.length; i++) { const coverageArea = contributor.coverageAreas[i]; const southWest = latLng(coverageArea.bbox[0], coverageArea.bbox[1]); const northEast = latLng(coverageArea.bbox[2], coverageArea.bbox[3]); map._esriAttributions.push({ attribution: contributor.attribution, score: coverageArea.score, bounds: latLngBounds(southWest, northEast), minZoom: coverageArea.zoomMin, maxZoom: coverageArea.zoomMax, }); } } map._esriAttributions.sort((a, b) => b.score - a.score); // pass the same argument as the map's 'moveend' event const obj = { target: map }; Util._updateMapAttribution(obj); }); } } /* utility to check if a service's tileInfo spatial reference is in Web Mercator used primarily by VectorTileLayer.js */ const WEB_MERCATOR_WKIDS = [3857, 102100, 102113]; export function isWebMercator(wkid) { return WEB_MERCATOR_WKIDS.indexOf(wkid) >= 0; } export const EsriUtil = { formatStyle, }; export default EsriUtil;