From 787f22efd4a26a7f254a99712ab6841c88eed138 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 26 Aug 2024 12:51:22 +0200 Subject: [PATCH] WIP: rename DataLayer/Map.settings to DataLayer/Map.metadata and more MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The main goal is to clarify the use of options/settings/properties… - `settings` should be reserved to Django - `options` should be reserved to pure Leaflet context - `properties` used in the geojson context, for user data Main changes: - `DataLayer.settings` is renamed to `DataLayer.metadata` - `DataLayer.geojson` is renamed to `Datalayer.data` - `DataLayer.metadata` are not saved anymore in the geojson data file - `Map.settings` is renamed to `Map.metadata` - `Map.metadata` is now a flat key/value object, not a geojson Feature anymore - `Map.zoom` is now populated and reused Note: U.Map is still inheriting from Leaflet Map, so it mixes `options` and `metadata`. This changes is very intrusive, it changes - the DB schema - the way we store data in the FS (now without metadata) - the way we backup map data fix #1636 prepares #1335 prepares #1635 prepares #175 --- umap/forms.py | 8 +- .../0022_rename_settings_to_metadata.py | 28 ++ umap/models.py | 75 +++--- umap/static/umap/js/components/fragment.js | 2 +- umap/static/umap/js/modules/browser.js | 2 +- umap/static/umap/js/modules/caption.js | 8 +- umap/static/umap/js/modules/data/features.js | 103 ++++---- umap/static/umap/js/modules/data/layer.js | 244 +++++++++--------- umap/static/umap/js/modules/formatter.js | 1 - umap/static/umap/js/modules/importer.js | 8 +- umap/static/umap/js/modules/permissions.js | 6 +- umap/static/umap/js/modules/rendering/icon.js | 4 +- .../js/modules/rendering/layers/classified.js | 82 +++--- .../js/modules/rendering/layers/cluster.js | 14 +- .../umap/js/modules/rendering/layers/heat.js | 20 +- umap/static/umap/js/modules/utils.js | 4 +- umap/static/umap/js/umap.controls.js | 4 +- umap/static/umap/js/umap.js | 51 ++-- umap/static/umap/unittests/utils.js | 2 +- umap/templates/umap/map_fragment.html | 2 +- umap/templates/umap/map_init.html | 2 +- umap/templates/umap/map_table.html | 2 +- umap/templatetags/umap_tags.py | 6 +- umap/tests/base.py | 68 ++--- .../fixtures/categorized_highway.geojson | 2 +- .../choropleth_region_chomage.geojson | 2 +- umap/tests/fixtures/display_on_load.umap | 4 +- .../tests/fixtures/test_circles_layer.geojson | 2 +- umap/tests/fixtures/test_upload_data.umap | 16 +- umap/tests/integration/helpers.py | 2 +- umap/tests/integration/test_browser.py | 26 +- umap/tests/integration/test_caption.py | 6 +- .../integration/test_categorized_layer.py | 28 +- umap/tests/integration/test_choropleth.py | 27 +- .../integration/test_conditional_rules.py | 20 +- umap/tests/integration/test_datalayer.py | 30 +-- umap/tests/integration/test_edit_marker.py | 5 +- umap/tests/integration/test_edit_polygon.py | 10 +- umap/tests/integration/test_export_map.py | 38 +-- umap/tests/integration/test_facets_browser.py | 31 +-- umap/tests/integration/test_import.py | 2 +- umap/tests/integration/test_map.py | 33 ++- umap/tests/integration/test_map_list.py | 6 +- .../integration/test_optimistic_merge.py | 36 +-- umap/tests/integration/test_picto.py | 8 +- umap/tests/integration/test_slideshow.py | 2 +- umap/tests/integration/test_statics.py | 4 +- umap/tests/integration/test_tableeditor.py | 5 +- umap/tests/integration/test_tilelayer.py | 12 +- umap/tests/integration/test_view_marker.py | 8 +- umap/tests/integration/test_view_polygon.py | 8 +- umap/tests/integration/test_view_polyline.py | 8 +- umap/tests/integration/test_websocket_sync.py | 10 +- umap/tests/test_datalayer.py | 30 +-- umap/tests/test_datalayer_views.py | 63 ++--- umap/tests/test_map.py | 6 +- umap/tests/test_map_views.py | 29 ++- umap/urls.py | 6 +- umap/views.py | 123 +++++---- 59 files changed, 695 insertions(+), 699 deletions(-) create mode 100644 umap/migrations/0022_rename_settings_to_metadata.py diff --git a/umap/forms.py b/umap/forms.py index d1225b22..4201efbc 100644 --- a/umap/forms.py +++ b/umap/forms.py @@ -55,7 +55,7 @@ class AnonymousMapPermissionsForm(forms.ModelForm): class DataLayerForm(forms.ModelForm): class Meta: model = DataLayer - fields = ("geojson", "name", "display_on_load", "rank", "settings") + fields = ("data", "name", "display_on_load", "rank", "metadata") class DataLayerPermissionsForm(forms.ModelForm): @@ -78,9 +78,9 @@ class AnonymousDataLayerPermissionsForm(forms.ModelForm): fields = ("edit_status",) -class MapSettingsForm(forms.ModelForm): +class MapMetadataForm(forms.ModelForm): def __init__(self, *args, **kwargs): - super(MapSettingsForm, self).__init__(*args, **kwargs) + super(MapMetadataForm, self).__init__(*args, **kwargs) self.fields["slug"].required = False self.fields["center"].widget.map_srid = 4326 @@ -102,7 +102,7 @@ class MapSettingsForm(forms.ModelForm): return self.cleaned_data["center"] class Meta: - fields = ("settings", "name", "center", "slug") + fields = ("metadata", "name", "center", "slug", "zoom") model = Map diff --git a/umap/migrations/0022_rename_settings_to_metadata.py b/umap/migrations/0022_rename_settings_to_metadata.py new file mode 100644 index 00000000..db608a21 --- /dev/null +++ b/umap/migrations/0022_rename_settings_to_metadata.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.8 on 2024-08-21 10:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("umap", "0021_remove_map_description"), + ] + + operations = [ + migrations.RenameField( + model_name="datalayer", + old_name="settings", + new_name="metadata", + ), + migrations.RenameField( + model_name="datalayer", + old_name="geojson", + new_name="data", + ), + migrations.RenameField( + model_name="map", + old_name="settings", + new_name="metadata", + ), + migrations.RunSQL("UPDATE umap_map SET metadata=metadata->'properties'"), + ] diff --git a/umap/models.py b/umap/models.py index 5efe3aba..9581750d 100644 --- a/umap/models.py +++ b/umap/models.py @@ -190,8 +190,8 @@ class Map(NamedModel): default=get_default_share_status, verbose_name=_("share status"), ) - settings = models.JSONField( - blank=True, null=True, verbose_name=_("settings"), default=dict + metadata = models.JSONField( + blank=True, null=True, verbose_name=_("metadata"), default=dict ) objects = models.Manager() @@ -200,18 +200,20 @@ class Map(NamedModel): @property def description(self): try: - return self.settings["properties"]["description"] + return self.metadata["description"] except KeyError: return "" @property - def preview_settings(self): + def geometry(self): + return {"type": "Point", "coordinates": [self.center.x, self.center.y]} + + @property + def preview_metadata(self): layers = self.datalayer_set.all() - datalayer_data = [c.metadata() for c in layers] - map_settings = self.settings - if "properties" not in map_settings: - map_settings["properties"] = {} - map_settings["properties"].update( + datalayer_data = [l.get_metadata() for l in layers] + metadata = self.metadata + metadata.update( { "tilelayers": [TileLayer.get_default().json], "datalayers": datalayer_data, @@ -224,22 +226,23 @@ class Map(NamedModel): "umap_id": self.pk, "schema": self.extra_schema, "slideshow": {}, + "geometry": self.geometry, } ) - return map_settings + return metadata def generate_umapjson(self, request): - umapjson = self.settings - umapjson["type"] = "umap" - umapjson["uri"] = request.build_absolute_uri(self.get_absolute_url()) - datalayers = [] + umapjson = { + "metadata": {"geometry": self.geometry, **self.metadata}, + "type": "umap", + "uri": request.build_absolute_uri(self.get_absolute_url()), + "layers": [], + } for datalayer in self.datalayer_set.all(): - with open(datalayer.geojson.path, "rb") as f: + with open(datalayer.data.path, "rb") as f: layer = json.loads(f.read()) - if datalayer.settings: - layer["_umap_options"] = datalayer.settings - datalayers.append(layer) - umapjson["layers"] = datalayers + layer["metadata"] = datalayer.metadata + umapjson["layers"].append(layer) return umapjson def get_absolute_url(self): @@ -397,15 +400,15 @@ class DataLayer(NamedModel): old_id = models.IntegerField(null=True, blank=True) map = models.ForeignKey(Map, on_delete=models.CASCADE) description = models.TextField(blank=True, null=True, verbose_name=_("description")) - geojson = models.FileField(upload_to=upload_to, blank=True, null=True) + data = models.FileField(upload_to=upload_to, blank=True, null=True) display_on_load = models.BooleanField( default=False, verbose_name=_("display on load"), help_text=_("Display this layer on load."), ) rank = models.SmallIntegerField(default=0) - settings = models.JSONField( - blank=True, null=True, verbose_name=_("settings"), default=dict + metadata = models.JSONField( + blank=True, null=True, verbose_name=_("metadata"), default=dict ) edit_status = models.SmallIntegerField( choices=EDIT_STATUS, @@ -423,10 +426,10 @@ class DataLayer(NamedModel): if is_new: force_insert, force_update = False, True filename = self.upload_to() - old_name = self.geojson.name - new_name = self.geojson.storage.save(filename, self.geojson) - self.geojson.storage.delete(old_name) - self.geojson.name = new_name + old_name = self.data.name + new_name = self.data.storage.save(filename, self.data) + self.data.storage.delete(old_name) + self.data.name = new_name super(DataLayer, self).save(force_insert, force_update, **kwargs) self.purge_gzip() self.purge_old_versions() @@ -443,10 +446,10 @@ class DataLayer(NamedModel): path.append(str(self.map.pk)) return os.path.join(*path) - def metadata(self, user=None, request=None): + def get_metadata(self, user=None, request=None): # Retrocompat: minimal settings for maps not saved after settings property # has been introduced - obj = self.settings or { + obj = self.metadata or { "name": self.name, "displayOnLoad": self.display_on_load, } @@ -463,7 +466,7 @@ class DataLayer(NamedModel): new.pk = None if map_inst: new.map = map_inst - new.geojson = File(new.geojson.file.file) + new.data = File(new.data.file.file) new.save() return new @@ -478,13 +481,13 @@ class DataLayer(NamedModel): return { "name": name, "at": els[1], - "size": self.geojson.storage.size(self.get_version_path(name)), + "size": self.data.storage.size(self.get_version_path(name)), } @property def versions(self): root = self.storage_root() - names = self.geojson.storage.listdir(root)[1] + names = self.data.storage.listdir(root)[1] names = [name for name in names if self.is_valid_version(name)] versions = [self.version_metadata(name) for name in names] versions.sort(reverse=True, key=operator.itemgetter("at")) @@ -492,7 +495,7 @@ class DataLayer(NamedModel): def get_version(self, name): path = self.get_version_path(name) - with self.geojson.storage.open(path, "r") as f: + with self.data.storage.open(path, "r") as f: return f.read() def get_version_path(self, name): @@ -505,23 +508,23 @@ class DataLayer(NamedModel): name = version["name"] # Should not be in the list, but ensure to not delete the file # currently used in database - if self.geojson.name.endswith(name): + if self.data.name.endswith(name): continue try: - self.geojson.storage.delete(os.path.join(root, name)) + self.data.storage.delete(os.path.join(root, name)) except FileNotFoundError: pass def purge_gzip(self): root = self.storage_root() - names = self.geojson.storage.listdir(root)[1] + names = self.data.storage.listdir(root)[1] prefixes = [f"{self.pk}_"] if self.old_id: prefixes.append(f"{self.old_id}_") prefixes = tuple(prefixes) for name in names: if name.startswith(prefixes) and name.endswith(".gz"): - self.geojson.storage.delete(os.path.join(root, name)) + self.data.storage.delete(os.path.join(root, name)) def can_edit(self, user=None, request=None): """ diff --git a/umap/static/umap/js/components/fragment.js b/umap/static/umap/js/components/fragment.js index 63613ca1..cd0aea9a 100644 --- a/umap/static/umap/js/components/fragment.js +++ b/umap/static/umap/js/components/fragment.js @@ -1,6 +1,6 @@ class UmapFragment extends HTMLElement { connectedCallback() { - new U.Map(this.firstElementChild.id, JSON.parse(this.dataset.settings)) + new U.Map(this.firstElementChild.id, JSON.parse(this.dataset.metadata)) } } diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js index 2c7cfdec..e3666b02 100644 --- a/umap/static/umap/js/modules/browser.js +++ b/umap/static/umap/js/modules/browser.js @@ -86,7 +86,7 @@ export default class Browser { DomEvent.on(toggle, 'click', toggleList) datalayer.renderToolbox(headline) const name = DomUtil.create('span', 'datalayer-name', headline) - name.textContent = datalayer.options.name + name.textContent = datalayer.metadata.name DomEvent.on(name, 'click', toggleList) container.innerHTML = '' datalayer.eachFeature((feature) => this.addFeature(feature, container)) diff --git a/umap/static/umap/js/modules/caption.js b/umap/static/umap/js/modules/caption.js index 36c63c4c..aa494340 100644 --- a/umap/static/umap/js/modules/caption.js +++ b/umap/static/umap/js/modules/caption.js @@ -39,20 +39,20 @@ export default class Caption { } addDataLayer(datalayer, container) { - if (!datalayer.options.inCaption) return + if (!datalayer.metadata.inCaption) return const p = DomUtil.create('p', 'datalayer-legend', container) const legend = DomUtil.create('span', '', p) const headline = DomUtil.create('strong', '', p) datalayer.renderLegend(legend) - if (datalayer.options.description) { + if (datalayer.metadata.description) { DomUtil.element({ tagName: 'span', parent: p, - safeHTML: Utils.toHTML(datalayer.options.description), + safeHTML: Utils.toHTML(datalayer.metadata.description), }) } datalayer.renderToolbox(headline) - DomUtil.add('span', '', headline, `${datalayer.options.name} `) + DomUtil.add('span', '', headline, `${datalayer.metadata.name} `) } addCredits(container) { diff --git a/umap/static/umap/js/modules/data/features.js b/umap/static/umap/js/modules/data/features.js index 4a1daf4c..a368309d 100644 --- a/umap/static/umap/js/modules/data/features.js +++ b/umap/static/umap/js/modules/data/features.js @@ -26,15 +26,14 @@ class Feature { // DataLayer the feature belongs to this.datalayer = datalayer - this.properties = { _umap_options: {}, ...(geojson.properties || {}) } + this.properties = { ...(geojson.properties || {}) } + this.metadata = {} this.staticOptions = {} if (geojson.coordinates) { geojson = { geometry: geojson } } - if (geojson.geometry) { - this.populate(geojson) - } + this.populate(geojson) if (id) { this.id = id @@ -187,7 +186,7 @@ class Feature { window.top.location = outlink break default: - window.open(this.properties._umap_options.outlink) + window.open(this.metadata.outlink) } return } @@ -298,13 +297,13 @@ class Feature { getInteractionOptions() { return [ - 'properties._umap_options.popupShape', - 'properties._umap_options.popupTemplate', - 'properties._umap_options.showLabel', - 'properties._umap_options.labelDirection', - 'properties._umap_options.labelInteractive', - 'properties._umap_options.outlink', - 'properties._umap_options.outlinkTarget', + 'metadata.popupShape', + 'metadata.popupTemplate', + 'metadata.showLabel', + 'metadata.labelDirection', + 'metadata.labelInteractive', + 'metadata.outlink', + 'metadata.outlinkTarget', ] } @@ -321,7 +320,7 @@ class Feature { } hasPopupFooter() { - if (this.datalayer.isRemoteLayer() && this.datalayer.options.remoteData.dynamic) { + if (this.datalayer.isRemoteLayer() && this.datalayer.metadata.remoteData.dynamic) { return false } return this.map.getOption('displayPopupFooter') @@ -375,20 +374,24 @@ class Feature { return [key, value] } - populate(geojson) { + populate(geojson = {}) { this._geometry = geojson.geometry this.properties = Object.fromEntries( Object.entries(geojson.properties || {}).map(this.cleanProperty) ) - this.properties._umap_options = L.extend( + this.metadata = L.extend( {}, this.properties._storage_options, - this.properties._umap_options + this.properties._umap_options, + geojson.metadata ) + // Legacy + delete this.properties._umap_options + delete this.properties._storage_options // Retrocompat - if (this.properties._umap_options.clickable === false) { - this.properties._umap_options.interactive = false - delete this.properties._umap_options.clickable + if (this.metadata.clickable === false) { + this.metadata.interactive = false + delete this.metadata.clickable } } @@ -408,8 +411,8 @@ class Feature { let value = fallback if (typeof this.staticOptions[option] !== 'undefined') { value = this.staticOptions[option] - } else if (U.Utils.usableOption(this.properties._umap_options, option)) { - value = this.properties._umap_options[option] + } else if (U.Utils.usableOption(this.metadata, option)) { + value = this.metadata[option] } else if (this.datalayer) { value = this.datalayer.getOption(option, this) } else { @@ -451,15 +454,12 @@ class Feature { return this.datalayer.getPreviousFeature(this) } + cloneMetadata() { + return L.extend({}, this.metadata) + } + cloneProperties() { - const properties = L.extend({}, this.properties) - properties._umap_options = L.extend({}, properties._umap_options) - if (Object.keys && Object.keys(properties._umap_options).length === 0) { - delete properties._umap_options // It can make a difference on big data sets - } - // Legacy - delete properties._storage_options - return properties + return L.extend({}, this.properties) } deleteProperty(property) { @@ -473,10 +473,16 @@ class Feature { } toGeoJSON() { - return Utils.CopyJSON({ + let metadata = this.cloneMetadata() + if (!Object.keys(metadata).length) { + // Remove empty object from exported json + metadata = undefined + } + return Utils.copyJSON({ type: 'Feature', geometry: this.geometry, properties: this.cloneProperties(), + metadata: metadata, id: this.id, }) } @@ -615,15 +621,15 @@ export class Point extends Feature { getShapeOptions() { return [ - 'properties._umap_options.color', - 'properties._umap_options.iconClass', - 'properties._umap_options.iconUrl', - 'properties._umap_options.iconOpacity', + 'metadata.color', + 'metadata.iconClass', + 'metadata.iconUrl', + 'metadata.iconOpacity', ] } getAdvancedOptions() { - return ['properties._umap_options.zoomTo'] + return ['metadata.zoomTo'] } appendEditFieldsets(container) { @@ -695,19 +701,11 @@ class Path extends Feature { } getShapeOptions() { - return [ - 'properties._umap_options.color', - 'properties._umap_options.opacity', - 'properties._umap_options.weight', - ] + return ['metadata.color', 'metadata.opacity', 'metadata.weight'] } getAdvancedOptions() { - return [ - 'properties._umap_options.smoothFactor', - 'properties._umap_options.dashArray', - 'properties._umap_options.zoomTo', - ] + return ['metadata.smoothFactor', 'metadata.dashArray', 'metadata.zoomTo'] } getBestZoom() { @@ -732,9 +730,10 @@ class Path extends Feature { isolateShape(latlngs) { const properties = this.cloneProperties() + const metadata = this.cloneMetadata() const type = this instanceof LineString ? 'LineString' : 'Polygon' const geometry = this.convertLatLngs(latlngs) - const other = this.datalayer.makeFeature({ type, geometry, properties }) + const other = this.datalayer.makeFeature({ type, geometry, properties, metadata }) other.edit() return other } @@ -919,10 +918,10 @@ export class Polygon extends Path { getShapeOptions() { const options = super.getShapeOptions() options.push( - 'properties._umap_options.stroke', - 'properties._umap_options.fill', - 'properties._umap_options.fillColor', - 'properties._umap_options.fillOpacity' + 'metadata.stroke', + 'metadata.fill', + 'metadata.fillColor', + 'metadata.fillOpacity' ) return options } @@ -937,7 +936,7 @@ export class Polygon extends Path { getInteractionOptions() { const options = super.getInteractionOptions() - options.push('properties._umap_options.interactive') + options.push('metadata.interactive') return options } @@ -956,7 +955,7 @@ export class Polygon extends Path { getAdvancedOptions() { const actions = super.getAdvancedOptions() - actions.push('properties._umap_options.mask') + actions.push('metadata.mask') return actions } diff --git a/umap/static/umap/js/modules/data/layer.js b/umap/static/umap/js/modules/data/layer.js index 151eee46..d32fe101 100644 --- a/umap/static/umap/js/modules/data/layer.js +++ b/umap/static/umap/js/modules/data/layer.js @@ -51,7 +51,7 @@ export class DataLayer { this.pane.dataset.id = stamp(this) // FIXME: should be on layer this.renderer = L.svg({ pane: this.pane }) - this.defaultOptions = { + this.defaultMetadata = { displayOnLoad: true, inCaption: true, browsable: true, @@ -61,21 +61,21 @@ export class DataLayer { this._isDirty = false this._isDeleted = false this.setUmapId(data.id) - this.setOptions(data) + this.setMetadata(data) - if (!Utils.isObject(this.options.remoteData)) { - this.options.remoteData = {} + if (!Utils.isObject(this.metadata.remoteData)) { + this.metadata.remoteData = {} } // Retrocompat - if (this.options.remoteData?.from) { - this.options.fromZoom = this.options.remoteData.from - delete this.options.remoteData.from + if (this.metadata.remoteData?.from) { + this.metadata.fromZoom = this.metadata.remoteData.from + delete this.metadata.remoteData.from } - if (this.options.remoteData?.to) { - this.options.toZoom = this.options.remoteData.to - delete this.options.remoteData.to + if (this.metadata.remoteData?.to) { + this.metadata.toZoom = this.metadata.remoteData.to + delete this.metadata.remoteData.to } - this.backupOptions() + this.backupMetadata() this.connectToMap() this.permissions = new DataLayerPermissions(this) if (!this.umap_id) { @@ -133,7 +133,7 @@ export class DataLayer { this.map.onDataLayersChanged() break case 'data': - if (fields.includes('options.type')) { + if (fields.includes('metadata.type')) { this.resetLayer() } this.hide() @@ -155,11 +155,11 @@ export class DataLayer { } autoLoaded() { - if (!this.map.datalayersFromQueryString) return this.options.displayOnLoad + if (!this.map.datalayersFromQueryString) return this.metadata.displayOnLoad const datalayerIds = this.map.datalayersFromQueryString let loadMe = datalayerIds.includes(this.umap_id.toString()) - if (this.options.old_id) { - loadMe = loadMe || datalayerIds.includes(this.options.old_id.toString()) + if (this.metadata.old_id) { + loadMe = loadMe || datalayerIds.includes(this.metadata.old_id.toString()) } return loadMe } @@ -186,7 +186,7 @@ export class DataLayer { // Only reset if type is defined (undefined is the default) and different from current type if ( this.layer && - (!this.options.type || this.options.type === this.layer.getType()) && + (!this.metadata.type || this.metadata.type === this.layer.getType()) && !force ) { return @@ -195,7 +195,7 @@ export class DataLayer { if (this.layer) this.layer.clearLayers() // delete this.layer? if (visible) this.map.removeLayer(this.layer) - const Class = LAYER_MAP[this.options.type] || DefaultLayer + const Class = LAYER_MAP[this.metadata.type] || DefaultLayer this.layer = new Class(this) // Rendering layer changed, so let's force reset the feature rendering too. this.eachFeature((feature) => feature.makeUI()) @@ -218,18 +218,8 @@ export class DataLayer { const [geojson, response, error] = await this.map.server.get(this._dataUrl()) if (!error) { this._reference_version = response.headers.get('X-Datalayer-Version') - // FIXME: for now this property is set dynamically from backend - // And thus it's not in the geojson file in the server - // So do not let all options to be reset - // Fix is a proper migration so all datalayers settings are - // in DB, and we remove it from geojson flat files. - if (geojson._umap_options) { - geojson._umap_options.editMode = this.options.editMode - } - // In case of maps pre 1.0 still around - if (geojson._storage) geojson._storage.editMode = this.options.editMode await this.fromUmapGeoJSON(geojson) - this.backupOptions() + this.backupMetadata() this._loading = false } } @@ -247,8 +237,11 @@ export class DataLayer { } async fromUmapGeoJSON(geojson) { - if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat - if (geojson._umap_options) this.setOptions(geojson._umap_options) + if (!geojson.metadata) { + // Retrocompat + geojson.metadata = geojson._umap_options || geojson._storage + } + if (geojson.metadata) this.setMetadata(geojson.metadata) if (this.isRemoteLayer()) await this.fetchRemoteData() else this.fromGeoJSON(geojson, false) this._loaded = true @@ -266,7 +259,7 @@ export class DataLayer { } backupData() { - this._geojson_bk = Utils.CopyJSON(this._geojson) + this._geojson_bk = Utils.copyJSON(this._geojson) } reindex() { @@ -276,29 +269,29 @@ export class DataLayer { } showAtZoom() { - const from = Number.parseInt(this.options.fromZoom, 10) - const to = Number.parseInt(this.options.toZoom, 10) + const from = Number.parseInt(this.metadata.fromZoom, 10) + const to = Number.parseInt(this.metadata.toZoom, 10) const zoom = this.map.getZoom() return !((!Number.isNaN(from) && zoom < from) || (!Number.isNaN(to) && zoom > to)) } hasDynamicData() { - return !!this.options.remoteData?.dynamic + return !!this.metadata.remoteData?.dynamic } async fetchRemoteData(force) { if (!this.isRemoteLayer()) return if (!this.hasDynamicData() && this.hasDataLoaded() && !force) return if (!this.isVisible()) return - let url = this.map.localizeUrl(this.options.remoteData.url) - if (this.options.remoteData.proxy) { - url = this.map.proxyUrl(url, this.options.remoteData.ttl) + let url = this.map.localizeUrl(this.metadata.remoteData.url) + if (this.metadata.remoteData.proxy) { + url = this.map.proxyUrl(url, this.metadata.remoteData.ttl) } const response = await this.map.request.get(url) if (response?.ok) { this.clear() this.map.formatter - .parse(await response.text(), this.options.remoteData.format) + .parse(await response.text(), this.metadata.remoteData.format) .then((geojson) => this.fromGeoJSON(geojson)) } } @@ -316,22 +309,22 @@ export class DataLayer { if (!this.umap_id && id) this.umap_id = id } - backupOptions() { - this._backupOptions = Utils.CopyJSON(this.options) + backupMetadata() { + this._backupMetadata = Utils.copyJSON(this.metadata) } - resetOptions() { - this.options = Utils.CopyJSON(this._backupOptions) + resetMetadata() { + this.metadata = Utils.copyJSON(this._backupMetadata) } - setOptions(options) { - delete options.geojson - this.options = Utils.CopyJSON(this.defaultOptions) // Start from fresh. - this.updateOptions(options) + setMetadata(metadata) { + delete metadata.geojson + this.metadata = Utils.copyJSON(this.defaultMetadata) // Start from fresh. + this.updateMetadata(metadata) } - updateOptions(options) { - this.options = Object.assign(this.options, options) + updateMetadata(metadata) { + this.metadata = Object.assign(this.metadata, metadata) this.resetLayer() } @@ -360,11 +353,11 @@ export class DataLayer { } isRemoteLayer() { - return Boolean(this.options.remoteData?.url && this.options.remoteData.format) + return Boolean(this.metadata.remoteData?.url && this.metadata.remoteData.format) } isClustered() { - return this.options.type === 'Cluster' + return this.metadata.type === 'Cluster' } showFeature(feature) { @@ -511,7 +504,7 @@ export class DataLayer { } getColor() { - return this.options.color || this.map.getOption('color') + return this.metadata.color || this.map.getOption('color') } getDeleteUrl() { @@ -548,11 +541,11 @@ export class DataLayer { } clone() { - const options = Utils.CopyJSON(this.options) - options.name = translate('Clone of {name}', { name: this.options.name }) - delete options.id - const geojson = Utils.CopyJSON(this._geojson) - const datalayer = this.map.createDataLayer(options) + const metadata = Utils.copyJSON(this.metadata) + metadata.name = translate('Clone of {name}', { name: this.metadata.name }) + delete metadata.id + const geojson = Utils.copyJSON(this._geojson) + const datalayer = this.map.createDataLayer(metadata) datalayer.fromGeoJSON(geojson) return datalayer } @@ -574,7 +567,7 @@ export class DataLayer { reset() { if (!this.umap_id) this.erase() - this.resetOptions() + this.resetMetadata() this.parentPane.appendChild(this.pane) if (this._leaflet_events_bk && !this._leaflet_events) { this._leaflet_events = this._leaflet_events_bk @@ -600,18 +593,18 @@ export class DataLayer { } const container = DomUtil.create('div', 'umap-layer-properties-container') const metadataFields = [ - 'options.name', - 'options.description', + 'metadata.name', + 'metadata.description', [ - 'options.type', + 'metadata.type', { handler: 'LayerTypeChooser', label: translate('Type of layer') }, ], [ - 'options.displayOnLoad', + 'metadata.displayOnLoad', { label: translate('Display on load'), handler: 'Switch' }, ], [ - 'options.browsable', + 'metadata.browsable', { label: translate('Data is browsable'), handler: 'Switch', @@ -619,7 +612,7 @@ export class DataLayer { }, ], [ - 'options.inCaption', + 'metadata.inCaption', { label: translate('Show this layer in the caption'), handler: 'Switch', @@ -630,7 +623,7 @@ export class DataLayer { let builder = new U.FormBuilder(this, metadataFields, { callback(e) { this.map.onDataLayersChanged() - if (e.helper.field === 'options.type') { + if (e.helper.field === 'metadata.type') { this.edit() } }, @@ -651,16 +644,16 @@ export class DataLayer { } const shapeOptions = [ - 'options.color', - 'options.iconClass', - 'options.iconUrl', - 'options.iconOpacity', - 'options.opacity', - 'options.stroke', - 'options.weight', - 'options.fill', - 'options.fillColor', - 'options.fillOpacity', + 'metadata.color', + 'metadata.iconClass', + 'metadata.iconUrl', + 'metadata.iconOpacity', + 'metadata.opacity', + 'metadata.stroke', + 'metadata.weight', + 'metadata.fill', + 'metadata.fillColor', + 'metadata.fillOpacity', ] builder = new U.FormBuilder(this, shapeOptions, { @@ -673,12 +666,12 @@ export class DataLayer { shapeProperties.appendChild(builder.build()) const optionsFields = [ - 'options.smoothFactor', - 'options.dashArray', - 'options.zoomTo', - 'options.fromZoom', - 'options.toZoom', - 'options.labelKey', + 'metadata.smoothFactor', + 'metadata.dashArray', + 'metadata.zoomTo', + 'metadata.fromZoom', + 'metadata.toZoom', + 'metadata.labelKey', ] builder = new U.FormBuilder(this, optionsFields, { @@ -691,14 +684,14 @@ export class DataLayer { advancedProperties.appendChild(builder.build()) const popupFields = [ - 'options.popupShape', - 'options.popupTemplate', - 'options.popupContentTemplate', - 'options.showLabel', - 'options.labelDirection', - 'options.labelInteractive', - 'options.outlinkTarget', - 'options.interactive', + 'metadata.popupShape', + 'metadata.popupTemplate', + 'metadata.popupContentTemplate', + 'metadata.showLabel', + 'metadata.labelDirection', + 'metadata.labelInteractive', + 'metadata.outlinkTarget', + 'metadata.interactive', ] builder = new U.FormBuilder(this, popupFields) const popupFieldset = DomUtil.createFieldset( @@ -709,23 +702,23 @@ export class DataLayer { // XXX I'm not sure **why** this is needed (as it's set during `this.initialize`) // but apparently it's needed. - if (!Utils.isObject(this.options.remoteData)) { - this.options.remoteData = {} + if (!Utils.isObject(this.metadata.remoteData)) { + this.metadata.remoteData = {} } const remoteDataFields = [ [ - 'options.remoteData.url', + 'metadata.remoteData.url', { handler: 'Url', label: translate('Url'), helpEntries: 'formatURL' }, ], [ - 'options.remoteData.format', + 'metadata.remoteData.format', { handler: 'DataFormat', label: translate('Format') }, ], - 'options.fromZoom', - 'options.toZoom', + 'metadata.fromZoom', + 'metadata.toZoom', [ - 'options.remoteData.dynamic', + 'metadata.remoteData.dynamic', { handler: 'Switch', label: translate('Dynamic'), @@ -733,7 +726,7 @@ export class DataLayer { }, ], [ - 'options.remoteData.licence', + 'metadata.remoteData.licence', { label: translate('Licence'), helpText: translate('Please be sure the licence is compliant with your use.'), @@ -742,14 +735,14 @@ export class DataLayer { ] if (this.map.options.urls.ajax_proxy) { remoteDataFields.push([ - 'options.remoteData.proxy', + 'metadata.remoteData.proxy', { handler: 'Switch', label: translate('Proxy request'), helpEntries: 'proxyRemoteData', }, ]) - remoteDataFields.push('options.remoteData.ttl') + remoteDataFields.push('metadata.remoteData.ttl') } const remoteDataContainer = DomUtil.createFieldset( @@ -828,7 +821,7 @@ export class DataLayer { } getOwnOption(option) { - if (Utils.usableOption(this.options, option)) return this.options[option] + if (Utils.usableOption(this.metadata, option)) return this.metadata[option] } getOption(option, feature) { @@ -879,8 +872,11 @@ export class DataLayer { this.getVersionUrl(version) ) if (!error) { - if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat. - if (geojson._umap_options) this.setOptions(geojson._umap_options) + if (!geojson.metadata) { + // Retrocompat. + geojson.metadata = geojson._umap_options || geojson._storage + } + if (geojson.metadata) this.setMetadata(geojson.metadata) this.empty() if (this.isRemoteLayer()) this.fetchRemoteData() else this.addData(geojson) @@ -930,7 +926,7 @@ export class DataLayer { // Is this layer browsable in theorie // AND the user allows it allowBrowse() { - return !!this.options.browsable && this.isBrowsable() + return !!this.metadata.browsable && this.isBrowsable() } // Is this layer browsable in theorie @@ -1003,21 +999,13 @@ export class DataLayer { return prev } - umapGeoJSON() { - return { - type: 'FeatureCollection', - features: this.isRemoteLayer() ? [] : this.featuresToGeoJSON(), - _umap_options: this.options, - } - } - getRank() { return this.map.datalayers_index.indexOf(this) } isReadOnly() { // isReadOnly must return true if unset - return this.options.editMode === 'disabled' + return this.metadata.editMode === 'disabled' } isDataReadOnly() { @@ -1025,20 +1013,32 @@ export class DataLayer { return this.isReadOnly() || this.isRemoteLayer() } + cleanedMetadata() { + const metadata = Utils.copyJSON(this.metadata) + delete metadata.permissions + delete metadata.editMode + delete metadata.id + return metadata + } + async save() { if (this.isDeleted) return this.saveDelete() if (!this.isLoaded()) { return } - const geojson = this.umapGeoJSON() + const geojson = { + type: 'FeatureCollection', + features: this.isRemoteLayer() ? [] : this.featuresToGeoJSON(), + } + const formData = new FormData() - formData.append('name', this.options.name) - formData.append('display_on_load', !!this.options.displayOnLoad) + formData.append('name', this.metadata.name) + formData.append('display_on_load', Boolean(this.metadata.displayOnLoad)) formData.append('rank', this.getRank()) - formData.append('settings', JSON.stringify(this.options)) + formData.append('metadata', JSON.stringify(this.cleanedMetadata())) // Filename support is shaky, don't do it for now. const blob = new Blob([JSON.stringify(geojson)], { type: 'application/json' }) - formData.append('geojson', blob) + formData.append('data', blob) const saveUrl = this.map.urls.get('datalayer_save', { map_id: this.map.options.umap_id, pk: this.umap_id, @@ -1067,17 +1067,17 @@ export class DataLayer { } else { // Response contains geojson only if save has conflicted and conflicts have // been resolved. So we need to reload to get extra data (added by someone else) - if (data.geojson) { + if (data.data) { this.clear() - this.fromGeoJSON(data.geojson) - delete data.geojson + this.fromGeoJSON(data.data) + delete data.data } this._reference_version = response.headers.get('X-Datalayer-Version') this.sync.update('_reference_version', this._reference_version) this.setUmapId(data.id) - this.updateOptions(data) - this.backupOptions() + this.updateMetadata(data) + this.backupMetadata() this.connectToMap() this._loaded = true this.redraw() // Needed for reordering features @@ -1099,7 +1099,7 @@ export class DataLayer { } getName() { - return this.options.name || translate('Untitled layer') + return this.metadata.name || translate('Untitled layer') } tableEdit() { diff --git a/umap/static/umap/js/modules/formatter.js b/umap/static/umap/js/modules/formatter.js index 99677e0f..8841c68a 100644 --- a/umap/static/umap/js/modules/formatter.js +++ b/umap/static/umap/js/modules/formatter.js @@ -23,7 +23,6 @@ export const EXPORT_FORMATS = { map.eachFeature((feature) => { const row = feature.toGeoJSON().properties const center = feature.center - delete row._umap_options row.Latitude = center.lat row.Longitude = center.lng table.push(row) diff --git a/umap/static/umap/js/modules/importer.js b/umap/static/umap/js/modules/importer.js index 65069c09..c69f3d3d 100644 --- a/umap/static/umap/js/modules/importer.js +++ b/umap/static/umap/js/modules/importer.js @@ -208,7 +208,7 @@ export default class Importer { DomUtil.element({ tagName: 'option', parent: layerSelect, - textContent: datalayer.options.name, + textContent: datalayer.metadata.name, value: L.stamp(datalayer), }) } @@ -275,13 +275,13 @@ export default class Importer { return false } const layer = this.layer - layer.options.remoteData = { + layer.metadata.remoteData = { url: this.url, format: this.format, } if (this.map.options.urls.ajax_proxy) { - layer.options.remoteData.proxy = true - layer.options.remoteData.ttl = SCHEMA.ttl.default + layer.metadata.remoteData.proxy = true + layer.metadata.remoteData.ttl = SCHEMA.ttl.default } layer.fetchRemoteData(true) } diff --git a/umap/static/umap/js/modules/permissions.js b/umap/static/umap/js/modules/permissions.js index 52ae216c..981f98f9 100644 --- a/umap/static/umap/js/modules/permissions.js +++ b/umap/static/umap/js/modules/permissions.js @@ -214,7 +214,7 @@ export class DataLayerPermissions { { edit_status: null, }, - datalayer.options.permissions + datalayer.metadata.permissions ) this.datalayer = datalayer @@ -277,8 +277,8 @@ export class DataLayerPermissions { } commit() { - this.datalayer.options.permissions = Object.assign( - this.datalayer.options.permissions, + this.datalayer.metadata.permissions = Object.assign( + this.datalayer.metadata.permissions, this.options ) } diff --git a/umap/static/umap/js/modules/rendering/icon.js b/umap/static/umap/js/modules/rendering/icon.js index 6a90d480..faa19dcd 100644 --- a/umap/static/umap/js/modules/rendering/icon.js +++ b/umap/static/umap/js/modules/rendering/icon.js @@ -217,8 +217,8 @@ export const Cluster = DivIcon.extend({ computeTextColor: function (el) { let color const backgroundColor = this.datalayer.getColor() - if (this.datalayer.options.cluster?.textColor) { - color = this.datalayer.options.cluster.textColor + if (this.datalayer.metadata.cluster?.textColor) { + color = this.datalayer.metadata.cluster.textColor } return color || DomUtil.TextColorFromBackgroundColor(el, backgroundColor) }, diff --git a/umap/static/umap/js/modules/rendering/layers/classified.js b/umap/static/umap/js/modules/rendering/layers/classified.js index 42c3067a..b2ac3b0f 100644 --- a/umap/static/umap/js/modules/rendering/layers/classified.js +++ b/umap/static/umap/js/modules/rendering/layers/classified.js @@ -14,11 +14,11 @@ const ClassifiedMixin = { .filter((k) => k !== 'schemeGroups') .sort() const key = this.getType().toLowerCase() - if (!Utils.isObject(this.datalayer.options[key])) { - this.datalayer.options[key] = {} + if (!Utils.isObject(this.datalayer.metadata[key])) { + this.datalayer.metadata[key] = {} } - this.ensureOptions(this.datalayer.options[key]) - FeatureGroup.prototype.initialize.call(this, [], this.datalayer.options[key]) + this.ensureOptions(this.datalayer.metadata[key]) + FeatureGroup.prototype.initialize.call(this, [], this.datalayer.metadata[key]) LayerMixin.onInit.call(this, this.datalayer.map) }, @@ -111,7 +111,7 @@ export const Choropleth = FeatureGroup.extend({ }, _getValue: function (feature) { - const key = this.datalayer.options.choropleth.property || 'value' + const key = this.datalayer.metadata.choropleth.property || 'value' const value = +feature.properties[key] if (!Number.isNaN(value)) return value }, @@ -124,12 +124,12 @@ export const Choropleth = FeatureGroup.extend({ this.options.colors = [] return } - const mode = this.datalayer.options.choropleth.mode - let classes = +this.datalayer.options.choropleth.classes || 5 + const mode = this.datalayer.metadata.choropleth.mode + let classes = +this.datalayer.metadata.choropleth.classes || 5 let breaks classes = Math.min(classes, values.length) if (mode === 'manual') { - const manualBreaks = this.datalayer.options.choropleth.breaks + const manualBreaks = this.datalayer.metadata.choropleth.breaks if (manualBreaks) { breaks = manualBreaks .split(',') @@ -148,10 +148,10 @@ export const Choropleth = FeatureGroup.extend({ breaks.push(ss.max(values)) // Needed for computing the legend } this.options.breaks = breaks || [] - this.datalayer.options.choropleth.breaks = this.options.breaks + this.datalayer.metadata.choropleth.breaks = this.options.breaks .map((b) => +b.toFixed(2)) .join(',') - let colorScheme = this.datalayer.options.choropleth.brewer + let colorScheme = this.datalayer.metadata.choropleth.brewer if (!colorbrewer[colorScheme]) colorScheme = 'Blues' this.options.colors = colorbrewer[colorScheme][this.options.breaks.length - 1] || [] }, @@ -169,24 +169,24 @@ export const Choropleth = FeatureGroup.extend({ onEdit: function (field, builder) { // Only compute the breaks if we're dealing with choropleth - if (!field.startsWith('options.choropleth')) return + if (!field.startsWith('metadata.choropleth')) return // If user touches the breaks, then force manual mode - if (field === 'options.choropleth.breaks') { - this.datalayer.options.choropleth.mode = 'manual' - if (builder) builder.helpers['options.choropleth.mode'].fetch() + if (field === 'metadata.choropleth.breaks') { + this.datalayer.metadata.choropleth.mode = 'manual' + if (builder) builder.helpers['metadata.choropleth.mode'].fetch() } this.compute() // If user changes the mode or the number of classes, // then update the breaks input value - if (field === 'options.choropleth.mode' || field === 'options.choropleth.classes') { - if (builder) builder.helpers['options.choropleth.breaks'].fetch() + if (field === 'metadata.choropleth.mode' || field === 'metadata.choropleth.classes') { + if (builder) builder.helpers['metadata.choropleth.breaks'].fetch() } }, getEditableOptions: function () { return [ [ - 'options.choropleth.property', + 'metadata.choropleth.property', { handler: 'Select', selectOptions: this.datalayer._propertiesIndex, @@ -194,7 +194,7 @@ export const Choropleth = FeatureGroup.extend({ }, ], [ - 'options.choropleth.brewer', + 'metadata.choropleth.brewer', { handler: 'Select', label: translate('Choropleth color palette'), @@ -202,7 +202,7 @@ export const Choropleth = FeatureGroup.extend({ }, ], [ - 'options.choropleth.classes', + 'metadata.choropleth.classes', { handler: 'Range', min: 3, @@ -213,7 +213,7 @@ export const Choropleth = FeatureGroup.extend({ }, ], [ - 'options.choropleth.breaks', + 'metadata.choropleth.breaks', { handler: 'BlurInput', label: translate('Choropleth breakpoints'), @@ -223,7 +223,7 @@ export const Choropleth = FeatureGroup.extend({ }, ], [ - 'options.choropleth.mode', + 'metadata.choropleth.mode', { handler: 'MultiChoice', default: 'kmeans', @@ -255,13 +255,13 @@ export const Circles = FeatureGroup.extend({ }, ensureOptions: function (options) { - if (!Utils.isObject(this.datalayer.options.circles.radius)) { - this.datalayer.options.circles.radius = {} + if (!Utils.isObject(this.datalayer.metadata.circles.radius)) { + this.datalayer.metadata.circles.radius = {} } }, _getValue: function (feature) { - const key = this.datalayer.options.circles.property || 'value' + const key = this.datalayer.metadata.circles.property || 'value' const value = +feature.properties[key] if (!Number.isNaN(value)) return value }, @@ -270,8 +270,8 @@ export const Circles = FeatureGroup.extend({ const values = this.getValues() this.options.minValue = Math.sqrt(Math.min(...values)) this.options.maxValue = Math.sqrt(Math.max(...values)) - this.options.minPX = this.datalayer.options.circles.radius?.min || 2 - this.options.maxPX = this.datalayer.options.circles.radius?.max || 50 + this.options.minPX = this.datalayer.metadata.circles.radius?.min || 2 + this.options.maxPX = this.datalayer.metadata.circles.radius?.max || 50 }, onEdit: function (field, builder) { @@ -375,7 +375,7 @@ export const Categorized = FeatureGroup.extend({ _getValue: function (feature) { const key = - this.datalayer.options.categorized.property || this.datalayer._propertiesIndex[0] + this.datalayer.metadata.categorized.property || this.datalayer._propertiesIndex[0] return feature.properties[key] }, @@ -397,10 +397,10 @@ export const Categorized = FeatureGroup.extend({ this.options.colors = [] return } - const mode = this.datalayer.options.categorized.mode + const mode = this.datalayer.metadata.categorized.mode let categories = [] if (mode === 'manual') { - const manualCategories = this.datalayer.options.categorized.categories + const manualCategories = this.datalayer.metadata.categorized.categories if (manualCategories) { categories = manualCategories.split(',') } @@ -410,8 +410,8 @@ export const Categorized = FeatureGroup.extend({ .sort(Utils.naturalSort) } this.options.categories = categories - this.datalayer.options.categorized.categories = this.options.categories.join(',') - const colorScheme = this.datalayer.options.categorized.brewer + this.datalayer.metadata.categorized.categories = this.options.categories.join(',') + const colorScheme = this.datalayer.metadata.categorized.brewer this._classes = this.options.categories.length if (colorbrewer[colorScheme]?.[this._classes]) { this.options.colors = colorbrewer[colorScheme][this._classes] @@ -425,7 +425,7 @@ export const Categorized = FeatureGroup.extend({ getEditableOptions: function () { return [ [ - 'options.categorized.property', + 'metadata.categorized.property', { handler: 'Select', selectOptions: this.datalayer._propertiesIndex, @@ -433,7 +433,7 @@ export const Categorized = FeatureGroup.extend({ }, ], [ - 'options.categorized.brewer', + 'metadata.categorized.brewer', { handler: 'Select', label: translate('Color palette'), @@ -441,7 +441,7 @@ export const Categorized = FeatureGroup.extend({ }, ], [ - 'options.categorized.categories', + 'metadata.categorized.categories', { handler: 'BlurInput', label: translate('Categories'), @@ -449,7 +449,7 @@ export const Categorized = FeatureGroup.extend({ }, ], [ - 'options.categorized.mode', + 'metadata.categorized.mode', { handler: 'MultiChoice', default: 'alpha', @@ -462,17 +462,17 @@ export const Categorized = FeatureGroup.extend({ onEdit: function (field, builder) { // Only compute the categories if we're dealing with categorized - if (!field.startsWith('options.categorized')) return + if (!field.startsWith('metadata.categorized')) return // If user touches the categories, then force manual mode - if (field === 'options.categorized.categories') { - this.datalayer.options.categorized.mode = 'manual' - if (builder) builder.helpers['options.categorized.mode'].fetch() + if (field === 'metadata.categorized.categories') { + this.datalayer.metadata.categorized.mode = 'manual' + if (builder) builder.helpers['metadata.categorized.mode'].fetch() } this.compute() // If user changes the mode // then update the categories input value - if (field === 'options.categorized.mode') { - if (builder) builder.helpers['options.categorized.categories'].fetch() + if (field === 'metadata.categorized.mode') { + if (builder) builder.helpers['metadata.categorized.categories'].fetch() } }, diff --git a/umap/static/umap/js/modules/rendering/layers/cluster.js b/umap/static/umap/js/modules/rendering/layers/cluster.js index af206795..f132d502 100644 --- a/umap/static/umap/js/modules/rendering/layers/cluster.js +++ b/umap/static/umap/js/modules/rendering/layers/cluster.js @@ -27,8 +27,8 @@ export const Cluster = L.MarkerClusterGroup.extend({ initialize: function (datalayer) { this.datalayer = datalayer - if (!Utils.isObject(this.datalayer.options.cluster)) { - this.datalayer.options.cluster = {} + if (!Utils.isObject(this.datalayer.metadata.cluster)) { + this.datalayer.metadata.cluster = {} } const options = { polygonOptions: { @@ -36,8 +36,8 @@ export const Cluster = L.MarkerClusterGroup.extend({ }, iconCreateFunction: (cluster) => new ClusterIcon(datalayer, cluster), } - if (this.datalayer.options.cluster?.radius) { - options.maxClusterRadius = this.datalayer.options.cluster.radius + if (this.datalayer.metadata.cluster?.radius) { + options.maxClusterRadius = this.datalayer.metadata.cluster.radius } L.MarkerClusterGroup.prototype.initialize.call(this, options) LayerMixin.onInit.call(this, this.datalayer.map) @@ -73,7 +73,7 @@ export const Cluster = L.MarkerClusterGroup.extend({ getEditableOptions: () => [ [ - 'options.cluster.radius', + 'metadata.cluster.radius', { handler: 'BlurIntInput', placeholder: translate('Clustering radius'), @@ -81,7 +81,7 @@ export const Cluster = L.MarkerClusterGroup.extend({ }, ], [ - 'options.cluster.textColor', + 'metadata.cluster.textColor', { handler: 'TextColorPicker', placeholder: translate('Auto'), @@ -91,7 +91,7 @@ export const Cluster = L.MarkerClusterGroup.extend({ ], onEdit: function (field, builder) { - if (field === 'options.cluster.radius') { + if (field === 'metadata.cluster.radius') { // No way to reset radius of an already instanciated MarkerClusterGroup... this.datalayer.resetLayer(true) return diff --git a/umap/static/umap/js/modules/rendering/layers/heat.js b/umap/static/umap/js/modules/rendering/layers/heat.js index a8d7dca0..014aec7e 100644 --- a/umap/static/umap/js/modules/rendering/layers/heat.js +++ b/umap/static/umap/js/modules/rendering/layers/heat.js @@ -20,10 +20,10 @@ export const Heat = L.HeatLayer.extend({ initialize: function (datalayer) { this.datalayer = datalayer - L.HeatLayer.prototype.initialize.call(this, [], this.datalayer.options.heat) + L.HeatLayer.prototype.initialize.call(this, [], this.datalayer.metadata.heat) LayerMixin.onInit.call(this, this.datalayer.map) - if (!Utils.isObject(this.datalayer.options.heat)) { - this.datalayer.options.heat = {} + if (!Utils.isObject(this.datalayer.metadata.heat)) { + this.datalayer.metadata.heat = {} } }, @@ -31,9 +31,9 @@ export const Heat = L.HeatLayer.extend({ if (layer instanceof Marker) { let latlng = layer.getLatLng() let alt - if (this.datalayer.options.heat?.intensityProperty) { + if (this.datalayer.metadata.heat?.intensityProperty) { alt = Number.parseFloat( - layer.feature.properties[this.datalayer.options.heat.intensityProperty || 0] + layer.feature.properties[this.datalayer.metadata.heat.intensityProperty || 0] ) latlng = new LatLng(latlng.lat, latlng.lng, alt) } @@ -63,7 +63,7 @@ export const Heat = L.HeatLayer.extend({ getEditableOptions: () => [ [ - 'options.heat.radius', + 'metadata.heat.radius', { handler: 'Range', min: 10, @@ -74,7 +74,7 @@ export const Heat = L.HeatLayer.extend({ }, ], [ - 'options.heat.intensityProperty', + 'metadata.heat.intensityProperty', { handler: 'BlurInput', placeholder: translate('Heatmap intensity property'), @@ -84,12 +84,12 @@ export const Heat = L.HeatLayer.extend({ ], onEdit: function (field, builder) { - if (field === 'options.heat.intensityProperty') { + if (field === 'metadata.heat.intensityProperty') { this.datalayer.resetLayer(true) // We need to repopulate the latlngs return } - if (field === 'options.heat.radius') { - this.options.radius = this.datalayer.options.heat.radius + if (field === 'metadata.heat.radius') { + this.options.radius = this.datalayer.metadata.heat.radius } this._updateOptions() }, diff --git a/umap/static/umap/js/modules/utils.js b/umap/static/umap/js/modules/utils.js index 1f4640a0..47bb7655 100644 --- a/umap/static/umap/js/modules/utils.js +++ b/umap/static/umap/js/modules/utils.js @@ -41,7 +41,7 @@ export function getImpactsFromSchema(fields, schema) { // remove the option prefix for fields // And only keep the first part in case of a subfield // (e.g "options.limitBounds.foobar" will just return "limitBounds") - return field.replace('options.', '').split('.')[0] + return field.replace('options.', '').replace('metadata.', '').split('.')[0] }) .reduce((acc, field) => { // retrieve the "impacts" field from the schema @@ -181,7 +181,7 @@ export function isObject(what) { return typeof what === 'object' && what !== null } -export function CopyJSON(geojson) { +export function copyJSON(geojson) { return JSON.parse(JSON.stringify(geojson)) } diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 7ec574b3..c849824d 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -725,9 +725,9 @@ const ControlsMixin = { const row = L.DomUtil.create('li', 'orderable', ul) L.DomUtil.createIcon(row, 'icon-drag', L._('Drag to reorder')) datalayer.renderToolbox(row) - const title = L.DomUtil.add('span', '', row, datalayer.options.name) + const title = L.DomUtil.add('span', '', row, datalayer.metadata.name) row.classList.toggle('off', !datalayer.isVisible()) - title.textContent = datalayer.options.name + title.textContent = datalayer.metadata.name row.dataset.id = L.stamp(datalayer) }) const onReorder = (src, dst, initialIndex, finalIndex) => { diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 2e8944dd..71de7910 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -29,30 +29,30 @@ L.Map.mergeOptions({ U.Map = L.Map.extend({ includes: [ControlsMixin], - initialize: async function (el, geojson) { + initialize: async function (el, metadata) { this.sync_engine = new U.SyncEngine(this) this.sync = this.sync_engine.proxy(this) // Locale name (pt_PT, en_US…) // To be used for Django localization - if (geojson.properties.locale) L.setLocale(geojson.properties.locale) + if (metadata.locale) L.setLocale(metadata.locale) // Language code (pt-pt, en-us…) // To be used in javascript APIs - if (geojson.properties.lang) L.lang = geojson.properties.lang + if (metadata.lang) L.lang = metadata.lang - this.setOptionsFromQueryString(geojson.properties) + this.setOptionsFromQueryString(metadata) // Prevent default creation of controls - const zoomControl = geojson.properties.zoomControl - const fullscreenControl = geojson.properties.fullscreenControl - geojson.properties.zoomControl = false - geojson.properties.fullscreenControl = false + const zoomControl = metadata.zoomControl + const fullscreenControl = metadata.fullscreenControl + metadata.zoomControl = false + metadata.fullscreenControl = false - L.Map.prototype.initialize.call(this, el, geojson.properties) + L.Map.prototype.initialize.call(this, el, metadata) - if (geojson.properties.schema) this.overrideSchema(geojson.properties.schema) + if (metadata.schema) this.overrideSchema(metadata.schema) // After calling parent initialize, as we are doing initCenter our-selves - if (geojson.geometry) this.options.center = this.latLng(geojson.geometry) + if (metadata.geometry) this.options.center = this.latLng(metadata.geometry) this.urls = new U.URLs(this.options.urls) this.panel = new U.Panel(this) @@ -805,7 +805,7 @@ U.Map = L.Map.extend({ const datalayer = new U.DataLayer(this, options, sync) if (sync !== false) { - datalayer.sync.upsert(datalayer.options) + datalayer.sync.upsert(datalayer.metadata) } return datalayer }, @@ -906,15 +906,16 @@ U.Map = L.Map.extend({ } if (importedData.geometry) this.options.center = this.latLng(importedData.geometry) - importedData.layers.forEach((geojson) => { - if (!geojson._umap_options && geojson._storage) { - geojson._umap_options = geojson._storage + for (const geojson of importedData.layers) { + if (!geojson.metadata) { + geojson.metadata = geojson._umap_options || geojson._storage + delete geojson._umap_options delete geojson._storage } - delete geojson._umap_options?.id // Never trust an id at this stage - const dataLayer = this.createDataLayer(geojson._umap_options) + delete geojson.metadata?.id // Never trust an id at this stage + const dataLayer = this.createDataLayer(geojson.metadata) dataLayer.fromUmapGeoJSON(geojson) - }) + } this.initTileLayers() this.renderControls() @@ -1032,22 +1033,14 @@ U.Map = L.Map.extend({ saveSelf: async function () { this.rules.commit() - const geojson = { - type: 'Feature', - geometry: this.geometry(), - properties: this.exportOptions(), - } const formData = new FormData() formData.append('name', this.options.name) formData.append('center', JSON.stringify(this.geometry())) - formData.append('settings', JSON.stringify(geojson)) + formData.append('zoom', this.getZoom()) + formData.append('metadata', JSON.stringify(this.exportOptions())) const uri = this.urls.get('map_save', { map_id: this.options.umap_id }) const [data, _, error] = await this.server.post(uri, {}, formData) - // FIXME: login_required response will not be an error, so it will not - // stop code while it should - if (error) { - return - } + if (error) return if (data.login_required) { window.onLogin = () => this.saveSelf() window.open(data.login_required) diff --git a/umap/static/umap/unittests/utils.js b/umap/static/umap/unittests/utils.js index 958ccfa1..b14266ba 100644 --- a/umap/static/umap/unittests/utils.js +++ b/umap/static/umap/unittests/utils.js @@ -682,7 +682,7 @@ describe('Utils', () => { describe('#copyJSON', () => { it('should actually copy the JSON', () => { const originalJSON = { some: 'json' } - const returned = Utils.CopyJSON(originalJSON) + const returned = Utils.copyJSON(originalJSON) // Change the original JSON originalJSON.anotherKey = 'value' diff --git a/umap/templates/umap/map_fragment.html b/umap/templates/umap/map_fragment.html index e758bd45..2b05d1ea 100644 --- a/umap/templates/umap/map_fragment.html +++ b/umap/templates/umap/map_fragment.html @@ -1,6 +1,6 @@ {% load umap_tags %} - +
diff --git a/umap/templates/umap/map_init.html b/umap/templates/umap/map_init.html index e8d4d739..5a43f559 100644 --- a/umap/templates/umap/map_init.html +++ b/umap/templates/umap/map_init.html @@ -6,7 +6,7 @@ diff --git a/umap/templates/umap/map_table.html b/umap/templates/umap/map_table.html index c8bd284c..6d3f9c4f 100644 --- a/umap/templates/umap/map_table.html +++ b/umap/templates/umap/map_table.html @@ -35,7 +35,7 @@ {{ map_inst.name }} - {{ map_inst.preview_settings|json_script:unique_id }} + {{ map_inst.preview_metadata|json_script:unique_id }}