From 8a8c2da7bb40cb4c910ea89a995236054e8f1094 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 31 Aug 2026 18:01:48 +0200 Subject: [PATCH] Let administrators choose the background map A settings page now offers every provider documented in README.tiles.md, plus one's own address, attribution, maximum zoom and subdomains. Settings live in Galette preferences under pref_maps_tiles_*, so the plugin needs no table of its own and no database version bump. The address is stored as a string rather than a URL. Galette's URL validation rejects a host starting with a brace, which would turn away the {s} subdomain form the OSM-FR and Humanitarian providers rely on. --- README.md | 2 + README.tiles.md | 7 + _define.php | 8 +- _routes.php | 10 + lang/maps.pot | 202 ++++++++++----- lang/maps_en_US.po | 227 ++++++++++++----- .../Controllers/MapsController.php | 91 ++++++- lib/GaletteMaps/PluginGaletteMaps.php | 26 +- lib/GaletteMaps/TileProviders.php | 236 ++++++++++++++++++ templates/default/common_scripts.html.twig | 30 ++- templates/default/headers.html.twig | 4 +- templates/default/maps_preferences.html.twig | 83 ++++++ .../GaletteMaps/tests/units/TileProviders.php | 145 +++++++++++ 13 files changed, 930 insertions(+), 141 deletions(-) create mode 100644 lib/GaletteMaps/TileProviders.php create mode 100644 templates/default/maps_preferences.html.twig create mode 100644 tests/GaletteMaps/tests/units/TileProviders.php diff --git a/README.md b/README.md index 65bab41..2d2f2e4 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ A [Galette](https://galette.eu) plugin to geolocalize members. Map display uses [Leaflet project](https://leafletjs.com/) while search is provided by [Nominatim](https://nominatim.openstreetmap.org/) API. Background tiles are the [OpenFreeMap](https://openfreemap.org/) vector tiles, rendered with [MapLibre GL](https://maplibre.org/): no API key, no registration, and the whole thing can be self-hosted. +That is only the default: an administrator picks another provider, or enters their own address, from Configuration. * website: https://galette.eu - https://doc.galette.eu/en/master/plugins/maps.html * bugs and features: https://bugs.galette.eu/projects/galette-plugin-maps @@ -27,6 +28,7 @@ Un plugin [Galette](https://galette.eu) pour gérer paiments de cotisation et de Les cartes sont affichées par le biais du [projet Leaflet](https://leafletjs.com/) tandis que la recherche est assurée par l'API [Nominatim](https://nominatim.openstreetmap.org/). Le fond de carte provient des tuiles vectorielles [OpenFreeMap](https://openfreemap.org/), rendues par [MapLibre GL](https://maplibre.org/) : sans clé d'API, sans inscription, et auto-hébergeable. +Ce n'est que le défaut : un administrateur choisit un autre fournisseur, ou saisit sa propre adresse, depuis la Configuration. * site web : https://galette.eu - https://doc.galette.eu/fr/master/plugins/maps.html * bogues et fonctionnalités : https://bugs.galette.eu/projects/galette-plugin-maps diff --git a/README.tiles.md b/README.tiles.md index 5da5ac6..6c37863 100644 --- a/README.tiles.md +++ b/README.tiles.md @@ -143,3 +143,10 @@ That is possible, but it now means opening an account on and adding the key to the URL. The exact form of the URL with a key is given by their interface — we do not reproduce it here, it is specific to each account. + +## Newer versions + +From the next release the background map is a setting: Configuration holds every +provider listed above as a ready-made choice, plus your own address, attribution +and maximum zoom. Editing a template by hand, as described here, is only needed +on the releases that came before it. diff --git a/_define.php b/_define.php index 4996c30..b170b7e 100644 --- a/_define.php +++ b/_define.php @@ -18,9 +18,11 @@ route: 'maps', //routing name and translation domain date: '2025-12-08', //Release date acls: [ //Permissions needed - 'maps_localize_member' => 'member', - 'maps_mymap' => 'member', - 'maps_ilivehere' => 'member' + 'maps_localize_member' => 'member', + 'maps_mymap' => 'member', + 'maps_ilivehere' => 'member', + 'maps_preferences' => 'admin', + 'maps_store_preferences' => 'admin' ], dbver: 1.00 ); diff --git a/_routes.php b/_routes.php index 194a858..6436a4d 100644 --- a/_routes.php +++ b/_routes.php @@ -53,6 +53,16 @@ [MapsController::class, 'map'] )->setName('maps_map')->add($check_js_middleware)->add(\Galette\Middleware\PublicPages::class); +$app->get( + '/preferences', + [MapsController::class, 'preferences'] +)->setName('maps_preferences')->add(Authenticate::class); + +$app->post( + '/preferences', + [MapsController::class, 'storePreferences'] +)->setName('maps_store_preferences')->add(Authenticate::class); + $app->post( '/i-live-here[/{id:\d+}]', [MapsController::class, 'ILiveHere'] diff --git a/lang/maps.pot b/lang/maps.pot index 27cb421..61d9a0a 100644 --- a/lang/maps.pot +++ b/lang/maps.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-12-07 10:22+0100\n" +"POT-Creation-Date: 2026-09-02 12:49+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,166 +17,250 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: ../_routes.php:34 +#: ../_routes.php:22 msgid "Javascript libraries has not been built!" msgstr "" -#: ../lib/GaletteMaps/PluginGaletteMaps.php:53 -#: ../lib/GaletteMaps/PluginGaletteMaps.php:99 -msgid "My localization" +#: ../lib/GaletteMaps/Controllers/MapsController.php:50 +#: ../lib/GaletteMaps/Controllers/MapsController.php:130 +#: ../lib/GaletteMaps/PluginGaletteMaps.php:94 +msgid "Maps" msgstr "" -#: ../lib/GaletteMaps/PluginGaletteMaps.php:74 -#: ../lib/GaletteMaps/Controllers/MapsController.php:64 -#: ../lib/GaletteMaps/Controllers/MapsController.php:145 -msgid "Maps" +#: ../lib/GaletteMaps/Controllers/MapsController.php:60 +msgid "" +"Coordinates has not been loaded. Maybe plugin tables does not exists in the " +"database?" msgstr "" -#: ../lib/GaletteMaps/PluginGaletteMaps.php:130 -msgid "Geolocalize" +#: ../lib/GaletteMaps/Controllers/MapsController.php:133 +msgid "%member geographic position" msgstr "" -#: ../lib/GaletteMaps/PluginGaletteMaps.php:134 -msgid "Geolocalize %membername" +#: ../lib/GaletteMaps/Controllers/MapsController.php:170 +#: ../lib/GaletteMaps/PluginGaletteMaps.php:60 +msgid "Maps settings" msgstr "" -#: ../lib/GaletteMaps/Controllers/MapsController.php:73 -msgid "" -"Coordinates has not been loaded. Maybe plugin tables does not exists in the " -"database?" +#: ../lib/GaletteMaps/Controllers/MapsController.php:216 +msgid "An address is required to use your own background map." msgstr "" -#: ../lib/GaletteMaps/Controllers/MapsController.php:148 -msgid "%member geographic position" +#: ../lib/GaletteMaps/Controllers/MapsController.php:232 +msgid "Maps settings have been saved." msgstr "" -#: ../lib/GaletteMaps/Controllers/MapsController.php:194 +#: ../lib/GaletteMaps/Controllers/MapsController.php:262 msgid "Superadmin cannot be localized." msgstr "" -#: ../lib/GaletteMaps/Controllers/MapsController.php:221 -#: ../lib/GaletteMaps/Controllers/MapsController.php:233 +#: ../lib/GaletteMaps/Controllers/MapsController.php:289 +#: ../lib/GaletteMaps/Controllers/MapsController.php:301 msgid "Coordinates has not been removed :(" msgstr "" -#: ../lib/GaletteMaps/Controllers/MapsController.php:231 +#: ../lib/GaletteMaps/Controllers/MapsController.php:299 msgid "Coordinates has been removed!" msgstr "" -#: ../lib/GaletteMaps/Controllers/MapsController.php:246 +#: ../lib/GaletteMaps/Controllers/MapsController.php:314 msgid "New coordinates has been stored!" msgstr "" -#: ../lib/GaletteMaps/Controllers/MapsController.php:248 +#: ../lib/GaletteMaps/Controllers/MapsController.php:316 msgid "Coordinates has not been stored :(" msgstr "" -#: ../lib/GaletteMaps/Controllers/MapsController.php:251 +#: ../lib/GaletteMaps/Controllers/MapsController.php:319 msgid "Something went wrong :(" msgstr "" -#: ../tempcache/common_html.html.twig:66 ../tempcache/common_html.html.twig:72 -msgid "Member (company)" +#: ../lib/GaletteMaps/TileProviders.php:123 +msgid "OpenFreeMap, light grey" msgstr "" -#: ../tempcache/common_html.html.twig:82 ../tempcache/common_html.html.twig:88 -msgid "Search result" +#: ../lib/GaletteMaps/TileProviders.php:124 +msgid "OpenFreeMap, colours" +msgstr "" + +#: ../lib/GaletteMaps/TileProviders.php:125 +msgid "OpenStreetMap" msgstr "" -#: ../tempcache/maps.html.twig:114 +#: ../lib/GaletteMaps/TileProviders.php:126 +msgid "OpenStreetMap France" +msgstr "" + +#: ../lib/GaletteMaps/TileProviders.php:127 +msgid "Humanitarian OSM Team" +msgstr "" + +#: ../lib/GaletteMaps/TileProviders.php:128 +msgid "OpenStreetMap Germany" +msgstr "" + +#: ../lib/GaletteMaps/TileProviders.php:129 +msgid "Esri, light grey" +msgstr "" + +#: ../lib/GaletteMaps/TileProviders.php:130 +#: ../tempcache/maps_preferences.html.twig:85 +msgid "Own values" +msgstr "" + +#: ../lib/GaletteMaps/PluginGaletteMaps.php:73 +#: ../lib/GaletteMaps/PluginGaletteMaps.php:119 +msgid "My localization" +msgstr "" + +#: ../lib/GaletteMaps/PluginGaletteMaps.php:150 +msgid "Geolocalize" +msgstr "" + +#: ../lib/GaletteMaps/PluginGaletteMaps.php:154 +msgid "Geolocalize %membername" +msgstr "" + +#: ../tempcache/maps.html.twig:115 msgid "aka" msgstr "" -#: ../tempcache/common_scripts.html.twig:134 +#: ../tempcache/common_scripts.html.twig:166 msgid "An error occurred during 'I live here' process :(" msgstr "" -#: ../tempcache/common_scripts.html.twig:179 +#: ../tempcache/common_scripts.html.twig:215 msgid "Display map in full screen" msgstr "" -#: ../tempcache/common_scripts.html.twig:183 +#: ../tempcache/common_scripts.html.twig:219 msgid "Exit full screen" msgstr "" -#: ../tempcache/common_scripts.html.twig:195 +#: ../tempcache/common_scripts.html.twig:231 msgid "Search your town..." msgstr "" -#: ../tempcache/common_scripts.html.twig:201 +#: ../tempcache/common_scripts.html.twig:237 msgid "Search a town..." msgstr "" -#: ../tempcache/common_scripts.html.twig:207 +#: ../tempcache/common_scripts.html.twig:243 msgid "Sorry, that town could not be found." msgstr "" -#: ../tempcache/common_scripts.html.twig:211 +#: ../tempcache/common_scripts.html.twig:247 msgid "Search" msgstr "" -#: ../tempcache/common_scripts.html.twig:235 +#: ../tempcache/common_scripts.html.twig:271 msgid "Show me where I am" msgstr "" -#: ../tempcache/common_scripts.html.twig:240 +#: ../tempcache/common_scripts.html.twig:276 msgid "You seem located outside the boundaries of the map" msgstr "" -#: ../tempcache/common_scripts.html.twig:252 -msgid "Map data (c)" +#: ../tempcache/maps_preferences.html.twig:66 +msgid "" +"Background maps come from an outside provider. Should it change its terms, " +"pick another one here rather than waiting for a new release." +msgstr "" + +#: ../tempcache/maps_preferences.html.twig:73 +msgid "Background map" +msgstr "" + +#: ../tempcache/maps_preferences.html.twig:91 +msgid "Vector tiles" +msgstr "" + +#: ../tempcache/maps_preferences.html.twig:91 +msgid "Tick for a MapLibre style, leave unticked for classic raster tiles." msgstr "" -#: ../tempcache/common_scripts.html.twig:254 -msgid "OpenStreetMap contributors" +#: ../tempcache/maps_preferences.html.twig:98 +msgid "Address" msgstr "" -#: ../tempcache/common_scripts.html.twig:256 -msgid "Imagery (c)" +#: ../tempcache/maps_preferences.html.twig:98 +msgid "Style address for vector tiles, tiles address for raster ones, such as:" msgstr "" -#: ../tempcache/mymap.html.twig:72 +#: ../tempcache/maps_preferences.html.twig:104 +msgid "Attribution" +msgstr "" + +#: ../tempcache/maps_preferences.html.twig:104 +msgid "" +"Credit the provider requires, HTML allowed. Data licences make it mandatory. " +"A vector style usually carries its own, and this can then be left empty." +msgstr "" + +#: ../tempcache/maps_preferences.html.twig:110 +msgid "Maximum zoom" +msgstr "" + +#: ../tempcache/maps_preferences.html.twig:116 +msgid "Subdomains" +msgstr "" + +#: ../tempcache/maps_preferences.html.twig:116 +msgid "" +"Letters the {s} token in the address is replaced with, such as abc. Raster " +"tiles only." +msgstr "" + +#: ../tempcache/mymap.html.twig:73 msgid "Choose your location" msgstr "" -#: ../tempcache/mymap.html.twig:78 +#: ../tempcache/mymap.html.twig:79 msgid "Choose %member location" msgstr "" -#: ../tempcache/mymap.html.twig:88 +#: ../tempcache/mymap.html.twig:89 msgid "Select your town." msgstr "" -#: ../tempcache/mymap.html.twig:92 +#: ../tempcache/mymap.html.twig:93 msgid "In the database, town is set to: '%town'" msgstr "" -#: ../tempcache/mymap.html.twig:186 +#: ../tempcache/mymap.html.twig:188 msgid "You clicked at %p" msgstr "" -#: ../tempcache/mymap.html.twig:189 ../tempcache/mymap.html.twig:310 -#: ../tempcache/mymap.html.twig:339 +#: ../tempcache/mymap.html.twig:191 ../tempcache/mymap.html.twig:312 +#: ../tempcache/mymap.html.twig:341 msgid "I live here!" msgstr "" -#: ../tempcache/mymap.html.twig:191 ../tempcache/mymap.html.twig:312 -#: ../tempcache/mymap.html.twig:341 +#: ../tempcache/mymap.html.twig:193 ../tempcache/mymap.html.twig:314 +#: ../tempcache/mymap.html.twig:343 msgid "Member lives here!" msgstr "" -#: ../tempcache/mymap.html.twig:223 +#: ../tempcache/mymap.html.twig:225 msgid "Remove my coordinates" msgstr "" -#: ../tempcache/mymap.html.twig:225 +#: ../tempcache/mymap.html.twig:227 msgid "Remove member coordinates" msgstr "" -#: ../tempcache/mymap.html.twig:231 +#: ../tempcache/mymap.html.twig:233 msgid "Are you sure you want to remove coordinates from the database?" msgstr "" -#: ../tempcache/mymap.html.twig:284 +#: ../tempcache/mymap.html.twig:286 msgid "An error occurred removing coordinates :(" msgstr "" + +#: ../tempcache/common_html.html.twig:68 ../tempcache/common_html.html.twig:74 +msgid "Member (company)" +msgstr "" + +#: ../tempcache/common_html.html.twig:84 ../tempcache/common_html.html.twig:90 +msgid "Search result" +msgstr "" diff --git a/lang/maps_en_US.po b/lang/maps_en_US.po index 00eae6c..7ac18bd 100644 --- a/lang/maps_en_US.po +++ b/lang/maps_en_US.po @@ -1,14 +1,14 @@ # English translations for PACKAGE package. -# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# Automatically generated, 2025. +# Automatically generated, 2026. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-12-07 10:22+0100\n" -"PO-Revision-Date: 2025-12-07 10:22+0100\n" +"POT-Creation-Date: 2026-09-02 12:49+0200\n" +"PO-Revision-Date: 2026-09-02 12:49+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: en_US\n" @@ -17,30 +17,17 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: ../_routes.php:34 +#: ../_routes.php:22 msgid "Javascript libraries has not been built!" msgstr "Javascript libraries has not been built!" -#: ../lib/GaletteMaps/PluginGaletteMaps.php:53 -#: ../lib/GaletteMaps/PluginGaletteMaps.php:99 -msgid "My localization" -msgstr "My localization" - -#: ../lib/GaletteMaps/PluginGaletteMaps.php:74 -#: ../lib/GaletteMaps/Controllers/MapsController.php:64 -#: ../lib/GaletteMaps/Controllers/MapsController.php:145 +#: ../lib/GaletteMaps/Controllers/MapsController.php:50 +#: ../lib/GaletteMaps/Controllers/MapsController.php:130 +#: ../lib/GaletteMaps/PluginGaletteMaps.php:94 msgid "Maps" msgstr "Maps" -#: ../lib/GaletteMaps/PluginGaletteMaps.php:130 -msgid "Geolocalize" -msgstr "Geolocalize" - -#: ../lib/GaletteMaps/PluginGaletteMaps.php:134 -msgid "Geolocalize %membername" -msgstr "Geolocalize %membername" - -#: ../lib/GaletteMaps/Controllers/MapsController.php:73 +#: ../lib/GaletteMaps/Controllers/MapsController.php:60 msgid "" "Coordinates has not been loaded. Maybe plugin tables does not exists in the " "database?" @@ -48,137 +35,241 @@ msgstr "" "Coordinates has not been loaded. Maybe plugin tables does not exists in the " "database?" -#: ../lib/GaletteMaps/Controllers/MapsController.php:148 +#: ../lib/GaletteMaps/Controllers/MapsController.php:133 msgid "%member geographic position" msgstr "%member geographic position" -#: ../lib/GaletteMaps/Controllers/MapsController.php:194 +#: ../lib/GaletteMaps/Controllers/MapsController.php:170 +#: ../lib/GaletteMaps/PluginGaletteMaps.php:60 +msgid "Maps settings" +msgstr "Maps settings" + +#: ../lib/GaletteMaps/Controllers/MapsController.php:216 +msgid "An address is required to use your own background map." +msgstr "An address is required to use your own background map." + +#: ../lib/GaletteMaps/Controllers/MapsController.php:232 +msgid "Maps settings have been saved." +msgstr "Maps settings have been saved." + +#: ../lib/GaletteMaps/Controllers/MapsController.php:262 msgid "Superadmin cannot be localized." msgstr "Superadmin cannot be localized." -#: ../lib/GaletteMaps/Controllers/MapsController.php:221 -#: ../lib/GaletteMaps/Controllers/MapsController.php:233 +#: ../lib/GaletteMaps/Controllers/MapsController.php:289 +#: ../lib/GaletteMaps/Controllers/MapsController.php:301 msgid "Coordinates has not been removed :(" msgstr "Coordinates has not been removed :(" -#: ../lib/GaletteMaps/Controllers/MapsController.php:231 +#: ../lib/GaletteMaps/Controllers/MapsController.php:299 msgid "Coordinates has been removed!" msgstr "Coordinates has been removed!" -#: ../lib/GaletteMaps/Controllers/MapsController.php:246 +#: ../lib/GaletteMaps/Controllers/MapsController.php:314 msgid "New coordinates has been stored!" msgstr "New coordinates has been stored!" -#: ../lib/GaletteMaps/Controllers/MapsController.php:248 +#: ../lib/GaletteMaps/Controllers/MapsController.php:316 msgid "Coordinates has not been stored :(" msgstr "Coordinates has not been stored :(" -#: ../lib/GaletteMaps/Controllers/MapsController.php:251 +#: ../lib/GaletteMaps/Controllers/MapsController.php:319 msgid "Something went wrong :(" msgstr "Something went wrong :(" -#: ../tempcache/common_html.html.twig:66 ../tempcache/common_html.html.twig:72 -msgid "Member (company)" -msgstr "Member (company)" +#: ../lib/GaletteMaps/TileProviders.php:123 +msgid "OpenFreeMap, light grey" +msgstr "OpenFreeMap, light grey" -#: ../tempcache/common_html.html.twig:82 ../tempcache/common_html.html.twig:88 -msgid "Search result" -msgstr "Search result" +#: ../lib/GaletteMaps/TileProviders.php:124 +msgid "OpenFreeMap, colours" +msgstr "OpenFreeMap, colours" + +#: ../lib/GaletteMaps/TileProviders.php:125 +msgid "OpenStreetMap" +msgstr "OpenStreetMap" + +#: ../lib/GaletteMaps/TileProviders.php:126 +msgid "OpenStreetMap France" +msgstr "OpenStreetMap France" + +#: ../lib/GaletteMaps/TileProviders.php:127 +msgid "Humanitarian OSM Team" +msgstr "Humanitarian OSM Team" + +#: ../lib/GaletteMaps/TileProviders.php:128 +msgid "OpenStreetMap Germany" +msgstr "OpenStreetMap Germany" + +#: ../lib/GaletteMaps/TileProviders.php:129 +msgid "Esri, light grey" +msgstr "Esri, light grey" + +#: ../lib/GaletteMaps/TileProviders.php:130 +#: ../tempcache/maps_preferences.html.twig:85 +msgid "Own values" +msgstr "Own values" + +#: ../lib/GaletteMaps/PluginGaletteMaps.php:73 +#: ../lib/GaletteMaps/PluginGaletteMaps.php:119 +msgid "My localization" +msgstr "My localization" + +#: ../lib/GaletteMaps/PluginGaletteMaps.php:150 +msgid "Geolocalize" +msgstr "Geolocalize" + +#: ../lib/GaletteMaps/PluginGaletteMaps.php:154 +msgid "Geolocalize %membername" +msgstr "Geolocalize %membername" -#: ../tempcache/maps.html.twig:114 +#: ../tempcache/maps.html.twig:115 msgid "aka" msgstr "aka" -#: ../tempcache/common_scripts.html.twig:134 +#: ../tempcache/common_scripts.html.twig:166 msgid "An error occurred during 'I live here' process :(" msgstr "An error occurred during 'I live here' process :(" -#: ../tempcache/common_scripts.html.twig:179 +#: ../tempcache/common_scripts.html.twig:215 msgid "Display map in full screen" msgstr "Display map in full screen" -#: ../tempcache/common_scripts.html.twig:183 +#: ../tempcache/common_scripts.html.twig:219 msgid "Exit full screen" msgstr "Exit full screen" -#: ../tempcache/common_scripts.html.twig:195 +#: ../tempcache/common_scripts.html.twig:231 msgid "Search your town..." msgstr "Search your town..." -#: ../tempcache/common_scripts.html.twig:201 +#: ../tempcache/common_scripts.html.twig:237 msgid "Search a town..." msgstr "Search a town..." -#: ../tempcache/common_scripts.html.twig:207 +#: ../tempcache/common_scripts.html.twig:243 msgid "Sorry, that town could not be found." msgstr "Sorry, that town could not be found." -#: ../tempcache/common_scripts.html.twig:211 +#: ../tempcache/common_scripts.html.twig:247 msgid "Search" msgstr "Search" -#: ../tempcache/common_scripts.html.twig:235 +#: ../tempcache/common_scripts.html.twig:271 msgid "Show me where I am" msgstr "Show me where I am" -#: ../tempcache/common_scripts.html.twig:240 +#: ../tempcache/common_scripts.html.twig:276 msgid "You seem located outside the boundaries of the map" msgstr "You seem located outside the boundaries of the map" -#: ../tempcache/common_scripts.html.twig:252 -msgid "Map data (c)" -msgstr "Map data (c)" +#: ../tempcache/maps_preferences.html.twig:66 +msgid "" +"Background maps come from an outside provider. Should it change its terms, " +"pick another one here rather than waiting for a new release." +msgstr "" +"Background maps come from an outside provider. Should it change its terms, " +"pick another one here rather than waiting for a new release." + +#: ../tempcache/maps_preferences.html.twig:73 +msgid "Background map" +msgstr "Background map" + +#: ../tempcache/maps_preferences.html.twig:91 +msgid "Vector tiles" +msgstr "Vector tiles" -#: ../tempcache/common_scripts.html.twig:254 -msgid "OpenStreetMap contributors" -msgstr "OpenStreetMap contributors" +#: ../tempcache/maps_preferences.html.twig:91 +msgid "Tick for a MapLibre style, leave unticked for classic raster tiles." +msgstr "Tick for a MapLibre style, leave unticked for classic raster tiles." -#: ../tempcache/common_scripts.html.twig:256 -msgid "Imagery (c)" -msgstr "Imagery (c)" +#: ../tempcache/maps_preferences.html.twig:98 +msgid "Address" +msgstr "Address" + +#: ../tempcache/maps_preferences.html.twig:98 +msgid "Style address for vector tiles, tiles address for raster ones, such as:" +msgstr "" +"Style address for vector tiles, tiles address for raster ones, such as:" -#: ../tempcache/mymap.html.twig:72 +#: ../tempcache/maps_preferences.html.twig:104 +msgid "Attribution" +msgstr "Attribution" + +#: ../tempcache/maps_preferences.html.twig:104 +msgid "" +"Credit the provider requires, HTML allowed. Data licences make it mandatory. " +"A vector style usually carries its own, and this can then be left empty." +msgstr "" +"Credit the provider requires, HTML allowed. Data licences make it mandatory. " +"A vector style usually carries its own, and this can then be left empty." + +#: ../tempcache/maps_preferences.html.twig:110 +msgid "Maximum zoom" +msgstr "Maximum zoom" + +#: ../tempcache/maps_preferences.html.twig:116 +msgid "Subdomains" +msgstr "Subdomains" + +#: ../tempcache/maps_preferences.html.twig:116 +msgid "" +"Letters the {s} token in the address is replaced with, such as abc. Raster " +"tiles only." +msgstr "" +"Letters the {s} token in the address is replaced with, such as abc. Raster " +"tiles only." + +#: ../tempcache/mymap.html.twig:73 msgid "Choose your location" msgstr "Choose your location" -#: ../tempcache/mymap.html.twig:78 +#: ../tempcache/mymap.html.twig:79 msgid "Choose %member location" msgstr "Choose %member location" -#: ../tempcache/mymap.html.twig:88 +#: ../tempcache/mymap.html.twig:89 msgid "Select your town." msgstr "Select your town." -#: ../tempcache/mymap.html.twig:92 +#: ../tempcache/mymap.html.twig:93 msgid "In the database, town is set to: '%town'" msgstr "In the database, town is set to: '%town'" -#: ../tempcache/mymap.html.twig:186 +#: ../tempcache/mymap.html.twig:188 msgid "You clicked at %p" msgstr "You clicked at %p" -#: ../tempcache/mymap.html.twig:189 ../tempcache/mymap.html.twig:310 -#: ../tempcache/mymap.html.twig:339 +#: ../tempcache/mymap.html.twig:191 ../tempcache/mymap.html.twig:312 +#: ../tempcache/mymap.html.twig:341 msgid "I live here!" msgstr "I live here!" -#: ../tempcache/mymap.html.twig:191 ../tempcache/mymap.html.twig:312 -#: ../tempcache/mymap.html.twig:341 +#: ../tempcache/mymap.html.twig:193 ../tempcache/mymap.html.twig:314 +#: ../tempcache/mymap.html.twig:343 msgid "Member lives here!" msgstr "Member lives here!" -#: ../tempcache/mymap.html.twig:223 +#: ../tempcache/mymap.html.twig:225 msgid "Remove my coordinates" msgstr "Remove my coordinates" -#: ../tempcache/mymap.html.twig:225 +#: ../tempcache/mymap.html.twig:227 msgid "Remove member coordinates" msgstr "Remove member coordinates" -#: ../tempcache/mymap.html.twig:231 +#: ../tempcache/mymap.html.twig:233 msgid "Are you sure you want to remove coordinates from the database?" msgstr "Are you sure you want to remove coordinates from the database?" -#: ../tempcache/mymap.html.twig:284 +#: ../tempcache/mymap.html.twig:286 msgid "An error occurred removing coordinates :(" msgstr "An error occurred removing coordinates :(" + +#: ../tempcache/common_html.html.twig:68 ../tempcache/common_html.html.twig:74 +msgid "Member (company)" +msgstr "Member (company)" + +#: ../tempcache/common_html.html.twig:84 ../tempcache/common_html.html.twig:90 +msgid "Search result" +msgstr "Search result" diff --git a/lib/GaletteMaps/Controllers/MapsController.php b/lib/GaletteMaps/Controllers/MapsController.php index 81a46c4..83a4b9b 100644 --- a/lib/GaletteMaps/Controllers/MapsController.php +++ b/lib/GaletteMaps/Controllers/MapsController.php @@ -15,6 +15,7 @@ use Galette\Entity\Adherent; use GaletteMaps\NominatimTowns; use GaletteMaps\Coordinates; +use GaletteMaps\TileProviders; use Slim\Psr7\Request; use Slim\Psr7\Response; use Analog\Analog; @@ -47,7 +48,8 @@ public function map(Request $request, Response $response): Response $params = [ 'require_dialog' => true, 'page_title' => _T('Maps', 'maps'), - 'module_id' => $this->getModuleId() + 'module_id' => $this->getModuleId(), + 'tiles' => TileProviders::resolve($this->preferences) ]; if ($list !== false) { @@ -133,7 +135,8 @@ public function localizeMember(Request $request, Response $response, ?int $id = 'member' => $member, 'require_dialog' => true, 'adh_map' => true, - 'module_id' => $this->getModuleId() + 'module_id' => $this->getModuleId(), + 'tiles' => TileProviders::resolve($this->preferences) ]; if ($towns !== false) { @@ -155,6 +158,90 @@ public function localizeMember(Request $request, Response $response, ?int $id = return $response; } + /** + * Tile provider settings + * + * @param Request $request PSR Request + * @param Response $response PSR Response + */ + public function preferences(Request $request, Response $response): Response + { + $params = [ + 'page_title' => _T('Maps settings', 'maps'), + 'module_id' => $this->getModuleId(), + 'providers' => TileProviders::getSelectValues(), + 'custom' => TileProviders::CUSTOM, + 'tiles' => TileProviders::resolve($this->preferences), + 'provider' => $this->preferences->getPluginValue(TileProviders::PREF_PROVIDER), + 'vector' => $this->preferences->getPluginValue(TileProviders::PREF_VECTOR), + 'url' => $this->preferences->getPluginValue(TileProviders::PREF_URL), + 'attribution' => $this->preferences->getPluginValue(TileProviders::PREF_ATTRIBUTION), + 'maxzoom' => $this->preferences->getPluginValue(TileProviders::PREF_MAXZOOM), + 'subdomains' => $this->preferences->getPluginValue(TileProviders::PREF_SUBDOMAINS), + ]; + + $this->view->render( + $response, + $this->getTemplate('maps_preferences'), + $params + ); + return $response; + } + + /** + * Store tile provider settings + * + * @param Request $request PSR Request + * @param Response $response PSR Response + */ + public function storePreferences(Request $request, Response $response): Response + { + $post = $request->getParsedBody(); + $provider = $post[TileProviders::PREF_PROVIDER] ?? TileProviders::DEFAULT; + + $values = [TileProviders::PREF_PROVIDER => $provider]; + if ($provider === TileProviders::CUSTOM) { + //own values are only meaningful along with the custom provider + $values += [ + TileProviders::PREF_VECTOR => (int)isset($post[TileProviders::PREF_VECTOR]), + TileProviders::PREF_URL => trim((string)($post[TileProviders::PREF_URL] ?? '')), + TileProviders::PREF_ATTRIBUTION => trim((string)($post[TileProviders::PREF_ATTRIBUTION] ?? '')), + TileProviders::PREF_MAXZOOM => (int)($post[TileProviders::PREF_MAXZOOM] ?? 19), + TileProviders::PREF_SUBDOMAINS => trim((string)($post[TileProviders::PREF_SUBDOMAINS] ?? '')), + ]; + + if ($values[TileProviders::PREF_URL] === '') { + $this->flash->addMessage( + 'error_detected', + _T('An address is required to use your own background map.', 'maps') + ); + return $response + ->withStatus(302) + ->withHeader('Location', $this->routeparser->urlFor('maps_preferences')); + } + } + + $stored = true; + foreach ($values as $name => $value) { + $stored = $this->preferences->setValue($name, $value, $this->login) && $stored; + } + + if ($stored) { + $this->flash->addMessage( + 'success_detected', + _T('Maps settings have been saved.', 'maps') + ); + } else { + foreach ($this->preferences->getErrors() as $error) { + $this->flash->addMessage('error_detected', $error); + } + } + + return $response + ->withStatus(302) + ->withHeader('Location', $this->routeparser->urlFor('maps_preferences')); + } + /** * Change member localization * diff --git a/lib/GaletteMaps/PluginGaletteMaps.php b/lib/GaletteMaps/PluginGaletteMaps.php index ff40e77..77f51e1 100644 --- a/lib/GaletteMaps/PluginGaletteMaps.php +++ b/lib/GaletteMaps/PluginGaletteMaps.php @@ -17,6 +17,7 @@ use Galette\Core\Plugins\MenuProviderInterface; use Galette\Core\Plugins\DashboardProviderInterface; use Galette\Core\Plugins\MemberActionProviderInterface; +use Galette\Core\Plugins\PreferencesProviderInterface; use Galette\Entity\Adherent; use Galette\Core\GalettePlugin; @@ -26,11 +27,21 @@ * @author Johan Cwiklinski */ -class PluginGaletteMaps extends GalettePlugin implements InstallableInterface, MenuProviderInterface, DashboardProviderInterface, MemberActionProviderInterface +class PluginGaletteMaps extends GalettePlugin implements InstallableInterface, MenuProviderInterface, DashboardProviderInterface, MemberActionProviderInterface, PreferencesProviderInterface { #[Inject] private readonly Db $zdb; //@phpstan-ignore-line injected from DI + /** + * Get the preferences the plugin declares + * + * @return array> + */ + public function getPreferences(): array + { + return TileProviders::getSchema(); + } + /** * Extra menus entries * @@ -42,6 +53,19 @@ public function getMenus(): array global $login; $menus = []; + if ($login->isAdmin()) { + $menus['configuration'] = [ + 'items' => [ + [ + 'label' => _T('Maps settings', 'maps'), + 'route' => [ + 'name' => 'maps_preferences', + ] + ], + ] + ]; + } + if ($login->isLogged() && !$login->isSuperAdmin()) { $menus['myaccount'] = [ 'items' => [ diff --git a/lib/GaletteMaps/TileProviders.php b/lib/GaletteMaps/TileProviders.php new file mode 100644 index 0000000..03ed7c5 --- /dev/null +++ b/lib/GaletteMaps/TileProviders.php @@ -0,0 +1,236 @@ + + */ +final class TileProviders +{ + public const string PREF_PROVIDER = 'pref_maps_tiles_provider'; + public const string PREF_VECTOR = 'pref_maps_tiles_vector'; + public const string PREF_URL = 'pref_maps_tiles_url'; + public const string PREF_ATTRIBUTION = 'pref_maps_tiles_attribution'; + public const string PREF_MAXZOOM = 'pref_maps_tiles_maxzoom'; + public const string PREF_SUBDOMAINS = 'pref_maps_tiles_subdomains'; + + /** Own values rather than one of the presets */ + public const string CUSTOM = 'custom'; + public const string DEFAULT = 'openfreemap-positron'; + /** Served to browsers without WebGL 2, which cannot render vector tiles */ + public const string RASTER_FALLBACK = 'osm'; + + private const string OSM_ATTRIBUTION + = '© OpenStreetMap'; + + /** + * Get every known provider + * + * A vector provider carries its attribution in the style it serves, so it + * declares none here: the Leaflet control picks it up once the style loads. + * + * @return array> + */ + public static function getPresets(): array + { + return [ + 'openfreemap-positron' => [ + 'vector' => true, + 'url' => 'https://tiles.openfreemap.org/styles/positron', + 'attribution' => '', + 'maxzoom' => 20, + 'subdomains' => '', + ], + 'openfreemap-liberty' => [ + 'vector' => true, + 'url' => 'https://tiles.openfreemap.org/styles/liberty', + 'attribution' => '', + 'maxzoom' => 20, + 'subdomains' => '', + ], + 'osm' => [ + 'vector' => false, + 'url' => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + 'attribution' => self::OSM_ATTRIBUTION, + 'maxzoom' => 19, + 'subdomains' => '', + ], + 'osmfr' => [ + 'vector' => false, + 'url' => 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png', + 'attribution' => self::OSM_ATTRIBUTION . ' — OSM-FR', + 'maxzoom' => 20, + 'subdomains' => 'abc', + ], + 'hot' => [ + 'vector' => false, + 'url' => 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', + 'attribution' => self::OSM_ATTRIBUTION + . ' — HOT', + 'maxzoom' => 20, + 'subdomains' => 'abc', + ], + 'osmde' => [ + //stops at 18: beyond that the server answers a 404 + 'vector' => false, + 'url' => 'https://tile.openstreetmap.de/{z}/{x}/{y}.png', + 'attribution' => self::OSM_ATTRIBUTION, + 'maxzoom' => 18, + 'subdomains' => '', + ], + 'esri-gray' => [ + //note the unusual {z}/{y}/{x} order, and the JPEG tiles + 'vector' => false, + 'url' => 'https://services.arcgisonline.com/ArcGIS/rest/services/' + . 'Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}', + 'attribution' => 'Tiles © Esri — Esri, DeLorme, NAVTEQ', + 'maxzoom' => 16, + 'subdomains' => '', + ], + ]; + } + + /** + * Get the translated name of a provider + * + * Kept apart from the structural list so `_T()` only runs when a name is + * actually displayed. + * + * @param string $id Provider identifier + */ + public static function getLabel(string $id): string + { + return match ($id) { + 'openfreemap-positron' => _T('OpenFreeMap, light grey', 'maps'), + 'openfreemap-liberty' => _T('OpenFreeMap, colours', 'maps'), + 'osm' => _T('OpenStreetMap', 'maps'), + 'osmfr' => _T('OpenStreetMap France', 'maps'), + 'hot' => _T('Humanitarian OSM Team', 'maps'), + 'osmde' => _T('OpenStreetMap Germany', 'maps'), + 'esri-gray' => _T('Esri, light grey', 'maps'), + self::CUSTOM => _T('Own values', 'maps'), + default => $id, + }; + } + + /** + * Get providers as the id => label map a select expects + * + * @return array + */ + public static function getSelectValues(): array + { + $values = []; + foreach (array_keys(self::getPresets()) as $id) { + $values[$id] = self::getLabel($id); + } + $values[self::CUSTOM] = self::getLabel(self::CUSTOM); + + return $values; + } + + /** + * Get the preferences the plugin stores + * + * @return array> + */ + public static function getSchema(): array + { + return [ + self::PREF_PROVIDER => [ + 'type' => PreferencesSchema::TYPE_STRING, + 'default' => self::DEFAULT, + ], + self::PREF_VECTOR => [ + 'type' => PreferencesSchema::TYPE_BOOL, + 'default' => true, + ], + //not TYPE_URL: its validation rejects the {s} subdomain token + //several providers rely on + self::PREF_URL => [ + 'type' => PreferencesSchema::TYPE_STRING, + 'default' => '', + ], + self::PREF_ATTRIBUTION => [ + 'type' => PreferencesSchema::TYPE_HTML, + 'default' => '', + ], + self::PREF_MAXZOOM => [ + 'type' => PreferencesSchema::TYPE_INT, + 'default' => 19, + 'min' => 0, + 'max' => 22, + 'error' => PreferencesSchema::ERR_POSITIVE_NUMBER, + ], + self::PREF_SUBDOMAINS => [ + 'type' => PreferencesSchema::TYPE_STRING, + 'default' => '', + ], + ]; + } + + /** + * Get the background map to display + * + * An unknown identifier falls back to the default: providers get retired, + * and a map showing nothing is worse than a map showing something else. + * + * @param Preferences $preferences Preferences instance + * + * @return array + */ + public static function resolve(Preferences $preferences): array + { + $id = (string)$preferences->getPluginValue(self::PREF_PROVIDER); + + if ($id === self::CUSTOM) { + $tiles = [ + 'vector' => (bool)$preferences->getPluginValue(self::PREF_VECTOR), + 'url' => (string)$preferences->getPluginValue(self::PREF_URL), + 'attribution' => (string)$preferences->getPluginValue(self::PREF_ATTRIBUTION), + 'maxzoom' => (int)$preferences->getPluginValue(self::PREF_MAXZOOM), + 'subdomains' => (string)$preferences->getPluginValue(self::PREF_SUBDOMAINS), + ]; + //an empty URL would display nothing at all + if ($tiles['url'] !== '') { + return $tiles + ['id' => self::CUSTOM, 'fallback' => self::rasterFallback()]; + } + $id = self::DEFAULT; + } + + $tiles = self::getPresets()[$id] ?? self::getPresets()[self::DEFAULT]; + $tiles['id'] = isset(self::getPresets()[$id]) ? $id : self::DEFAULT; + $tiles['fallback'] = self::rasterFallback(); + + return $tiles; + } + + /** + * Get the raster provider to use when the browser cannot render vector tiles + * + * @return array + */ + private static function rasterFallback(): array + { + return self::getPresets()[self::RASTER_FALLBACK]; + } +} diff --git a/templates/default/common_scripts.html.twig b/templates/default/common_scripts.html.twig index 2296921..de0affb 100644 --- a/templates/default/common_scripts.html.twig +++ b/templates/default/common_scripts.html.twig @@ -4,9 +4,12 @@ # SPDX-License-Identifier: GPL-3.0-or-later #} -{% if cur_route starts with 'maps_' %} +{% if cur_route in ['maps_map', 'maps_localize_member', 'maps_mymap'] %} + {% if tiles.vector %} + {# a megabyte of MapLibre, only worth loading for a vector background #} + {% endif %} {% endif %} {% if cur_route == 'maps_localize_member' or cur_route == 'maps_mymap' %} @@ -31,6 +34,7 @@ shadowSize: [41, 41] }); +{% if tiles.vector %} /** * Vector tiles need WebGL 2; without it we fall back to raster tiles */ @@ -41,6 +45,7 @@ return false; } } +{% endif %} {% if cur_route != 'maps_map' %} function _iLiveHere(_id){ @@ -90,7 +95,7 @@ var map = L.map( 'map', { gestureHandling: true, - maxZoom: 19 + maxZoom: {{ tiles.maxzoom }} } ).setView([_lat, _lon], {% if town is defined %}12{% else %}6{% endif %}); @@ -131,17 +136,28 @@ }).addTo(map); {% endif %} +{% if tiles.vector %} if (_hasWebGL2()) { - {# Attribution comes from the style sources, the Leaflet control picks it up on load #} L.maplibreGL({ - style: 'https://tiles.openfreemap.org/styles/positron' + style: '{{ tiles.url|e("js") }}'{% if tiles.attribution != '' %}, + {# given one, it wins; otherwise the bridge reads it off the style sources #} + attributionControl: {customAttribution: '{{ tiles.attribution|e("js") }}'}{% endif %} }).addTo(map); } else { - L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, - attribution: '{{ _T("Map data (c)", "maps")|e("js") }} {{ _T("OpenStreetMap contributors", "maps")|e("js") }}' + L.tileLayer('{{ tiles.fallback.url|e("js") }}', { + maxZoom: {{ tiles.fallback.maxzoom }}, + attribution: '{{ tiles.fallback.attribution|e("js") }}' }).addTo(map); } +{% else %} + L.tileLayer('{{ tiles.url|e("js") }}', { + maxZoom: {{ tiles.maxzoom }}, +{% if tiles.subdomains != '' %} + subdomains: '{{ tiles.subdomains|e("js") }}', +{% endif %} + attribution: '{{ tiles.attribution|e("js") }}' + }).addTo(map); +{% endif %} try { _mapsBinded(map); diff --git a/templates/default/headers.html.twig b/templates/default/headers.html.twig index c22873c..309bd07 100644 --- a/templates/default/headers.html.twig +++ b/templates/default/headers.html.twig @@ -4,9 +4,11 @@ # SPDX-License-Identifier: GPL-3.0-or-later #} -{% if cur_route starts with 'maps_' %} +{% if cur_route in ['maps_map', 'maps_localize_member', 'maps_mymap'] %} + {% if tiles.vector %} + {% endif %} {% endif %} {% if cur_route == 'maps_localize_member' or cur_route == 'maps_mymap' %} diff --git a/templates/default/maps_preferences.html.twig b/templates/default/maps_preferences.html.twig new file mode 100644 index 0000000..0ea1a35 --- /dev/null +++ b/templates/default/maps_preferences.html.twig @@ -0,0 +1,83 @@ +{# + # This file is part of Galette Maps plugin (https://galette.eu). + # SPDX-FileCopyrightText: Copyright © 2012-2026 The Galette Team + # SPDX-License-Identifier: GPL-3.0-or-later + #} + +{% extends 'page.html.twig' %} + +{% block content %} +
+
+

{{ _T("Background maps come from an outside provider. Should it change its terms, pick another one here rather than waiting for a new release.", "maps") }}

+ + {% include "components/forms/select.html.twig" with { + id: 'pref_maps_tiles_provider', + value: provider, + values: providers, + label: _T("Background map", "maps"), + required: true + } %} +
+ +
+

{{ _T("Own values", "maps") }}

+ + {% include "components/forms/checkbox.html.twig" with { + id: 'pref_maps_tiles_vector', + value: '1', + checked: vector, + label: _T("Vector tiles", "maps"), + tip: _T("Tick for a MapLibre style, leave unticked for classic raster tiles.", "maps") + } %} + + {# the example is kept out of the translatable string, as gettext advises #} + {% include "components/forms/text.html.twig" with { + id: 'pref_maps_tiles_url', + value: url, + label: _T("Address", "maps"), + tip: _T("Style address for vector tiles, tiles address for raster ones, such as:", "maps") ~ ' https://tile.openstreetmap.org/{z}/{x}/{y}.png' + } %} + + {% include "components/forms/text.html.twig" with { + id: 'pref_maps_tiles_attribution', + value: attribution, + label: _T("Attribution", "maps"), + tip: _T("Credit the provider requires, HTML allowed. Data licences make it mandatory. A vector style usually carries its own, and this can then be left empty.", "maps") + } %} + + {% include "components/forms/number.html.twig" with { + id: 'pref_maps_tiles_maxzoom', + value: maxzoom, + label: _T("Maximum zoom", "maps"), + min: 0, + max: 22 + } %} + + {% include "components/forms/text.html.twig" with { + id: 'pref_maps_tiles_subdomains', + value: subdomains, + label: _T("Subdomains", "maps"), + tip: _T("Letters the {s} token in the address is replaced with, such as abc. Raster tiles only.", "maps") + } %} +
+ +
+ +
+
+{% endblock %} + +{% block javascripts %} + +{% endblock %} diff --git a/tests/GaletteMaps/tests/units/TileProviders.php b/tests/GaletteMaps/tests/units/TileProviders.php new file mode 100644 index 0000000..3a5fa2f --- /dev/null +++ b/tests/GaletteMaps/tests/units/TileProviders.php @@ -0,0 +1,145 @@ + + */ +class TileProviders extends GaletteTestCase +{ + protected int $seed = 20240517214956; + + /** + * Cleanup after each test method + */ + public function tearDown(): void + { + foreach (array_keys(Providers::getSchema()) as $name) { + $this->preferences->resetValue($name, $this->login); + } + parent::tearDown(); + } + + /** + * Every provider describes itself the same way + */ + public function testPresetsAreComplete(): void + { + foreach (Providers::getPresets() as $id => $preset) { + foreach (['vector', 'url', 'attribution', 'maxzoom', 'subdomains'] as $key) { + $this->assertArrayHasKey($key, $preset, $id . ' has no ' . $key); + } + $this->assertNotSame('', $preset['url'], $id . ' has no address'); + //a raster provider has to credit its source itself, a vector one + //carries it in the style it serves + if ($preset['vector'] === false) { + $this->assertNotSame('', $preset['attribution'], $id . ' credits nobody'); + } + $this->assertNotSame($id, Providers::getLabel($id), $id . ' has no name'); + } + + $this->assertArrayHasKey(Providers::DEFAULT, Providers::getPresets()); + $this->assertArrayHasKey(Providers::RASTER_FALLBACK, Providers::getPresets()); + $this->assertFalse(Providers::getPresets()[Providers::RASTER_FALLBACK]['vector']); + } + + /** + * Out of the box, the vector default applies + */ + public function testDefault(): void + { + //without this the rest passes vacuously: an undeclared preference reads + //as false, and resolve() then falls back to the default anyway + $this->assertTrue( + \Galette\Core\PreferencesSchema::has(Providers::PREF_PROVIDER), + 'the plugin did not declare its preferences' + ); + $this->assertSame('maps', \Galette\Core\PreferencesSchema::getOwner(Providers::PREF_PROVIDER)); + + $tiles = Providers::resolve($this->preferences); + + $this->assertSame(Providers::DEFAULT, $tiles['id']); + $this->assertTrue($tiles['vector']); + $this->assertFalse($tiles['fallback']['vector']); + } + + /** + * A preset applies as it is declared + */ + public function testPreset(): void + { + $this->preferences->setValue(Providers::PREF_PROVIDER, 'osmfr', $this->login); + + $tiles = Providers::resolve($this->preferences); + + $this->assertSame('osmfr', $tiles['id']); + $this->assertFalse($tiles['vector']); + $this->assertSame('abc', $tiles['subdomains']); + $this->assertSame(20, $tiles['maxzoom']); + } + + /** + * A provider that no longer exists falls back to the default + * + * Providers do get retired, and a map showing nothing would be worse than + * a map showing something else. + */ + public function testRetiredProvider(): void + { + $this->preferences->setValue(Providers::PREF_PROVIDER, 'cartodb-light', $this->login); + + $this->assertSame(Providers::DEFAULT, Providers::resolve($this->preferences)['id']); + } + + /** + * Own values apply + */ + public function testOwnValues(): void + { + $this->preferences->setValue(Providers::PREF_PROVIDER, Providers::CUSTOM, $this->login); + $this->preferences->setValue(Providers::PREF_VECTOR, false, $this->login); + //the {s} token is why the address is stored as a string: Galette's URL + //validation rejects a host that starts with a brace + $this->assertTrue( + $this->preferences->setValue( + Providers::PREF_URL, + 'https://{s}.tile.example.org/{z}/{x}/{y}.png', + $this->login + ), + print_r($this->preferences->getErrors(), true) + ); + $this->preferences->setValue(Providers::PREF_SUBDOMAINS, 'abc', $this->login); + $this->preferences->setValue(Providers::PREF_MAXZOOM, 17, $this->login); + + $tiles = Providers::resolve($this->preferences); + + $this->assertSame(Providers::CUSTOM, $tiles['id']); + $this->assertFalse($tiles['vector']); + $this->assertSame('https://{s}.tile.example.org/{z}/{x}/{y}.png', $tiles['url']); + $this->assertSame('abc', $tiles['subdomains']); + $this->assertSame(17, $tiles['maxzoom']); + } + + /** + * Own values without an address display nothing, so the default applies + */ + public function testOwnValuesWithoutAddress(): void + { + $this->preferences->setValue(Providers::PREF_PROVIDER, Providers::CUSTOM, $this->login); + + $this->assertSame(Providers::DEFAULT, Providers::resolve($this->preferences)['id']); + } +}