diff --git a/.app_version b/.app_version index 5daaa7ba..a803cc22 100644 --- a/.app_version +++ b/.app_version @@ -1 +1 @@ -0.13.7 +0.14.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index e4377e02..2e481ce5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.14.0] — 2024-09-15 + +### Added + +- 17 new tile layers to choose from. Now you can select the tile layer that suits you the best. You can find the list of available tile layers in the map controls in the top right corner of the map under the layers icon. + + ## [0.13.7] — 2024-09-15 ### Added diff --git a/app/controllers/api/v1/settings_controller.rb b/app/controllers/api/v1/settings_controller.rb index a5bf7e87..5ca7a809 100644 --- a/app/controllers/api/v1/settings_controller.rb +++ b/app/controllers/api/v1/settings_controller.rb @@ -30,7 +30,8 @@ class Api::V1::SettingsController < ApiController def settings_params params.require(:settings).permit( :meters_between_routes, :minutes_between_routes, :fog_of_war_meters, - :time_threshold_minutes, :merge_threshold_minutes, :route_opacity + :time_threshold_minutes, :merge_threshold_minutes, :route_opacity, + :preferred_map_layer ) end end diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index cb9a17d3..6d1af78d 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -8,6 +8,23 @@ import { formatDate } from "../maps/helpers"; import { haversineDistance } from "../maps/helpers"; import { osmMapLayer } from "../maps/layers"; import { osmHotMapLayer } from "../maps/layers"; +import { OPNVMapLayer } from "../maps/layers"; +import { openTopoMapLayer } from "../maps/layers"; +import { stadiaAlidadeSmoothMapLayer } from "../maps/layers"; +import { stadiaAlidadeSmoothDarkMapLayer } from "../maps/layers"; +import { stadiaAlidadeSatelliteMapLayer } from "../maps/layers"; +import { stadiaOsmBrightMapLayer } from "../maps/layers"; +import { stadiaOutdoorMapLayer } from "../maps/layers"; +import { stadiaStamenTonerMapLayer } from "../maps/layers"; +import { stadiaStamenTonerBackgroundMapLayer } from "../maps/layers"; +import { stadiaStamenTonerLiteMapLayer } from "../maps/layers"; +import { stadiaStamenWatercolorMapLayer } from "../maps/layers"; +import { stadiaStamenTerrainMapLayer } from "../maps/layers"; +import { cyclOsmMapLayer } from "../maps/layers"; +import { esriWorldStreetMapLayer } from "../maps/layers"; +import { esriWorldTopoMapLayer } from "../maps/layers"; +import { esriWorldImageryMapLayer } from "../maps/layers"; +import { esriWorldGrayCanvasMapLayer } from "../maps/layers"; import "leaflet-draw"; export default class extends Controller { @@ -119,9 +136,28 @@ export default class extends Controller { } baseMaps() { + let selectedLayerName = this.userSettings.preferred_map_layer || "OpenStreetMap"; + return { - OpenStreetMap: osmMapLayer(this.map), - "OpenStreetMap.HOT": osmHotMapLayer(), + OpenStreetMap: osmMapLayer(this.map, selectedLayerName), + "OpenStreetMap.HOT": osmHotMapLayer(this.map, selectedLayerName), + OPNV: OPNVMapLayer(this.map, selectedLayerName), + openTopo: openTopoMapLayer(this.map, selectedLayerName), + stadiaAlidadeSmooth: stadiaAlidadeSmoothMapLayer(this.map, selectedLayerName), + stadiaAlidadeSmoothDark: stadiaAlidadeSmoothDarkMapLayer(this.map, selectedLayerName), + stadiaAlidadeSatellite: stadiaAlidadeSatelliteMapLayer(this.map, selectedLayerName), + stadiaOsmBright: stadiaOsmBrightMapLayer(this.map, selectedLayerName), + stadiaOutdoor: stadiaOutdoorMapLayer(this.map, selectedLayerName), + stadiaStamenToner: stadiaStamenTonerMapLayer(this.map, selectedLayerName), + stadiaStamenTonerBackground: stadiaStamenTonerBackgroundMapLayer(this.map, selectedLayerName), + stadiaStamenTonerLite: stadiaStamenTonerLiteMapLayer(this.map, selectedLayerName), + stadiaStamenWatercolor: stadiaStamenWatercolorMapLayer(this.map, selectedLayerName), + stadiaStamenTerrain: stadiaStamenTerrainMapLayer(this.map, selectedLayerName), + cyclOsm: cyclOsmMapLayer(this.map, selectedLayerName), + esriWorldStreet: esriWorldStreetMapLayer(this.map, selectedLayerName), + esriWorldTopo: esriWorldTopoMapLayer(this.map, selectedLayerName), + esriWorldImagery: esriWorldImageryMapLayer(this.map, selectedLayerName), + esriWorldGrayCanvas: esriWorldGrayCanvasMapLayer(this.map, selectedLayerName) }; } @@ -172,6 +208,32 @@ export default class extends Controller { // Ensure only one listener is attached by removing any existing ones first this.removeEventListeners(); document.addEventListener('click', this.handleDeleteClick); + + // Add an event listener for base layer change in Leaflet + this.map.on('baselayerchange', (event) => { + const selectedLayerName = event.name; + this.updatePreferredBaseLayer(selectedLayerName); + }); + } + + updatePreferredBaseLayer(selectedLayerName) { + fetch(`/api/v1/settings?api_key=${this.apiKey}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + settings: { + preferred_map_layer: selectedLayerName + }, + }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.status === 'success') { + this.showFlashMessage('notice', `Preferred map layer updated to: ${selectedLayerName}`); + } else { + this.showFlashMessage('error', data.message); + } + }); } deletePoint(id, apiKey) { diff --git a/app/javascript/maps/layers.js b/app/javascript/maps/layers.js index a8813ff7..9f700c83 100644 --- a/app/javascript/maps/layers.js +++ b/app/javascript/maps/layers.js @@ -1,14 +1,285 @@ +// Yeah I know it should be DRY but this is me doing a KISS at 21:00 on a Sunday night -export function osmMapLayer(map) { - return L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { +export function osmMapLayer(map, selectedLayerName) { + let layerName = 'osm'; + + let layer = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "© OpenStreetMap", - }).addTo(map); + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } } -export function osmHotMapLayer() { - return L.tileLayer("https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", { +export function osmHotMapLayer(map, selectedLayerName) { + let layerName = 'osmHot'; + let layer = L.tileLayer("https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "© OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France", }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function OPNVMapLayer(map, selectedLayerName) { + let layerName = 'OPNV'; + let layer = L.tileLayer('https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', { + maxZoom: 18, + attribution: 'Map memomaps.de CC-BY-SA, map data © OpenStreetMap contributors' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function openTopoMapLayer(map, selectedLayerName) { + let layerName = 'openTopo'; + let layer = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { + maxZoom: 17, + attribution: 'Map data: © OpenStreetMap contributors, SRTM | Map style: © OpenTopoMap (CC-BY-SA)' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function stadiaAlidadeSmoothMapLayer(map, selectedLayerName) { + let layerName = 'stadiaAlidadeSmooth'; + let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.{ext}', { + minZoom: 0, + maxZoom: 20, + attribution: '© Stadia Maps © OpenMapTiles © OpenStreetMap contributors', + ext: 'png' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function stadiaAlidadeSmoothDarkMapLayer(map, selectedLayerName) { + let layerName = 'stadiaAlidadeSmoothDark'; + let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.{ext}', { + minZoom: 0, + maxZoom: 20, + attribution: '© Stadia Maps © OpenMapTiles © OpenStreetMap contributors', + ext: 'png' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function stadiaAlidadeSatelliteMapLayer(map, selectedLayerName) { + let layerName = 'stadiaAlidadeSatellite'; + let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_satellite/{z}/{x}/{y}{r}.{ext}', { + minZoom: 0, + maxZoom: 20, + attribution: '© CNES, Distribution Airbus DS, © Airbus DS, © PlanetObserver (Contains Copernicus Data) | © Stadia Maps © OpenMapTiles © OpenStreetMap contributors', + ext: 'jpg' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function stadiaOsmBrightMapLayer(map, selectedLayerName) { + let layerName = 'stadiaOsmBright'; + let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/osm_bright/{z}/{x}/{y}{r}.{ext}', { + minZoom: 0, + maxZoom: 20, + attribution: '© Stadia Maps © OpenMapTiles © OpenStreetMap contributors', + ext: 'png' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function stadiaOutdoorMapLayer(map, selectedLayerName) { + let layerName = 'stadiaOutdoor'; + let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/outdoors/{z}/{x}/{y}{r}.{ext}', { + minZoom: 0, + maxZoom: 20, + attribution: '© Stadia Maps © OpenMapTiles © OpenStreetMap contributors', + ext: 'png' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function stadiaStamenTonerMapLayer(map, selectedLayerName) { + let layerName = 'stadiaStamenToner'; + let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}{r}.{ext}', { + minZoom: 0, + maxZoom: 20, + attribution: '© Stadia Maps © Stamen Design © OpenMapTiles © OpenStreetMap contributors', + ext: 'png' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function stadiaStamenTonerBackgroundMapLayer(map, selectedLayerName) { + let layerName = 'stadiaStamenTonerBackground'; + let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_toner_background/{z}/{x}/{y}{r}.{ext}', { + minZoom: 0, + maxZoom: 20, + attribution: '© Stadia Maps © Stamen Design © OpenMapTiles © OpenStreetMap contributors', + ext: 'png' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function stadiaStamenTonerLiteMapLayer(map, selectedLayerName) { + let layerName = 'stadiaStamenTonerLite'; + let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_toner_lite/{z}/{x}/{y}{r}.{ext}', { + minZoom: 0, + maxZoom: 20, + attribution: '© Stadia Maps © Stamen Design © OpenMapTiles © OpenStreetMap contributors', + ext: 'png' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function stadiaStamenWatercolorMapLayer(map, selectedLayerName) { + let layerName = 'stadiaStamenWatercolor'; + let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.{ext}', { + minZoom: 1, + maxZoom: 16, + attribution: '© Stadia Maps © Stamen Design © OpenMapTiles © OpenStreetMap contributors', + ext: 'jpg' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function stadiaStamenTerrainMapLayer(map, selectedLayerName) { + let layerName = 'stadiaStamenTerrain'; + let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_terrain/{z}/{x}/{y}{r}.{ext}', { + minZoom: 0, + maxZoom: 18, + attribution: '© Stadia Maps © Stamen Design © OpenMapTiles © OpenStreetMap contributors', + ext: 'png' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function cyclOsmMapLayer(map, selectedLayerName) { + let layerName = 'cyclOsm'; + let layer = L.tileLayer('https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', { + maxZoom: 20, + attribution: 'CyclOSM | Map data: © OpenStreetMap contributors' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function esriWorldStreetMapLayer(map, selectedLayerName) { + let layerName = 'esriWorldStreet'; + let layer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', { + attribution: 'Tiles © Esri — Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function esriWorldTopoMapLayer(map, selectedLayerName) { + let layerName = 'esriWorldTopo'; + let layer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', { + attribution: 'Tiles © 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' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function esriWorldImageryMapLayer(map, selectedLayerName) { + let layerName = 'esriWorldImagery'; + let layer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } +} + +export function esriWorldGrayCanvasMapLayer(map, selectedLayerName) { + let layerName = 'esriWorldGrayCanvas'; + let layer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}', { + attribution: 'Tiles © Esri — Esri, DeLorme, NAVTEQ', + maxZoom: 16 + }); + + if (selectedLayerName === layerName) { + return layer.addTo(map); + } else { + return layer; + } }