UNPKG

esri-leaflet-vector

Version:

Esri vector basemap and vector tile layer plugin for Leaflet.

408 lines (354 loc) 12.6 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, function (error, style) { if (error) { loadItem(itemId, options, function (error, item) { if (error) { callback(error); return; } loadStyleFromService(item.url, options, callback); }); } else { loadItem(itemId, options, function (error, item) { if (error) { callback(error); return; } loadService(item.url, options, function (error, service) { callback(error, style, itemStyleUrl, service, item.url); }); }); } }); } function loadStyleFromService (serviceUrl, options, callback) { loadService(serviceUrl, options, function (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, function (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, {}, function (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(function (a, b) { return 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: formatStyle }; export default EsriUtil;