wip: (almost) fix tests

This commit is contained in:
Yohan Boniface 2024-07-25 22:43:23 +02:00
parent 7aa07709b3
commit 081323dc8a
9 changed files with 355 additions and 339 deletions

View file

@ -23,6 +23,9 @@ class Feature {
this.properties = { _umap_options: {}, ...(geojson.properties || {}) } this.properties = { _umap_options: {}, ...(geojson.properties || {}) }
this.staticOptions = {} this.staticOptions = {}
if (geojson.coordinates) {
geojson = { geometry: geojson }
}
if (geojson.geometry) { if (geojson.geometry) {
this.populate(geojson) this.populate(geojson)
} }
@ -68,6 +71,10 @@ class Feature {
return this.ui.getCenter() return this.ui.getCenter()
} }
get bounds() {
return this.ui.getBounds()
}
getClassName() { getClassName() {
return this.staticOptions.className return this.staticOptions.className
} }
@ -162,7 +169,7 @@ class Feature {
this.redraw() this.redraw()
} }
edit(e) { edit(event) {
if (!this.map.editEnabled || this.isReadOnly()) return if (!this.map.editEnabled || this.isReadOnly()) return
const container = DomUtil.create('div', 'umap-feature-container') const container = DomUtil.create('div', 'umap-feature-container')
DomUtil.createTitle( DomUtil.createTitle(
@ -176,7 +183,7 @@ class Feature {
[['datalayer', { handler: 'DataLayerSwitcher' }]], [['datalayer', { handler: 'DataLayerSwitcher' }]],
{ {
callback() { callback() {
this.edit(e) this.edit(event)
}, // removeLayer step will close the edit panel, let's reopen it }, // removeLayer step will close the edit panel, let's reopen it
} }
) )
@ -207,11 +214,11 @@ class Feature {
builder.helpers['properties.name'].input.focus() builder.helpers['properties.name'].input.focus()
}) })
this.map.editedFeature = this this.map.editedFeature = this
if (!this.isOnScreen()) this.zoomTo(e) if (!this.isOnScreen()) this.zoomTo(event)
} }
getAdvancedEditActions(container) { 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()) this.confirmDelete().then(() => this.map.editPanel.close())
}) })
} }
@ -261,7 +268,7 @@ class Feature {
endEdit() {} endEdit() {}
getDisplayName(fallback) { getDisplayName(fallback) {
if (fallback === undefined) fallback = this.datalayer.options.name if (fallback === undefined) fallback = this.datalayer.getName()
const key = this.getOption('labelKey') || 'name' const key = this.getOption('labelKey') || 'name'
// Variables mode. // Variables mode.
if (U.Utils.hasVar(key)) if (U.Utils.hasVar(key))
@ -308,7 +315,8 @@ class Feature {
connectToDataLayer(datalayer) { connectToDataLayer(datalayer) {
this.datalayer = 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) { disconnectFromDataLayer(datalayer) {
@ -422,16 +430,12 @@ class Feature {
} }
toGeoJSON() { toGeoJSON() {
return { return Utils.CopyJSON({
type: 'Feature', type: 'Feature',
geometry: this.geometry, geometry: this.geometry,
properties: this.cloneProperties(), properties: this.cloneProperties(),
id: this.id, id: this.id,
} })
}
getPopupToolbarAnchor() {
return [0, 0]
} }
getInplaceToolbarActions() { getInplaceToolbarActions() {
@ -442,73 +446,6 @@ class Feature {
return this.map 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() { isFiltered() {
const filterKeys = this.datalayer.getFilterKeys() const filterKeys = this.datalayer.getFilterKeys()
const filter = this.map.browser.options.filter const filter = this.map.browser.options.filter
@ -519,7 +456,7 @@ class Feature {
matchFilter(filter, keys) { matchFilter(filter, keys) {
filter = filter.toLowerCase() filter = filter.toLowerCase()
if (U.Utils.hasVar(keys)) { if (Utils.hasVar(keys)) {
return this.getDisplayName().toLowerCase().indexOf(filter) !== -1 return this.getDisplayName().toLowerCase().indexOf(filter) !== -1
} }
keys = keys.split(',') keys = keys.split(',')
@ -552,10 +489,6 @@ class Feature {
return true return true
} }
getVertexActions() {
return [U.DeleteVertexAction]
}
isMulti() { isMulti() {
return false return false
} }
@ -578,7 +511,7 @@ class Feature {
if (L.lang) properties.lang = L.lang if (L.lang) properties.lang = L.lang
properties.rank = this.getRank() + 1 properties.rank = this.getRank() + 1
properties.layer = this.datalayer.getName() properties.layer = this.datalayer.getName()
if (this.map && this.hasGeom()) { if (this.ui._map && this.hasGeom()) {
const center = this.center const center = this.center
properties.lat = center.lat properties.lat = center.lat
properties.lon = center.lng properties.lon = center.lng
@ -600,13 +533,6 @@ class Feature {
this.ui._redraw() 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 { export class Point extends Feature {
@ -630,14 +556,6 @@ export class Point extends Feature {
return new LeafletMarker(this) 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() { hasGeom() {
return Boolean(this.coordinates) return Boolean(this.coordinates)
} }
@ -693,10 +611,6 @@ export class Point extends Feature {
bounds = bounds || this.map.getBounds() bounds = bounds || this.map.getBounds()
return bounds.contains(this.coordinates) return bounds.contains(this.coordinates)
} }
// getPopupToolbarAnchor() {
// return this.options.icon.options.popupAnchor
// }
} }
class Path extends Feature { class Path extends Feature {
@ -704,10 +618,20 @@ class Path extends Feature {
return !this.isEmpty() 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) { connectToDataLayer(datalayer) {
super.connectToDataLayer(datalayer) super.connectToDataLayer(datalayer)
// We keep markers on their own layer on top of the paths. // 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) { edit(event) {
@ -717,17 +641,17 @@ class Path extends Feature {
} }
} }
_toggleEditing(e) { _toggleEditing(event) {
if (this.map.editEnabled) { if (this.map.editEnabled) {
if (this.ui.editEnabled()) { if (this.ui.editEnabled()) {
this.endEdit() this.endEdit()
this.map.editPanel.close() this.map.editPanel.close()
} else { } else {
this.edit(e) this.edit(event)
} }
} }
// FIXME: disable when disabling global edit // FIXME: disable when disabling global edit
L.DomEvent.stop(e) L.DomEvent.stop(event)
} }
getStyleOptions() { getStyleOptions() {
@ -772,7 +696,7 @@ class Path extends Feature {
} }
getBestZoom() { getBestZoom() {
return this.getOption('zoomTo') || this.map.getBoundsZoom(this.getBounds(), true) return this.getOption('zoomTo') || this.map.getBoundsZoom(this.bounds, true)
} }
endEdit() { endEdit() {
@ -781,100 +705,36 @@ class Path extends Feature {
} }
transferShape(at, to) { transferShape(at, to) {
const shape = this.enableEdit().deleteShapeAt(at) const shape = this.ui.enableEdit().deleteShapeAt(at)
this.disableEdit() // FIXME: make Leaflet.Editable send an event instead
this.ui.geometryChanged()
this.ui.disableEdit()
if (!shape) return if (!shape) return
to.enableEdit().appendShape(shape) to.ui.enableEdit().appendShape(shape)
if (!this._latlngs.length || !this._latlngs[0].length) this.del() to.ui.geometryChanged()
if (this.isEmpty()) this.del()
} }
isolateShape(at) { isolateShape(at) {
if (!this.isMulti()) return if (!this.isMulti()) return
const shape = this.enableEdit().deleteShapeAt(at) const shape = this.ui.enableEdit().deleteShapeAt(at)
this.disableEdit() this.ui.disableEdit()
if (!shape) return if (!shape) return
const properties = this.cloneProperties() const properties = this.cloneProperties()
const other = new (this instanceof U.Polyline ? U.Polyline : U.Polygon)( const other = new (this instanceof LineString ? LineString : Polygon)(
this.map, this.datalayer,
shape,
{ {
geojson: { properties }, properties,
geometry: this._toGeometry(shape),
} }
) )
this.datalayer.addLayer(other) this.datalayer.addFeature(other)
other.edit() other.edit()
return other return other
} }
getContextMenuItems(e) { getInplaceToolbarActions(event) {
let items = super.getContextMenuItems(e) const items = super.getInplaceToolbarActions(event)
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)
if (this.isMulti()) { if (this.isMulti()) {
items.push(U.DeleteShapeAction) items.push(U.DeleteShapeAction)
items.push(U.ExtractShapeFromMultiAction) items.push(U.ExtractShapeFromMultiAction)
@ -884,16 +744,16 @@ class Path extends Feature {
isOnScreen(bounds) { isOnScreen(bounds) {
bounds = bounds || this.map.getBounds() bounds = bounds || this.map.getBounds()
return bounds.overlaps(this.ui.getBounds()) return bounds.overlaps(this.bounds)
} }
zoomTo({ easing, callback }) { zoomTo({ easing, callback }) {
// Use bounds instead of centroid for paths. // Use bounds instead of centroid for paths.
easing = easing || this.map.getOption('easing') easing = easing || this.map.getOption('easing')
if (easing) { if (easing) {
this.map.flyToBounds(this.getBounds(), this.getBestZoom()) this.map.flyToBounds(this.bounds, this.getBestZoom())
} else { } 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) if (callback) callback.call(this)
} }
@ -910,17 +770,22 @@ export class LineString extends Path {
} }
} }
get coordinates() { _toLatlngs(geometry) {
return GeoJSON.coordsToLatLngs( return GeoJSON.coordsToLatLngs(
this.geometry.coordinates, geometry.coordinates,
this.geometry.type === 'LineString' ? 0 : 1 geometry.type === 'LineString' ? 0 : 1
) )
} }
set coordinates(latlngs) { _toGeometry(latlngs) {
const multi = !LineUtil.isFlat(latlngs) let multi = !LineUtil.isFlat(latlngs)
this.geometry.coordinates = GeoJSON.latLngsToCoords(latlngs, multi ? 1 : 0, false) let coordinates = GeoJSON.latLngsToCoords(latlngs, multi ? 1 : 0, false)
this.geometry.type = multi ? 'MultiLineString' : 'LineString' if (coordinates.length === 1 && typeof coordinates[0][0] !== 'number') {
coordinates = Utils.flattenCoordinates(coordinates)
multi = false
}
const type = multi ? 'MultiLineString' : 'LineString'
return { coordinates, type }
} }
isEmpty() { isEmpty() {
@ -940,51 +805,11 @@ export class LineString extends Path {
return L.GeoUtil.readableDistance(length, this.map.measureTools.getMeasureUnit()) 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() { toPolygon() {
const geojson = this.toGeoJSON() const geojson = this.toGeoJSON()
geojson.geometry.type = 'Polygon' geojson.geometry.type = 'Polygon'
geojson.geometry.coordinates = [ 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. delete geojson.id // delete the copied id, a new one will be generated.
@ -1053,15 +878,6 @@ export class LineString extends Path {
isMulti() { isMulti() {
return !LineUtil.isFlat(this.coordinates) && this.coordinates.length > 1 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 { export class Polygon extends Path {
@ -1073,22 +889,22 @@ export class Polygon extends Path {
} }
} }
get coordinates() { _toLatlngs(geometry) {
return GeoJSON.coordsToLatLngs( return GeoJSON.coordsToLatLngs(
this.geometry.coordinates, geometry.coordinates,
this.geometry.type === 'Polygon' ? 1 : 2 geometry.type === 'Polygon' ? 1 : 2
) )
} }
set coordinates(latlngs) { _toGeometry(latlngs) {
const holes = !LineUtil.isFlat(latlngs) const holes = !LineUtil.isFlat(latlngs)
const multi = holes && !LineUtil.isFlat(latlngs[0]) 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) { if (!holes) {
coords = [coords] coordinates = [coordinates]
} }
this.geometry.coordinates = coords const type = multi ? 'MultiPolygon' : 'Polygon'
this.geometry.type = multi ? 'MultiPolygon' : 'Polygon' return { coordinates, type }
} }
isEmpty() { isEmpty() {
@ -1133,35 +949,12 @@ export class Polygon extends Path {
return L.GeoUtil.readableArea(area, this.map.measureTools.getMeasureUnit()) return L.GeoUtil.readableArea(area, this.map.measureTools.getMeasureUnit())
} }
getContextMenuEditItems(e) { toLineString() {
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() {
const geojson = this.toGeoJSON() const geojson = this.toGeoJSON()
delete geojson.id delete geojson.id
delete geojson.properties.id delete geojson.properties.id
geojson.geometry.type = 'LineString' geojson.geometry.type = 'LineString'
geojson.geometry.coordinates = U.Utils.flattenCoordinates( geojson.geometry.coordinates = Utils.flattenCoordinates(
geojson.geometry.coordinates geojson.geometry.coordinates
) )
const polyline = this.datalayer.geojsonToFeatures(geojson) const polyline = this.datalayer.geojsonToFeatures(geojson)
@ -1171,17 +964,18 @@ export class Polygon extends Path {
getAdvancedEditActions(container) { getAdvancedEditActions(container) {
super.getAdvancedEditActions(container) super.getAdvancedEditActions(container)
const toPolyline = DomUtil.createButton( const toLineString = DomUtil.createButton(
'button umap-to-polyline', 'button umap-to-polyline',
container, container,
translate('Transform to lines'), translate('Transform to lines'),
this.toPolyline, this.toLineString,
this this
) )
} }
isMulti() { isMulti() {
// Change me when Leaflet#3279 is merged. // Change me when Leaflet#3279 is merged.
// FIXME use TurfJS
return ( return (
!LineUtil.isFlat(this.coordinates) && !LineUtil.isFlat(this.coordinates) &&
!LineUtil.isFlat(this.coordinates[0]) && !LineUtil.isFlat(this.coordinates[0]) &&
@ -1189,8 +983,8 @@ export class Polygon extends Path {
) )
} }
getInplaceToolbarActions(e) { getInplaceToolbarActions(event) {
const items = super.getInplaceToolbarActions(e) const items = super.getInplaceToolbarActions(event)
items.push(U.CreateHoleAction) items.push(U.CreateHoleAction)
return items return items
} }

View file

@ -41,6 +41,7 @@ export class DataLayer {
this.parentPane = this.map.getPane('overlayPane') this.parentPane = this.map.getPane('overlayPane')
this.pane = this.map.createPane(`datalayer${stamp(this)}`, this.parentPane) this.pane = this.map.createPane(`datalayer${stamp(this)}`, this.parentPane)
this.pane.dataset.id = stamp(this) this.pane.dataset.id = stamp(this)
// FIXME: should be on layer
this.renderer = L.svg({ pane: this.pane }) this.renderer = L.svg({ pane: this.pane })
this.defaultOptions = { this.defaultOptions = {
displayOnLoad: true, displayOnLoad: true,
@ -481,11 +482,10 @@ export class DataLayer {
break break
} }
if (feature) { if (feature) {
feature.setLatLng(latlng) feature.coordinates = latlng
return feature return feature
} }
return new Point(this, geojson) return new Point(this, geojson)
// return this._pointToLayer(geojson, latlng, id)
case 'MultiLineString': case 'MultiLineString':
case 'LineString': case 'LineString':
@ -495,21 +495,19 @@ export class DataLayer {
) )
if (!latlngs.length) break if (!latlngs.length) break
if (feature) { if (feature) {
feature.setLatLngs(latlngs) feature.coordinates = latlngs
return feature return feature
} }
return new LineString(this, geojson) return new LineString(this, geojson)
// return this._lineToLayer(geojson, latlngs, id)
case 'MultiPolygon': case 'MultiPolygon':
case 'Polygon': case 'Polygon':
latlngs = GeoJSON.coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2) latlngs = GeoJSON.coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2)
if (feature) { if (feature) {
feature.setLatLngs(latlngs) feature.coordinates = latlngs
return feature return feature
} }
return new Polygon(this, geojson) return new Polygon(this, geojson)
// return this._polygonToLayer(geojson, latlngs, id)
case 'GeometryCollection': case 'GeometryCollection':
return this.geojsonToFeatures(geometry.geometries) return this.geojsonToFeatures(geometry.geometries)
@ -900,9 +898,9 @@ export class DataLayer {
getOption(option, feature) { getOption(option, feature) {
if (this.layer?.getOption) { if (this.layer?.getOption) {
const value = this.layer.getOption(option, feature) 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) return this.getOwnOption(option)
} }
if (this.layer?.defaults?.[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. // 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. // 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.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 if (this.map.options.sortKey) return this.map.options.sortKey
return 'name' return 'name'
} }

View file

@ -22,7 +22,7 @@ export const EXPORT_FORMATS = {
const table = [] const table = []
map.eachFeature((feature) => { map.eachFeature((feature) => {
const row = feature.toGeoJSON().properties const row = feature.toGeoJSON().properties
const center = feature.getCenter() const center = feature.center
delete row._umap_options delete row._umap_options
row.Latitude = center.lat row.Latitude = center.lat
row.Longitude = center.lng row.Longitude = center.lng

View file

@ -4,6 +4,7 @@
import { translate } from '../../i18n.js' import { translate } from '../../i18n.js'
import { LayerMixin } from './base.js' import { LayerMixin } from './base.js'
import * as Utils from '../../utils.js' import * as Utils from '../../utils.js'
import { Evented } from '../../../../vendors/leaflet/leaflet-src.esm.js'
const MarkerCluster = L.MarkerCluster.extend({ const MarkerCluster = L.MarkerCluster.extend({
// Custom class so we can call computeTextColor // 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) return L.MarkerClusterGroup.prototype.onAdd.call(this, map)
}, },
onRemove: function (map) { onRemove: function (map) {
// In some situation, the onRemove is called before the layer is really // In some situation, the onRemove is called before the layer is really
// added to the map: basically when combining a defaultView=data + max/minZoom // 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() 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)
// },
}) })

View file

@ -1,10 +1,14 @@
// Goes here all code related to Leaflet, DOM and user interactions.
import { import {
Marker, Marker,
Polyline, Polyline,
Polygon, Polygon,
DomUtil, DomUtil,
LineUtil,
} from '../../../vendors/leaflet/leaflet-src.esm.js' } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from '../i18n.js' import { translate } from '../i18n.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import * as Utils from '../utils.js'
const FeatureMixin = { const FeatureMixin = {
initialize: function (feature) { initialize: function (feature) {
@ -27,15 +31,88 @@ const FeatureMixin = {
}, },
addInteractions: function () { addInteractions: function () {
this.on('contextmenu editable:vertex:contextmenu', this.feature._showContextMenu, this.feature) this.on('contextmenu editable:vertex:contextmenu', this._showContextMenu)
}, },
onVertexRawClick: function (e) { resetTooltip: function () {
new L.Toolbar.Popup(e.latlng, { if (!this.feature.hasGeom()) return
className: 'leaflet-inplace-toolbar', const displayName = this.feature.getDisplayName(null)
actions: this.getVertexActions(e), let showLabel = this.feature.getOption('showLabel')
}).addTo(this.map, this, e.latlng, e.vertex) 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({ export const LeafletMarker = Marker.extend({
@ -47,22 +124,17 @@ export const LeafletMarker = Marker.extend({
this.setIcon(this.getIcon()) this.setIcon(this.getIcon())
}, },
onCommit: function () { geometryChanged: function() {
this.feature.coordinates = this._latlng this.feature.coordinates = this._latlng
this.feature.onCommit()
}, },
addInteractions() { addInteractions() {
FeatureMixin.addInteractions.call(this) FeatureMixin.addInteractions.call(this)
this.on( this.on('dragend', (event) => {
'dragend', this.isDirty = true
function (e) { this.feature.edit(event)
this.isDirty = true this.feature.sync.update('geometry', this.feature.getGeometry())
this.feature.edit(e) })
this.feature.sync.update('geometry', this.getGeometry())
},
this
)
this.on('editable:drawing:commit', this.onCommit) this.on('editable:drawing:commit', this.onCommit)
if (!this.feature.isReadOnly()) this.on('mouseover', this._enableDragging) if (!this.feature.isReadOnly()) this.on('mouseover', this._enableDragging)
this.on('mouseout', this._onMouseOut) this.on('mouseout', this._onMouseOut)
@ -104,7 +176,7 @@ export const LeafletMarker = Marker.extend({
Marker.prototype._initIcon.call(this) Marker.prototype._initIcon.call(this)
// Allow to run code when icon is actually part of the DOM // Allow to run code when icon is actually part of the DOM
this.options.icon.onAdd() this.options.icon.onAdd()
// this.resetTooltip() this.resetTooltip()
}, },
getIconClass: function () { getIconClass: function () {
@ -118,7 +190,7 @@ export const LeafletMarker = Marker.extend({
_getTooltipAnchor: function () { _getTooltipAnchor: function () {
const anchor = this.options.icon.options.tooltipAnchor.clone() const anchor = this.options.icon.options.tooltipAnchor.clone()
const direction = this.getOption('labelDirection') const direction = this.feature.getOption('labelDirection')
if (direction === 'left') { if (direction === 'left') {
anchor.x *= -1 anchor.x *= -1
} else if (direction === 'bottom') { } else if (direction === 'bottom') {
@ -138,6 +210,14 @@ export const LeafletMarker = Marker.extend({
getCenter: function () { getCenter: function () {
return this._latlng 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 = { const PathMixin = {
@ -149,9 +229,8 @@ const PathMixin = {
} }
}, },
onCommit: function () { geometryChanged: function () {
this.feature.coordinates = this._latlngs this.feature.coordinates = this._latlngs
this.feature.onCommit()
}, },
addInteractions: function () { addInteractions: function () {
@ -172,7 +251,7 @@ const PathMixin = {
}, },
_onDrag: function () { _onDrag: function () {
this.feature.coordinates = this._latlngs this.geometryChanged()
if (this._tooltip) this._tooltip.setLatLng(this.getCenter()) if (this._tooltip) this._tooltip.setLatLng(this.getCenter())
}, },
@ -181,7 +260,7 @@ const PathMixin = {
this.setStyle() this.setStyle()
FeatureMixin.onAdd.call(this, map) FeatureMixin.onAdd.call(this, map)
if (this.editing?.enabled()) this.editing.addHooks() if (this.editing?.enabled()) this.editing.addHooks()
// this.resetTooltip() this.resetTooltip()
this._path.dataset.feature = this.feature.id this._path.dataset.feature = this.feature.id
}, },
@ -200,16 +279,162 @@ const PathMixin = {
_redraw: function () { _redraw: function () {
this.setStyle() 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({ export const LeafletPolyline = Polyline.extend({
parentClass: Polyline, parentClass: Polyline,
includes: [FeatureMixin, PathMixin], 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({ export const LeafletPolygon = Polygon.extend({
parentClass: Polygon, parentClass: Polygon,
includes: [FeatureMixin, PathMixin], 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)
},
}) })

View file

@ -79,7 +79,7 @@ export class FeatureUpdater extends BaseUpdater {
id, id,
feature, feature,
}) })
datalayer.addLayer(feature) datalayer.addFeature(feature)
} }
// Update a property of an object // Update a property of an object

View file

@ -155,7 +155,6 @@ U.AddPolygonShapeAction = U.AddPolylineShapeAction.extend({
U.BaseFeatureAction = L.ToolbarAction.extend({ U.BaseFeatureAction = L.ToolbarAction.extend({
initialize: function (map, feature, latlng) { initialize: function (map, feature, latlng) {
console.log("Toolbar init", latlng)
this.map = map this.map = map
this.feature = feature this.feature = feature
this.latlng = latlng this.latlng = latlng
@ -183,8 +182,8 @@ U.CreateHoleAction = U.BaseFeatureAction.extend({
}, },
}, },
onClick: function (e) { onClick: function (event) {
this.feature.startHole(e) this.feature.ui.startHole(event)
}, },
}) })
@ -196,11 +195,11 @@ U.ToggleEditAction = U.BaseFeatureAction.extend({
}, },
}, },
onClick: function (e) { onClick: function (event) {
if (this.feature._toggleEditing) { if (this.feature._toggleEditing) {
this.feature._toggleEditing(e) // Path this.feature._toggleEditing(event) // Path
} else { } 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 // Leaflet.Editable will delete the drawn shape if invalid
// (eg. line has only one drawn point) // (eg. line has only one drawn point)
// So let's check if the layer has no more shape // 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()) { if (!event.layer.feature.hasGeom()) {
event.layer.feature.del() event.layer.feature.del()
} else { } else {
@ -1169,7 +1167,6 @@ U.Editable = L.Editable.extend({
this.on('editable:editing', (event) => { this.on('editable:editing', (event) => {
const layer = event.layer const layer = event.layer
layer.feature.isDirty = true layer.feature.isDirty = true
console.log('editing')
if (layer instanceof L.Marker) { if (layer instanceof L.Marker) {
layer.feature.coordinates = layer._latlng layer.feature.coordinates = layer._latlng
} else { } else {

View file

@ -1857,11 +1857,9 @@ U.Map = L.Map.extend({
if (feature._toggleEditing) feature._toggleEditing(event) if (feature._toggleEditing) feature._toggleEditing(event)
else feature.edit(event) else feature.edit(event)
} }
} else { } else if (!this.editTools?.drawing()) {
console.log('should show toolbar')
new L.Toolbar.Popup(event.latlng, { new L.Toolbar.Popup(event.latlng, {
className: 'leaflet-inplace-toolbar', className: 'leaflet-inplace-toolbar',
anchor: feature.getPopupToolbarAnchor(),
actions: feature.getInplaceToolbarActions(event), actions: feature.getInplaceToolbarActions(event),
}).addTo(this, feature, event.latlng) }).addTo(this, feature, event.latlng)
} }

View file

@ -187,7 +187,7 @@ def test_can_transfer_shape_from_simple_polyline(live_server, page, tilelayer):
map.click(position={"x": 100, "y": 200}) map.click(position={"x": 100, "y": 200})
expect(lines).to_have_count(1) expect(lines).to_have_count(1)
# Draw another polygon # Draw another line
page.get_by_title("Draw a polyline").click() page.get_by_title("Draw a polyline").click()
map.click(position={"x": 250, "y": 250}) map.click(position={"x": 250, "y": 250})
map.click(position={"x": 200, "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}) map.click(position={"x": 200, "y": 200})
expect(lines).to_have_count(2) 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 # and transfer shape
lines.first.click(position={"x": 10, "y": 1}, button="right") lines.first.click(position={"x": 10, "y": 1}, button="right")
page.get_by_role("link", name="Transfer shape to edited feature").click() 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}) map.click(position={"x": 300, "y": 300})
expect(lines).to_have_count(2) 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 # and transfer shape
lines.first.click(position={"x": 10, "y": 1}, button="right") lines.first.click(position={"x": 10, "y": 1}, button="right")
page.get_by_role("link", name="Transfer shape to edited feature").click() page.get_by_role("link", name="Transfer shape to edited feature").click()
expect(lines).to_have_count(2) expect(lines).to_have_count(2)
data = save_and_get_json(page) data = save_and_get_json(page)
# FIXME this should be a LineString, not MultiLineString
assert data["features"][0]["geometry"] == { assert data["features"][0]["geometry"] == {
"coordinates": [ "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"] == { assert data["features"][1]["geometry"] == {
"coordinates": [ "coordinates": [