diff --git a/app/javascript/maplibre/basemaps.js b/app/javascript/maplibre/basemaps.js index 98a09b71..51329b73 100644 --- a/app/javascript/maplibre/basemaps.js +++ b/app/javascript/maplibre/basemaps.js @@ -1,6 +1,11 @@ -// Maptiler SDK shortcuts: https://docs.maptiler.com/sdk-js/api/map-styles/#mapstylelist - +// Default glyphs for Raster maps const openmaptilesGlyphs = 'https://fonts.openmaptiles.org/{fontstack}/{range}.pbf' +// fonts must be available via glyphs: +// openmaptiles: https://github.com/openmaptiles/fonts/tree/gh-pages +// maptiler: https://docs.maptiler.com/gl-style-specification/glyphs/ +// versatiles: https://github.com/versatiles-org/versatiles-fonts/tree/main/fonts +// Emojis are not in the character range: https://github.com/maplibre/maplibre-gl-js/issues/2307 +export const defaultFont = 'Klokantech Noto Sans Bold' // avail from openmaptiles + maplibre const defaultRasterLayer = [ { id: 'simple-tiles', @@ -15,137 +20,151 @@ const host = new URL(window.location.href).origin export const basemaps = { // static test tile test: { - version: 8, - sources: { - 'raster-tiles': { - type: 'raster', - tiles: ['/layers/test_tile.png'], - tileSize: 1024 - } - }, - layers: defaultRasterLayer, - glyphs: openmaptilesGlyphs + style: { + version: 8, + sources: { + 'raster-tiles': { + type: 'raster', + tiles: ['/layers/test_tile.png'], + tileSize: 1024 + } + }, + layers: defaultRasterLayer, + glyphs: openmaptilesGlyphs + } }, // Stadia maps stamenWatercolorTiles: { - version: 8, - sources: { - 'raster-tiles': { - type: 'raster', - tiles: [ + style: { + version: 8, + sources: { + 'raster-tiles': { + type: 'raster', + tiles: [ // NOTE: Layers from Stadia Maps do not require an API key for localhost development or most production // web deployments. See https://docs.stadiamaps.com/authentication/ for details. - 'https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg' - ], - tileSize: 256, - attribution: 'Map tiles by Stamen Design; Hosting by Stadia Maps. Data © OpenStreetMap contributors' - } - }, - layers: defaultRasterLayer, - glyphs: openmaptilesGlyphs + 'https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg' + ], + tileSize: 256, + attribution: 'Map tiles by Stamen Design; Hosting by Stadia Maps. Data © OpenStreetMap contributors' + } + }, + layers: defaultRasterLayer, + glyphs: openmaptilesGlyphs + } }, stamenTonerTiles: { - version: 8, - sources: { - 'raster-tiles': { - type: 'raster', - tiles: [ + style: { + version: 8, + sources: { + 'raster-tiles': { + type: 'raster', + tiles: [ // NOTE: Layers from Stadia Maps do not require an API key for localhost development or most production // web deployments. See https://docs.stadiamaps.com/authentication/ for details. - 'https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}.jpg' - ], - tileSize: 256, - attribution: 'Map tiles by Stamen Design; Hosting by Stadia Maps. Data © OpenStreetMap contributors' - } - }, - layers: defaultRasterLayer, - glyphs: openmaptilesGlyphs + 'https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}.jpg' + ], + tileSize: 256, + attribution: 'Map tiles by Stamen Design; Hosting by Stadia Maps. Data © OpenStreetMap contributors' + } + }, + layers: defaultRasterLayer, + glyphs: openmaptilesGlyphs + } }, // free maps openTopoTiles: { - version: 8, - sources: { - 'raster-tiles': { - type: 'raster', - tiles: [ - // https://opentopomap.org/about#verwendung - 'https://a.tile.opentopomap.org/{z}/{x}/{y}.png' - ], - tileSize: 256, - attribution: 'Kartendaten: © ' + - 'OpenStreetMap-Mitwirkende ' + - 'SRTM | Kartendarstellung: © ' + - 'OpenTopoMap ' + - '(CC-BY-SA)' - } - }, - layers: defaultRasterLayer, - glyphs: openmaptilesGlyphs + style: { + version: 8, + sources: { + 'raster-tiles': { + type: 'raster', + tiles: [ + // https://opentopomap.org/about#verwendung + 'https://a.tile.opentopomap.org/{z}/{x}/{y}.png' + ], + tileSize: 256, + attribution: 'Kartendaten: © ' + + 'OpenStreetMap-Mitwirkende ' + + 'SRTM | Kartendarstellung: © ' + + 'OpenTopoMap ' + + '(CC-BY-SA)' + } + }, + layers: defaultRasterLayer, + glyphs: openmaptilesGlyphs + } }, satelliteTiles: { - version: 8, - projection: { type: 'globe' }, - sources: { - 'raster-tiles': { - type: 'raster', - tiles: [ - 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}' - ], - tileSize: 256, - attribution: 'Powered by Esri, ' + + style: { + version: 8, + projection: { type: 'globe' }, + sources: { + 'raster-tiles': { + type: 'raster', + tiles: [ + 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}' + ], + tileSize: 256, + attribution: 'Powered by Esri, ' + 'Sources: Esri, DigitalGlobe, GeoEye, i-cubed, USDA FSA, USGS, AEX, Getmapping, Aerogrid, IGN, IGP, swisstopo, and the GIS User Community' - } - }, - sky: { - 'atmosphere-blend': [ - 'interpolate', - ['linear'], - ['zoom'], - 0, 1, - 5, 1, - 7, 0 - ] - }, - layers: defaultRasterLayer, - glyphs: openmaptilesGlyphs + } + }, + sky: { + 'atmosphere-blend': [ + 'interpolate', + ['linear'], + ['zoom'], + 0, 1, + 5, 1, + 7, 0 + ] + }, + layers: defaultRasterLayer, + glyphs: openmaptilesGlyphs + } }, osmRasterTiles: { - version: 8, - sources: { - 'raster-tiles': { - type: 'raster', - tiles: [ - 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png' - ], - tileSize: 256, - attribution: '© OpenStreetMap Contributors' - } - }, - layers: defaultRasterLayer, - glyphs: openmaptilesGlyphs + style: { + version: 8, + sources: { + 'raster-tiles': { + type: 'raster', + tiles: [ + 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png' + ], + tileSize: 256, + attribution: '© OpenStreetMap Contributors' + } + }, + layers: defaultRasterLayer, + glyphs: openmaptilesGlyphs + } }, // openfreemap.org - openfreemapPositron: 'https://tiles.openfreemap.org/styles/positron', - openfreemapBright: 'https://tiles.openfreemap.org/styles/bright', - openfreemapLiberty: 'https://tiles.openfreemap.org/styles/liberty', + openfreemapPositron: { style: 'https://tiles.openfreemap.org/styles/positron', font: 'Noto Sans Regular' }, + openfreemapBright: { style: 'https://tiles.openfreemap.org/styles/bright', font: 'Noto Sans Regular' }, + openfreemapLiberty: { style: 'https://tiles.openfreemap.org/styles/liberty', font: 'Noto Sans Regular' }, // https://github.com/versatiles-org/versatiles-style - versatilesColorful: 'https://tiles.versatiles.org/assets/styles/colorful.json', - versatilesGraybeard: 'https://tiles.versatiles.org/assets/styles/graybeard.json', - versatilesNeutrino: 'https://tiles.versatiles.org/assets/styles/neutrino.json', + // fonts: https://github.com/versatiles-org/versatiles-fonts + versatilesColorful: { style: 'https://tiles.versatiles.org/assets/styles/colorful.json', font: 'noto_sans_regular' }, + versatilesGraybeard: { style: 'https://tiles.versatiles.org/assets/styles/graybeard.json', font: 'noto_sans_regular' }, + versatilesNeutrino: { style: 'https://tiles.versatiles.org/assets/styles/neutrino.json', font: 'noto_sans_regular' }, + // Maptiler maps: https://docs.maptiler.com/sdk-js/api/map-styles/#mapstylelist // 3D Houses - maptilerBasic: 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + window.gon.map_keys.maptiler, - maptilerOpenStreetmap: 'https://api.maptiler.com/maps/openstreetmap/style.json?key=' + window.gon.map_keys.maptiler, - maptilerBuildings: 'https://api.maptiler.com/maps/streets-v2/style.json?key=' + window.gon.map_keys.maptiler, - maptilerDataviz: 'https://api.maptiler.com/maps/dataviz/style.json?key=' + window.gon.map_keys.maptiler, - maptilerStreets: host + '/layers/streets.json?key=' + window.gon.map_keys.maptiler, - maptilerNoStreets: host + '/layers/nostreets.json?key=' + window.gon.map_keys.maptiler, - maptilerSatellite: 'https://api.maptiler.com/maps/satellite/style.json?key=' + window.gon.map_keys.maptiler, - maptilerWinter: 'https://api.maptiler.com/maps/winter-v2/style.json?key=' + window.gon.map_keys.maptiler, - maptilerBike: 'https://api.maptiler.com/maps/64d03850-97e0-4aaa-bd1d-8287a9792de1/style.json?key=' + window.gon.map_keys.maptiler, - maptilerHybrid: 'https://api.maptiler.com/maps/hybrid/style.json?key=' + window.gon.map_keys.maptiler + maptilerBasic: { style: 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + window.gon.map_keys.maptiler }, + maptilerOpenStreetmap: { style: 'https://api.maptiler.com/maps/openstreetmap/style.json?key=' + window.gon.map_keys.maptiler }, + maptilerBuildings: { style: 'https://api.maptiler.com/maps/streets-v2/style.json?key=' + window.gon.map_keys.maptiler }, + maptilerDataviz: { style: 'https://api.maptiler.com/maps/dataviz/style.json?key=' + window.gon.map_keys.maptiler }, + maptilerStreets: { style: host + '/layers/streets.json?key=' + window.gon.map_keys.maptiler }, + maptilerNoStreets: { style: host + '/layers/nostreets.json?key=' + window.gon.map_keys.maptiler }, + maptilerSatellite: { style: 'https://api.maptiler.com/maps/satellite/style.json?key=' + window.gon.map_keys.maptiler }, + maptilerWinter: { style: 'https://api.maptiler.com/maps/winter-v2/style.json?key=' + window.gon.map_keys.maptiler }, + maptilerBike: { style: 'https://api.maptiler.com/maps/64d03850-97e0-4aaa-bd1d-8287a9792de1/style.json?key=' + window.gon.map_keys.maptiler }, + maptilerHybrid: { style: 'https://api.maptiler.com/maps/hybrid/style.json?key=' + window.gon.map_keys.maptiler } } diff --git a/app/javascript/maplibre/edit.js b/app/javascript/maplibre/edit.js index dc4b0802..85450973 100644 --- a/app/javascript/maplibre/edit.js +++ b/app/javascript/maplibre/edit.js @@ -58,7 +58,7 @@ export function initializeEditMode () { combine_features: false // uncombine_features }, - styles: editStyles, + styles: editStyles(), clickBuffer: 5, touchBuffer: 25, // default 25 // user properties are available, prefixed with 'user_' diff --git a/app/javascript/maplibre/edit_styles.js b/app/javascript/maplibre/edit_styles.js index 51807d20..117571c5 100644 --- a/app/javascript/maplibre/edit_styles.js +++ b/app/javascript/maplibre/edit_styles.js @@ -5,10 +5,10 @@ export function initializeEditStyles () { // MapboxDraw cannot render symbol+text styles. // Adding those as extra layers to the map. // render the extrusion layer from "source: 'geojson-source' without having it available for edit in draw - map.addLayer(styles['polygon-layer-extrusion']) - map.addLayer(styles['symbols-border-layer']) - map.addLayer(styles['symbols-layer']) - map.addLayer(styles['text-layer']) + map.addLayer(styles()['polygon-layer-extrusion']) + map.addLayer(styles()['symbols-border-layer']) + map.addLayer(styles()['symbols-layer']) + map.addLayer(styles()['text-layer']) sortLayers() console.log('Edit styles added') @@ -27,135 +27,137 @@ export function initializeEditStyles () { const highlightColor = '#fbb03b' -export const editStyles = [ +export function editStyles () { + return [ - removeSource(styles['polygon-layer']), // gl-draw-polygon-fill-inactive - removeSource(styles['line-layer-outline']), - removeSource(styles['line-layer']), // 'gl-draw-line-inactive', 'gl-draw-polygon-stroke-inactive', + removeSource(styles()['polygon-layer']), // gl-draw-polygon-fill-inactive + removeSource(styles()['line-layer-outline']), + removeSource(styles()['line-layer']), // 'gl-draw-line-inactive', 'gl-draw-polygon-stroke-inactive', - // active polygon outline - { - id: 'gl-draw-polygon-stroke-active', - type: 'line', - filter: ['all', - ['==', 'active', 'true'], - ['==', '$type', 'Polygon']], - layout: { - 'line-cap': 'round', - 'line-join': 'round' + // active polygon outline + { + id: 'gl-draw-polygon-stroke-active', + type: 'line', + filter: ['all', + ['==', 'active', 'true'], + ['==', '$type', 'Polygon']], + layout: { + 'line-cap': 'round', + 'line-join': 'round' + }, + paint: { + 'line-color': highlightColor, + 'line-dasharray': [0.2, 2], + 'line-width': 5 + } }, - paint: { - 'line-color': highlightColor, - 'line-dasharray': [0.2, 2], - 'line-width': 5 - } - }, - // active linestring - { - id: 'gl-draw-line-active', - type: 'line', - filter: ['all', - ['==', '$type', 'LineString'], - ['==', 'active', 'true'] - ], - layout: { - 'line-cap': 'round', - 'line-join': 'round' + // active linestring + { + id: 'gl-draw-line-active', + type: 'line', + filter: ['all', + ['==', '$type', 'LineString'], + ['==', 'active', 'true'] + ], + layout: { + 'line-cap': 'round', + 'line-join': 'round' + }, + paint: { + 'line-color': highlightColor, + 'line-dasharray': [0.2, 2], + 'line-width': 5 + } + }, + // midpoints to extend lines/polygons + { + id: 'gl-draw-polygon-midpoint', + type: 'circle', + filter: ['all', + ['==', '$type', 'Point'], + ['==', 'meta', 'midpoint']], + paint: { + 'circle-radius': pointSize, + 'circle-color': 'grey', + 'circle-opacity': 0.8, + 'circle-stroke-color': '#ffffff', + 'circle-stroke-width': 1 + } + }, + // default point behind symbols, transparent points etc. + { + id: 'gl-draw-point-point-stroke-inactive', + type: 'circle', + filter: ['all', + ['==', 'active', 'false'], + ['==', '$type', 'Point'], + ['==', 'meta', 'feature'], + ['!=', 'mode', 'static'] + ], + paint: { + 'circle-radius': pointSize, + 'circle-opacity': 0.2, + 'circle-color': '#ffffff', + 'circle-stroke-color': '#c0c0c0', + 'circle-stroke-width': 1 + } }, - paint: { - 'line-color': highlightColor, - 'line-dasharray': [0.2, 2], - 'line-width': 5 - } - }, - // midpoints to extend lines/polygons - { - id: 'gl-draw-polygon-midpoint', - type: 'circle', - filter: ['all', - ['==', '$type', 'Point'], - ['==', 'meta', 'midpoint']], - paint: { - 'circle-radius': pointSize, - 'circle-color': 'grey', - 'circle-opacity': 0.8, - 'circle-stroke-color': '#ffffff', - 'circle-stroke-width': 1 - } - }, - // default point behind symbols, transparent points etc. - { - id: 'gl-draw-point-point-stroke-inactive', - type: 'circle', - filter: ['all', - ['==', 'active', 'false'], - ['==', '$type', 'Point'], - ['==', 'meta', 'feature'], - ['!=', 'mode', 'static'] - ], - paint: { - 'circle-radius': pointSize, - 'circle-opacity': 0.2, - 'circle-color': '#ffffff', - 'circle-stroke-color': '#c0c0c0', - 'circle-stroke-width': 1 - } - }, - // active point, either single or on a line / polygon - { - id: 'gl-draw-point-stroke-active', - type: 'circle', - filter: ['all', - ['==', '$type', 'Point'], - ['==', 'active', 'true'], - ['!=', 'meta', 'midpoint'] - ], - paint: { - 'circle-radius': ['*', pointSizeMax, 2], - 'circle-color': '#ffffff', - 'circle-opacity': 0.2, - 'circle-stroke-color': highlightColor, - 'circle-stroke-width': 3 - } - }, - // inactive single point features - removeSource(styles['points-border-layer']), - removeSource(styles['points-layer']), + // active point, either single or on a line / polygon + { + id: 'gl-draw-point-stroke-active', + type: 'circle', + filter: ['all', + ['==', '$type', 'Point'], + ['==', 'active', 'true'], + ['!=', 'meta', 'midpoint'] + ], + paint: { + 'circle-radius': ['*', pointSizeMax, 2], + 'circle-color': '#ffffff', + 'circle-opacity': 0.2, + 'circle-stroke-color': highlightColor, + 'circle-stroke-width': 3 + } + }, + // inactive single point features + removeSource(styles()['points-border-layer']), + removeSource(styles()['points-layer']), - // inactive vertex points on lines + polygons, outline - // renderingoutline seperately to generate nicer overlay effect - { - id: 'gl-draw-polygon-and-line-vertex-outline-inactive', - type: 'circle', - filter: ['all', - ['==', 'meta', 'vertex'], - ['==', '$type', 'Point'], - ['!=', 'mode', 'static'] - ], - paint: { - 'circle-radius': pointSize, - 'circle-opacity': 0, - 'circle-stroke-color': '#444', - 'circle-stroke-width': pointOutlineSize, - 'circle-stroke-opacity': 1 - } - }, - // inactive vertex points on lines + polygons - { - id: 'gl-draw-polygon-and-line-vertex-inactive', - type: 'circle', - filter: ['all', - ['==', 'meta', 'vertex'], - ['==', '$type', 'Point'], - ['!=', 'mode', 'static'] - ], - paint: { - 'circle-radius': pointSize, - 'circle-color': highlightColor + // inactive vertex points on lines + polygons, outline + // renderingoutline seperately to generate nicer overlay effect + { + id: 'gl-draw-polygon-and-line-vertex-outline-inactive', + type: 'circle', + filter: ['all', + ['==', 'meta', 'vertex'], + ['==', '$type', 'Point'], + ['!=', 'mode', 'static'] + ], + paint: { + 'circle-radius': pointSize, + 'circle-opacity': 0, + 'circle-stroke-color': '#444', + 'circle-stroke-width': pointOutlineSize, + 'circle-stroke-opacity': 1 + } + }, + // inactive vertex points on lines + polygons + { + id: 'gl-draw-polygon-and-line-vertex-inactive', + type: 'circle', + filter: ['all', + ['==', 'meta', 'vertex'], + ['==', '$type', 'Point'], + ['!=', 'mode', 'static'] + ], + paint: { + 'circle-radius': pointSize, + 'circle-color': highlightColor + } } - } -] + ] +} function removeSource (style) { const { source, ...filteredStyle } = style diff --git a/app/javascript/maplibre/map.js b/app/javascript/maplibre/map.js index 5b3c5a54..66af3a5c 100644 --- a/app/javascript/maplibre/map.js +++ b/app/javascript/maplibre/map.js @@ -1,7 +1,7 @@ -import { basemaps } from 'maplibre/basemaps' +import { basemaps, defaultFont } from 'maplibre/basemaps' import { draw } from 'maplibre/edit' import { resetControls, initSettingsModal, geocoderConfig, initCtrlTooltips } from 'maplibre/controls' -import { initializeViewStyles } from 'maplibre/styles' +import { initializeViewStyles, setStyleDefaultFont } from 'maplibre/styles' import { highlightFeature, resetHighlightedFeature } from 'maplibre/feature' import { AnimatePointAnimation } from 'maplibre/animations' import * as functions from 'helpers/functions' @@ -16,8 +16,9 @@ export let geojsonData //= { type: 'FeatureCollection', features: [] } export let mapProperties export let lastMousePosition export let highlightedFeature +export let backgroundMapLayer + let mapInteracted -let backgroundMapLayer let backgroundTerrain // workflow of event based map loading: @@ -349,12 +350,13 @@ export function setBackgroundMapLayer (mapName = mapProperties.base_map, force = if (backgroundMapLayer === mapName && backgroundTerrain === mapProperties.terrain && !force) { return } if (basemaps[mapName]) { map.once('style.load', () => { status('Loaded base map ' + mapName) }) - map.setStyle(basemaps[mapName], + backgroundMapLayer = mapName + backgroundTerrain = mapProperties.terrain + setStyleDefaultFont(basemaps[mapName].font || defaultFont) + map.setStyle(basemaps[mapName].style, // adding 'diff: false' so that 'style.load' gets triggered (https://github.com/maplibre/maplibre-gl-js/issues/2587) // which will trigger loadGeoJsonData() { diff: false, strictMode: true }) - backgroundMapLayer = mapName - backgroundTerrain = mapProperties.terrain } else { console.error('Base map ' + mapName + ' not available!') } diff --git a/app/javascript/maplibre/styles.js b/app/javascript/maplibre/styles.js index a77b4b44..b1179347 100644 --- a/app/javascript/maplibre/styles.js +++ b/app/javascript/maplibre/styles.js @@ -18,9 +18,11 @@ export const viewStyleNames = [ 'polygon-layer-extrusion' ] +export function setStyleDefaultFont (font) { labelFont = [font] } + export function initializeViewStyles () { viewStyleNames.forEach(styleName => { - map.addLayer(styles[styleName]) + map.addLayer(styles()[styleName]) }) sortLayers() console.log('View styles added') @@ -173,260 +175,257 @@ const iconSizeFactor = ['/', pointSizeMax, 6] const iconSize = ['*', 1 / 8, iconSizeFactor] // const iconSizeActive = ['*', 1.1, iconSize] // icon-size is not a paint property const labelSize = ['to-number', ['coalesce', ['get', 'user_label-size'], ['get', 'label-size'], 16]] +// default font is set in basemap def basemaps[backgroundMapLayer]['font'] +let labelFont -// font must be available via glyphs: -// openmaptiles: https://github.com/openmaptiles/fonts/tree/gh-pages -// maptiler: https://docs.maptiler.com/gl-style-specification/glyphs/ -// versatiles: https://github.com/versatiles-org/versatiles-fonts/tree/main/fonts -// Emojis are not in the character range: https://github.com/maplibre/maplibre-gl-js/issues/2307 -const labelFont = ['Klokantech Noto Sans Bold'] - -export const styles = { - 'polygon-layer': { - id: 'polygon-layer', - type: 'fill', - source: 'geojson-source', - filter: ['all', - ['in', '$type', 'Polygon']], - paint: { - 'fill-color': fillColor, - 'fill-opacity': [ - 'case', - ['boolean', ['feature-state', 'active'], false], - fillOpacityActive, - fillOpacity - ] - } - }, - 'polygon-layer-extrusion': { - id: 'polygon-layer-extrusion', - type: 'fill-extrusion', - source: 'geojson-source', - filter: ['all', - ['in', '$type', 'Polygon'], - ['>', 'fill-extrusion-height', 0]], - paint: { - 'fill-extrusion-color': ['coalesce', - ['get', 'fill-extrusion-color'], - ['get', 'user_fill-extrusion-color'], - ['get', 'fill'], - ['get', 'user_fill'], - featureColor], - 'fill-extrusion-height': ['to-number', ['coalesce', - ['get', 'fill-extrusion-height'], - ['get', 'user_fill-extrusion-height']]], - 'fill-extrusion-base': ['to-number', ['coalesce', - ['get', 'fill-extrusion-base'], - ['get', 'user_fill-extrusion-base']]], - // opacity does not support data expressions!?! - 'fill-extrusion-opacity': 0.8 - } - }, - 'line-layer-outline': { - id: 'line-layer-outline', - type: 'line', - source: 'geojson-source', - filter: ['all', - ['in', '$type', 'LineString']], - layout: { - 'line-join': 'round', - 'line-cap': 'round' +export function styles () { + return { + 'polygon-layer': { + id: 'polygon-layer', + type: 'fill', + source: 'geojson-source', + filter: ['all', + ['in', '$type', 'Polygon']], + paint: { + 'fill-color': fillColor, + 'fill-opacity': [ + 'case', + ['boolean', ['feature-state', 'active'], false], + fillOpacityActive, + fillOpacity + ] + } }, - paint: { - 'line-color': outlineColor, - 'line-width': outlineWidth, - 'line-opacity': lineOpacity - } - }, - // lines + polygon outlines - 'line-layer': { - id: 'line-layer', - type: 'line', - source: 'geojson-source', - filter: ['all', - ['in', '$type', 'LineString', 'Polygon']], - layout: { - 'line-join': 'round', - 'line-cap': 'round' + 'polygon-layer-extrusion': { + id: 'polygon-layer-extrusion', + type: 'fill-extrusion', + source: 'geojson-source', + filter: ['all', + ['in', '$type', 'Polygon'], + ['>', 'fill-extrusion-height', 0]], + paint: { + 'fill-extrusion-color': ['coalesce', + ['get', 'fill-extrusion-color'], + ['get', 'user_fill-extrusion-color'], + ['get', 'fill'], + ['get', 'user_fill'], + featureColor], + 'fill-extrusion-height': ['to-number', ['coalesce', + ['get', 'fill-extrusion-height'], + ['get', 'user_fill-extrusion-height']]], + 'fill-extrusion-base': ['to-number', ['coalesce', + ['get', 'fill-extrusion-base'], + ['get', 'user_fill-extrusion-base']]], + // opacity does not support data expressions!?! + 'fill-extrusion-opacity': 0.8 + } }, - paint: { - 'line-color': [ - 'case', - ['boolean', ['==', ['geometry-type'], 'LineString'], true], - lineColor, lineColorPolygon - ], - 'line-width': lineWidth, - 'line-opacity': [ - 'case', - ['boolean', ['==', ['geometry-type'], 'LineString'], true], - ['case', ['boolean', ['feature-state', 'active'], false], - lineOpacityActive, lineOpacity - ], 1 - ] - } - }, - 'line-layer-hit': { - id: 'line-layer-hit', - type: 'line', - source: 'geojson-source', - filter: ['all', - ['in', '$type', 'LineString']], - paint: { - 'line-width': ['+', 15, outlineWidthMax], - 'line-opacity': 0 - } - }, - 'points-border-layer': { - id: 'points-border-layer', - type: 'circle', - source: 'geojson-source', - filter: ['all', - ['==', '$type', 'Point'], - ['!=', 'meta', 'midpoint'], - ['!=', 'meta', 'vertex'], - ['none', ['has', 'user_marker-image-url'], ['has', 'marker-image-url'], - ['has', 'user_marker-symbol'], ['has', 'marker-symbol']] - ], - paint: { - 'circle-pitch-scale': 'map', // points get bigger when camera is closer - 'circle-radius': pointSize, - 'circle-opacity': 0, - 'circle-stroke-color': pointOutlineColor, - 'circle-blur': 0.1, - 'circle-stroke-width': [ - 'case', - ['boolean', ['feature-state', 'active'], false], - pointOutlineSizeActive, - pointOutlineSize - ], - 'circle-stroke-opacity': pointOpacity + 0.2 - } - }, - 'points-layer': { - id: 'points-layer', - type: 'circle', - source: 'geojson-source', - filter: ['all', - ['==', '$type', 'Point'], - ['!=', 'meta', 'midpoint'], - ['none', ['has', 'user_marker-image-url'], ['has', 'marker-image-url'], - ['has', 'user_marker-symbol'], ['has', 'marker-symbol']] - ], - paint: { - 'circle-pitch-scale': 'map', // points get bigger when camera is closer - 'circle-radius': pointSize, - 'circle-color': pointColor, - 'circle-opacity': [ - 'match', - ['coalesce', ['get', 'user_marker-color'], ['get', 'marker-color']], - 'transparent', 0, // If marker-color is 'transparent', set circle-radius to 0 - [ + 'line-layer-outline': { + id: 'line-layer-outline', + type: 'line', + source: 'geojson-source', + filter: ['all', + ['in', '$type', 'LineString']], + layout: { + 'line-join': 'round', + 'line-cap': 'round' + }, + paint: { + 'line-color': outlineColor, + 'line-width': outlineWidth, + 'line-opacity': lineOpacity + } + }, + // lines + polygon outlines + 'line-layer': { + id: 'line-layer', + type: 'line', + source: 'geojson-source', + filter: ['all', + ['in', '$type', 'LineString', 'Polygon']], + layout: { + 'line-join': 'round', + 'line-cap': 'round' + }, + paint: { + 'line-color': [ 'case', - ['boolean', ['feature-state', 'active'], false], - pointOpacityActive, - pointOpacity - ]], - 'circle-blur': 0.1 - } - }, - 'points-hit-layer': { - id: 'points-hit-layer', - type: 'circle', - source: 'geojson-source', - filter: ['all', - ['==', '$type', 'Point'], - ['!=', 'active', 'true'] - ], - paint: { - 'circle-radius': ['+', 5, pointSizeMax], - 'circle-opacity': 0 - } - }, - // background + border for symbols - 'symbols-border-layer': { - id: 'symbols-border-layer', - type: 'circle', - source: 'geojson-source', - filter: ['all', - ['==', '$type', 'Point'], - ['!=', 'meta', 'midpoint'], - ['!=', 'meta', 'vertex'], - ['any', ['has', 'user_marker-image-url'], ['has', 'marker-image-url'], - ['has', 'user_marker-symbol'], ['has', 'marker-symbol']] - ], - paint: { - 'circle-pitch-scale': 'map', // points get bigger when camera is closer - 'circle-radius': pointSize, - 'circle-color': pointColor, - 'circle-opacity': [ - 'match', - ['coalesce', ['get', 'user_marker-color'], ['get', 'marker-color']], - 'transparent', 0, // If marker-color is 'transparent', set circle-radius to 0 - [ + ['boolean', ['==', ['geometry-type'], 'LineString'], true], + lineColor, lineColorPolygon + ], + 'line-width': lineWidth, + 'line-opacity': [ + 'case', + ['boolean', ['==', ['geometry-type'], 'LineString'], true], + ['case', ['boolean', ['feature-state', 'active'], false], + lineOpacityActive, lineOpacity + ], 1 + ] + } + }, + 'line-layer-hit': { + id: 'line-layer-hit', + type: 'line', + source: 'geojson-source', + filter: ['all', + ['in', '$type', 'LineString']], + paint: { + 'line-width': ['+', 15, outlineWidthMax], + 'line-opacity': 0 + } + }, + 'points-border-layer': { + id: 'points-border-layer', + type: 'circle', + source: 'geojson-source', + filter: ['all', + ['==', '$type', 'Point'], + ['!=', 'meta', 'midpoint'], + ['!=', 'meta', 'vertex'], + ['none', ['has', 'user_marker-image-url'], ['has', 'marker-image-url'], + ['has', 'user_marker-symbol'], ['has', 'marker-symbol']] + ], + paint: { + 'circle-pitch-scale': 'map', // points get bigger when camera is closer + 'circle-radius': pointSize, + 'circle-opacity': 0, + 'circle-stroke-color': pointOutlineColor, + 'circle-blur': 0.1, + 'circle-stroke-width': [ 'case', ['boolean', ['feature-state', 'active'], false], - pointOpacityActive, - pointOpacity - ]], - 'circle-stroke-color': pointOutlineColor, - 'circle-blur': 0.05, - 'circle-stroke-width': [ - 'case', - ['boolean', ['feature-state', 'active'], false], - pointOutlineSizeActive, - pointOutlineSize + pointOutlineSizeActive, + pointOutlineSize + ], + 'circle-stroke-opacity': pointOpacity + 0.2 + } + }, + 'points-layer': { + id: 'points-layer', + type: 'circle', + source: 'geojson-source', + filter: ['all', + ['==', '$type', 'Point'], + ['!=', 'meta', 'midpoint'], + ['none', ['has', 'user_marker-image-url'], ['has', 'marker-image-url'], + ['has', 'user_marker-symbol'], ['has', 'marker-symbol']] ], - 'circle-stroke-opacity': pointOpacity + 0.2 - } - }, - // support symbols on all feature types - 'symbols-layer': { - id: 'symbols-layer', - type: 'symbol', - source: 'geojson-source', - filter: ['!=', 'active', 'true'], - // minzoom: 15, // TODO: only static values possible right now - layout: { - 'symbol-sort-key': ['to-number', ['coalesce', ['get', 'user_sort-key'], ['get', 'sort-key'], 1]], - 'icon-image': ['coalesce', - ['get', 'marker-image-url'], - // replacing marker-symbol value with path to emoji png - ['case', - ['has', 'marker-symbol'], - ['concat', '/emojis/noto/', ['get', 'marker-symbol'], '.png'], - ''] + paint: { + 'circle-pitch-scale': 'map', // points get bigger when camera is closer + 'circle-radius': pointSize, + 'circle-color': pointColor, + 'circle-opacity': [ + 'match', + ['coalesce', ['get', 'user_marker-color'], ['get', 'marker-color']], + 'transparent', 0, // If marker-color is 'transparent', set circle-radius to 0 + [ + 'case', + ['boolean', ['feature-state', 'active'], false], + pointOpacityActive, + pointOpacity + ]], + 'circle-blur': 0.1 + } + }, + 'points-hit-layer': { + id: 'points-hit-layer', + type: 'circle', + source: 'geojson-source', + filter: ['all', + ['==', '$type', 'Point'], + ['!=', 'active', 'true'] ], - 'icon-size': iconSize, // cannot scale on hover/zoom because it's not a paint property - 'icon-overlap': 'always' // https://maplibre.org/maplibre-style-spec/layers/#icon-overlap - // 'icon-ignore-placement': true // other symbols can be visible even if they collide with the icon + paint: { + 'circle-radius': ['+', 5, pointSizeMax], + 'circle-opacity': 0 + } + }, + // background + border for symbols + 'symbols-border-layer': { + id: 'symbols-border-layer', + type: 'circle', + source: 'geojson-source', + filter: ['all', + ['==', '$type', 'Point'], + ['!=', 'meta', 'midpoint'], + ['!=', 'meta', 'vertex'], + ['any', ['has', 'user_marker-image-url'], ['has', 'marker-image-url'], + ['has', 'user_marker-symbol'], ['has', 'marker-symbol']] + ], + paint: { + 'circle-pitch-scale': 'map', // points get bigger when camera is closer + 'circle-radius': pointSize, + 'circle-color': pointColor, + 'circle-opacity': [ + 'match', + ['coalesce', ['get', 'user_marker-color'], ['get', 'marker-color']], + 'transparent', 0, // If marker-color is 'transparent', set circle-radius to 0 + [ + 'case', + ['boolean', ['feature-state', 'active'], false], + pointOpacityActive, + pointOpacity + ]], + 'circle-stroke-color': pointOutlineColor, + 'circle-blur': 0.05, + 'circle-stroke-width': [ + 'case', + ['boolean', ['feature-state', 'active'], false], + pointOutlineSizeActive, + pointOutlineSize + ], + 'circle-stroke-opacity': pointOpacity + 0.2 + } }, - paint: { + // support symbols on all feature types + 'symbols-layer': { + id: 'symbols-layer', + type: 'symbol', + source: 'geojson-source', + filter: ['!=', 'active', 'true'], + // minzoom: 15, // TODO: only static values possible right now + layout: { + 'symbol-sort-key': ['to-number', ['coalesce', ['get', 'user_sort-key'], ['get', 'sort-key'], 1]], + 'icon-image': ['coalesce', + ['get', 'marker-image-url'], + // replacing marker-symbol value with path to emoji png + ['case', + ['has', 'marker-symbol'], + ['concat', '/emojis/noto/', ['get', 'marker-symbol'], '.png'], + ''] + ], + 'icon-size': iconSize, // cannot scale on hover/zoom because it's not a paint property + 'icon-overlap': 'always' // https://maplibre.org/maplibre-style-spec/layers/#icon-overlap + // 'icon-ignore-placement': true // other symbols can be visible even if they collide with the icon + }, + paint: { // cannot set circle-stroke-* in the symbol layer :-( - } - }, - 'text-layer': { - id: 'text-layer', - type: 'symbol', - source: 'geojson-source', - filter: ['has', 'label'], - layout: { - 'icon-overlap': 'never', - 'text-field': ['coalesce', ['get', 'label'], ['get', 'room']], - 'text-size': labelSize, - 'text-font': labelFont, - // arrange text to avoid collision - 'text-variable-anchor': ['top'], // text under point - // distance of the text in 'em' - 'text-radial-offset': ['/', pointSizeMax, 14], - 'text-justify': 'auto', - 'text-ignore-placement': false, // hide on collision - // TODO: sort keys on text are ascending, on symbols descending??? - 'symbol-sort-key': ['-', 1000, ['to-number', ['coalesce', ['get', 'user_sort-key'], ['get', 'sort-key'], 1]]] + } }, - paint: { - 'text-color': ['coalesce', ['get', 'user_label-color'], ['get', 'label-color'], '#000'], - 'text-halo-color': ['coalesce', ['get', 'user_label-shadow'], ['get', 'label-shadow'], '#fff'], - 'text-halo-width': 1 + 'text-layer': { + id: 'text-layer', + type: 'symbol', + source: 'geojson-source', + filter: ['has', 'label'], + layout: { + 'icon-overlap': 'never', + 'text-field': ['coalesce', ['get', 'label'], ['get', 'room']], + 'text-size': labelSize, + 'text-font': labelFont, + // arrange text to avoid collision + 'text-variable-anchor': ['top'], // text under point + // distance of the text in 'em' + 'text-radial-offset': ['/', pointSizeMax, 14], + 'text-justify': 'auto', + 'text-ignore-placement': false, // hide on collision + // TODO: sort keys on text are ascending, on symbols descending??? + 'symbol-sort-key': ['-', 1000, ['to-number', ['coalesce', ['get', 'user_sort-key'], ['get', 'sort-key'], 1]]] + }, + paint: { + 'text-color': ['coalesce', ['get', 'user_label-color'], ['get', 'label-color'], '#000'], + 'text-halo-color': ['coalesce', ['get', 'user_label-shadow'], ['get', 'label-shadow'], '#fff'], + 'text-halo-width': 1 + } } } }