diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..e8bb23a1 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,58 @@ +name: Tests +on: [push, pull_request] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Install requirements + run: pip install flake8 pycodestyle + - name: Check syntax + run: flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics --exclude ckan + + test: + needs: lint + strategy: + matrix: + ckan-version: ["2.11", "2.10", 2.9] + fail-fast: false + + name: CKAN ${{ matrix.ckan-version }} + runs-on: ubuntu-latest + container: + image: ckan/ckan-dev:${{ matrix.ckan-version }} + services: + solr: + image: ckan/ckan-solr:${{ matrix.ckan-version }}-solr9 + postgres: + image: ckan/ckan-postgres-dev:${{ matrix.ckan-version }} + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + redis: + image: redis:3 + env: + CKAN_SQLALCHEMY_URL: postgresql://ckan_default:pass@postgres/ckan_test + CKAN_DATASTORE_WRITE_URL: postgresql://datastore_write:pass@postgres/datastore_test + CKAN_DATASTORE_READ_URL: postgresql://datastore_read:pass@postgres/datastore_test + CKAN_SOLR_URL: http://solr:8983/solr/ckan + CKAN_REDIS_URL: redis://redis:6379/1 + + steps: + - uses: actions/checkout@v4 + - name: Install requirements + run: | + pip install -r dev-requirements.txt + pip install -e . + # Replace default path to CKAN core config file with the one on the container + sed -i -e 's/use = config:.*/use = config:\/srv\/app\/src\/ckan\/test-core.ini/' test.ini + - name: Setup extension + run: | + ckan -c test.ini db init + - name: Run tests + run: pytest --ckan-ini=test.ini --disable-warnings ckanext/geoview/tests diff --git a/README.rst b/README.rst index 87ef3345..8304318c 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,7 @@ used to be part of ckanext-spatial_. **Note:** This is a work in progress, if you can help with `OpenLayers`_ or `Leaflet`_ development, check the `Issues` section for what needs to be done or add a new issue. -This extensions supports CKAN 2.6 onwards, including Python 3 support on CKAN 2.9 or higher. +This extensions supports CKAN 2.7 onwards, including Python 3 support on CKAN 2.9 or higher. ------------ Installation @@ -76,11 +76,11 @@ OpenLayers Viewer The OpenLayers_ viewer provides access to different geospatial formats and services: -To enable it, add ``geo_view`` to your ``ckan.plugins`` setting. (use ``geo_preview`` if you are using CKAN < 2.3):: +To enable it, add ``geo_view`` to your ``ckan.plugins`` setting.:: ckan.plugins = ... resource_proxy geo_view -On CKAN >= 2.3, if you want the geospatial views to be created by default, add the plugin to the following setting:: +If you want the geospatial views to be created by default, add the plugin to the following setting:: ckan.views.default_views = ... geo_view @@ -247,11 +247,11 @@ Leaflet GeoJSON Viewer The Leaflet_ GeoJSON_ viewer will render GeoJSON files on a map and add a popup showing the features properties, for those resources that have a ``geojson`` format. -To enable it, add ``geojson_view`` to your ``ckan.plugins`` setting. (use ``geojson_preview`` if you are using CKAN < 2.3):: +To enable it, add ``geojson_view`` to your ``ckan.plugins`` setting.:: ckan.plugins = ... resource_proxy geojson_view -On CKAN >= 2.3, if you want the views to be created by default on all GeoJSON files, add the plugin to the following setting:: +If you want the views to be created by default on all GeoJSON files, add the plugin to the following setting:: ckan.views.default_views = ... geojson_view @@ -269,11 +269,11 @@ Leaflet WMTS Viewer The Leaflet_ WMTS viewer will render WMTS (Web Map Tile Service) layers on a map for those resources that have a ``wmts`` format. -To enable it, add ``wmts_view`` to your ``ckan.plugins`` setting. (use ``wmts_preview`` if you are using CKAN < 2.3):: +To enable it, add ``wmts_view`` to your ``ckan.plugins`` setting.:: ckan.plugins = ... resource_proxy wmts_view -On CKAN >= 2.3, if you want the views to be created by default on all WMTS resources, add the plugin to the following setting:: +If you want the views to be created by default on all WMTS resources, add the plugin to the following setting:: ckan.views.default_views = ... wmts_view @@ -286,11 +286,11 @@ Leaflet ESRI Shapefile Viewer The Leaflet_ Shapefile_ viewer will render ESRI Shapfiles (A ZIP archive contains the .shp, .shx, .dbf, and .prj files) on a map and add a popup showing the features properties, for those resources that have a ``shp`` format. -To enable it, add ``shp_view`` to your ``ckan.plugins`` setting. (use ``shp_preview`` if you are using CKAN < 2.3):: +To enable it, add ``shp_view`` to your ``ckan.plugins`` setting.:: ckan.plugins = ... resource_proxy shp_view -On CKAN >= 2.3, if you want the views to be created by default on all Shapefiles, add the plugin to the following setting:: +If you want the views to be created by default on all Shapefiles, add the plugin to the following setting:: ckan.views.default_views = ... shp_view @@ -310,7 +310,7 @@ Common base layers for Map Widgets The geospatial view plugins support the same base map configurations than the ckanext-spatial `widgets`_. -Check the following page to learn how to choose a different base map layer (Stamen, MapBox or custom): +Check the following page to learn how to choose a different base map layer: http://docs.ckan.org/projects/ckanext-spatial/en/latest/map-widgets.html diff --git a/ckanext/geoview/plugin/__init__.py b/ckanext/geoview/plugin/__init__.py index 606105b9..b7d41d4f 100644 --- a/ckanext/geoview/plugin/__init__.py +++ b/ckanext/geoview/plugin/__init__.py @@ -5,11 +5,11 @@ import mimetypes from six.moves.urllib.parse import urlparse -import ckantoolkit as toolkit from ckan import plugins as p from ckan.common import json from ckan.lib.datapreview import on_same_domain +from ckan.plugins import toolkit import ckanext.geoview.utils as utils @@ -258,9 +258,16 @@ def info(self): def can_view(self, data_dict): resource = data_dict["resource"] format_lower = resource.get("format", "").lower() + same_domain = False + try: + same_domain = on_same_domain(data_dict) + except KeyError as e: + log.error( + "Unable to determine if url is on same domain: {}".format(e) + ) if format_lower in self.WMTS: - return self.same_domain or self.proxy_enabled + return same_domain or self.proxy_enabled return False def view_template(self, context, data_dict): @@ -307,9 +314,16 @@ def can_view(self, data_dict): name_lower = "" if resource.get("name"): name_lower = resource.get("name", "").lower() + same_domain = False + try: + same_domain = on_same_domain(data_dict) + except KeyError as e: + log.error( + "Unable to determine if url is on same domain: {}".format(e) + ) if format_lower in self.SHP or any([shp in name_lower for shp in self.SHP]): - return self.same_domain or self.proxy_enabled + return same_domain or self.proxy_enabled return False def view_template(self, context, data_dict): diff --git a/ckanext/geoview/public/css/geo-resource-styles.css b/ckanext/geoview/public/css/geo-resource-styles.css index 678189ef..3f4ecbfd 100644 --- a/ckanext/geoview/public/css/geo-resource-styles.css +++ b/ckanext/geoview/public/css/geo-resource-styles.css @@ -1,4 +1,4 @@ -.label[data-format=wfs] { +.badge[data-format=wfs] { background-color: #7aae3d; } .format-label[data-format=wfs], @@ -11,7 +11,7 @@ height: 35px; } -.label[data-format=wms] { +.badge[data-format=wms] { background-color: #adc717; } .format-label[data-format=wms], @@ -24,7 +24,7 @@ height: 35px; } -.label[data-format=gml] { +.badge[data-format=gml] { background-color: #7aae3d; } .format-label[data-format=gml], @@ -37,7 +37,7 @@ height: 35px; } -.label[data-format=kml] { +.badge[data-format=kml] { background-color: #7aae3d; } .format-label[data-format=kml], @@ -50,7 +50,7 @@ height: 35px; } -.label[data-format=geojson] { +.badge[data-format=geojson] { background-color: #9855e0; } .format-label[data-format=geojson], @@ -63,7 +63,7 @@ height: 35px; } -.label[data-format=wmts] { +.badge[data-format=wmts] { background-color: #3333ff; } .format-label[data-format=wmts], @@ -76,7 +76,7 @@ height: 35px; } -.label[data-format=shp] { +.badge[data-format=shp] { background-color: #0080ff; } .format-label[data-format=shp], @@ -89,6 +89,6 @@ height: 35px; } -.label[data-format=arcgis_rest] { +.badge[data-format=arcgis_rest] { background-color: #5c3ee0; } diff --git a/ckanext/geoview/public/css/geojson_preview.css b/ckanext/geoview/public/css/geojson_preview.css index 35375065..b5cb3efe 100644 --- a/ckanext/geoview/public/css/geojson_preview.css +++ b/ckanext/geoview/public/css/geojson_preview.css @@ -19,3 +19,13 @@ html, body { height: 300px; overflow: auto; } + +.leaflet-control-no-provider { + background-color: white; + margin-right: 10px; + padding: 10px; + border: 1px solid #b1b1b1; +} +.leaflet-control-attribution { + font-size: 11px; +} diff --git a/ckanext/geoview/public/css/shp_preview.css b/ckanext/geoview/public/css/shp_preview.css index 35e2b7e0..b73b7c9e 100644 --- a/ckanext/geoview/public/css/shp_preview.css +++ b/ckanext/geoview/public/css/shp_preview.css @@ -36,3 +36,13 @@ html, body { overflow: auto; max-height: 200px; } + +.leaflet-control-no-provider { + background-color: white; + margin-right: 10px; + padding: 10px; + border: 1px solid #b1b1b1; +} +.leaflet-control-attribution { + font-size: 11px; +} diff --git a/ckanext/geoview/public/css/wmts_preview.css b/ckanext/geoview/public/css/wmts_preview.css index d69e2d9d..6d322402 100644 --- a/ckanext/geoview/public/css/wmts_preview.css +++ b/ckanext/geoview/public/css/wmts_preview.css @@ -65,3 +65,13 @@ html, body { .ui-opacity .handle:hover { background: #303030; } + +.leaflet-control-no-provider { + background-color: white; + margin-right: 10px; + padding: 10px; + border: 1px solid #b1b1b1; +} +.leaflet-control-attribution { + font-size: 11px; +} diff --git a/ckanext/geoview/public/js/common_map.js b/ckanext/geoview/public/js/common_map.js index 515af673..f279f108 100644 --- a/ckanext/geoview/public/js/common_map.js +++ b/ckanext/geoview/public/js/common_map.js @@ -35,6 +35,8 @@ maxZoom: 18 }); + var baseLayer; + map = new L.Map(container, leafletMapOptions); if (mapConfig.type == 'mapbox') { @@ -44,28 +46,64 @@ 'See http://www.mapbox.com/developers/api-overview/ for details'; } - baseLayerUrl = '//{s}.tiles.mapbox.com/v4/' + mapConfig['mapbox.map_id'] + '/{z}/{x}/{y}.png?access_token=' + mapConfig['mapbox.access_token']; - leafletBaseLayerOptions.handle = mapConfig['mapbox.map_id']; - leafletBaseLayerOptions.subdomains = mapConfig.subdomains || 'abcd'; - leafletBaseLayerOptions.attribution = mapConfig.attribution || 'Data: OpenStreetMap, Design: MapBox'; + baseLayer = L.tileLayer.provider('MapBox', { + id: mapConfig['mapbox.map_id'], + accessToken: mapConfig['mapbox.access_token'] + }); + } else if (mapConfig.type == 'custom') { // Custom XYZ layer - baseLayerUrl = mapConfig['custom.url']; - if (!baseLayerUrl) - throw '[CKAN Map Widgets] Custom URL must be set when using Custom Map type'; - + baseLayerUrl = mapConfig['custom_url'] || mapConfig['custom.url']; if (mapConfig.subdomains) leafletBaseLayerOptions.subdomains = mapConfig.subdomains; if (mapConfig.tms) leafletBaseLayerOptions.tms = mapConfig.tms; leafletBaseLayerOptions.attribution = mapConfig.attribution; + + baseLayer = new L.TileLayer(baseLayerUrl, leafletBaseLayerOptions); + + } else if (mapConfig.type == 'wms') { + + baseLayerUrl = mapConfig['wms.url']; + wmsOptions = {} + wmsOptions['layers'] = mapConfig['wms.layers']; + wmsOptions['styles'] = mapConfig['wms.styles'] || ''; + wmsOptions['format'] = mapConfig['wms.format'] || 'image/png'; + if(mapConfig['wms.srs'] || mapConfig['wms.crs']) { + wmsOptions['crs'] = mapConfig['wms.srs'] || mapConfig['wms.crs']; + } + wmsOptions['version'] = mapConfig['wms.version'] || '1.1.1'; + + baseLayer = new L.TileLayer.WMS(baseLayerUrl, wmsOptions); + + + } else if (mapConfig.type) { + + baseLayer = L.tileLayer.provider(mapConfig.type, mapConfig) + } else { - // Default to Stamen base map - baseLayerUrl = 'https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png'; - leafletBaseLayerOptions.subdomains = mapConfig.subdomains || 'abcd'; - leafletBaseLayerOptions.attribution = mapConfig.attribution || 'Map tiles by Stamen Design (CC BY 3.0). Data by OpenStreetMap (CC BY SA)'; + let c = L.Control.extend({ + + onAdd: (map) => { + let element = document.createElement("div"); + element.className = "leaflet-control-no-provider"; + element.innerHTML = 'No map provider set. Please check the documentation'; + return element; + }, + onRemove: (map) => {} + }) + map.addControl(new c({position: "bottomleft"})) + } - var baseLayer = new L.TileLayer(baseLayerUrl, leafletBaseLayerOptions); - map.addLayer(baseLayer); + if (baseLayer) { + let attribution = L.control.attribution({"prefix": false}); + attribution.addTo(map) + + map.addLayer(baseLayer); + + if (mapConfig.attribution) { + attribution.addAttribution(mapConfig.attribution); + } + } return map; diff --git a/ckanext/geoview/public/js/geojson_preview.js b/ckanext/geoview/public/js/geojson_preview.js index cf563c24..d9cb09d5 100644 --- a/ckanext/geoview/public/js/geojson_preview.js +++ b/ckanext/geoview/public/js/geojson_preview.js @@ -29,7 +29,7 @@ ckan.module('geojsonpreview', function (jQuery, _) { self.el.append($("
").attr("id","map")); - self.map = ckan.commonLeafletMap('map', this.options.map_config); + self.map = ckan.commonLeafletMap('map', this.options.map_config, {attributionControl: false}); // hack to make leaflet use a particular location to look for images L.Icon.Default.imagePath = this.options.site_url + 'js/vendor/leaflet/dist/images'; diff --git a/ckanext/geoview/public/js/ol_preview.js b/ckanext/geoview/public/js/ol_preview.js index 6bc0fd2d..c9763d11 100644 --- a/ckanext/geoview/public/js/ol_preview.js +++ b/ckanext/geoview/public/js/ol_preview.js @@ -126,11 +126,8 @@ } else if (mapConfig.type == 'custom') { mapConfig.type = 'XYZ' } else if (!mapConfig.type || mapConfig.type.toLowerCase() == 'osm') { - // default to Stamen base map - mapConfig.type = 'Stamen'; - mapConfig.url = 'https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png'; - mapConfig.subdomains = mapConfig.subdomains || 'abcd'; - mapConfig.attribution = mapConfig.attribution || 'Map tiles by Stamen Design (CC BY 3.0). Data by OpenStreetMap (CC BY SA)'; + + mapConfig.type = 'OSM' } return OL_HELPERS.createLayerFromConfig(mapConfig, true, callback); diff --git a/ckanext/geoview/public/js/shp_preview.js b/ckanext/geoview/public/js/shp_preview.js index 1fcd253d..9089df19 100644 --- a/ckanext/geoview/public/js/shp_preview.js +++ b/ckanext/geoview/public/js/shp_preview.js @@ -19,7 +19,7 @@ ckan.module('shppreview', function (jQuery, _) { self.el.empty(); self.el.append($('
').attr('id', 'map')); - self.map = ckan.commonLeafletMap('map', this.options.map_config); + self.map = ckan.commonLeafletMap('map', this.options.map_config, {attributionControl: false}); // hack to make leaflet use a particular location to look for images L.Icon.Default.imagePath = this.options.site_url + 'js/vendor/leaflet/dist/images'; diff --git a/ckanext/geoview/public/js/vendor/leaflet-providers.js b/ckanext/geoview/public/js/vendor/leaflet-providers.js new file mode 100644 index 00000000..bcde1ed7 --- /dev/null +++ b/ckanext/geoview/public/js/vendor/leaflet-providers.js @@ -0,0 +1,1178 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['leaflet'], factory); + } else if (typeof modules === 'object' && module.exports) { + // define a Common JS module that relies on 'leaflet' + module.exports = factory(require('leaflet')); + } else { + // Assume Leaflet is loaded into global object L already + factory(L); + } +}(this, function (L) { + 'use strict'; + + L.TileLayer.Provider = L.TileLayer.extend({ + initialize: function (arg, options) { + var providers = L.TileLayer.Provider.providers; + + var parts = arg.split('.'); + + var providerName = parts[0]; + var variantName = parts[1]; + + if (!providers[providerName]) { + throw 'No such provider (' + providerName + ')'; + } + + var provider = { + url: providers[providerName].url, + options: providers[providerName].options + }; + + // overwrite values in provider from variant. + if (variantName && 'variants' in providers[providerName]) { + if (!(variantName in providers[providerName].variants)) { + throw 'No such variant of ' + providerName + ' (' + variantName + ')'; + } + var variant = providers[providerName].variants[variantName]; + var variantOptions; + if (typeof variant === 'string') { + variantOptions = { + variant: variant + }; + } else { + variantOptions = variant.options; + } + provider = { + url: variant.url || provider.url, + options: L.Util.extend({}, provider.options, variantOptions) + }; + } + + // replace attribution placeholders with their values from toplevel provider attribution, + // recursively + var attributionReplacer = function (attr) { + if (attr.indexOf('{attribution.') === -1) { + return attr; + } + return attr.replace(/\{attribution.(\w*)\}/g, + function (match, attributionName) { + return attributionReplacer(providers[attributionName].options.attribution); + } + ); + }; + provider.options.attribution = attributionReplacer(provider.options.attribution); + + // Compute final options combining provider options with any user overrides + var layerOpts = L.Util.extend({}, provider.options, options); + L.TileLayer.prototype.initialize.call(this, provider.url, layerOpts); + } + }); + + /** + * Definition of providers. + * see http://leafletjs.com/reference.html#tilelayer for options in the options map. + */ + + L.TileLayer.Provider.providers = { + OpenStreetMap: { + url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + options: { + maxZoom: 19, + attribution: + '© OpenStreetMap contributors' + }, + variants: { + Mapnik: {}, + DE: { + url: 'https://tile.openstreetmap.de/{z}/{x}/{y}.png', + options: { + maxZoom: 18 + } + }, + CH: { + url: 'https://tile.osm.ch/switzerland/{z}/{x}/{y}.png', + options: { + maxZoom: 18, + bounds: [[45, 5], [48, 11]] + } + }, + France: { + url: 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png', + options: { + maxZoom: 20, + attribution: '© OpenStreetMap France | {attribution.OpenStreetMap}' + } + }, + HOT: { + url: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', + options: { + attribution: + '{attribution.OpenStreetMap}, ' + + 'Tiles style by Humanitarian OpenStreetMap Team ' + + 'hosted by OpenStreetMap France' + } + }, + BZH: { + url: 'https://tile.openstreetmap.bzh/br/{z}/{x}/{y}.png', + options: { + attribution: '{attribution.OpenStreetMap}, Tiles courtesy of Breton OpenStreetMap Team', + bounds: [[46.2, -5.5], [50, 0.7]] + } + } + } + }, + MapTilesAPI: { + url: 'https://maptiles.p.rapidapi.com/{variant}/{z}/{x}/{y}.png?rapidapi-key={apikey}', + options: { + attribution: + '© MapTiles API, {attribution.OpenStreetMap}', + variant: 'en/map/v1', + // Get your own MapTiles API access token here : https://www.maptilesapi.com/ + // NB : this is a demonstration key that comes with no guarantee and not to be used in production + apikey: '', + maxZoom: 19 + }, + variants: { + OSMEnglish: { + options: { + variant: 'en/map/v1' + } + }, + OSMFrancais: { + options: { + variant: 'fr/map/v1' + } + }, + OSMEspagnol: { + options: { + variant: 'es/map/v1' + } + } + } + }, + OpenSeaMap: { + url: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', + options: { + attribution: 'Map data: © OpenSeaMap contributors' + } + }, + OPNVKarte: { + url: 'https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', + options: { + maxZoom: 18, + attribution: 'Map memomaps.de CC-BY-SA, map data {attribution.OpenStreetMap}' + } + }, + OpenTopoMap: { + url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', + options: { + maxZoom: 17, + attribution: 'Map data: {attribution.OpenStreetMap}, SRTM | Map style: © OpenTopoMap (CC-BY-SA)' + } + }, + OpenRailwayMap: { + url: 'https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', + options: { + maxZoom: 19, + attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © OpenRailwayMap (CC-BY-SA)' + } + }, + OpenFireMap: { + url: 'http://openfiremap.org/hytiles/{z}/{x}/{y}.png', + options: { + maxZoom: 19, + attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © OpenFireMap (CC-BY-SA)' + } + }, + SafeCast: { + url: 'https://s3.amazonaws.com/te512.safecast.org/{z}/{x}/{y}.png', + options: { + maxZoom: 16, + attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © SafeCast (CC-BY-SA)' + } + }, + Stadia: { + url: 'https://tiles.stadiamaps.com/tiles/{variant}/{z}/{x}/{y}{r}.{ext}', + options: { + minZoom: 0, + maxZoom: 20, + attribution: + '© Stadia Maps ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'alidade_smooth', + ext: 'png' + }, + variants: { + AlidadeSmooth: 'alidade_smooth', + AlidadeSmoothDark: 'alidade_smooth_dark', + OSMBright: 'osm_bright', + Outdoors: 'outdoors', + StamenToner: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_toner' + } + }, + StamenTonerBackground: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_toner_background' + } + }, + StamenTonerLines: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_toner_lines' + } + }, + StamenTonerLabels: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_toner_labels' + } + }, + StamenTonerLite: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_toner_lite' + } + }, + StamenWatercolor: { + url: 'https://tiles.stadiamaps.com/tiles/{variant}/{z}/{x}/{y}.{ext}', + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_watercolor', + ext: 'jpg', + minZoom: 1, + maxZoom: 16 + } + }, + StamenTerrain: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_terrain', + minZoom: 0, + maxZoom: 18 + } + }, + StamenTerrainBackground: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_terrain_background', + minZoom: 0, + maxZoom: 18 + } + }, + StamenTerrainLabels: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_terrain_labels', + minZoom: 0, + maxZoom: 18 + } + }, + StamenTerrainLines: { + options: { + attribution: + '© Stadia Maps ' + + '© Stamen Design ' + + '© OpenMapTiles ' + + '{attribution.OpenStreetMap}', + variant: 'stamen_terrain_lines', + minZoom: 0, + maxZoom: 18 + } + } + } + }, + Thunderforest: { + url: 'https://{s}.tile.thunderforest.com/{variant}/{z}/{x}/{y}.png?apikey={apikey}', + options: { + attribution: + '© Thunderforest, {attribution.OpenStreetMap}', + variant: 'cycle', + apikey: '', + maxZoom: 22 + }, + variants: { + OpenCycleMap: 'cycle', + Transport: { + options: { + variant: 'transport' + } + }, + TransportDark: { + options: { + variant: 'transport-dark' + } + }, + SpinalMap: { + options: { + variant: 'spinal-map' + } + }, + Landscape: 'landscape', + Outdoors: 'outdoors', + Pioneer: 'pioneer', + MobileAtlas: 'mobile-atlas', + Neighbourhood: 'neighbourhood' + } + }, + CyclOSM: { + url: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', + options: { + maxZoom: 20, + attribution: 'CyclOSM | Map data: {attribution.OpenStreetMap}' + } + }, + Jawg: { + url: 'https://{s}.tile.jawg.io/{variant}/{z}/{x}/{y}{r}.png?access-token={accessToken}', + options: { + attribution: + '© JawgMaps ' + + '{attribution.OpenStreetMap}', + minZoom: 0, + maxZoom: 22, + subdomains: 'abcd', + variant: 'jawg-terrain', + // Get your own Jawg access token here : https://www.jawg.io/lab/ + // NB : this is a demonstration key that comes with no guarantee + accessToken: '', + }, + variants: { + Streets: 'jawg-streets', + Terrain: 'jawg-terrain', + Sunny: 'jawg-sunny', + Dark: 'jawg-dark', + Light: 'jawg-light', + Matrix: 'jawg-matrix' + } + }, + MapBox: { + url: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}{r}?access_token={accessToken}', + options: { + attribution: + '© Mapbox ' + + '{attribution.OpenStreetMap} ' + + 'Improve this map', + tileSize: 512, + maxZoom: 18, + zoomOffset: -1, + id: 'mapbox/streets-v11', + accessToken: '', + } + }, + MapTiler: { + url: 'https://api.maptiler.com/maps/{variant}/{z}/{x}/{y}{r}.{ext}?key={key}', + options: { + attribution: + '© MapTiler © OpenStreetMap contributors', + variant: 'streets', + ext: 'png', + key: '', + tileSize: 512, + zoomOffset: -1, + minZoom: 0, + maxZoom: 21 + }, + variants: { + Streets: 'streets', + Basic: 'basic', + Bright: 'bright', + Pastel: 'pastel', + Positron: 'positron', + Hybrid: { + options: { + variant: 'hybrid', + ext: 'jpg' + } + }, + Toner: 'toner', + Topo: 'topo', + Voyager: 'voyager' + } + }, + TomTom: { + url: 'https://{s}.api.tomtom.com/map/1/tile/{variant}/{style}/{z}/{x}/{y}.{ext}?key={apikey}', + options: { + variant: 'basic', + maxZoom: 22, + attribution: + '© 1992 - ' + new Date().getFullYear() + ' TomTom. ', + subdomains: 'abcd', + style: 'main', + ext: 'png', + apikey: '', + }, + variants: { + Basic: 'basic', + Hybrid: 'hybrid', + Labels: 'labels' + } + }, + Esri: { + url: 'https://server.arcgisonline.com/ArcGIS/rest/services/{variant}/MapServer/tile/{z}/{y}/{x}', + options: { + variant: 'World_Street_Map', + attribution: 'Tiles © Esri' + }, + variants: { + WorldStreetMap: { + options: { + attribution: + '{attribution.Esri} — ' + + 'Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012' + } + }, + DeLorme: { + options: { + variant: 'Specialty/DeLorme_World_Base_Map', + minZoom: 1, + maxZoom: 11, + attribution: '{attribution.Esri} — Copyright: ©2012 DeLorme' + } + }, + WorldTopoMap: { + options: { + variant: 'World_Topo_Map', + attribution: + '{attribution.Esri} — ' + + 'Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community' + } + }, + WorldImagery: { + options: { + variant: 'World_Imagery', + attribution: + '{attribution.Esri} — ' + + 'Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' + } + }, + WorldTerrain: { + options: { + variant: 'World_Terrain_Base', + maxZoom: 13, + attribution: + '{attribution.Esri} — ' + + 'Source: USGS, Esri, TANA, DeLorme, and NPS' + } + }, + WorldShadedRelief: { + options: { + variant: 'World_Shaded_Relief', + maxZoom: 13, + attribution: '{attribution.Esri} — Source: Esri' + } + }, + WorldPhysical: { + options: { + variant: 'World_Physical_Map', + maxZoom: 8, + attribution: '{attribution.Esri} — Source: US National Park Service' + } + }, + OceanBasemap: { + options: { + variant: 'Ocean/World_Ocean_Base', + maxZoom: 13, + attribution: '{attribution.Esri} — Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri' + } + }, + NatGeoWorldMap: { + options: { + variant: 'NatGeo_World_Map', + maxZoom: 16, + attribution: '{attribution.Esri} — National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC' + } + }, + WorldGrayCanvas: { + options: { + variant: 'Canvas/World_Light_Gray_Base', + maxZoom: 16, + attribution: '{attribution.Esri} — Esri, DeLorme, NAVTEQ' + } + } + } + }, + OpenWeatherMap: { + url: 'http://{s}.tile.openweathermap.org/map/{variant}/{z}/{x}/{y}.png?appid={apiKey}', + options: { + maxZoom: 19, + attribution: 'Map data © OpenWeatherMap', + apiKey: '', + opacity: 0.5 + }, + variants: { + Clouds: 'clouds', + CloudsClassic: 'clouds_cls', + Precipitation: 'precipitation', + PrecipitationClassic: 'precipitation_cls', + Rain: 'rain', + RainClassic: 'rain_cls', + Pressure: 'pressure', + PressureContour: 'pressure_cntr', + Wind: 'wind', + Temperature: 'temp', + Snow: 'snow' + } + }, + HERE: { + /* + * HERE maps, formerly Nokia maps. + * These basemaps are free, but you need an api id and app key. Please sign up at + * https://developer.here.com/plans + */ + url: + 'https://{s}.{base}.maps.api.here.com/maptile/2.1/' + + '{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?' + + 'app_id={app_id}&app_code={app_code}&lg={language}', + options: { + attribution: + 'Map © 1987-' + new Date().getFullYear() + ' HERE', + subdomains: '1234', + mapID: 'newest', + 'app_id': '', + 'app_code': '', + base: 'base', + variant: 'normal.day', + maxZoom: 20, + type: 'maptile', + language: 'eng', + format: 'png8', + size: '256' + }, + variants: { + normalDay: 'normal.day', + normalDayCustom: 'normal.day.custom', + normalDayGrey: 'normal.day.grey', + normalDayMobile: 'normal.day.mobile', + normalDayGreyMobile: 'normal.day.grey.mobile', + normalDayTransit: 'normal.day.transit', + normalDayTransitMobile: 'normal.day.transit.mobile', + normalDayTraffic: { + options: { + variant: 'normal.traffic.day', + base: 'traffic', + type: 'traffictile' + } + }, + normalNight: 'normal.night', + normalNightMobile: 'normal.night.mobile', + normalNightGrey: 'normal.night.grey', + normalNightGreyMobile: 'normal.night.grey.mobile', + normalNightTransit: 'normal.night.transit', + normalNightTransitMobile: 'normal.night.transit.mobile', + reducedDay: 'reduced.day', + reducedNight: 'reduced.night', + basicMap: { + options: { + type: 'basetile' + } + }, + mapLabels: { + options: { + type: 'labeltile', + format: 'png' + } + }, + trafficFlow: { + options: { + base: 'traffic', + type: 'flowtile' + } + }, + carnavDayGrey: 'carnav.day.grey', + hybridDay: { + options: { + base: 'aerial', + variant: 'hybrid.day' + } + }, + hybridDayMobile: { + options: { + base: 'aerial', + variant: 'hybrid.day.mobile' + } + }, + hybridDayTransit: { + options: { + base: 'aerial', + variant: 'hybrid.day.transit' + } + }, + hybridDayGrey: { + options: { + base: 'aerial', + variant: 'hybrid.grey.day' + } + }, + hybridDayTraffic: { + options: { + variant: 'hybrid.traffic.day', + base: 'traffic', + type: 'traffictile' + } + }, + pedestrianDay: 'pedestrian.day', + pedestrianNight: 'pedestrian.night', + satelliteDay: { + options: { + base: 'aerial', + variant: 'satellite.day' + } + }, + terrainDay: { + options: { + base: 'aerial', + variant: 'terrain.day' + } + }, + terrainDayMobile: { + options: { + base: 'aerial', + variant: 'terrain.day.mobile' + } + } + } + }, + HEREv3: { + /* + * HERE maps API Version 3. + * These basemaps are free, but you need an API key. Please sign up at + * https://developer.here.com/plans + * Version 3 deprecates the app_id and app_code access in favor of apiKey + * + * Supported access methods as of 2019/12/21: + * @see https://developer.here.com/faqs#access-control-1--how-do-you-control-access-to-here-location-services + */ + url: + 'https://{s}.{base}.maps.ls.hereapi.com/maptile/2.1/' + + '{type}/{mapID}/{variant}/{z}/{x}/{y}/{size}/{format}?' + + 'apiKey={apiKey}&lg={language}', + options: { + attribution: + 'Map © 1987-' + new Date().getFullYear() + ' HERE', + subdomains: '1234', + mapID: 'newest', + apiKey: '', + base: 'base', + variant: 'normal.day', + maxZoom: 20, + type: 'maptile', + language: 'eng', + format: 'png8', + size: '256' + }, + variants: { + normalDay: 'normal.day', + normalDayCustom: 'normal.day.custom', + normalDayGrey: 'normal.day.grey', + normalDayMobile: 'normal.day.mobile', + normalDayGreyMobile: 'normal.day.grey.mobile', + normalDayTransit: 'normal.day.transit', + normalDayTransitMobile: 'normal.day.transit.mobile', + normalNight: 'normal.night', + normalNightMobile: 'normal.night.mobile', + normalNightGrey: 'normal.night.grey', + normalNightGreyMobile: 'normal.night.grey.mobile', + normalNightTransit: 'normal.night.transit', + normalNightTransitMobile: 'normal.night.transit.mobile', + reducedDay: 'reduced.day', + reducedNight: 'reduced.night', + basicMap: { + options: { + type: 'basetile' + } + }, + mapLabels: { + options: { + type: 'labeltile', + format: 'png' + } + }, + trafficFlow: { + options: { + base: 'traffic', + type: 'flowtile' + } + }, + carnavDayGrey: 'carnav.day.grey', + hybridDay: { + options: { + base: 'aerial', + variant: 'hybrid.day' + } + }, + hybridDayMobile: { + options: { + base: 'aerial', + variant: 'hybrid.day.mobile' + } + }, + hybridDayTransit: { + options: { + base: 'aerial', + variant: 'hybrid.day.transit' + } + }, + hybridDayGrey: { + options: { + base: 'aerial', + variant: 'hybrid.grey.day' + } + }, + pedestrianDay: 'pedestrian.day', + pedestrianNight: 'pedestrian.night', + satelliteDay: { + options: { + base: 'aerial', + variant: 'satellite.day' + } + }, + terrainDay: { + options: { + base: 'aerial', + variant: 'terrain.day' + } + }, + terrainDayMobile: { + options: { + base: 'aerial', + variant: 'terrain.day.mobile' + } + } + } + }, + FreeMapSK: { + url: 'https://{s}.freemap.sk/T/{z}/{x}/{y}.jpeg', + options: { + minZoom: 8, + maxZoom: 16, + subdomains: 'abcd', + bounds: [[47.204642, 15.996093], [49.830896, 22.576904]], + attribution: + '{attribution.OpenStreetMap}, visualization CC-By-SA 2.0 Freemap.sk' + } + }, + MtbMap: { + url: 'http://tile.mtbmap.cz/mtbmap_tiles/{z}/{x}/{y}.png', + options: { + attribution: + '{attribution.OpenStreetMap} & USGS' + } + }, + CartoDB: { + url: 'https://{s}.basemaps.cartocdn.com/{variant}/{z}/{x}/{y}{r}.png', + options: { + attribution: '{attribution.OpenStreetMap} © CARTO', + subdomains: 'abcd', + maxZoom: 20, + variant: 'light_all' + }, + variants: { + Positron: 'light_all', + PositronNoLabels: 'light_nolabels', + PositronOnlyLabels: 'light_only_labels', + DarkMatter: 'dark_all', + DarkMatterNoLabels: 'dark_nolabels', + DarkMatterOnlyLabels: 'dark_only_labels', + Voyager: 'rastertiles/voyager', + VoyagerNoLabels: 'rastertiles/voyager_nolabels', + VoyagerOnlyLabels: 'rastertiles/voyager_only_labels', + VoyagerLabelsUnder: 'rastertiles/voyager_labels_under' + } + }, + HikeBike: { + url: 'https://tiles.wmflabs.org/{variant}/{z}/{x}/{y}.png', + options: { + maxZoom: 19, + attribution: '{attribution.OpenStreetMap}', + variant: 'hikebike' + }, + variants: { + HikeBike: {}, + HillShading: { + options: { + maxZoom: 15, + variant: 'hillshading' + } + } + } + }, + BasemapAT: { + url: 'https://mapsneu.wien.gv.at/basemap/{variant}/{type}/google3857/{z}/{y}/{x}.{format}', + options: { + maxZoom: 19, + attribution: 'Datenquelle: basemap.at', + type: 'normal', + format: 'png', + bounds: [[46.358770, 8.782379], [49.037872, 17.189532]], + variant: 'geolandbasemap' + }, + variants: { + basemap: { + options: { + maxZoom: 20, // currently only in Vienna + variant: 'geolandbasemap' + } + }, + grau: 'bmapgrau', + overlay: 'bmapoverlay', + terrain: { + options: { + variant: 'bmapgelaende', + type: 'grau', + format: 'jpeg' + } + }, + surface: { + options: { + variant: 'bmapoberflaeche', + type: 'grau', + format: 'jpeg' + } + }, + highdpi: { + options: { + variant: 'bmaphidpi', + format: 'jpeg' + } + }, + orthofoto: { + options: { + maxZoom: 20, // currently only in Vienna + variant: 'bmaporthofoto30cm', + format: 'jpeg' + } + } + } + }, + nlmaps: { + url: 'https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0/{variant}/EPSG:3857/{z}/{x}/{y}.png', + options: { + minZoom: 6, + maxZoom: 19, + bounds: [[50.5, 3.25], [54, 7.6]], + attribution: 'Kaartgegevens © Kadaster' + }, + variants: { + 'standaard': 'standaard', + 'pastel': 'pastel', + 'grijs': 'grijs', + 'water': 'water', + 'luchtfoto': { + 'url': 'https://service.pdok.nl/hwh/luchtfotorgb/wmts/v1_0/Actueel_ortho25/EPSG:3857/{z}/{x}/{y}.jpeg', + } + } + }, + NASAGIBS: { + url: 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/{variant}/default/{time}/{tilematrixset}{maxZoom}/{z}/{y}/{x}.{format}', + options: { + attribution: + 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System ' + + '(ESDIS) with funding provided by NASA/HQ.', + bounds: [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]], + minZoom: 1, + maxZoom: 9, + format: 'jpg', + time: '', + tilematrixset: 'GoogleMapsCompatible_Level' + }, + variants: { + ModisTerraTrueColorCR: 'MODIS_Terra_CorrectedReflectance_TrueColor', + ModisTerraBands367CR: 'MODIS_Terra_CorrectedReflectance_Bands367', + ViirsEarthAtNight2012: { + options: { + variant: 'VIIRS_CityLights_2012', + maxZoom: 8 + } + }, + ModisTerraLSTDay: { + options: { + variant: 'MODIS_Terra_Land_Surface_Temp_Day', + format: 'png', + maxZoom: 7, + opacity: 0.75 + } + }, + ModisTerraSnowCover: { + options: { + variant: 'MODIS_Terra_NDSI_Snow_Cover', + format: 'png', + maxZoom: 8, + opacity: 0.75 + } + }, + ModisTerraAOD: { + options: { + variant: 'MODIS_Terra_Aerosol', + format: 'png', + maxZoom: 6, + opacity: 0.75 + } + }, + ModisTerraChlorophyll: { + options: { + variant: 'MODIS_Terra_Chlorophyll_A', + format: 'png', + maxZoom: 7, + opacity: 0.75 + } + } + } + }, + NLS: { + // NLS maps are copyright National library of Scotland. + // http://maps.nls.uk/projects/api/index.html + // Please contact NLS for anything other than non-commercial low volume usage + // + // Map sources: Ordnance Survey 1:1m to 1:63K, 1920s-1940s + // z0-9 - 1:1m + // z10-11 - quarter inch (1:253440) + // z12-18 - one inch (1:63360) + url: 'https://nls-{s}.tileserver.com/nls/{z}/{x}/{y}.jpg', + options: { + attribution: 'National Library of Scotland Historic Maps', + bounds: [[49.6, -12], [61.7, 3]], + minZoom: 1, + maxZoom: 18, + subdomains: '0123', + } + }, + JusticeMap: { + // Justice Map (http://www.justicemap.org/) + // Visualize race and income data for your community, county and country. + // Includes tools for data journalists, bloggers and community activists. + url: 'https://www.justicemap.org/tile/{size}/{variant}/{z}/{x}/{y}.png', + options: { + attribution: 'Justice Map', + // one of 'county', 'tract', 'block' + size: 'county', + // Bounds for USA, including Alaska and Hawaii + bounds: [[14, -180], [72, -56]] + }, + variants: { + income: 'income', + americanIndian: 'indian', + asian: 'asian', + black: 'black', + hispanic: 'hispanic', + multi: 'multi', + nonWhite: 'nonwhite', + white: 'white', + plurality: 'plural' + } + }, + GeoportailFrance: { + url: 'https://wxs.ign.fr/{apikey}/geoportail/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE={style}&TILEMATRIXSET=PM&FORMAT={format}&LAYER={variant}&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}', + options: { + attribution: 'Geoportail France', + bounds: [[-75, -180], [81, 180]], + minZoom: 2, + maxZoom: 18, + // Get your own geoportail apikey here : http://professionnels.ign.fr/ign/contrats/ + // NB : 'choisirgeoportail' is a demonstration key that comes with no guarantee + apikey: 'choisirgeoportail', + format: 'image/png', + style: 'normal', + variant: 'GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2' + }, + variants: { + plan: 'GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2', + parcels: { + options: { + variant: 'CADASTRALPARCELS.PARCELLAIRE_EXPRESS', + style: 'PCI vecteur', + maxZoom: 20 + } + }, + orthos: { + options: { + maxZoom: 19, + format: 'image/jpeg', + variant: 'ORTHOIMAGERY.ORTHOPHOTOS' + } + } + } + }, + OneMapSG: { + url: 'https://maps-{s}.onemap.sg/v3/{variant}/{z}/{x}/{y}.png', + options: { + variant: 'Default', + minZoom: 11, + maxZoom: 18, + bounds: [[1.56073, 104.11475], [1.16, 103.502]], + attribution: ' New OneMap | Map data © contributors, Singapore Land Authority' + }, + variants: { + Default: 'Default', + Night: 'Night', + Original: 'Original', + Grey: 'Grey', + LandLot: 'LandLot' + } + }, + USGS: { + url: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}', + options: { + maxZoom: 20, + attribution: 'Tiles courtesy of the U.S. Geological Survey' + }, + variants: { + USTopo: {}, + USImagery: { + url: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}' + }, + USImageryTopo: { + url: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer/tile/{z}/{y}/{x}' + } + } + }, + WaymarkedTrails: { + url: 'https://tile.waymarkedtrails.org/{variant}/{z}/{x}/{y}.png', + options: { + maxZoom: 18, + attribution: 'Map data: {attribution.OpenStreetMap} | Map style: © waymarkedtrails.org (CC-BY-SA)' + }, + variants: { + hiking: 'hiking', + cycling: 'cycling', + mtb: 'mtb', + slopes: 'slopes', + riding: 'riding', + skating: 'skating' + } + }, + OpenAIP: { + url: 'https://{s}.tile.maps.openaip.net/geowebcache/service/tms/1.0.0/openaip_basemap@EPSG%3A900913@png/{z}/{x}/{y}.{ext}', + options: { + attribution: 'openAIP Data (CC-BY-NC-SA)', + ext: 'png', + minZoom: 4, + maxZoom: 14, + tms: true, + detectRetina: true, + subdomains: '12' + } + }, + OpenSnowMap: { + url: 'https://tiles.opensnowmap.org/{variant}/{z}/{x}/{y}.png', + options: { + minZoom: 9, + maxZoom: 18, + attribution: 'Map data: {attribution.OpenStreetMap} & ODbL, © www.opensnowmap.org CC-BY-SA' + }, + variants: { + pistes: 'pistes', + } + }, + AzureMaps: { + url: + 'https://atlas.microsoft.com/map/tile?api-version={apiVersion}'+ + '&tilesetId={variant}&x={x}&y={y}&zoom={z}&language={language}'+ + '&subscription-key={subscriptionKey}', + options: { + attribution: 'See https://docs.microsoft.com/en-us/rest/api/maps/render-v2/get-map-tile for details.', + apiVersion: '2.0', + variant: 'microsoft.imagery', + subscriptionKey: '', + language: 'en-US', + }, + variants: { + MicrosoftImagery: 'microsoft.imagery', + MicrosoftBaseDarkGrey: 'microsoft.base.darkgrey', + MicrosoftBaseRoad: 'microsoft.base.road', + MicrosoftBaseHybridRoad: 'microsoft.base.hybrid.road', + MicrosoftTerraMain: 'microsoft.terra.main', + MicrosoftWeatherInfraredMain: { + url: + 'https://atlas.microsoft.com/map/tile?api-version={apiVersion}'+ + '&tilesetId={variant}&x={x}&y={y}&zoom={z}'+ + '&timeStamp={timeStamp}&language={language}' + + '&subscription-key={subscriptionKey}', + options: { + timeStamp: '2021-05-08T09:03:00Z', + attribution: 'See https://docs.microsoft.com/en-us/rest/api/maps/render-v2/get-map-tile#uri-parameters for details.', + variant: 'microsoft.weather.infrared.main', + }, + }, + MicrosoftWeatherRadarMain: { + url: + 'https://atlas.microsoft.com/map/tile?api-version={apiVersion}'+ + '&tilesetId={variant}&x={x}&y={y}&zoom={z}'+ + '&timeStamp={timeStamp}&language={language}' + + '&subscription-key={subscriptionKey}', + options: { + timeStamp: '2021-05-08T09:03:00Z', + attribution: 'See https://docs.microsoft.com/en-us/rest/api/maps/render-v2/get-map-tile#uri-parameters for details.', + variant: 'microsoft.weather.radar.main', + }, + } + }, + }, + SwissFederalGeoportal: { + url: 'https://wmts.geo.admin.ch/1.0.0/{variant}/default/current/3857/{z}/{x}/{y}.jpeg', + options: { + attribution: '© swisstopo', + minZoom: 2, + maxZoom: 18, + bounds: [[45.398181, 5.140242], [48.230651, 11.47757]] + }, + variants: { + NationalMapColor: 'ch.swisstopo.pixelkarte-farbe', + NationalMapGrey: 'ch.swisstopo.pixelkarte-grau', + SWISSIMAGE: { + options: { + variant: 'ch.swisstopo.swissimage', + maxZoom: 19 + } + } + } + } + }; + + L.tileLayer.provider = function (provider, options) { + return new L.TileLayer.Provider(provider, options); + }; + + return L; +})); diff --git a/ckanext/geoview/public/js/wmts_preview.js b/ckanext/geoview/public/js/wmts_preview.js index d6b9176b..6f8fbe55 100644 --- a/ckanext/geoview/public/js/wmts_preview.js +++ b/ckanext/geoview/public/js/wmts_preview.js @@ -6,7 +6,7 @@ ckan.module('wmtspreview', function (jQuery, _) { self.el.empty(); self.el.append($('
').attr('id', 'map')); - self.map = ckan.commonLeafletMap('map', this.options.map_config, {center: [0, 0], zoom: 3}); + self.map = ckan.commonLeafletMap('map', this.options.map_config, {attributionControl: false, center: [0, 0], zoom: 3}); $.ajaxSetup({ beforeSend: function (xhr) { diff --git a/ckanext/geoview/public/webassets.yml b/ckanext/geoview/public/webassets.yml index 62676acb..205b211b 100644 --- a/ckanext/geoview/public/webassets.yml +++ b/ckanext/geoview/public/webassets.yml @@ -31,6 +31,7 @@ geojson_js: - base/main contents: - js/vendor/leaflet/dist/leaflet.js + - js/vendor/leaflet-providers.js - js/vendor/proj4js/proj4.js - js/vendor/proj4leaflet/src/proj4leaflet.js - js/common_map.js @@ -49,6 +50,7 @@ wmts_js: - base/main contents: - js/vendor/leaflet/dist/leaflet.js + - js/vendor/leaflet-providers.js - js/vendor/proj4js/proj4.js - js/common_map.js - js/wmts_preview.js @@ -79,6 +81,7 @@ shp_js: - base/main contents: - js/vendor/leaflet/dist/leaflet.js + - js/vendor/leaflet-providers.js - js/vendor/proj4js/proj4.js - js/vendor/spinjs/spin.js - js/vendor/leaflet.spin/leaflet.spin.js diff --git a/ckanext/geoview/templates/dataviewer/geojson.html b/ckanext/geoview/templates/dataviewer/geojson.html index 08041fb1..b670072c 100644 --- a/ckanext/geoview/templates/dataviewer/geojson.html +++ b/ckanext/geoview/templates/dataviewer/geojson.html @@ -7,7 +7,7 @@ {% endblock %} {% set map_config = h.get_common_map_config_geojson() %}
diff --git a/ckanext/geoview/templates/dataviewer/openlayers.html b/ckanext/geoview/templates/dataviewer/openlayers.html index 46682ad2..12cfc7e2 100644 --- a/ckanext/geoview/templates/dataviewer/openlayers.html +++ b/ckanext/geoview/templates/dataviewer/openlayers.html @@ -10,7 +10,7 @@ data-module-gapi_key="{{ gapi_key }}" data-module-proxy_url="{{ proxy_url }}" data-module-proxy_service_url="{{ proxy_service_url }}" - data-module-site_url="{{ h.dump_json(h.url('/', locale='default', qualified=true)) }}" + data-module-site_url="{{ h.dump_json(h.url_for('/', locale='default', qualified=true)) }}" data-module-map_config="{{ h.dump_json(map_config) }}" data-module-ol_config="{{ h.dump_json(ol_config) }}" {% if resource_view_json %} data-module-resource-view = "{{ h.dump_json(resource_view_json) }}" {% endif %} diff --git a/ckanext/geoview/templates/dataviewer/shp.html b/ckanext/geoview/templates/dataviewer/shp.html index 137067a3..f80280a9 100644 --- a/ckanext/geoview/templates/dataviewer/shp.html +++ b/ckanext/geoview/templates/dataviewer/shp.html @@ -8,7 +8,7 @@ {% set map_config = h.get_common_map_config_shp() %} {% set shp_config = h.get_shapefile_viewer_config() %} -
+

{{ _('Loading...') }}
diff --git a/ckanext/geoview/templates/dataviewer/wmts.html b/ckanext/geoview/templates/dataviewer/wmts.html index 30e97f83..ef40c913 100644 --- a/ckanext/geoview/templates/dataviewer/wmts.html +++ b/ckanext/geoview/templates/dataviewer/wmts.html @@ -1,14 +1,17 @@ {% extends "dataviewer/base.html" %} {% block page %} + {%- block styles %} + {% set type = 'asset' if h.ckan_version().split('.')[1] | int >= 9 else 'resource' %} + {% include 'geoview/snippets/wmts_' ~ type ~ '.html' %} + {% endblock %} + {% set map_config = h.get_common_map_config_wmts() %} -
+

{{ _('Loading...') }}

- {% resource 'ckanext-geoview/wmts' %} - {% endblock %} diff --git a/ckanext/geoview/templates/geoview/snippets/geojson_asset.html b/ckanext/geoview/templates/geoview/snippets/geojson_asset.html index 84afd901..6c412a70 100644 --- a/ckanext/geoview/templates/geoview/snippets/geojson_asset.html +++ b/ckanext/geoview/templates/geoview/snippets/geojson_asset.html @@ -1,6 +1,11 @@ -{% set main_css = h.get_rtl_css() if h.is_rtl_language() else g.main_css %} -{# strip '/base/' prefix and '.css' suffix #} -{% asset main_css[6:-4] %} +{% if h.ckan_version().split('.') | map('int')| list >= [2, 9, 6] %} + {% set theme = h.get_rtl_theme() if h.is_rtl_language() else g.theme %} + {% asset theme %} +{% else %} + {% set main_css = h.get_rtl_css() if h.is_rtl_language() else g.main_css %} + {# strip '/base/' prefix and '.css' suffix #} + {% asset main_css[6:-4] %} +{% endif %} {% asset 'ckanext-geoview/geojson_js' %} {% asset 'ckanext-geoview/geojson_css' %} diff --git a/ckanext/geoview/templates/geoview/snippets/shp_asset.html b/ckanext/geoview/templates/geoview/snippets/shp_asset.html index ed452bec..71e932db 100644 --- a/ckanext/geoview/templates/geoview/snippets/shp_asset.html +++ b/ckanext/geoview/templates/geoview/snippets/shp_asset.html @@ -1,6 +1,11 @@ -{% set main_css = h.get_rtl_css() if h.is_rtl_language() else g.main_css %} -{# strip '/base/' prefix and '.css' suffix #} -{% asset main_css[6:-4] %} +{% if h.ckan_version().split('.') | map('int')| list >= [2, 9, 6] %} + {% set theme = h.get_rtl_theme() if h.is_rtl_language() else g.theme %} + {% asset theme %} +{% else %} + {% set main_css = h.get_rtl_css() if h.is_rtl_language() else g.main_css %} + {# strip '/base/' prefix and '.css' suffix #} + {% asset main_css[6:-4] %} +{% endif %} {% asset 'ckanext-geoview/shp_js' %} {% asset 'ckanext-geoview/shp_css' %} diff --git a/ckanext/geoview/tests/__init__.py b/ckanext/geoview/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ckanext/geoview/tests/test_geojson.py b/ckanext/geoview/tests/test_geojson.py new file mode 100644 index 00000000..d1e1e749 --- /dev/null +++ b/ckanext/geoview/tests/test_geojson.py @@ -0,0 +1,37 @@ +import pytest + +from ckan.tests import factories +from ckan.plugins import toolkit + +from ckanext.geoview.plugin import GeoJSONView + +@pytest.mark.ckan_config('ckan.views.default_views', 'geojson_view') +def test_geojson_view_is_rendered(app): + view_default_title = GeoJSONView().info()["title"] + dataset = factories.Dataset() + + for format in GeoJSONView.GeoJSON: + resource = factories.Resource( + name='My Resource', + format=format, + package_id=dataset['id'] + ) + + if toolkit.check_ckan_version("2.9"): + url = toolkit.url_for( + "{}_resource.read".format(dataset["type"]), + id=dataset["name"], + resource_id=resource["id"], + ) + else: + url = toolkit.url_for( + controller="package", + action="resource_read", + id=resource["package_id"], + resource_id=resource["id"], + ) + + res = app.get(url) + assert 'class="resource-view"' in res.body + assert 'data-title="{}"'.format(view_default_title) in res.body + assert 'id="view-' in res.body diff --git a/ckanext/geoview/tests/test_plugin.py b/ckanext/geoview/tests/test_plugin.py new file mode 100644 index 00000000..f04f0014 --- /dev/null +++ b/ckanext/geoview/tests/test_plugin.py @@ -0,0 +1,7 @@ +from ckanext.geoview import plugin + +def test_plugin(): + """This is here just as a sanity test + """ + p = plugin.OLGeoView() + assert p \ No newline at end of file diff --git a/ckanext/geoview/tests/test_shpview.py b/ckanext/geoview/tests/test_shpview.py new file mode 100644 index 00000000..242fcab5 --- /dev/null +++ b/ckanext/geoview/tests/test_shpview.py @@ -0,0 +1,37 @@ +import pytest + +from ckan.tests import factories +from ckan.plugins import toolkit + +from ckanext.geoview.plugin import SHPView + +@pytest.mark.ckan_config('ckan.views.default_views', 'shp_view') +def test_geojson_view_is_rendered(app): + view_default_title = SHPView().info()["title"] + dataset = factories.Dataset() + + for format in SHPView.SHP: + resource = factories.Resource( + name='My Resource', + format=format, + package_id=dataset['id'] + ) + + if toolkit.check_ckan_version("2.9"): + url = toolkit.url_for( + "{}_resource.read".format(dataset["type"]), + id=dataset["name"], + resource_id=resource["id"], + ) + else: + url = toolkit.url_for( + controller="package", + action="resource_read", + id=resource["package_id"], + resource_id=resource["id"], + ) + + res = app.get(url) + assert 'class="resource-view"' in res.body + assert 'data-title="{}"'.format(view_default_title) in res.body + assert 'id="view-' in res.body diff --git a/ckanext/geoview/utils.py b/ckanext/geoview/utils.py index 00671dd4..2fca9be0 100644 --- a/ckanext/geoview/utils.py +++ b/ckanext/geoview/utils.py @@ -6,14 +6,8 @@ import requests -import ckan.lib.base as base -import ckan.lib.helpers as h -import ckan.logic as logic - -import ckantoolkit as toolkit - from ckan import plugins as p - +from ckan.plugins import toolkit log = logging.getLogger(__name__) @@ -46,7 +40,7 @@ def proxy_service_resource(request, context, data_dict): than the maximum file size. """ resource_id = data_dict["resource_id"] log.info("Proxify resource {id}".format(id=resource_id)) - resource = logic.get_action("resource_show")(context, {"id": resource_id}) + resource = toolkit.get_action("resource_show")(context, {"id": resource_id}) url = resource["url"] return proxy_service_url(request, url) @@ -55,7 +49,7 @@ def proxy_service_url(req, url): parts = urlsplit(url) if not parts.scheme or not parts.netloc: - base.abort(409, detail="Invalid URL.") + toolkit.abort(409, detail="Invalid URL.") try: method = req.environ["REQUEST_METHOD"] @@ -63,7 +57,7 @@ def proxy_service_url(req, url): params = parse_qs(parts.query) if not p.toolkit.asbool( - base.config.get( + toolkit.config.get( "ckanext.geoview.forward_ogc_request_params", "False" ) ): @@ -88,7 +82,7 @@ def proxy_service_url(req, url): cl = r.headers.get("content-length") if cl and int(cl) > MAX_FILE_SIZE: - base.abort( + toolkit.abort( 409, ( """Content is too large to be proxied. Allowed @@ -101,7 +95,7 @@ def proxy_service_url(req, url): response = make_response() else: - response = base.response + response = toolkit.response response.content_type = r.headers["content-type"] response.charset = r.encoding @@ -115,7 +109,7 @@ def proxy_service_url(req, url): length += len(chunk) if length >= MAX_FILE_SIZE: - base.abort( + toolkit.abort( 409, ( """Content is too large to be proxied. Allowed @@ -129,17 +123,17 @@ def proxy_service_url(req, url): error.response.status_code, error.response.reason, ) - base.abort(409, detail=details) + toolkit.abort(409, detail=details) except requests.exceptions.ConnectionError as error: details = ( """Could not proxy resource because a connection error occurred. %s""" % error ) - base.abort(502, detail=details) + toolkit.abort(502, detail=details) except requests.exceptions.Timeout as error: details = "Could not proxy resource because the connection timed out." - base.abort(504, detail=details) + toolkit.abort(504, detail=details) return response @@ -213,7 +207,7 @@ def get_proxified_service_url(data_dict): :param data_dict: contains a resource and package dict :type data_dict: dictionary """ - url = h.url_for( + url = toolkit.url_for( action="proxy_service", controller='service_proxy', id=data_dict["package"]["name"], diff --git a/ckanext/geoview/views.py b/ckanext/geoview/views.py index 9543c355..bfef2c6c 100644 --- a/ckanext/geoview/views.py +++ b/ckanext/geoview/views.py @@ -4,9 +4,8 @@ from flask import Blueprint -import ckan.lib.base as base - from ckan import plugins as p +from ckan.plugins import toolkit import ckanext.geoview.utils as utils @@ -17,15 +16,13 @@ def proxy_service(id, resource_id): data_dict = {"resource_id": resource_id} context = { - "model": base.model, - "session": base.model.Session, - "user": base.c.user or base.c.author, + "user": toolkit.c.user or toolkit.c.author, } return utils.proxy_service_resource(p.toolkit.request, context, data_dict) def proxy_service_url(map_id): - url = base.config.get("ckanext.spatial.common_map." + map_id + ".url") + url = toolkit.config.get("ckanext.spatial.common_map." + map_id + ".url") req = p.toolkit.request return utils.proxy_service_url(req, url) diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 00000000..172902c4 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1 @@ +pytest-ckan \ No newline at end of file diff --git a/pip-requirements.txt b/pip-requirements.txt deleted file mode 100644 index 53808bbe..00000000 --- a/pip-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -requests>=1.1.0 -ckantoolkit diff --git a/setup.py b/setup.py index 18638620..400b5103 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '0.0.18' +version = '0.1.0' setup( name='ckanext-geoview', diff --git a/test.ini b/test.ini new file mode 100644 index 00000000..3aa377ce --- /dev/null +++ b/test.ini @@ -0,0 +1,45 @@ +[DEFAULT] +debug = false +smtp_server = localhost +error_email_from = ckan@localhost + +[app:main] +use = config:../ckan/test-core.ini + +# Insert any custom config settings to be used when running your extension's +# tests here. These will override the one defined in CKAN core's test-core.ini +ckan.plugins = resource_proxy geo_view geojson_view shp_view + + +# Logging configuration +[loggers] +keys = root, ckan, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_ckan] +qualname = ckan +handlers = +level = WARN + +[logger_sqlalchemy] +handlers = +qualname = sqlalchemy.engine +level = WARN + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s \ No newline at end of file