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'
}