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() %}
-
+
- {% 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