feat: add experimental "map preview"

This is the same as "map new", but it is not in edit mode. This
allow to click on the elements and see the popups instead of editing
it when using a `dataUrl` query string.
This way of using uMap is not documented, but it's used by some
partners (Deveco recently, data.gouv.fr historicaly).

In the same time, this PR adds two things:
- possibility to pass data direclty in querystring (instead of an URL):
  in the case of Deveco, they have pages where only point is shown (for
  each company)
- possibility to pass style options directly from query string: may
  allow for example to control the `popupTemplate`, eg. to use a table
  one that will display all properties of the clicked feature

Note: dataUrl and such also works in normal "map new" view. There are
two use cases around those parameters, from external sites:
- see this data on uMap (should point on map preview)
- create a map with those data (should point on map new)
This commit is contained in:
Yohan Boniface 2024-02-01 12:24:22 +01:00
parent 2b08d499ac
commit c334f7554e
4 changed files with 128 additions and 19 deletions

View file

@ -233,18 +233,35 @@ L.U.Map.include({
// Creation mode // Creation mode
if (!this.options.umap_id) { if (!this.options.umap_id) {
if (!this.options.preview) {
this.isDirty = true this.isDirty = true
this.enableEdit()
}
this._default_extent = true this._default_extent = true
this.options.name = L._('Untitled map') this.options.name = L._('Untitled map')
this.options.editMode = 'advanced' let style = L.Util.queryString('style', null)
this.enableEdit() if (style) {
style = decodeURIComponent(style)
try {
style = JSON.parse(style)
L.Util.setOptions(this, style)
} catch (error) {
console.error(error)
}
}
let data = L.Util.queryString('data', null)
let dataUrl = L.Util.queryString('dataUrl', null) let dataUrl = L.Util.queryString('dataUrl', null)
const dataFormat = L.Util.queryString('dataFormat', 'geojson') const dataFormat = L.Util.queryString('dataFormat', 'geojson')
if (dataUrl) { if (dataUrl) {
dataUrl = decodeURIComponent(dataUrl) dataUrl = decodeURIComponent(dataUrl)
dataUrl = this.localizeUrl(dataUrl) dataUrl = this.localizeUrl(dataUrl)
dataUrl = this.proxyUrl(dataUrl) dataUrl = this.proxyUrl(dataUrl)
const datalayer = this.createDataLayer()
datalayer.importFromUrl(dataUrl, dataFormat) datalayer.importFromUrl(dataUrl, dataFormat)
} else if (data) {
data = decodeURIComponent(data)
const datalayer = this.createDataLayer()
datalayer.importRaw(data, dataFormat)
} }
} }
@ -290,7 +307,7 @@ L.U.Map.include({
} }
}) })
window.onbeforeunload = () => this.isDirty || null window.onbeforeunload = () => this.editEnabled && this.isDirty || null
this.backup() this.backup()
this.initContextMenu() this.initContextMenu()
this.on('click contextmenu.show', this.closeInplaceToolbar) this.on('click contextmenu.show', this.closeInplaceToolbar)

View file

@ -0,0 +1,78 @@
import json
from urllib.parse import quote
import pytest
from playwright.sync_api import expect
pytestmark = pytest.mark.django_db
GEOJSON = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "Niagara Falls",
},
"geometry": {
"type": "Point",
"coordinates": [-79.04, 43.08],
},
}
],
}
CSV = "name,latitude,longitude\nNiagara Falls,43.08,-79.04"
def test_map_preview(page, live_server, tilelayer):
page.goto(f"{live_server.url}/map/")
# Edit mode is not enabled
edit_button = page.get_by_role("button", name="Edit")
expect(edit_button).to_be_visible()
def test_map_preview_can_load_remote_geojson(page, live_server, tilelayer):
def handle(route):
route.fulfill(json=GEOJSON)
# Intercept the route to the proxy
page.route("*/**/ajax-proxy/**", handle)
page.goto(f"{live_server.url}/map/?dataUrl=http://some.org/geo.json")
markers = page.locator(".leaflet-marker-icon")
expect(markers).to_have_count(1)
def test_map_preview_can_load_remote_csv(page, live_server, tilelayer):
def handle(route):
csv = """name,latitude,longitude\nNiagara Falls,43.08,-79.04"""
route.fulfill(body=csv)
# Intercept the route to the proxy
page.route("*/**/ajax-proxy/**", handle)
page.goto(f"{live_server.url}/map/?dataUrl=http://some.org/geo.csv&dataFormat=csv")
markers = page.locator(".leaflet-marker-icon")
expect(markers).to_have_count(1)
def test_map_preview_can_load_geojson_in_querystring(page, live_server, tilelayer):
page.goto(f"{live_server.url}/map/?data={quote(json.dumps(GEOJSON))}")
markers = page.locator(".leaflet-marker-icon")
expect(markers).to_have_count(1)
def test_map_preview_can_load_csv_in_querystring(page, live_server, tilelayer):
page.goto(f"{live_server.url}/map/?data={quote(CSV)}&dataFormat=csv")
markers = page.locator(".leaflet-marker-icon")
expect(markers).to_have_count(1)
def test_map_preview_can_change_styling_from_querystring(page, live_server, tilelayer):
style = {"color": "DarkRed"}
page.goto(
f"{live_server.url}/map/?data={quote(json.dumps(GEOJSON))}&style={quote(json.dumps(style))}"
)
markers = page.locator(".leaflet-marker-icon .icon_container")
expect(markers).to_have_count(1)
expect(markers).to_have_css("background-color", "rgb(139, 0, 0)")

View file

@ -96,6 +96,7 @@ i18n_urls += decorated_patterns(
) )
i18n_urls += decorated_patterns( i18n_urls += decorated_patterns(
[ensure_csrf_cookie], [ensure_csrf_cookie],
re_path(r"^map/$", views.MapPreview.as_view(), name="map_preview"),
re_path(r"^map/new/$", views.MapNew.as_view(), name="map_new"), re_path(r"^map/new/$", views.MapNew.as_view(), name="map_new"),
) )
i18n_urls += decorated_patterns( i18n_urls += decorated_patterns(

View file

@ -455,8 +455,7 @@ class MapDetailMixin:
if domain and "{" not in domain: if domain and "{" not in domain:
context["preconnect_domains"] = [f"//{domain}"] context["preconnect_domains"] = [f"//{domain}"]
def get_context_data(self, **kwargs): def get_map_properties(self):
context = super().get_context_data(**kwargs)
user = self.request.user user = self.request.user
properties = { properties = {
"urls": _urls_for_js(), "urls": _urls_for_js(),
@ -486,6 +485,17 @@ class MapDetailMixin:
if self.get_short_url(): if self.get_short_url():
properties["shortUrl"] = self.get_short_url() properties["shortUrl"] = self.get_short_url()
if not user.is_anonymous:
properties["user"] = {
"id": user.pk,
"name": str(user),
"url": reverse("user_dashboard"),
}
return properties
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
properties = self.get_map_properties()
if settings.USE_I18N: if settings.USE_I18N:
lang = settings.LANGUAGE_CODE lang = settings.LANGUAGE_CODE
# Check attr in case the middleware is not active # Check attr in case the middleware is not active
@ -495,19 +505,13 @@ class MapDetailMixin:
locale = to_locale(lang) locale = to_locale(lang)
properties["locale"] = locale properties["locale"] = locale
context["locale"] = locale context["locale"] = locale
if not user.is_anonymous: geojson = self.get_geojson()
properties["user"] = { if "properties" not in geojson:
"id": user.pk, geojson["properties"] = {}
"name": str(user), geojson["properties"].update(properties)
"url": reverse("user_dashboard"), geojson["properties"]["datalayers"] = self.get_datalayers()
} context["map_settings"] = json.dumps(geojson, indent=settings.DEBUG)
map_settings = self.get_geojson() self.set_preconnect(geojson["properties"], context)
if "properties" not in map_settings:
map_settings["properties"] = {}
map_settings["properties"].update(properties)
map_settings["properties"]["datalayers"] = self.get_datalayers()
context["map_settings"] = json.dumps(map_settings, indent=settings.DEBUG)
self.set_preconnect(map_settings["properties"], context)
return context return context
def get_datalayers(self): def get_datalayers(self):
@ -709,6 +713,15 @@ class MapNew(MapDetailMixin, TemplateView):
template_name = "umap/map_detail.html" template_name = "umap/map_detail.html"
class MapPreview(MapDetailMixin, TemplateView):
template_name = "umap/map_detail.html"
def get_map_properties(self):
properties = super().get_map_properties()
properties["preview"] = True
return properties
class MapCreate(FormLessEditMixin, PermissionsMixin, CreateView): class MapCreate(FormLessEditMixin, PermissionsMixin, CreateView):
model = Map model = Map
form_class = MapSettingsForm form_class = MapSettingsForm