From 3f3f6872336b0e2c3a937d1fce24e68d86010f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Mon, 20 Nov 2023 15:19:16 +0100 Subject: [PATCH] Support all projections from SwissTopo Use the service Capabilities --- base_geoengine/fields.py | 2 +- base_geoengine/geo_view/geo_raster_layer.py | 11 +- .../src/js/widgets/geoengine_widgets.js | 110 +++++----- .../geo_view/geo_raster_layer.py | 60 +++++- .../geo_view/geo_raster_layer_view.xml | 4 +- .../i18n/geoengine_swisstopo.pot | 2 +- .../static/src/js/geoengine_swisstopo.js | 190 +++++++++--------- 7 files changed, 220 insertions(+), 159 deletions(-) diff --git a/base_geoengine/fields.py b/base_geoengine/fields.py index f584e94443..7d7e18b4a2 100644 --- a/base_geoengine/fields.py +++ b/base_geoengine/fields.py @@ -48,7 +48,7 @@ def convert_to_column(self, value, record, values=None): """Convert value to database format value can be geojson, wkt, shapely geometry object. - If geo_direct_write in context you can pass diretly WKT""" + If geo_direct_write in context you can pass directly WKT""" if not value: return None shape_to_write = self.entry_to_shape(value, same_type=True) diff --git a/base_geoengine/geo_view/geo_raster_layer.py b/base_geoengine/geo_view/geo_raster_layer.py index bc602f6cef..9753f16b19 100644 --- a/base_geoengine/geo_view/geo_raster_layer.py +++ b/base_geoengine/geo_view/geo_raster_layer.py @@ -41,17 +41,12 @@ class GeoRasterLayer(models.Model): matrix_set = fields.Char("matrixSet") format_suffix = fields.Char("formatSuffix", help="eg. png") request_encoding = fields.Char("requestEncoding", help="eg. REST") - projection = fields.Char("projection", help="eg. EPSG:21781") + projection = fields.Char("projection", help="eg. EPSG:3857") units = fields.Char(help="eg. m") resolutions = fields.Char("resolutions") max_extent = fields.Char("max_extent") - dimensions = fields.Char( - "dimensions", - help="List of dimensions separated by ','") - params = fields.Char( - "params", - help="Dictiorary of values for dimensions as JSON" - ) + dimensions = fields.Char("dimensions", help="List of dimensions separated by ','") + params = fields.Char("params", help="Dictionary of values for dimensions as JSON") # technical field to display or not layer type has_type = fields.Boolean(compute='_compute_has_type') diff --git a/base_geoengine/static/src/js/widgets/geoengine_widgets.js b/base_geoengine/static/src/js/widgets/geoengine_widgets.js index 6a4bf94018..9f941348f9 100644 --- a/base_geoengine/static/src/js/widgets/geoengine_widgets.js +++ b/base_geoengine/static/src/js/widgets/geoengine_widgets.js @@ -10,13 +10,13 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { "use strict"; - var core = require("web.core"); - var AbstractField = require("web.AbstractField"); - var geoengine_common = require("base_geoengine.geoengine_common"); - var BackgroundLayers = require("base_geoengine.BackgroundLayers"); - var registry = require("web.field_registry"); + const core = require("web.core"); + const AbstractField = require("web.AbstractField"); + const geoengine_common = require("base_geoengine.geoengine_common"); + const BackgroundLayers = require("base_geoengine.BackgroundLayers"); + const registry = require("web.field_registry"); - var FieldGeoEngineEditMap = AbstractField.extend( + const FieldGeoEngineEditMap = AbstractField.extend( geoengine_common.GeoengineMixin, { // eslint-disable-line max-len @@ -43,7 +43,7 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { * @override */ start: function () { - var def = this._super(); + let def = this._super(); // Add a listener on parent tab if it exists in order to refresh // geoengine view we need to trigger it on DOM update for changes @@ -95,7 +95,7 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { }, _createFeatureStyles: function () { - var styles = { + let styles = { edit: new ol.style.Style({ fill: new ol.style.Fill({ opacity: 0.7, @@ -144,7 +144,7 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { this.vectorLayers = []; this.source = {}; this.features = {}; - var styles = this._createFeatureStyles(); + let styles = this._createFeatureStyles(); this.vectorLayers.push( this._createVectorLayer(this.name, styles.edit) ); @@ -169,7 +169,7 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { _addVectorLayers: function () { // first create the readonly layers to have a // lower zIndex - var readonlyLayers = this.vectorLayers.slice(1); + let readonlyLayers = this.vectorLayers.slice(1); if (readonlyLayers) { this.readonlyOverlaysGroup = new ol.layer.Group({ title: "Readonly Overlays", @@ -220,10 +220,10 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { }, _updateMapEmpty: function () { - var map_view = this.map.getView(); + let map_view = this.map.getView(); // Default extent if (map_view) { - var extent = this.defaultExtent + let extent = this.defaultExtent .replace(/\s/g, "") .split(","); extent = extent.map((coord) => Number(coord)); @@ -232,18 +232,18 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { }, _updateMapZoom: function (zoom) { - var map_zoom = typeof zoom === "undefined" ? true : zoom; + let map_zoom = typeof zoom === "undefined" ? true : zoom; if (this.source[this.name]) { - var extent = this.source[this.name].getExtent(); - var infinite_extent = [ + let extent = this.source[this.name].getExtent(); + let infinite_extent = [ Infinity, Infinity, -Infinity, -Infinity, ]; if (map_zoom && extent !== infinite_extent) { - var map_view = this.map.getView(); + let map_view = this.map.getView(); if (map_view) { map_view.fit(extent, { maxZoom: 15 }); } @@ -286,7 +286,7 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { }, _isTabVisible: function () { - var tab = this.$el.closest("div.tab-pane"); + let tab = this.$el.closest("div.tab-pane"); if (!tab.length) { return false; } @@ -294,7 +294,7 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { }, _onUIChange: function () { - var value = null; + let value = null; if (this._geometry) { value = this.format.writeGeometry(this._geometry); } @@ -308,7 +308,7 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { * As modify needs to get pointer position on map it requires * the map to be rendered before being created */ - var handler = null; + let handler = null; if (this.geoType === "POLYGON") { handler = "Polygon"; } else if (this.geoType === "MULTIPOLYGON") { @@ -325,7 +325,7 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { // FIXME: unsupported geo type } - var drawControl = function (options) { + let drawControl = function (options) { ol.interaction.Draw.call(this, options); }; ol.inherits(drawControl, ol.interaction.Draw); @@ -396,8 +396,8 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { _renderMap: function () { if (!this.map) { - var projection = new ol.proj.get(this.projectionCode); - var $el = this.$el[0]; + let projection = new ol.proj.get(this.projectionCode); + let $el = this.$el[0]; $($el).css({ width: "100%", height: "100%" }); let view = new ol.View({ center: [0, 0], @@ -414,7 +414,7 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { this._addVectorLayers(); this.format = new ol.format.GeoJSON({ featureProjection: projection, - defaultDataProjection: "EPSG:" + this.srid, + defaultDataProjection: `EPSG:${this.srid}`, }); $(document).trigger("FieldGeoEngineEditMap:ready", [ @@ -459,7 +459,7 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { // TODO migrate the following widgets - var FieldGeoPointXY = AbstractField.extend({ + let FieldGeoPointXY = AbstractField.extend({ template: "FieldGeoPointXY", start: function () { @@ -474,10 +474,10 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { * * @return [x, y] */ - var x = openerp.web.parse_value(this.$input.eq(0).val(), { + let x = openerp.web.parse_value(this.$input.eq(0).val(), { type: "float", }); - var y = openerp.web.parse_value(this.$input.eq(1).val(), { + let y = openerp.web.parse_value(this.$input.eq(1).val(), { type: "float", }); return [x, y]; @@ -491,7 +491,7 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { this._super.apply(this, arguments); if (value) { - var geo_obj = JSON.parse(value); + let geo_obj = JSON.parse(value); this.$input.eq(0).val(geo_obj.coordinates[0]); this.$input.eq(1).val(geo_obj.coordinates[1]); } else { @@ -500,9 +500,9 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { }, _onUIChange: function () { - var coords = this.get_coords(); + let coords = this.get_coords(); if (coords[0] && coords[1]) { - var json = this.make_GeoJSON(coords); + let json = this.make_GeoJSON(coords); this.value = JSON.stringify(json); } else { this.value = false; @@ -513,7 +513,7 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { this.invalid = false; try { // Get coords to check if floats - var coords = this.get_coords(); + let coords = this.get_coords(); // Make sure the two coordinates are set or None this.invalid = @@ -535,14 +535,14 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { }, }); - var FieldGeoPointXYReadonly = FieldGeoPointXY.extend({ + let FieldGeoPointXYReadonly = FieldGeoPointXY.extend({ template: "FieldGeoPointXY.readonly", _setValue: function (value) { this._super.apply(this, arguments); - var show_value = ""; + let show_value = ""; if (value) { - var geo_obj = JSON.parse(value); + let geo_obj = JSON.parse(value); show_value = "(" + geo_obj.coordinates[0] + @@ -559,7 +559,7 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { }, }); - var FieldGeoRect = AbstractField.extend({ + let FieldGeoRect = AbstractField.extend({ template: "FieldGeoRect", start: function () { @@ -574,16 +574,16 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { * * @return [[x1, y1],[x2, y2]] */ - var x1 = openerp.web.parse_value(this.$input.eq(0).val(), { + let x1 = openerp.web.parse_value(this.$input.eq(0).val(), { type: "float", }); - var y1 = openerp.web.parse_value(this.$input.eq(1).val(), { + let y1 = openerp.web.parse_value(this.$input.eq(1).val(), { type: "float", }); - var x2 = openerp.web.parse_value(this.$input.eq(2).val(), { + let x2 = openerp.web.parse_value(this.$input.eq(2).val(), { type: "float", }); - var y2 = openerp.web.parse_value(this.$input.eq(3).val(), { + let y2 = openerp.web.parse_value(this.$input.eq(3).val(), { type: "float", }); @@ -594,12 +594,12 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { }, make_GeoJSON: function (coords) { - var p1 = coords[0]; - var p2 = [coords[0][0], coords[1][1]]; - var p3 = coords[1]; - var p4 = [coords[1][0], coords[0][1]]; + let p1 = coords[0]; + let p2 = [coords[0][0], coords[1][1]]; + let p3 = coords[1]; + let p4 = [coords[1][0], coords[0][1]]; // Create a loop in clockwise - var points = [[p1, p2, p3, p4, p1]]; + let points = [[p1, p2, p3, p4, p1]]; return { type: "Polygon", coordinates: points }; }, @@ -607,7 +607,7 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { this._super.apply(this, arguments); if (value) { - var geo_obj = JSON.parse(value); + let geo_obj = JSON.parse(value); this.$input.eq(0).val(geo_obj.coordinates[0][0][0]); this.$input.eq(1).val(geo_obj.coordinates[0][0][1]); this.$input.eq(2).val(geo_obj.coordinates[0][2][0]); @@ -623,16 +623,16 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { * * @return [[x1, y1],[x2, y2]] */ - var x1 = coords[0][0], + let x1 = coords[0][0], y1 = coords[0][1], x2 = coords[1][0], y2 = coords[1][1]; - var minx = Math.min(x1, x2); - var maxx = Math.max(x1, x2); + let minx = Math.min(x1, x2); + let maxx = Math.max(x1, x2); - var miny = Math.min(y1, y2); - var maxy = Math.max(y1, y2); + let miny = Math.min(y1, y2); + let maxy = Math.max(y1, y2); return [ [minx, miny], @@ -641,10 +641,10 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { }, _onUIChange: function () { - var coords = this.get_coords(); + let coords = this.get_coords(); if (this.all_are_set(coords)) { coords = this.correct_bounds(coords); - var json = this.make_GeoJSON(coords); + let json = this.make_GeoJSON(coords); this.value = JSON.stringify(json); } else { this.value = false; @@ -673,7 +673,7 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { this.invalid = false; try { // Get coords to check if floats - var coords = this.get_coords(); + let coords = this.get_coords(); // Make sure all the coordinates are set // if not None or if required @@ -695,14 +695,14 @@ odoo.define("base_geoengine.geoengine_widgets", function (require) { }, }); - var FieldGeoRectReadonly = FieldGeoRect.extend({ + let FieldGeoRectReadonly = FieldGeoRect.extend({ template: "FieldGeoRect.readonly", _setValue: function (value) { this._super.apply(this, arguments); - var show_value = ""; + let show_value = ""; if (value) { - var geo_obj = JSON.parse(value); + let geo_obj = JSON.parse(value); show_value = "(" + geo_obj.coordinates[0][0][0] + diff --git a/geoengine_swisstopo/geo_view/geo_raster_layer.py b/geoengine_swisstopo/geo_view/geo_raster_layer.py index 3e873964b5..6f44e184a9 100644 --- a/geoengine_swisstopo/geo_view/geo_raster_layer.py +++ b/geoengine_swisstopo/geo_view/geo_raster_layer.py @@ -1,12 +1,66 @@ # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import fields, models +import logging + +import requests +from odoo import api, fields, models + + +_LOGGER = logging.getLogger(__name__) +_CAPABILITIES_URL = ( + "https://wmts.geo.admin.ch/EPSG/{matrix_set}/1.0.0/WMTSCapabilities.xml" +) class GeoRasterLayer(models.Model): _inherit = "geoengine.raster.layer" raster_type = fields.Selection(selection_add=[("swisstopo", "Swisstopo")]) - layername = fields.Char("Layer Machine Name") - time = fields.Char("Time Dimension") + projection = fields.Char( + "Projection", compute="_get_projection", readonly=True, store=True + ) + layername = fields.Char("Layer Name", default="ch.swisstopo.pixelkarte-farbe") + matrix_set = fields.Selection( + [ + ("2056", "LV95/CH1903+ (EPSG:2056)"), + ("21781", "LV03/CH1903 (EPSG:21781)"), + ("4326", "WGS84 (EPSG:4326, lat-lon)"), + ( + "3857", + "Spherical Mercator (EPSG:3857, as used in OSM, Bing, Google Map)", + ), + ], + default="2056", + string="TileMatrixSet", + ) + time = fields.Char("Time Dimension (optional)", default=None) + capabilities = fields.Char(compute="_get_capabilities", readonly=True, store=True) + + @api.depends("raster_type", "matrix_set") + def _get_projection(self): + for record in self: + if record.raster_type == "swisstopo": + record.projection = f"EPSG:{record.matrix_set}" + else: + record.projection = False + + @api.depends("raster_type", "matrix_set") + def _get_capabilities(self): + for record in self: + if record.raster_type == "swisstopo": + url = _CAPABILITIES_URL.format(matrix_set=record.matrix_set or "2056") + response = requests.get(url, timeout=30) + if response.ok: + record.capabilities = response.text + else: + _LOGGER.error( + "Swisstopo WMTS Capabilities request (%s)\n" + "failed with status code %s:\n%s", + url, + response.status_code, + response.text, + ) + record.capabilities = False + else: + record.capabilities = False diff --git a/geoengine_swisstopo/geo_view/geo_raster_layer_view.xml b/geoengine_swisstopo/geo_view/geo_raster_layer_view.xml index 73ac646635..17a765275b 100644 --- a/geoengine_swisstopo/geo_view/geo_raster_layer_view.xml +++ b/geoengine_swisstopo/geo_view/geo_raster_layer_view.xml @@ -14,10 +14,12 @@ -

See GeoAdmin API documentation for registration info and available layers. As for now only projection EPSG:21781 is supported.

+

See GeoAdmin API documentation for registration info and available layers.

+ +
diff --git a/geoengine_swisstopo/i18n/geoengine_swisstopo.pot b/geoengine_swisstopo/i18n/geoengine_swisstopo.pot index e09b634a63..26eab0af27 100644 --- a/geoengine_swisstopo/i18n/geoengine_swisstopo.pot +++ b/geoengine_swisstopo/i18n/geoengine_swisstopo.pot @@ -80,6 +80,6 @@ msgstr "" #. module: geoengine_swisstopo #: model_terms:ir.ui.view,arch_db:geoengine_swisstopo.geo_raster_view_form -msgid "for registration info and available layers. As for now only projection EPSG:21781 is supported." +msgid "for registration info and available layers." msgstr "" diff --git a/geoengine_swisstopo/static/src/js/geoengine_swisstopo.js b/geoengine_swisstopo/static/src/js/geoengine_swisstopo.js index 60062ac93f..1e4a5ef33a 100644 --- a/geoengine_swisstopo/static/src/js/geoengine_swisstopo.js +++ b/geoengine_swisstopo/static/src/js/geoengine_swisstopo.js @@ -1,69 +1,88 @@ -/** - * Available resolutions as defined in - * https://api3.geo.admin.ch/services/sdiservices.html#wmts. - * @const {!Array.} - */ -var RESOLUTIONS = [ - 4000, 3750, 3500, 3250, 3000, 2750, 2500, 2250, 2000, 1750, 1500, 1250, - 1000, 750, 650, 500, 250, 100, 50, 20, 10, 5, 2.5, 2, 1.5, 1, 0.5, - 0.25, 0.1 -]; - -var BASE_URL = 'https://wmts{0-9}.geo.admin.ch/1.0.0/{Layer}/default/{Time}/21781/{TileMatrix}/{TileRow}/{TileCol}.{format}'; - -var ATTRIBUTIONS = 'swisstopo'; - -/** - * Extents of Swiss projections. (EPSG:21781) - */ -var EXTENT = [420000, 30000, 900000, 350000]; +const ATTRIBUTIONS = + 'swisstopo'; + +const PROJECTION_DEFINITIONS = { + "EPSG:21781": [ + "+proj=somerc", + "+lat_0=46.95240555555556", + "+lon_0=7.439583333333333", + "+k_0=1", + "+x_0=600000", + "+y_0=200000", + "+ellps=bessel", + "+towgs84=674.4,15.1,405.3,0,0,0,0", + "+units=m", + "+no_defs", + ].join(" "), + "EPSG:2056": [ + "+proj=somerc", + "+lat_0=46.95240555555556", + "+lon_0=7.43958333333333", + "+k_0=1", + "+x_0=2600000", + "+y_0=1200000", + "+ellps=bessel", + "+towgs84=674.374,15.056,405.346,0,0,0,0", + "+units=m", + "+no_defs", + "+type=crs", + ].join(" "), + "EPSG:4326": [ + "+proj=longlat", + "+datum=WGS84", + "+no_defs", + "+type=crs", + ].join(" "), + "EPSG:3857": [ + "+proj=merc", + "+a=6378137", + "+b=6378137", + "+lat_ts=0", + "+lon_0=0", + "+x_0=0", + "+y_0=0", + "+k=1", + "+units=m", + "+nadgrids=@null", + "+wktext", + "+no_defs", + "+type=crs", + ].join(" "), +}; -var PROJECTION_CODE = "EPSG:21781"; +const DEFAULT_PROJECTION_CODE = "EPSG:2056"; -var init_EPSG_21781 = function (self) { +function init_proj4(self) { // Adding proj4 - self.jsLibs.push( - '/geoengine_swisstopo/static/lib/proj4.js' - ); -}; - -var define_EPSG_21781 = function () { - // add swiss projection to allow conversions - if (!ol.proj.get(PROJECTION_CODE)) { - proj4.defs('EPSG:21781', '+proj=somerc +lat_0=46.95240555555556 ' + - '+lon_0=7.439583333333333 +k_0=1 +x_0=600000 +y_0=200000 +ellps=bessel ' + - '+towgs84=674.4,15.1,405.3,0,0,0,0 +units=m +no_defs'); + self.jsLibs.push("/geoengine_swisstopo/static/lib/proj4.js"); +} + +function define_projections() { + // add the required projections to allow conversions + for (let code in PROJECTION_DEFINITIONS) { + if (!ol.proj.get(code)) { + proj4.defs(code, PROJECTION_DEFINITIONS[code]); + } } -}; +} - -odoo.define('geoengine_swisstopo.projection_EPSG_21781', function (require) { +odoo.define("geoengine_swisstopo.projection", function (require) { "use strict"; - var GeoengineWidgets = require('base_geoengine.geoengine_widgets'); - var GeoengineView = require('base_geoengine.GeoengineView'); + const GeoengineWidgets = require("base_geoengine.geoengine_widgets"); + const GeoengineView = require("base_geoengine.GeoengineView"); GeoengineWidgets.FieldGeoEngineEditMap.include({ init: function (parent) { this._super.apply(this, arguments); - init_EPSG_21781(this); - }, - _render: function (parent) { - define_EPSG_21781(); - this._super.apply(this, arguments); + init_proj4(this); }, - }); GeoengineView.include({ init: function (parent) { this._super.apply(this, arguments); - init_EPSG_21781(this); + init_proj4(this); }, - _render: function (parent) { - define_EPSG_21781(); - this._super.apply(this, arguments); - }, - }); }); @@ -71,54 +90,45 @@ odoo.define('geoengine_swisstopo.projection_EPSG_21781', function (require) { odoo.define('geoengine_swisstopo.BackgroundLayers', function (require) { "use strict"; - var BackgroundLayers = require('base_geoengine.BackgroundLayers'); + const BackgroundLayers = require("base_geoengine.BackgroundLayers"); BackgroundLayers.include({ - - createTileGrid: function() { - return new ol.tilegrid.WMTS({ - extent: EXTENT, - resolutions: RESOLUTIONS, - matrixIds: RESOLUTIONS.map(function(item, index) { - return String(index); - }), - }); - }, - - handleCustomLayers: function(l) { - var out = this._super.apply(this, arguments); - if (l.raster_type == 'swisstopo') { - var format = l.format_suffix || 'jpeg'; - var layer = l.layername || 'ch.swisstopo.pixelkarte-farbe'; - - var url = BASE_URL.replace('{format}', format); - var projection = ol.proj.get(PROJECTION_CODE); - var source = new ol.source.WMTS({ - attributions: [ - new ol.Attribution({ - html: ATTRIBUTIONS, - }) - ], - url: url, - dimensions: { - 'Time': l.time || 'current', + handleCustomLayers: function (l) { + define_projections(); + let out = this._super.apply(this, arguments); + if (l.raster_type == "swisstopo") { + let format = l.format_suffix || "jpeg"; + let projection_code = l.projection || DEFAULT_PROJECTION_CODE; + let options = ol.source.WMTS.optionsFromCapabilities( + new ol.format.WMTSCapabilities().read(l.capabilities), + { + crossOrigin: "anonymous", + layer: l.layername, + projection: projection_code, + format: `image/${format}`, }, - projection: projection, - requestEncoding: 'REST', - layer: layer, - style: 'default', - matrixSet: '21781', - format: 'image/' + format, - tileGrid: this.createTileGrid(), - crossOrigin: 'anonymous', - }); + ); + if (!options) { + console.error("the layer is not in the capabilities"); + return out; + } + if (l.time && options.dimensions.Time) { + options.dimensions.Time = l.time; + } + options.attributions = [ + new ol.Attribution({ + html: ATTRIBUTIONS, + }), + ]; + + let source = new ol.source.WMTS(options); out.push( new ol.layer.Tile({ title: l.name, visible: !l.overlay, - type: 'base', - source: source - }) + type: "base", + source: source, + }), ); } return out;