diff --git a/umap/static/umap/js/modules/data/features.js b/umap/static/umap/js/modules/data/features.js index 61abc277..cfecb891 100644 --- a/umap/static/umap/js/modules/data/features.js +++ b/umap/static/umap/js/modules/data/features.js @@ -23,6 +23,9 @@ class Feature { this.properties = { _umap_options: {}, ...(geojson.properties || {}) } this.staticOptions = {} + if (geojson.coordinates) { + geojson = { geometry: geojson } + } if (geojson.geometry) { this.populate(geojson) } @@ -68,6 +71,10 @@ class Feature { return this.ui.getCenter() } + get bounds() { + return this.ui.getBounds() + } + getClassName() { return this.staticOptions.className } @@ -162,7 +169,7 @@ class Feature { this.redraw() } - edit(e) { + edit(event) { if (!this.map.editEnabled || this.isReadOnly()) return const container = DomUtil.create('div', 'umap-feature-container') DomUtil.createTitle( @@ -176,7 +183,7 @@ class Feature { [['datalayer', { handler: 'DataLayerSwitcher' }]], { callback() { - this.edit(e) + this.edit(event) }, // removeLayer step will close the edit panel, let's reopen it } ) @@ -207,11 +214,11 @@ class Feature { builder.helpers['properties.name'].input.focus() }) this.map.editedFeature = this - if (!this.isOnScreen()) this.zoomTo(e) + if (!this.isOnScreen()) this.zoomTo(event) } getAdvancedEditActions(container) { - DomUtil.createButton('button umap-delete', container, translate('Delete'), (e) => { + DomUtil.createButton('button umap-delete', container, translate('Delete'), () => { this.confirmDelete().then(() => this.map.editPanel.close()) }) } @@ -261,7 +268,7 @@ class Feature { endEdit() {} getDisplayName(fallback) { - if (fallback === undefined) fallback = this.datalayer.options.name + if (fallback === undefined) fallback = this.datalayer.getName() const key = this.getOption('labelKey') || 'name' // Variables mode. if (U.Utils.hasVar(key)) @@ -308,7 +315,8 @@ class Feature { connectToDataLayer(datalayer) { this.datalayer = datalayer - // this.options.renderer = this.datalayer.renderer + // FIXME should be in layer/ui + this.ui.options.renderer = this.datalayer.renderer } disconnectFromDataLayer(datalayer) { @@ -422,16 +430,12 @@ class Feature { } toGeoJSON() { - return { + return Utils.CopyJSON({ type: 'Feature', geometry: this.geometry, properties: this.cloneProperties(), id: this.id, - } - } - - getPopupToolbarAnchor() { - return [0, 0] + }) } getInplaceToolbarActions() { @@ -442,73 +446,6 @@ class Feature { return this.map } - getContextMenuItems(e) { - const permalink = this.getPermalink() - let items = [] - if (permalink) - items.push({ - text: translate('Permalink'), - callback: () => { - window.open(permalink) - }, - }) - if (this.map.editEnabled && !this.isReadOnly()) { - items = items.concat(this.getContextMenuEditItems(e)) - } - return items - } - - getContextMenuEditItems() { - let items = ['-'] - if (this.map.editedFeature !== this) { - items.push({ - text: `${translate('Edit this feature')} (⇧+Click)`, - callback: this.edit, - context: this, - iconCls: 'umap-edit', - }) - } - items = items.concat( - { - text: this.map.help.displayLabel('EDIT_FEATURE_LAYER'), - callback: this.datalayer.edit, - context: this.datalayer, - iconCls: 'umap-edit', - }, - { - text: translate('Delete this feature'), - callback: this.confirmDelete, - context: this, - iconCls: 'umap-delete', - }, - { - text: translate('Clone this feature'), - callback: this.clone, - context: this, - } - ) - return items - } - - resetTooltip() { - if (!this.hasGeom()) return - const displayName = this.getDisplayName(null) - let showLabel = this.getOption('showLabel') - const oldLabelHover = this.getOption('labelHover') - - const options = { - direction: this.getOption('labelDirection'), - interactive: this.getOption('labelInteractive'), - } - - if (oldLabelHover && showLabel) showLabel = null // Retrocompat. - options.permanent = showLabel === true - this.unbindTooltip() - if ((showLabel === true || showLabel === null) && displayName) { - this.bindTooltip(Utils.escapeHTML(displayName), options) - } - } - isFiltered() { const filterKeys = this.datalayer.getFilterKeys() const filter = this.map.browser.options.filter @@ -519,7 +456,7 @@ class Feature { matchFilter(filter, keys) { filter = filter.toLowerCase() - if (U.Utils.hasVar(keys)) { + if (Utils.hasVar(keys)) { return this.getDisplayName().toLowerCase().indexOf(filter) !== -1 } keys = keys.split(',') @@ -552,10 +489,6 @@ class Feature { return true } - getVertexActions() { - return [U.DeleteVertexAction] - } - isMulti() { return false } @@ -578,7 +511,7 @@ class Feature { if (L.lang) properties.lang = L.lang properties.rank = this.getRank() + 1 properties.layer = this.datalayer.getName() - if (this.map && this.hasGeom()) { + if (this.ui._map && this.hasGeom()) { const center = this.center properties.lat = center.lat properties.lon = center.lng @@ -600,13 +533,6 @@ class Feature { this.ui._redraw() } } - - _showContextMenu(e) { - L.DomEvent.stop(e) - const pt = this.map.mouseEventToContainerPoint(e.originalEvent) - e.relatedTarget = this - this.map.contextmenu.showAt(pt, e) - } } export class Point extends Feature { @@ -630,14 +556,6 @@ export class Point extends Feature { return new LeafletMarker(this) } - highlight() { - DomUtil.addClass(this.options.icon.elements.main, 'umap-icon-active') - } - - resetHighlight() { - DomUtil.removeClass(this.options.icon.elements.main, 'umap-icon-active') - } - hasGeom() { return Boolean(this.coordinates) } @@ -693,10 +611,6 @@ export class Point extends Feature { bounds = bounds || this.map.getBounds() return bounds.contains(this.coordinates) } - - // getPopupToolbarAnchor() { - // return this.options.icon.options.popupAnchor - // } } class Path extends Feature { @@ -704,10 +618,20 @@ class Path extends Feature { return !this.isEmpty() } + get coordinates() { + return this._toLatlngs(this.geometry) + } + + set coordinates(latlngs) { + const { coordinates, type } = this._toGeometry(latlngs) + this.geometry.coordinates = coordinates + this.geometry.type = type + } + connectToDataLayer(datalayer) { super.connectToDataLayer(datalayer) // We keep markers on their own layer on top of the paths. - // this.options.pane = this.datalayer.pane + this.ui.options.pane = this.datalayer.pane } edit(event) { @@ -717,17 +641,17 @@ class Path extends Feature { } } - _toggleEditing(e) { + _toggleEditing(event) { if (this.map.editEnabled) { if (this.ui.editEnabled()) { this.endEdit() this.map.editPanel.close() } else { - this.edit(e) + this.edit(event) } } // FIXME: disable when disabling global edit - L.DomEvent.stop(e) + L.DomEvent.stop(event) } getStyleOptions() { @@ -772,7 +696,7 @@ class Path extends Feature { } getBestZoom() { - return this.getOption('zoomTo') || this.map.getBoundsZoom(this.getBounds(), true) + return this.getOption('zoomTo') || this.map.getBoundsZoom(this.bounds, true) } endEdit() { @@ -781,100 +705,36 @@ class Path extends Feature { } transferShape(at, to) { - const shape = this.enableEdit().deleteShapeAt(at) - this.disableEdit() + const shape = this.ui.enableEdit().deleteShapeAt(at) + // FIXME: make Leaflet.Editable send an event instead + this.ui.geometryChanged() + this.ui.disableEdit() if (!shape) return - to.enableEdit().appendShape(shape) - if (!this._latlngs.length || !this._latlngs[0].length) this.del() + to.ui.enableEdit().appendShape(shape) + to.ui.geometryChanged() + if (this.isEmpty()) this.del() } isolateShape(at) { if (!this.isMulti()) return - const shape = this.enableEdit().deleteShapeAt(at) - this.disableEdit() + const shape = this.ui.enableEdit().deleteShapeAt(at) + this.ui.disableEdit() if (!shape) return const properties = this.cloneProperties() - const other = new (this instanceof U.Polyline ? U.Polyline : U.Polygon)( - this.map, - shape, + const other = new (this instanceof LineString ? LineString : Polygon)( + this.datalayer, { - geojson: { properties }, + properties, + geometry: this._toGeometry(shape), } ) - this.datalayer.addLayer(other) + this.datalayer.addFeature(other) other.edit() return other } - getContextMenuItems(e) { - let items = super.getContextMenuItems(e) - items.push({ - text: translate('Display measure'), - callback() { - U.Alert.info(this.getMeasure()) - }, - context: this, - }) - if (this.map.editEnabled && !this.isReadOnly() && this.isMulti()) { - items = items.concat(this.getContextMenuMultiItems(e)) - } - return items - } - - getContextMenuMultiItems(e) { - const items = [ - '-', - { - text: translate('Remove shape from the multi'), - callback() { - this.enableEdit().deleteShapeAt(e.latlng) - }, - context: this, - }, - ] - const shape = this.ui.shapeAt(e.latlng) - if (this.ui._latlngs.indexOf(shape) > 0) { - items.push({ - text: translate('Make main shape'), - callback() { - this.enableEdit().deleteShape(shape) - this.editor.prependShape(shape) - }, - context: this, - }) - } - return items - } - - getContextMenuEditItems(e) { - const items = super.getContextMenuEditItems(e) - if ( - this.map.editedFeature && - this.isSameClass(this.map.editedFeature) && - this.map.editedFeature !== this - ) { - items.push({ - text: translate('Transfer shape to edited feature'), - callback() { - this.transferShape(e.latlng, this.map.editedFeature) - }, - context: this, - }) - } - if (this.isMulti()) { - items.push({ - text: translate('Extract shape to separate feature'), - callback() { - this.isolateShape(e.latlng, this.map.editedFeature) - }, - context: this, - }) - } - return items - } - - getInplaceToolbarActions(e) { - const items = super.getInplaceToolbarActions(e) + getInplaceToolbarActions(event) { + const items = super.getInplaceToolbarActions(event) if (this.isMulti()) { items.push(U.DeleteShapeAction) items.push(U.ExtractShapeFromMultiAction) @@ -884,16 +744,16 @@ class Path extends Feature { isOnScreen(bounds) { bounds = bounds || this.map.getBounds() - return bounds.overlaps(this.ui.getBounds()) + return bounds.overlaps(this.bounds) } zoomTo({ easing, callback }) { // Use bounds instead of centroid for paths. easing = easing || this.map.getOption('easing') if (easing) { - this.map.flyToBounds(this.getBounds(), this.getBestZoom()) + this.map.flyToBounds(this.bounds, this.getBestZoom()) } else { - this.map.fitBounds(this.getBounds(), this.getBestZoom() || this.map.getZoom()) + this.map.fitBounds(this.bounds, this.getBestZoom() || this.map.getZoom()) } if (callback) callback.call(this) } @@ -910,17 +770,22 @@ export class LineString extends Path { } } - get coordinates() { + _toLatlngs(geometry) { return GeoJSON.coordsToLatLngs( - this.geometry.coordinates, - this.geometry.type === 'LineString' ? 0 : 1 + geometry.coordinates, + geometry.type === 'LineString' ? 0 : 1 ) } - set coordinates(latlngs) { - const multi = !LineUtil.isFlat(latlngs) - this.geometry.coordinates = GeoJSON.latLngsToCoords(latlngs, multi ? 1 : 0, false) - this.geometry.type = multi ? 'MultiLineString' : 'LineString' + _toGeometry(latlngs) { + let multi = !LineUtil.isFlat(latlngs) + let coordinates = GeoJSON.latLngsToCoords(latlngs, multi ? 1 : 0, false) + if (coordinates.length === 1 && typeof coordinates[0][0] !== 'number') { + coordinates = Utils.flattenCoordinates(coordinates) + multi = false + } + const type = multi ? 'MultiLineString' : 'LineString' + return { coordinates, type } } isEmpty() { @@ -940,51 +805,11 @@ export class LineString extends Path { return L.GeoUtil.readableDistance(length, this.map.measureTools.getMeasureUnit()) } - getContextMenuEditItems(e) { - const items = super.getContextMenuEditItems(e) - const vertexClicked = e.vertex - let index - if (!this.isMulti()) { - items.push({ - text: translate('Transform to polygon'), - callback: this.toPolygon, - context: this, - }) - } - if (vertexClicked) { - index = e.vertex.getIndex() - if (index !== 0 && index !== e.vertex.getLastIndex()) { - items.push({ - text: translate('Split line'), - callback: e.vertex.split, - context: e.vertex, - }) - } else if (index === 0 || index === e.vertex.getLastIndex()) { - items.push({ - text: this.map.help.displayLabel('CONTINUE_LINE'), - callback: e.vertex.continue, - context: e.vertex.continue, - }) - } - } - return items - } - - getContextMenuMultiItems(e) { - const items = super.getContextMenuMultiItems(e) - items.push({ - text: translate('Merge lines'), - callback: this.mergeShapes, - context: this, - }) - return items - } - toPolygon() { const geojson = this.toGeoJSON() geojson.geometry.type = 'Polygon' geojson.geometry.coordinates = [ - U.Utils.flattenCoordinates(geojson.geometry.coordinates), + Utils.flattenCoordinates(geojson.geometry.coordinates), ] delete geojson.id // delete the copied id, a new one will be generated. @@ -1053,15 +878,6 @@ export class LineString extends Path { isMulti() { return !LineUtil.isFlat(this.coordinates) && this.coordinates.length > 1 } - - getVertexActions(e) { - const actions = super.getVertexActions(e) - const index = e.vertex.getIndex() - if (index === 0 || index === e.vertex.getLastIndex()) - actions.push(U.ContinueLineAction) - else actions.push(U.SplitLineAction) - return actions - } } export class Polygon extends Path { @@ -1073,22 +889,22 @@ export class Polygon extends Path { } } - get coordinates() { + _toLatlngs(geometry) { return GeoJSON.coordsToLatLngs( - this.geometry.coordinates, - this.geometry.type === 'Polygon' ? 1 : 2 + geometry.coordinates, + geometry.type === 'Polygon' ? 1 : 2 ) } - set coordinates(latlngs) { + _toGeometry(latlngs) { const holes = !LineUtil.isFlat(latlngs) const multi = holes && !LineUtil.isFlat(latlngs[0]) - let coords = GeoJSON.latLngsToCoords(latlngs, multi ? 2 : holes ? 1 : 0, true) + let coordinates = GeoJSON.latLngsToCoords(latlngs, multi ? 2 : holes ? 1 : 0, true) if (!holes) { - coords = [coords] + coordinates = [coordinates] } - this.geometry.coordinates = coords - this.geometry.type = multi ? 'MultiPolygon' : 'Polygon' + const type = multi ? 'MultiPolygon' : 'Polygon' + return { coordinates, type } } isEmpty() { @@ -1133,35 +949,12 @@ export class Polygon extends Path { return L.GeoUtil.readableArea(area, this.map.measureTools.getMeasureUnit()) } - getContextMenuEditItems(e) { - const items = super.getContextMenuEditItems(e) - const shape = this.ui.shapeAt(e.latlng) - // No multi and no holes. - if (shape && !this.isMulti() && (LineUtil.isFlat(shape) || shape.length === 1)) { - items.push({ - text: translate('Transform to lines'), - callback: this.toPolyline, - context: this, - }) - } - items.push({ - text: translate('Start a hole here'), - callback: this.startHole, - context: this, - }) - return items - } - - startHole(event) { - this.ui.enableEdit().newHole(event.latlng) - } - - toPolyline() { + toLineString() { const geojson = this.toGeoJSON() delete geojson.id delete geojson.properties.id geojson.geometry.type = 'LineString' - geojson.geometry.coordinates = U.Utils.flattenCoordinates( + geojson.geometry.coordinates = Utils.flattenCoordinates( geojson.geometry.coordinates ) const polyline = this.datalayer.geojsonToFeatures(geojson) @@ -1171,17 +964,18 @@ export class Polygon extends Path { getAdvancedEditActions(container) { super.getAdvancedEditActions(container) - const toPolyline = DomUtil.createButton( + const toLineString = DomUtil.createButton( 'button umap-to-polyline', container, translate('Transform to lines'), - this.toPolyline, + this.toLineString, this ) } isMulti() { // Change me when Leaflet#3279 is merged. + // FIXME use TurfJS return ( !LineUtil.isFlat(this.coordinates) && !LineUtil.isFlat(this.coordinates[0]) && @@ -1189,8 +983,8 @@ export class Polygon extends Path { ) } - getInplaceToolbarActions(e) { - const items = super.getInplaceToolbarActions(e) + getInplaceToolbarActions(event) { + const items = super.getInplaceToolbarActions(event) items.push(U.CreateHoleAction) return items } diff --git a/umap/static/umap/js/modules/data/layer.js b/umap/static/umap/js/modules/data/layer.js index b2bd0d85..01e88451 100644 --- a/umap/static/umap/js/modules/data/layer.js +++ b/umap/static/umap/js/modules/data/layer.js @@ -41,6 +41,7 @@ export class DataLayer { this.parentPane = this.map.getPane('overlayPane') this.pane = this.map.createPane(`datalayer${stamp(this)}`, this.parentPane) this.pane.dataset.id = stamp(this) + // FIXME: should be on layer this.renderer = L.svg({ pane: this.pane }) this.defaultOptions = { displayOnLoad: true, @@ -481,11 +482,10 @@ export class DataLayer { break } if (feature) { - feature.setLatLng(latlng) + feature.coordinates = latlng return feature } return new Point(this, geojson) - // return this._pointToLayer(geojson, latlng, id) case 'MultiLineString': case 'LineString': @@ -495,21 +495,19 @@ export class DataLayer { ) if (!latlngs.length) break if (feature) { - feature.setLatLngs(latlngs) + feature.coordinates = latlngs return feature } return new LineString(this, geojson) - // return this._lineToLayer(geojson, latlngs, id) case 'MultiPolygon': case 'Polygon': latlngs = GeoJSON.coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2) if (feature) { - feature.setLatLngs(latlngs) + feature.coordinates = latlngs return feature } return new Polygon(this, geojson) - // return this._polygonToLayer(geojson, latlngs, id) case 'GeometryCollection': return this.geojsonToFeatures(geometry.geometries) @@ -900,9 +898,9 @@ export class DataLayer { getOption(option, feature) { if (this.layer?.getOption) { const value = this.layer.getOption(option, feature) - if (typeof value !== 'undefined') return value + if (value !== undefined) return value } - if (typeof this.getOwnOption(option) !== 'undefined') { + if (this.getOwnOption(option) !== undefined) { return this.getOwnOption(option) } if (this.layer?.defaults?.[option]) { @@ -1179,7 +1177,7 @@ export class DataLayer { // By default, it will we use the "name" property, which is also the one used as label in the features list. // When map owner has configured another label or sort key, we try to be smart and search in the same keys. if (this.map.options.filterKey) return this.map.options.filterKey - if (this.options.labelKey) return this.options.labelKey + if (this.getOption('labelKey')) return this.getOption('labelKey') if (this.map.options.sortKey) return this.map.options.sortKey return 'name' } diff --git a/umap/static/umap/js/modules/formatter.js b/umap/static/umap/js/modules/formatter.js index 64ac5e2b..a4d864f8 100644 --- a/umap/static/umap/js/modules/formatter.js +++ b/umap/static/umap/js/modules/formatter.js @@ -22,7 +22,7 @@ export const EXPORT_FORMATS = { const table = [] map.eachFeature((feature) => { const row = feature.toGeoJSON().properties - const center = feature.getCenter() + const center = feature.center delete row._umap_options row.Latitude = center.lat row.Longitude = center.lng diff --git a/umap/static/umap/js/modules/rendering/layers/cluster.js b/umap/static/umap/js/modules/rendering/layers/cluster.js index 14d6f900..a10fe662 100644 --- a/umap/static/umap/js/modules/rendering/layers/cluster.js +++ b/umap/static/umap/js/modules/rendering/layers/cluster.js @@ -4,6 +4,7 @@ import { translate } from '../../i18n.js' import { LayerMixin } from './base.js' import * as Utils from '../../utils.js' +import { Evented } from '../../../../vendors/leaflet/leaflet-src.esm.js' const MarkerCluster = L.MarkerCluster.extend({ // Custom class so we can call computeTextColor @@ -49,7 +50,6 @@ export const Cluster = L.MarkerClusterGroup.extend({ return L.MarkerClusterGroup.prototype.onAdd.call(this, map) }, - onRemove: function (map) { // In some situation, the onRemove is called before the layer is really // added to the map: basically when combining a defaultView=data + max/minZoom @@ -100,4 +100,9 @@ export const Cluster = L.MarkerClusterGroup.extend({ this.options.polygonOptions.color = this.datalayer.getColor() } }, + + // listens: function (type, propagate) { + // L.MarkerClusterGroup.prototype.listens.call(this, type, propagate) + // return Evented.prototype.listens.call(this, type, propagate) + // }, }) diff --git a/umap/static/umap/js/modules/rendering/ui.js b/umap/static/umap/js/modules/rendering/ui.js index f64ba483..143d96de 100644 --- a/umap/static/umap/js/modules/rendering/ui.js +++ b/umap/static/umap/js/modules/rendering/ui.js @@ -1,10 +1,14 @@ +// Goes here all code related to Leaflet, DOM and user interactions. import { Marker, Polyline, Polygon, DomUtil, + LineUtil, } from '../../../vendors/leaflet/leaflet-src.esm.js' import { translate } from '../i18n.js' +import { uMapAlert as Alert } from '../../components/alerts/alert.js' +import * as Utils from '../utils.js' const FeatureMixin = { initialize: function (feature) { @@ -27,15 +31,88 @@ const FeatureMixin = { }, addInteractions: function () { - this.on('contextmenu editable:vertex:contextmenu', this.feature._showContextMenu, this.feature) + this.on('contextmenu editable:vertex:contextmenu', this._showContextMenu) }, - onVertexRawClick: function (e) { - new L.Toolbar.Popup(e.latlng, { - className: 'leaflet-inplace-toolbar', - actions: this.getVertexActions(e), - }).addTo(this.map, this, e.latlng, e.vertex) + resetTooltip: function () { + if (!this.feature.hasGeom()) return + const displayName = this.feature.getDisplayName(null) + let showLabel = this.feature.getOption('showLabel') + const oldLabelHover = this.feature.getOption('labelHover') + + const options = { + direction: this.feature.getOption('labelDirection'), + interactive: this.feature.getOption('labelInteractive'), + } + + if (oldLabelHover && showLabel) showLabel = null // Retrocompat. + options.permanent = showLabel === true + this.unbindTooltip() + if ((showLabel === true || showLabel === null) && displayName) { + this.bindTooltip(Utils.escapeHTML(displayName), options) + } }, + + _showContextMenu: function (event) { + L.DomEvent.stop(event) + const pt = this._map.mouseEventToContainerPoint(event.originalEvent) + event.relatedTarget = this + this._map.contextmenu.showAt(pt, event) + }, + + getContextMenuItems: function (event) { + const permalink = this.feature.getPermalink() + let items = [] + if (permalink) + items.push({ + text: translate('Permalink'), + callback: () => { + window.open(permalink) + }, + }) + if (this._map.editEnabled && !this.feature.isReadOnly()) { + items = items.concat(this.getContextMenuEditItems(event)) + } + return items + }, + + getContextMenuEditItems: function () { + let items = ['-'] + if (this._map.editedFeature !== this) { + items.push({ + text: `${translate('Edit this feature')} (⇧+Click)`, + callback: this.feature.edit, + context: this.feature, + iconCls: 'umap-edit', + }) + } + items = items.concat( + { + text: this._map.help.displayLabel('EDIT_FEATURE_LAYER'), + callback: this.feature.datalayer.edit, + context: this.feature.datalayer, + iconCls: 'umap-edit', + }, + { + text: translate('Delete this feature'), + callback: this.feature.confirmDelete, + context: this.feature, + iconCls: 'umap-delete', + }, + { + text: translate('Clone this feature'), + callback: this.feature.clone, + context: this.feature, + } + ) + return items + }, + + onCommit: function () { + this.geometryChanged() + this.feature.onCommit() + }, + } export const LeafletMarker = Marker.extend({ @@ -47,22 +124,17 @@ export const LeafletMarker = Marker.extend({ this.setIcon(this.getIcon()) }, - onCommit: function () { + geometryChanged: function() { this.feature.coordinates = this._latlng - this.feature.onCommit() }, addInteractions() { FeatureMixin.addInteractions.call(this) - this.on( - 'dragend', - function (e) { - this.isDirty = true - this.feature.edit(e) - this.feature.sync.update('geometry', this.getGeometry()) - }, - this - ) + this.on('dragend', (event) => { + this.isDirty = true + this.feature.edit(event) + this.feature.sync.update('geometry', this.feature.getGeometry()) + }) this.on('editable:drawing:commit', this.onCommit) if (!this.feature.isReadOnly()) this.on('mouseover', this._enableDragging) this.on('mouseout', this._onMouseOut) @@ -104,7 +176,7 @@ export const LeafletMarker = Marker.extend({ Marker.prototype._initIcon.call(this) // Allow to run code when icon is actually part of the DOM this.options.icon.onAdd() - // this.resetTooltip() + this.resetTooltip() }, getIconClass: function () { @@ -118,7 +190,7 @@ export const LeafletMarker = Marker.extend({ _getTooltipAnchor: function () { const anchor = this.options.icon.options.tooltipAnchor.clone() - const direction = this.getOption('labelDirection') + const direction = this.feature.getOption('labelDirection') if (direction === 'left') { anchor.x *= -1 } else if (direction === 'bottom') { @@ -138,6 +210,14 @@ export const LeafletMarker = Marker.extend({ getCenter: function () { return this._latlng }, + + highlight: function () { + DomUtil.addClass(this.options.icon.elements.main, 'umap-icon-active') + }, + + resetHighlight: function () { + DomUtil.removeClass(this.options.icon.elements.main, 'umap-icon-active') + }, }) const PathMixin = { @@ -149,9 +229,8 @@ const PathMixin = { } }, - onCommit: function () { + geometryChanged: function () { this.feature.coordinates = this._latlngs - this.feature.onCommit() }, addInteractions: function () { @@ -172,7 +251,7 @@ const PathMixin = { }, _onDrag: function () { - this.feature.coordinates = this._latlngs + this.geometryChanged() if (this._tooltip) this._tooltip.setLatLng(this.getCenter()) }, @@ -181,7 +260,7 @@ const PathMixin = { this.setStyle() FeatureMixin.onAdd.call(this, map) if (this.editing?.enabled()) this.editing.addHooks() - // this.resetTooltip() + this.resetTooltip() this._path.dataset.feature = this.feature.id }, @@ -200,16 +279,162 @@ const PathMixin = { _redraw: function () { this.setStyle() - // this.resetTooltip() + this.resetTooltip() + }, + + getVertexActions: () => [U.DeleteVertexAction], + + onVertexRawClick: function (event) { + new L.Toolbar.Popup(event.latlng, { + className: 'leaflet-inplace-toolbar', + actions: this.getVertexActions(event), + }).addTo(this._map, this, event.latlng, event.vertex) + }, + + getContextMenuItems: function (event) { + let items = FeatureMixin.getContextMenuItems.call(this, event) + items.push({ + text: translate('Display measure'), + callback: () => Alert.info(this.feature.getMeasure()), + }) + if (this._map.editEnabled && !this.feature.isReadOnly() && this.feature.isMulti()) { + items = items.concat(this.getContextMenuMultiItems(event)) + } + return items + }, + + getContextMenuMultiItems: function (event) { + const items = [ + '-', + { + text: translate('Remove shape from the multi'), + callback: () => { + this.enableEdit().deleteShapeAt(event.latlng) + }, + }, + ] + const shape = this.shapeAt(event.latlng) + if (this._latlngs.indexOf(shape) > 0) { + items.push({ + text: translate('Make main shape'), + callback: () => { + this.enableEdit().deleteShape(shape) + this.editor.prependShape(shape) + }, + }) + } + return items + }, + + getContextMenuEditItems: function (event) { + const items = FeatureMixin.getContextMenuEditItems.call(this, event) + if ( + this._map?.editedFeature !== this && + this.feature.isSameClass(this._map.editedFeature) + ) { + items.push({ + text: translate('Transfer shape to edited feature'), + callback: () => { + this.feature.transferShape(event.latlng, this._map.editedFeature) + }, + }) + } + if (this.feature.isMulti()) { + items.push({ + text: translate('Extract shape to separate feature'), + callback: () => { + this.feature.isolateShape(event.latlng, this._map.editedFeature) + }, + }) + } + return items }, } export const LeafletPolyline = Polyline.extend({ parentClass: Polyline, includes: [FeatureMixin, PathMixin], + + getVertexActions: function (event) { + const actions = PathMixin.getVertexActions.call(this, event) + const index = event.vertex.getIndex() + if (index === 0 || index === event.vertex.getLastIndex()) { + actions.push(U.ContinueLineAction) + } else { + actions.push(U.SplitLineAction) + } + return actions + }, + + getContextMenuEditItems: function (event) { + const items = PathMixin.getContextMenuEditItems.call(this, event) + const vertexClicked = event.vertex + let index + if (!this.feature.isMulti()) { + items.push({ + text: translate('Transform to polygon'), + callback: this.feature.toPolygon, + context: this.feature, + }) + } + if (vertexClicked) { + index = event.vertex.getIndex() + if (index !== 0 && index !== event.vertex.getLastIndex()) { + items.push({ + text: translate('Split line'), + callback: event.vertex.split, + context: event.vertex, + }) + } else if (index === 0 || index === event.vertex.getLastIndex()) { + items.push({ + text: this._map.help.displayLabel('CONTINUE_LINE'), + callback: event.vertex.continue, + context: event.vertex.continue, + }) + } + } + return items + }, + + getContextMenuMultiItems: function (event) { + const items = PathMixin.getContextMenuMultiItems.call(this, event) + items.push({ + text: translate('Merge lines'), + callback: this.feature.mergeShapes, + context: this.feature, + }) + return items + }, }) export const LeafletPolygon = Polygon.extend({ parentClass: Polygon, includes: [FeatureMixin, PathMixin], + + getContextMenuEditItems: function (event) { + const items = PathMixin.getContextMenuEditItems.call(this, event) + const shape = this.shapeAt(event.latlng) + // No multi and no holes. + if ( + shape && + !this.feature.isMulti() && + (LineUtil.isFlat(shape) || shape.length === 1) + ) { + items.push({ + text: translate('Transform to lines'), + callback: this.feature.toLineString, + context: this.feature, + }) + } + items.push({ + text: translate('Start a hole here'), + callback: this.startHole, + context: this, + }) + return items + }, + + startHole: function (event) { + this.enableEdit().newHole(event.latlng) + }, }) diff --git a/umap/static/umap/js/modules/sync/updaters.js b/umap/static/umap/js/modules/sync/updaters.js index 4bd7ae51..ce6ef5cc 100644 --- a/umap/static/umap/js/modules/sync/updaters.js +++ b/umap/static/umap/js/modules/sync/updaters.js @@ -79,7 +79,7 @@ export class FeatureUpdater extends BaseUpdater { id, feature, }) - datalayer.addLayer(feature) + datalayer.addFeature(feature) } // Update a property of an object diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 853cce89..2321e9fe 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -155,7 +155,6 @@ U.AddPolygonShapeAction = U.AddPolylineShapeAction.extend({ U.BaseFeatureAction = L.ToolbarAction.extend({ initialize: function (map, feature, latlng) { - console.log("Toolbar init", latlng) this.map = map this.feature = feature this.latlng = latlng @@ -183,8 +182,8 @@ U.CreateHoleAction = U.BaseFeatureAction.extend({ }, }, - onClick: function (e) { - this.feature.startHole(e) + onClick: function (event) { + this.feature.ui.startHole(event) }, }) @@ -196,11 +195,11 @@ U.ToggleEditAction = U.BaseFeatureAction.extend({ }, }, - onClick: function (e) { + onClick: function (event) { if (this.feature._toggleEditing) { - this.feature._toggleEditing(e) // Path + this.feature._toggleEditing(event) // Path } else { - this.feature.edit(e) // Marker + this.feature.edit(event) // Marker } }, }) @@ -1151,7 +1150,6 @@ U.Editable = L.Editable.extend({ // Leaflet.Editable will delete the drawn shape if invalid // (eg. line has only one drawn point) // So let's check if the layer has no more shape - console.log(event.layer.feature.coordinates, event.layer.feature.hasGeom()) if (!event.layer.feature.hasGeom()) { event.layer.feature.del() } else { @@ -1169,7 +1167,6 @@ U.Editable = L.Editable.extend({ this.on('editable:editing', (event) => { const layer = event.layer layer.feature.isDirty = true - console.log('editing') if (layer instanceof L.Marker) { layer.feature.coordinates = layer._latlng } else { diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 8863205a..26db751f 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -1857,11 +1857,9 @@ U.Map = L.Map.extend({ if (feature._toggleEditing) feature._toggleEditing(event) else feature.edit(event) } - } else { - console.log('should show toolbar') + } else if (!this.editTools?.drawing()) { new L.Toolbar.Popup(event.latlng, { className: 'leaflet-inplace-toolbar', - anchor: feature.getPopupToolbarAnchor(), actions: feature.getInplaceToolbarActions(event), }).addTo(this, feature, event.latlng) } diff --git a/umap/tests/integration/test_draw_polyline.py b/umap/tests/integration/test_draw_polyline.py index fadb3d52..17f40865 100644 --- a/umap/tests/integration/test_draw_polyline.py +++ b/umap/tests/integration/test_draw_polyline.py @@ -187,7 +187,7 @@ def test_can_transfer_shape_from_simple_polyline(live_server, page, tilelayer): map.click(position={"x": 100, "y": 200}) expect(lines).to_have_count(1) - # Draw another polygon + # Draw another line page.get_by_title("Draw a polyline").click() map.click(position={"x": 250, "y": 250}) map.click(position={"x": 200, "y": 250}) @@ -196,7 +196,7 @@ def test_can_transfer_shape_from_simple_polyline(live_server, page, tilelayer): map.click(position={"x": 200, "y": 200}) expect(lines).to_have_count(2) - # Now that polygon 2 is selected, right click on first one + # Now that line 2 is selected, right click on first one # and transfer shape lines.first.click(position={"x": 10, "y": 1}, button="right") page.get_by_role("link", name="Transfer shape to edited feature").click() @@ -235,18 +235,17 @@ def test_can_transfer_shape_from_multi(live_server, page, tilelayer, settings): map.click(position={"x": 300, "y": 300}) expect(lines).to_have_count(2) - # Now that polygon 2 is selected, right click on first one + # Now that line 2 is selected, right click on first one # and transfer shape lines.first.click(position={"x": 10, "y": 1}, button="right") page.get_by_role("link", name="Transfer shape to edited feature").click() expect(lines).to_have_count(2) data = save_and_get_json(page) - # FIXME this should be a LineString, not MultiLineString assert data["features"][0]["geometry"] == { "coordinates": [ - [[-6.569824, 52.49616], [-7.668457, 52.49616], [-7.668457, 53.159947]] + [-6.569824, 52.49616], [-7.668457, 52.49616], [-7.668457, 53.159947] ], - "type": "MultiLineString", + "type": "LineString", } assert data["features"][1]["geometry"] == { "coordinates": [