diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index ead01a8ff8..75f505e95a 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -145,4 +145,27 @@ $(document).ready(function () { $("#edit_tab") .attr("title", I18n.t("javascripts.site.edit_disabled_tooltip")); + + OSM.darkMode = new L.OSM.DarkMode({ + darkFilter: "brightness(.8)", + darkFilterMenuItems: [ + { + text: I18n.t("javascripts.map.filters.brightness100"), + filter: "" + }, + { + text: I18n.t("javascripts.map.filters.brightness80"), + filter: "brightness(.8)" + }, + { + text: I18n.t("javascripts.map.filters.brightness60"), + filter: "brightness(.6)" + }, + { + text: I18n.t("javascripts.map.filters.invert"), + filter: "invert(.8) hue-rotate(180deg)" + } + ] + }); + new L.OSM.PrefersColorSchemeWatcher(OSM.darkMode).watch(); }); diff --git a/app/assets/javascripts/index/contextmenu.js b/app/assets/javascripts/index/contextmenu.js index ea284f29b9..7189b4f184 100644 --- a/app/assets/javascripts/index/contextmenu.js +++ b/app/assets/javascripts/index/contextmenu.js @@ -74,6 +74,8 @@ OSM.initializeContextMenu = function (map) { } }); + OSM.darkMode.manageMapContextMenu(map); + map.on("mousedown", function (e) { if (e.originalEvent.shiftKey) map.contextmenu.disable(); else map.contextmenu.enable(); diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index 31ce7dd28a..2f99852c7a 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -502,7 +502,6 @@ body.small-nav { } @include color-mode(dark) { - .leaflet-tile-container .leaflet-tile, .mapkey-table-entry td:first-child > * { filter: brightness(.8); } diff --git a/config/locales/en.yml b/config/locales/en.yml index f68488c09c..8ac959316c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3073,6 +3073,11 @@ en: tracestrack: Tracestrack hotosm_credit: "Tiles style by %{hotosm_link} hosted by %{osm_france_link}" hotosm_name: Humanitarian OpenStreetMap Team + filters: + brightness100: 100% brightness + brightness80: 80% brightness + brightness60: 60% brightness + invert: Invert site: edit_tooltip: Edit the map edit_disabled_tooltip: Zoom in to edit the map diff --git a/vendor/assets/leaflet/leaflet.osm.js b/vendor/assets/leaflet/leaflet.osm.js index 0e51f34086..59eb1e2a25 100644 --- a/vendor/assets/leaflet/leaflet.osm.js +++ b/vendor/assets/leaflet/leaflet.osm.js @@ -1,5 +1,219 @@ L.OSM = {}; +L.OSM.PrefersColorSchemeWatcher = L.Class.extend({ + initialize: function (darkMode) { + this._darkMode = darkMode; + }, + + watch: function () { + if (!this._prefersDarkQuery) { + this._darkModeWasEnabled = this._darkMode.isEnabled(); + this._prefersDarkQuery = matchMedia("(prefers-color-scheme: dark)"); + this._prefersDarkListener(); + L.DomEvent.on(this._prefersDarkQuery, 'change', this._prefersDarkListener, this); + } + return this; + }, + unwatch: function () { + if (this._prefersDarkQuery) { + L.DomEvent.off(this._prefersDarkQuery, 'change', this._prefersDarkListener, this); + this._prefersDarkQuery = undefined; + this._darkMode.toggle(this._darkModeWasEnabled); + this._darkModeWasEnabled = undefined; + } + return this; + }, + + _prefersDarkListener: function () { + if (this._prefersDarkQuery) { + this._darkMode.toggle(this._prefersDarkQuery.matches); + } + } +}); + +L.OSM.DarkMode = L.Class.extend({ + statics: { + _darkModes: [], + _layers: [], + + _addLayer: function (layer) { + this._layers.push(layer); + this._darkModes.forEach(function (darkMode) { + darkMode._addLayer(layer); + }); + }, + _removeLayer: function (layer) { + this._darkModes.forEach(function (darkMode) { + darkMode._removeLayer(layer); + }); + var index = this._layers.indexOf(layer); + if (index > -1) { + this._layers.splice(index, 1); + } + } + }, + + options: { + darkFilter: '', + darkFilterMenuItems: [] + }, + + initialize: function (options) { + L.Util.setOptions(this, options); + this._darkFilter = this.options.darkFilter; + this._enabled = false; + this._contextMenuUpdateHandlers = []; + L.OSM.DarkMode._darkModes.push(this); + }, + + enable: function () { + if (!this._enabled) { + this._enabled = true; + L.OSM.DarkMode._layers.forEach(function (layer) { + this._enableLayerDarkVariant(layer); + }, this); + this._contextMenuUpdateHandlers.forEach(function (handler) { + handler(); + }); + } + return this; + }, + disable: function () { + if (this._enabled) { + this._enabled = false; + L.OSM.DarkMode._layers.forEach(function (layer) { + this._disableLayerDarkVariant(layer); + }, this); + this._contextMenuUpdateHandlers.forEach(function (handler) { + handler(); + }); + } + return this; + }, + toggle: function (requestEnable) { + if (requestEnable !== undefined) { + if (requestEnable) { + this.enable(); + } else { + this.disable(); + } + } else { + if (this._enabled) { + this.disable(); + } else { + this.enable(); + } + } + return this; + }, + isEnabled: function () { + return this._enabled; + }, + + // requires Leaflet.contextmenu plugin + manageMapContextMenu: function (map) { + var contextMenuElements = []; + + if (this.options.darkFilterMenuItems.length > 0) { + var separator = map.contextmenu.addItem({ + separator: true + }); + contextMenuElements.push(separator); + } + this.options.darkFilterMenuItems.forEach(function (menuItem) { + var menuElement = map.contextmenu.addItem({ + text: menuItem.text, + callback: function () { + this._darkFilter = menuItem.filter; + this._contextMenuUpdateHandlers.forEach(function (handler) { + handler(); + }); + if (this._enabled) { + L.OSM.DarkMode._layers.forEach(function (layer) { + this._enableLayerDarkVariant(layer); + }, this); + } + }.bind(this) + }); + this._decorateContextMenuElement(menuElement, menuItem); + contextMenuElements.push(menuElement); + }, this); + + var updateContextMenuElements = function () { + var numberOfLayersWithApplicableFilter = 0; + map.eachLayer(function (layer) { + if (layer instanceof L.OSM.TileLayer) { + if (!layer.options.darkUrl) { + numberOfLayersWithApplicableFilter++; + } + } + }); + contextMenuElements.forEach(function (menuElement) { + menuElement.hidden = !this._enabled || numberOfLayersWithApplicableFilter == 0; + if ('filter' in menuElement.dataset) { + menuElement.firstChild.checked = menuElement.dataset.filter === this._darkFilter; + } + }, this); + }.bind(this); + updateContextMenuElements(); + this._contextMenuUpdateHandlers.push(updateContextMenuElements); + map.on("layeradd", updateContextMenuElements); + map.on("layerremove", updateContextMenuElements); + + return this; + }, + + _addLayer: function (layer) { + if (this._enabled) { + this._enableLayerDarkVariant(layer); + } + }, + _removeLayer: function (layer) { + if (this._enabled) { + this._disableLayerDarkVariant(layer); + } + }, + + _enableLayerDarkVariant: function (layer) { + if (layer.options.darkUrl) { + layer.setUrl(layer.options.darkUrl); + } else { + this._enableLayerDarkFilter(layer); + } + }, + _disableLayerDarkVariant: function (layer) { + if (layer.options.darkUrl) { + layer.setUrl(layer.options.url); + } else { + this._disableLayerDarkFilter(layer); + } + }, + + _enableLayerDarkFilter: function (layer) { + var container = layer.getContainer(); + if (container) { + container.style.setProperty('filter', this._darkFilter); + } + }, + _disableLayerDarkFilter: function (layer) { + var container = layer.getContainer(); + if (container) { + layer.getContainer().style.removeProperty('filter'); + } + }, + + _decorateContextMenuElement: function (menuElement, menuItem) { + menuElement.dataset.filter = menuItem.filter; + var radio = document.createElement('input'); + radio.type = 'radio'; + radio.tabIndex = -1; + radio.classList.add('leaflet-contextmenu-icon'); + radio.style.pointerEvents = 'none'; + radio.style.transform = 'scale(80%)'; + menuElement.prepend(radio, " "); + } +}); + L.OSM.TileLayer = L.TileLayer.extend({ options: { url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', @@ -9,6 +223,12 @@ L.OSM.TileLayer = L.TileLayer.extend({ initialize: function (options) { options = L.Util.setOptions(this, options); L.TileLayer.prototype.initialize.call(this, options.url); + + this.on("add", function () { + L.OSM.DarkMode._addLayer(this); + }).on("remove", function () { + L.OSM.DarkMode._removeLayer(this); + }); } }); @@ -39,6 +259,7 @@ L.OSM.CycleMap = L.OSM.TileLayer.extend({ L.OSM.TransportMap = L.OSM.TileLayer.extend({ options: { url: 'https://{s}.tile.thunderforest.com/transport/{z}/{x}/{y}{r}.png?apikey={apikey}', + darkUrl: 'https://{s}.tile.thunderforest.com/transport-dark/{z}/{x}/{y}{r}.png?apikey={apikey}', maxZoom: 21, attribution: '© OpenStreetMap contributors. Tiles courtesy of Andy Allan' }