mirror of
https://github.com/umap-project/umap.git
synced 2025-04-28 19:42:36 +02:00
Merge pull request #2019 from umap-project/features-to-modules
wip: first step in moving features to modules (work in progress)
This commit is contained in:
commit
daa6e37073
22 changed files with 1850 additions and 1525 deletions
997
umap/static/umap/js/modules/data/features.js
Normal file
997
umap/static/umap/js/modules/data/features.js
Normal file
|
@ -0,0 +1,997 @@
|
|||
import {
|
||||
DomUtil,
|
||||
DomEvent,
|
||||
stamp,
|
||||
GeoJSON,
|
||||
LineUtil,
|
||||
} from '../../../vendors/leaflet/leaflet-src.esm.js'
|
||||
import * as Utils from '../utils.js'
|
||||
import { SCHEMA } from '../schema.js'
|
||||
import { translate } from '../i18n.js'
|
||||
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
|
||||
import { LeafletMarker, LeafletPolyline, LeafletPolygon } from '../rendering/ui.js'
|
||||
|
||||
class Feature {
|
||||
constructor(datalayer, geojson = {}, id = null) {
|
||||
this.sync = datalayer.map.sync_engine.proxy(this)
|
||||
this._marked_for_deletion = false
|
||||
this._isDirty = false
|
||||
this._ui = null
|
||||
|
||||
// DataLayer the feature belongs to
|
||||
this.datalayer = datalayer
|
||||
this.properties = { _umap_options: {}, ...(geojson.properties || {}) }
|
||||
this.staticOptions = {}
|
||||
|
||||
if (geojson.coordinates) {
|
||||
geojson = { geometry: geojson }
|
||||
}
|
||||
if (geojson.geometry) {
|
||||
this.populate(geojson)
|
||||
}
|
||||
|
||||
if (id) {
|
||||
this.id = id
|
||||
} else {
|
||||
let geojson_id
|
||||
if (geojson) {
|
||||
geojson_id = geojson.id
|
||||
}
|
||||
|
||||
// Each feature needs an unique identifier
|
||||
if (Utils.checkId(geojson_id)) {
|
||||
this.id = geojson_id
|
||||
} else {
|
||||
this.id = Utils.generateId()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set isDirty(status) {
|
||||
this._isDirty = status
|
||||
if (this.datalayer) {
|
||||
this.datalayer.isDirty = status
|
||||
}
|
||||
}
|
||||
|
||||
get isDirty() {
|
||||
return this._isDirty
|
||||
}
|
||||
|
||||
get ui() {
|
||||
if (!this._ui) this._ui = this.makeUI()
|
||||
return this._ui
|
||||
}
|
||||
|
||||
get map() {
|
||||
return this.datalayer?.map
|
||||
}
|
||||
|
||||
get center() {
|
||||
return this.ui.getCenter()
|
||||
}
|
||||
|
||||
get bounds() {
|
||||
return this.ui.getBounds()
|
||||
}
|
||||
|
||||
get geometry() {
|
||||
return this._geometry
|
||||
}
|
||||
|
||||
set geometry(value) {
|
||||
this._geometry = value
|
||||
this.geometryChanged()
|
||||
}
|
||||
|
||||
getClassName() {
|
||||
return this.staticOptions.className
|
||||
}
|
||||
|
||||
getPreviewColor() {
|
||||
return this.getDynamicOption(this.staticOptions.mainColor)
|
||||
}
|
||||
|
||||
getSyncMetadata() {
|
||||
return {
|
||||
subject: 'feature',
|
||||
metadata: {
|
||||
id: this.id,
|
||||
layerId: this.datalayer?.umap_id || null,
|
||||
featureType: this.getClassName(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
onCommit() {
|
||||
// When the layer is a remote layer, we don't want to sync the creation of the
|
||||
// points via the websocket, as the other peers will get them themselves.
|
||||
if (this.datalayer?.isRemoteLayer()) return
|
||||
|
||||
// The "endEdit" event is triggered at the end of an edition,
|
||||
// and will trigger the sync.
|
||||
// In the case of a deletion (or a change of layer), we don't want this
|
||||
// event triggered to cause a sync event, as it would reintroduce
|
||||
// deleted features.
|
||||
// The `._marked_for_deletion` private property is here to track this status.
|
||||
if (this._marked_for_deletion === true) {
|
||||
this._marked_for_deletion = false
|
||||
return
|
||||
}
|
||||
this.sync.upsert(this.toGeoJSON())
|
||||
}
|
||||
|
||||
isReadOnly() {
|
||||
return this.datalayer?.isDataReadOnly()
|
||||
}
|
||||
|
||||
getSlug() {
|
||||
return this.properties[this.map.getOption('slugKey') || 'name'] || ''
|
||||
}
|
||||
|
||||
getPermalink() {
|
||||
const slug = this.getSlug()
|
||||
if (slug)
|
||||
return `${Utils.getBaseUrl()}?${Utils.buildQueryString({ feature: slug })}${
|
||||
window.location.hash
|
||||
}`
|
||||
}
|
||||
|
||||
view({ latlng } = {}) {
|
||||
const outlink = this.getOption('outlink')
|
||||
const target = this.getOption('outlinkTarget')
|
||||
if (outlink) {
|
||||
switch (target) {
|
||||
case 'self':
|
||||
window.location = outlink
|
||||
break
|
||||
case 'parent':
|
||||
window.top.location = outlink
|
||||
break
|
||||
default:
|
||||
window.open(this.properties._umap_options.outlink)
|
||||
}
|
||||
return
|
||||
}
|
||||
// TODO deal with an event instead?
|
||||
if (this.map.slideshow) {
|
||||
this.map.slideshow.current = this
|
||||
}
|
||||
this.map.currentFeature = this
|
||||
this.attachPopup()
|
||||
this.ui.openPopup(latlng || this.center)
|
||||
}
|
||||
|
||||
render(fields) {
|
||||
const impactData = fields.some((field) => {
|
||||
return field.startsWith('properties.')
|
||||
})
|
||||
if (impactData) {
|
||||
if (this.map.currentFeature === this) {
|
||||
this.view()
|
||||
}
|
||||
}
|
||||
this.redraw()
|
||||
}
|
||||
|
||||
edit(event) {
|
||||
if (!this.map.editEnabled || this.isReadOnly()) return
|
||||
const container = DomUtil.create('div', 'umap-feature-container')
|
||||
DomUtil.createTitle(
|
||||
container,
|
||||
translate('Feature properties'),
|
||||
`icon-${this.getClassName()}`
|
||||
)
|
||||
|
||||
let builder = new U.FormBuilder(
|
||||
this,
|
||||
[['datalayer', { handler: 'DataLayerSwitcher' }]],
|
||||
{
|
||||
callback() {
|
||||
this.edit(event)
|
||||
}, // removeLayer step will close the edit panel, let's reopen it
|
||||
}
|
||||
)
|
||||
container.appendChild(builder.build())
|
||||
|
||||
const properties = []
|
||||
for (const property of this.datalayer._propertiesIndex) {
|
||||
if (['name', 'description'].includes(property)) {
|
||||
continue
|
||||
}
|
||||
properties.push([`properties.${property}`, { label: property }])
|
||||
}
|
||||
// We always want name and description for now (properties management to come)
|
||||
properties.unshift('properties.description')
|
||||
properties.unshift('properties.name')
|
||||
builder = new U.FormBuilder(this, properties, {
|
||||
id: 'umap-feature-properties',
|
||||
})
|
||||
container.appendChild(builder.build())
|
||||
this.appendEditFieldsets(container)
|
||||
const advancedActions = DomUtil.createFieldset(
|
||||
container,
|
||||
translate('Advanced actions')
|
||||
)
|
||||
this.getAdvancedEditActions(advancedActions)
|
||||
const onLoad = this.map.editPanel.open({ content: container })
|
||||
onLoad.then(() => {
|
||||
builder.helpers['properties.name'].input.focus()
|
||||
})
|
||||
this.map.editedFeature = this
|
||||
if (!this.isOnScreen()) this.zoomTo(event)
|
||||
}
|
||||
|
||||
getAdvancedEditActions(container) {
|
||||
DomUtil.createButton('button umap-delete', container, translate('Delete'), () => {
|
||||
this.confirmDelete().then(() => this.map.editPanel.close())
|
||||
})
|
||||
}
|
||||
|
||||
appendEditFieldsets(container) {
|
||||
const optionsFields = this.getShapeOptions()
|
||||
let builder = new U.FormBuilder(this, optionsFields, {
|
||||
id: 'umap-feature-shape-properties',
|
||||
})
|
||||
const shapeProperties = DomUtil.createFieldset(
|
||||
container,
|
||||
translate('Shape properties')
|
||||
)
|
||||
shapeProperties.appendChild(builder.build())
|
||||
|
||||
const advancedOptions = this.getAdvancedOptions()
|
||||
builder = new U.FormBuilder(this, advancedOptions, {
|
||||
id: 'umap-feature-advanced-properties',
|
||||
})
|
||||
const advancedProperties = DomUtil.createFieldset(
|
||||
container,
|
||||
translate('Advanced properties')
|
||||
)
|
||||
advancedProperties.appendChild(builder.build())
|
||||
|
||||
const interactionOptions = this.getInteractionOptions()
|
||||
builder = new U.FormBuilder(this, interactionOptions)
|
||||
const popupFieldset = DomUtil.createFieldset(
|
||||
container,
|
||||
translate('Interaction options')
|
||||
)
|
||||
popupFieldset.appendChild(builder.build())
|
||||
}
|
||||
|
||||
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',
|
||||
]
|
||||
}
|
||||
|
||||
endEdit() {}
|
||||
|
||||
getDisplayName(fallback) {
|
||||
if (fallback === undefined) fallback = this.datalayer.getName()
|
||||
const key = this.getOption('labelKey') || 'name'
|
||||
// Variables mode.
|
||||
if (U.Utils.hasVar(key))
|
||||
return U.Utils.greedyTemplate(key, this.extendedProperties())
|
||||
// Simple mode.
|
||||
return this.properties[key] || this.properties.title || fallback
|
||||
}
|
||||
|
||||
hasPopupFooter() {
|
||||
if (this.datalayer.isRemoteLayer() && this.datalayer.options.remoteData.dynamic) {
|
||||
return false
|
||||
}
|
||||
return this.map.getOption('displayPopupFooter')
|
||||
}
|
||||
|
||||
getPopupClass() {
|
||||
const old = this.getOption('popupTemplate') // Retrocompat.
|
||||
return U.Popup[this.getOption('popupShape') || old] || U.Popup
|
||||
}
|
||||
|
||||
attachPopup() {
|
||||
const Class = this.getPopupClass()
|
||||
this.ui.bindPopup(new Class(this))
|
||||
}
|
||||
|
||||
async confirmDelete() {
|
||||
const confirmed = await this.map.dialog.confirm(
|
||||
translate('Are you sure you want to delete the feature?')
|
||||
)
|
||||
if (confirmed) {
|
||||
this.del()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
del(sync) {
|
||||
this.isDirty = true
|
||||
this.map.closePopup()
|
||||
if (this.datalayer) {
|
||||
this.datalayer.removeFeature(this, sync)
|
||||
}
|
||||
}
|
||||
|
||||
connectToDataLayer(datalayer) {
|
||||
this.datalayer = datalayer
|
||||
// FIXME should be in layer/ui
|
||||
this.ui.options.renderer = this.datalayer.renderer
|
||||
}
|
||||
|
||||
disconnectFromDataLayer(datalayer) {
|
||||
if (this.datalayer === datalayer) {
|
||||
this.datalayer = null
|
||||
}
|
||||
}
|
||||
|
||||
cleanProperty([key, value]) {
|
||||
// dot in key will break the dot based property access
|
||||
// while editing the feature
|
||||
key = key.replace('.', '_')
|
||||
return [key, value]
|
||||
}
|
||||
|
||||
populate(geojson) {
|
||||
this._geometry = geojson.geometry
|
||||
this.properties = Object.fromEntries(
|
||||
Object.entries(geojson.properties || {}).map(this.cleanProperty)
|
||||
)
|
||||
this.properties._umap_options = L.extend(
|
||||
{},
|
||||
this.properties._storage_options,
|
||||
this.properties._umap_options
|
||||
)
|
||||
// Retrocompat
|
||||
if (this.properties._umap_options.clickable === false) {
|
||||
this.properties._umap_options.interactive = false
|
||||
delete this.properties._umap_options.clickable
|
||||
}
|
||||
}
|
||||
|
||||
changeDataLayer(datalayer) {
|
||||
if (this.datalayer) {
|
||||
this.datalayer.isDirty = true
|
||||
this.datalayer.removeFeature(this)
|
||||
}
|
||||
|
||||
datalayer.addFeature(this)
|
||||
this.sync.upsert(this.toGeoJSON())
|
||||
datalayer.isDirty = true
|
||||
this.redraw()
|
||||
}
|
||||
|
||||
getOption(option, fallback) {
|
||||
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 (this.datalayer) {
|
||||
value = this.datalayer.getOption(option, this)
|
||||
} else {
|
||||
value = this.map.getOption(option)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
getDynamicOption(option, fallback) {
|
||||
let value = this.getOption(option, fallback)
|
||||
// There is a variable inside.
|
||||
if (U.Utils.hasVar(value)) {
|
||||
value = U.Utils.greedyTemplate(value, this.properties, true)
|
||||
if (U.Utils.hasVar(value)) value = this.map.getDefaultOption(option)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
zoomTo({ easing, latlng, callback } = {}) {
|
||||
if (easing === undefined) easing = this.map.getOption('easing')
|
||||
if (callback) this.map.once('moveend', callback.call(this))
|
||||
if (easing) {
|
||||
this.map.flyTo(this.center, this.getBestZoom())
|
||||
} else {
|
||||
latlng = latlng || this.center
|
||||
this.map.setView(latlng, this.getBestZoom() || this.map.getZoom())
|
||||
}
|
||||
}
|
||||
|
||||
getBestZoom() {
|
||||
return this.getOption('zoomTo')
|
||||
}
|
||||
|
||||
getNext() {
|
||||
return this.datalayer.getNextFeature(this)
|
||||
}
|
||||
|
||||
getPrevious() {
|
||||
return this.datalayer.getPreviousFeature(this)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
deleteProperty(property) {
|
||||
delete this.properties[property]
|
||||
this.isDirty = true
|
||||
}
|
||||
|
||||
renameProperty(from, to) {
|
||||
this.properties[to] = this.properties[from]
|
||||
this.deleteProperty(from)
|
||||
}
|
||||
|
||||
toGeoJSON() {
|
||||
return Utils.CopyJSON({
|
||||
type: 'Feature',
|
||||
geometry: this.geometry,
|
||||
properties: this.cloneProperties(),
|
||||
id: this.id,
|
||||
})
|
||||
}
|
||||
|
||||
getInplaceToolbarActions() {
|
||||
return [U.ToggleEditAction, U.DeleteFeatureAction]
|
||||
}
|
||||
|
||||
getMap() {
|
||||
return this.map
|
||||
}
|
||||
|
||||
isFiltered() {
|
||||
const filterKeys = this.datalayer.getFilterKeys()
|
||||
const filter = this.map.browser.options.filter
|
||||
if (filter && !this.matchFilter(filter, filterKeys)) return true
|
||||
if (!this.matchFacets()) return true
|
||||
return false
|
||||
}
|
||||
|
||||
matchFilter(filter, keys) {
|
||||
filter = filter.toLowerCase()
|
||||
if (Utils.hasVar(keys)) {
|
||||
return this.getDisplayName().toLowerCase().indexOf(filter) !== -1
|
||||
}
|
||||
keys = keys.split(',')
|
||||
for (let i = 0, value; i < keys.length; i++) {
|
||||
value = `${this.properties[keys[i]] || ''}`
|
||||
if (value.toLowerCase().indexOf(filter) !== -1) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
matchFacets() {
|
||||
const selected = this.map.facets.selected
|
||||
for (const [name, { type, min, max, choices }] of Object.entries(selected)) {
|
||||
let value = this.properties[name]
|
||||
const parser = this.map.facets.getParser(type)
|
||||
value = parser(value)
|
||||
switch (type) {
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
case 'number':
|
||||
if (!Number.isNaN(min) && !Number.isNaN(value) && min > value) return false
|
||||
if (!Number.isNaN(max) && !Number.isNaN(value) && max < value) return false
|
||||
break
|
||||
default:
|
||||
value = value || translate('<empty value>')
|
||||
if (choices?.length && !choices.includes(value)) return false
|
||||
break
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
isMulti() {
|
||||
return false
|
||||
}
|
||||
|
||||
clone() {
|
||||
const geojson = this.toGeoJSON()
|
||||
delete geojson.id
|
||||
delete geojson.properties.id
|
||||
const feature = this.datalayer.makeFeature(geojson)
|
||||
feature.isDirty = true
|
||||
feature.edit()
|
||||
return feature
|
||||
}
|
||||
|
||||
extendedProperties() {
|
||||
// Include context properties
|
||||
const properties = this.map.getGeoContext()
|
||||
const locale = L.getLocale()
|
||||
if (locale) properties.locale = locale
|
||||
if (L.lang) properties.lang = L.lang
|
||||
properties.rank = this.getRank() + 1
|
||||
properties.layer = this.datalayer.getName()
|
||||
if (this.ui._map && this.hasGeom()) {
|
||||
const center = this.center
|
||||
properties.lat = center.lat
|
||||
properties.lon = center.lng
|
||||
properties.lng = center.lng
|
||||
properties.alt = center?.alt
|
||||
if (typeof this.getMeasure !== 'undefined') {
|
||||
properties.measure = this.getMeasure()
|
||||
}
|
||||
}
|
||||
return L.extend(properties, this.properties)
|
||||
}
|
||||
|
||||
getRank() {
|
||||
return this.datalayer._index.indexOf(L.stamp(this))
|
||||
}
|
||||
|
||||
redraw() {
|
||||
if (this.datalayer?.isVisible()) {
|
||||
this.ui._redraw()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Point extends Feature {
|
||||
constructor(datalayer, geojson, id) {
|
||||
super(datalayer, geojson, id)
|
||||
this.staticOptions = {
|
||||
mainColor: 'color',
|
||||
className: 'marker',
|
||||
}
|
||||
}
|
||||
|
||||
get coordinates() {
|
||||
return GeoJSON.coordsToLatLng(this.geometry.coordinates)
|
||||
}
|
||||
|
||||
set coordinates(latlng) {
|
||||
this.geometry.coordinates = GeoJSON.latLngToCoords(latlng)
|
||||
}
|
||||
|
||||
geometryChanged() {
|
||||
this.ui.setLatLng(this.coordinates)
|
||||
}
|
||||
|
||||
makeUI() {
|
||||
return new LeafletMarker(this)
|
||||
}
|
||||
|
||||
hasGeom() {
|
||||
return Boolean(this.coordinates)
|
||||
}
|
||||
|
||||
_getIconUrl(name = 'icon') {
|
||||
return this.getOption(`${name}Url`)
|
||||
}
|
||||
|
||||
getShapeOptions() {
|
||||
return [
|
||||
'properties._umap_options.color',
|
||||
'properties._umap_options.iconClass',
|
||||
'properties._umap_options.iconUrl',
|
||||
'properties._umap_options.iconOpacity',
|
||||
]
|
||||
}
|
||||
|
||||
getAdvancedOptions() {
|
||||
return ['properties._umap_options.zoomTo']
|
||||
}
|
||||
|
||||
appendEditFieldsets(container) {
|
||||
super.appendEditFieldsets(container)
|
||||
// FIXME edit feature geometry.coordinates instead
|
||||
// (by learning FormBuilder to deal with array indexes ?)
|
||||
const coordinatesOptions = [
|
||||
['ui._latlng.lat', { handler: 'FloatInput', label: translate('Latitude') }],
|
||||
['ui._latlng.lng', { handler: 'FloatInput', label: translate('Longitude') }],
|
||||
]
|
||||
const builder = new U.FormBuilder(this, coordinatesOptions, {
|
||||
callback: () => {
|
||||
if (!this.ui._latlng.isValid()) {
|
||||
Alert.error(translate('Invalid latitude or longitude'))
|
||||
builder.restoreField('ui._latlng.lat')
|
||||
builder.restoreField('ui._latlng.lng')
|
||||
}
|
||||
this.zoomTo({ easing: false })
|
||||
},
|
||||
})
|
||||
const fieldset = DomUtil.createFieldset(container, translate('Coordinates'))
|
||||
fieldset.appendChild(builder.build())
|
||||
}
|
||||
|
||||
zoomTo(event) {
|
||||
if (this.datalayer.isClustered() && !this._icon) {
|
||||
// callback is mandatory for zoomToShowLayer
|
||||
this.datalayer.layer.zoomToShowLayer(this, event.callback || (() => {}))
|
||||
} else {
|
||||
super.zoomTo(event)
|
||||
}
|
||||
}
|
||||
|
||||
isOnScreen(bounds) {
|
||||
bounds = bounds || this.map.getBounds()
|
||||
return bounds.contains(this.coordinates)
|
||||
}
|
||||
}
|
||||
|
||||
class Path extends Feature {
|
||||
hasGeom() {
|
||||
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
|
||||
}
|
||||
|
||||
geometryChanged() {
|
||||
this.ui.setLatLngs(this.coordinates)
|
||||
}
|
||||
|
||||
connectToDataLayer(datalayer) {
|
||||
super.connectToDataLayer(datalayer)
|
||||
// We keep markers on their own layer on top of the paths.
|
||||
this.ui.options.pane = this.datalayer.pane
|
||||
}
|
||||
|
||||
edit(event) {
|
||||
if (this.map.editEnabled) {
|
||||
if (!this.ui.editEnabled()) this.ui.enableEdit()
|
||||
super.edit(event)
|
||||
}
|
||||
}
|
||||
|
||||
_toggleEditing(event) {
|
||||
if (this.map.editEnabled) {
|
||||
if (this.ui.editEnabled()) {
|
||||
this.endEdit()
|
||||
this.map.editPanel.close()
|
||||
} else {
|
||||
this.edit(event)
|
||||
}
|
||||
}
|
||||
// FIXME: disable when disabling global edit
|
||||
L.DomEvent.stop(event)
|
||||
}
|
||||
|
||||
getStyleOptions() {
|
||||
return [
|
||||
'smoothFactor',
|
||||
'color',
|
||||
'opacity',
|
||||
'stroke',
|
||||
'weight',
|
||||
'fill',
|
||||
'fillColor',
|
||||
'fillOpacity',
|
||||
'dashArray',
|
||||
'interactive',
|
||||
]
|
||||
}
|
||||
|
||||
getShapeOptions() {
|
||||
return [
|
||||
'properties._umap_options.color',
|
||||
'properties._umap_options.opacity',
|
||||
'properties._umap_options.weight',
|
||||
]
|
||||
}
|
||||
|
||||
getAdvancedOptions() {
|
||||
return [
|
||||
'properties._umap_options.smoothFactor',
|
||||
'properties._umap_options.dashArray',
|
||||
'properties._umap_options.zoomTo',
|
||||
]
|
||||
}
|
||||
|
||||
getStyle() {
|
||||
const options = {}
|
||||
for (const option of this.getStyleOptions()) {
|
||||
options[option] = this.getDynamicOption(option)
|
||||
}
|
||||
if (options.interactive) options.pointerEvents = 'visiblePainted'
|
||||
else options.pointerEvents = 'stroke'
|
||||
return options
|
||||
}
|
||||
|
||||
getBestZoom() {
|
||||
return this.getOption('zoomTo') || this.map.getBoundsZoom(this.bounds, true)
|
||||
}
|
||||
|
||||
endEdit() {
|
||||
this.ui.disableEdit()
|
||||
super.endEdit()
|
||||
}
|
||||
|
||||
transferShape(at, to) {
|
||||
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.ui.enableEdit().appendShape(shape)
|
||||
to.ui.geometryChanged()
|
||||
if (this.isEmpty()) this.del()
|
||||
}
|
||||
|
||||
isolateShape(latlngs) {
|
||||
const properties = this.cloneProperties()
|
||||
const type = this instanceof LineString ? 'LineString' : 'Polygon'
|
||||
const geometry = this._toGeometry(latlngs)
|
||||
const other = this.datalayer.makeFeature({ type, geometry, properties })
|
||||
other.edit()
|
||||
return other
|
||||
}
|
||||
|
||||
getInplaceToolbarActions(event) {
|
||||
const items = super.getInplaceToolbarActions(event)
|
||||
if (this.isMulti()) {
|
||||
items.push(U.DeleteShapeAction)
|
||||
items.push(U.ExtractShapeFromMultiAction)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
isOnScreen(bounds) {
|
||||
bounds = bounds || this.map.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.bounds, this.getBestZoom())
|
||||
} else {
|
||||
this.map.fitBounds(this.bounds, this.getBestZoom() || this.map.getZoom())
|
||||
}
|
||||
if (callback) callback.call(this)
|
||||
}
|
||||
}
|
||||
|
||||
export class LineString extends Path {
|
||||
constructor(datalayer, geojson, id) {
|
||||
super(datalayer, geojson, id)
|
||||
this.staticOptions = {
|
||||
stroke: true,
|
||||
fill: false,
|
||||
mainColor: 'color',
|
||||
className: 'polyline',
|
||||
}
|
||||
}
|
||||
|
||||
_toLatlngs(geometry) {
|
||||
return GeoJSON.coordsToLatLngs(
|
||||
geometry.coordinates,
|
||||
geometry.type === 'LineString' ? 0 : 1
|
||||
)
|
||||
}
|
||||
|
||||
_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() {
|
||||
return !this.coordinates.length
|
||||
}
|
||||
|
||||
makeUI() {
|
||||
return new LeafletPolyline(this)
|
||||
}
|
||||
|
||||
isSameClass(other) {
|
||||
return other instanceof LineString
|
||||
}
|
||||
|
||||
getMeasure(shape) {
|
||||
const length = L.GeoUtil.lineLength(this.map, shape || this.ui._defaultShape())
|
||||
return L.GeoUtil.readableDistance(length, this.map.measureTools.getMeasureUnit())
|
||||
}
|
||||
|
||||
toPolygon() {
|
||||
const geojson = this.toGeoJSON()
|
||||
geojson.geometry.type = 'Polygon'
|
||||
geojson.geometry.coordinates = [
|
||||
Utils.flattenCoordinates(geojson.geometry.coordinates),
|
||||
]
|
||||
|
||||
delete geojson.id // delete the copied id, a new one will be generated.
|
||||
|
||||
const polygon = this.datalayer.makeFeature(geojson)
|
||||
polygon.edit()
|
||||
this.del()
|
||||
}
|
||||
|
||||
getAdvancedEditActions(container) {
|
||||
super.getAdvancedEditActions(container)
|
||||
DomUtil.createButton(
|
||||
'button umap-to-polygon',
|
||||
container,
|
||||
translate('Transform to polygon'),
|
||||
this.toPolygon,
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
_mergeShapes(from, to) {
|
||||
const toLeft = to[0]
|
||||
const toRight = to[to.length - 1]
|
||||
const fromLeft = from[0]
|
||||
const fromRight = from[from.length - 1]
|
||||
const l2ldistance = toLeft.distanceTo(fromLeft)
|
||||
const l2rdistance = toLeft.distanceTo(fromRight)
|
||||
const r2ldistance = toRight.distanceTo(fromLeft)
|
||||
const r2rdistance = toRight.distanceTo(fromRight)
|
||||
let toMerge
|
||||
if (l2rdistance < Math.min(l2ldistance, r2ldistance, r2rdistance)) {
|
||||
toMerge = [from, to]
|
||||
} else if (r2ldistance < Math.min(l2ldistance, l2rdistance, r2rdistance)) {
|
||||
toMerge = [to, from]
|
||||
} else if (r2rdistance < Math.min(l2ldistance, l2rdistance, r2ldistance)) {
|
||||
from.reverse()
|
||||
toMerge = [to, from]
|
||||
} else {
|
||||
from.reverse()
|
||||
toMerge = [from, to]
|
||||
}
|
||||
const a = toMerge[0]
|
||||
const b = toMerge[1]
|
||||
const p1 = this.map.latLngToContainerPoint(a[a.length - 1])
|
||||
const p2 = this.map.latLngToContainerPoint(b[0])
|
||||
const tolerance = 5 // px on screen
|
||||
if (Math.abs(p1.x - p2.x) <= tolerance && Math.abs(p1.y - p2.y) <= tolerance) {
|
||||
a.pop()
|
||||
}
|
||||
return a.concat(b)
|
||||
}
|
||||
|
||||
mergeShapes() {
|
||||
if (!this.isMulti()) return
|
||||
const latlngs = this.getLatLngs()
|
||||
if (!latlngs.length) return
|
||||
while (latlngs.length > 1) {
|
||||
latlngs.splice(0, 2, this._mergeShapes(latlngs[1], latlngs[0]))
|
||||
}
|
||||
this.setLatLngs(latlngs[0])
|
||||
if (!this.editEnabled()) this.edit()
|
||||
this.editor.reset()
|
||||
this.isDirty = true
|
||||
}
|
||||
|
||||
isMulti() {
|
||||
return !LineUtil.isFlat(this.coordinates) && this.coordinates.length > 1
|
||||
}
|
||||
}
|
||||
|
||||
export class Polygon extends Path {
|
||||
constructor(datalayer, geojson, id) {
|
||||
super(datalayer, geojson, id)
|
||||
this.staticOptions = {
|
||||
mainColor: 'fillColor',
|
||||
className: 'polygon',
|
||||
}
|
||||
}
|
||||
|
||||
_toLatlngs(geometry) {
|
||||
return GeoJSON.coordsToLatLngs(
|
||||
geometry.coordinates,
|
||||
geometry.type === 'Polygon' ? 1 : 2
|
||||
)
|
||||
}
|
||||
|
||||
_toGeometry(latlngs) {
|
||||
const holes = !LineUtil.isFlat(latlngs)
|
||||
let multi = holes && !LineUtil.isFlat(latlngs[0])
|
||||
let coordinates = GeoJSON.latLngsToCoords(latlngs, multi ? 2 : holes ? 1 : 0, true)
|
||||
if (Utils.polygonMustBeFlattened(coordinates)) {
|
||||
coordinates = coordinates[0]
|
||||
multi = false
|
||||
}
|
||||
const type = multi ? 'MultiPolygon' : 'Polygon'
|
||||
return { coordinates, type }
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return !this.coordinates.length || !this.coordinates[0].length
|
||||
}
|
||||
|
||||
makeUI() {
|
||||
return new LeafletPolygon(this)
|
||||
}
|
||||
|
||||
isSameClass(other) {
|
||||
return other instanceof Polygon
|
||||
}
|
||||
|
||||
getShapeOptions() {
|
||||
const options = super.getShapeOptions()
|
||||
options.push(
|
||||
'properties._umap_options.stroke',
|
||||
'properties._umap_options.fill',
|
||||
'properties._umap_options.fillColor',
|
||||
'properties._umap_options.fillOpacity'
|
||||
)
|
||||
return options
|
||||
}
|
||||
|
||||
getPreviewColor() {
|
||||
// If user set a fillColor, use it, otherwise default to color
|
||||
// which is usually the only one set
|
||||
const color = this.getDynamicOption(this.staticOptions.mainColor)
|
||||
if (color && color !== SCHEMA.color.default) return color
|
||||
return this.getDynamicOption('color')
|
||||
}
|
||||
|
||||
getInteractionOptions() {
|
||||
const options = super.getInteractionOptions()
|
||||
options.push('properties._umap_options.interactive')
|
||||
return options
|
||||
}
|
||||
|
||||
getMeasure(shape) {
|
||||
const area = L.GeoUtil.geodesicArea(shape || this.ui._defaultShape())
|
||||
return L.GeoUtil.readableArea(area, this.map.measureTools.getMeasureUnit())
|
||||
}
|
||||
|
||||
toLineString() {
|
||||
const geojson = this.toGeoJSON()
|
||||
delete geojson.id
|
||||
delete geojson.properties.id
|
||||
geojson.geometry.type = 'LineString'
|
||||
geojson.geometry.coordinates = Utils.flattenCoordinates(
|
||||
geojson.geometry.coordinates
|
||||
)
|
||||
const polyline = this.datalayer.makeFeature(geojson)
|
||||
polyline.edit()
|
||||
this.del()
|
||||
}
|
||||
|
||||
getAdvancedEditActions(container) {
|
||||
super.getAdvancedEditActions(container)
|
||||
const toLineString = DomUtil.createButton(
|
||||
'button umap-to-polyline',
|
||||
container,
|
||||
translate('Transform to lines'),
|
||||
this.toLineString,
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
isMulti() {
|
||||
// Change me when Leaflet#3279 is merged.
|
||||
// FIXME use TurfJS
|
||||
return (
|
||||
!LineUtil.isFlat(this.coordinates) &&
|
||||
!LineUtil.isFlat(this.coordinates[0]) &&
|
||||
this.coordinates.length > 1
|
||||
)
|
||||
}
|
||||
|
||||
getInplaceToolbarActions(event) {
|
||||
const items = super.getInplaceToolbarActions(event)
|
||||
items.push(U.CreateHoleAction)
|
||||
return items
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
// Uses U.Marker, U.Polygon, U.Polyline, U.TableEditor not yet ES modules
|
||||
// Uses U.FormBuilder not available as ESM
|
||||
|
||||
// FIXME: this module should not depend on Leaflet
|
||||
|
@ -19,6 +18,7 @@ import {
|
|||
} from '../../components/alerts/alert.js'
|
||||
import { translate } from '../i18n.js'
|
||||
import { DataLayerPermissions } from '../permissions.js'
|
||||
import { Point, LineString, Polygon } from './features.js'
|
||||
|
||||
export const LAYER_TYPES = [DefaultLayer, Cluster, Heat, Choropleth, Categorized]
|
||||
|
||||
|
@ -32,7 +32,7 @@ export class DataLayer {
|
|||
this.map = map
|
||||
this.sync = map.sync_engine.proxy(this)
|
||||
this._index = Array()
|
||||
this._layers = {}
|
||||
this._features = {}
|
||||
this._geojson = null
|
||||
this._propertiesIndex = []
|
||||
this._loaded = false // Are layer metadata loaded
|
||||
|
@ -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,
|
||||
|
@ -188,23 +189,14 @@ export class DataLayer {
|
|||
if (visible) this.map.removeLayer(this.layer)
|
||||
const Class = LAYER_MAP[this.options.type] || DefaultLayer
|
||||
this.layer = new Class(this)
|
||||
this.eachLayer(this.showFeature)
|
||||
this.eachFeature(this.showFeature)
|
||||
if (visible) this.show()
|
||||
this.propagateRemote()
|
||||
}
|
||||
|
||||
eachLayer(method, context) {
|
||||
for (const i in this._layers) {
|
||||
method.call(context || this, this._layers[i])
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
eachFeature(method, context) {
|
||||
if (this.isBrowsable()) {
|
||||
for (let i = 0; i < this._index.length; i++) {
|
||||
method.call(context || this, this._layers[this._index[i]])
|
||||
}
|
||||
for (const idx of this._index) {
|
||||
method.call(context || this, this._features[idx])
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
@ -254,7 +246,7 @@ export class DataLayer {
|
|||
|
||||
clear() {
|
||||
this.layer.clearLayers()
|
||||
this._layers = {}
|
||||
this._features = {}
|
||||
this._index = Array()
|
||||
if (this._geojson) {
|
||||
this.backupData()
|
||||
|
@ -268,13 +260,9 @@ export class DataLayer {
|
|||
}
|
||||
|
||||
reindex() {
|
||||
const features = []
|
||||
this.eachFeature((feature) => features.push(feature))
|
||||
const features = Object.values(this._features)
|
||||
Utils.sortFeatures(features, this.map.getOption('sortKey'), L.lang)
|
||||
this._index = []
|
||||
for (let i = 0; i < features.length; i++) {
|
||||
this._index.push(stamp(features[i]))
|
||||
}
|
||||
this._index = features.map((feature) => stamp(feature))
|
||||
}
|
||||
|
||||
showAtZoom() {
|
||||
|
@ -371,28 +359,28 @@ export class DataLayer {
|
|||
|
||||
showFeature(feature) {
|
||||
if (feature.isFiltered()) return
|
||||
this.layer.addLayer(feature)
|
||||
this.layer.addLayer(feature.ui)
|
||||
}
|
||||
|
||||
addLayer(feature) {
|
||||
addFeature(feature) {
|
||||
const id = stamp(feature)
|
||||
feature.connectToDataLayer(this)
|
||||
this._index.push(id)
|
||||
this._layers[id] = feature
|
||||
this._features[id] = feature
|
||||
this.indexProperties(feature)
|
||||
this.map.features_index[feature.getSlug()] = feature
|
||||
this.showFeature(feature)
|
||||
if (this.hasDataLoaded()) this.dataChanged()
|
||||
}
|
||||
|
||||
removeLayer(feature, sync) {
|
||||
removeFeature(feature, sync) {
|
||||
const id = stamp(feature)
|
||||
if (sync !== false) feature.sync.delete()
|
||||
this.layer.removeLayer(feature)
|
||||
this.layer.removeLayer(feature.ui)
|
||||
delete this.map.features_index[feature.getSlug()]
|
||||
feature.disconnectFromDataLayer(this)
|
||||
this._index.splice(this._index.indexOf(id), 1)
|
||||
delete this._layers[id]
|
||||
delete this.map.features_index[feature.getSlug()]
|
||||
delete this._features[id]
|
||||
if (this.hasDataLoaded() && this.isVisible()) this.dataChanged()
|
||||
}
|
||||
|
||||
|
@ -416,7 +404,7 @@ export class DataLayer {
|
|||
}
|
||||
|
||||
sortedValues(property) {
|
||||
return Object.values(this._layers)
|
||||
return Object.values(this._features)
|
||||
.map((feature) => feature.properties[property])
|
||||
.filter((val, idx, arr) => arr.indexOf(val) === idx)
|
||||
.sort(Utils.naturalSort)
|
||||
|
@ -426,135 +414,57 @@ export class DataLayer {
|
|||
try {
|
||||
// Do not fail if remote data is somehow invalid,
|
||||
// otherwise the layer becomes uneditable.
|
||||
this.geojsonToFeatures(geojson, sync)
|
||||
this.makeFeatures(geojson, sync)
|
||||
} catch (err) {
|
||||
console.log('Error with DataLayer', this.umap_id)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// The choice of the name is not ours, because it is required by Leaflet.
|
||||
// It is misleading, as the returned objects are uMap objects, and not
|
||||
// GeoJSON features.
|
||||
geojsonToFeatures(geojson, sync) {
|
||||
if (!geojson) return
|
||||
const features = Array.isArray(geojson) ? geojson : geojson.features
|
||||
let i
|
||||
let len
|
||||
|
||||
if (features) {
|
||||
Utils.sortFeatures(features, this.map.getOption('sortKey'), L.lang)
|
||||
for (i = 0, len = features.length; i < len; i++) {
|
||||
this.geojsonToFeatures(features[i])
|
||||
makeFeatures(geojson = {}, sync = true) {
|
||||
if (geojson.type === 'Feature' || geojson.coordinates) {
|
||||
geojson = [geojson]
|
||||
}
|
||||
return this // Why returning "this" ?
|
||||
}
|
||||
|
||||
const geometry = geojson.type === 'Feature' ? geojson.geometry : geojson
|
||||
|
||||
const feature = this.geoJSONToLeaflet({ geometry, geojson })
|
||||
if (feature) {
|
||||
this.addLayer(feature)
|
||||
if (sync) feature.onCommit()
|
||||
return feature
|
||||
const collection = Array.isArray(geojson)
|
||||
? geojson
|
||||
: geojson.features || geojson.geometries
|
||||
Utils.sortFeatures(collection, this.map.getOption('sortKey'), L.lang)
|
||||
for (const feature of collection) {
|
||||
this.makeFeature(feature, sync)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update Leaflet features from GeoJSON geometries.
|
||||
*
|
||||
* If no `feature` is provided, a new feature will be created.
|
||||
* If `feature` is provided, it will be updated with the passed geometry.
|
||||
*
|
||||
* GeoJSON and Leaflet use incompatible formats to encode coordinates.
|
||||
* This method takes care of the convertion.
|
||||
*
|
||||
* @param geometry GeoJSON geometry field
|
||||
* @param geojson Enclosing GeoJSON. If none is provided, a new one will
|
||||
* be created
|
||||
* @param id Id of the feature
|
||||
* @param feature Leaflet feature that should be updated with the new geometry
|
||||
* @returns Leaflet feature.
|
||||
*/
|
||||
geoJSONToLeaflet({ geometry, geojson = null, id = null, feature = null } = {}) {
|
||||
if (!geometry) return // null geometry is valid geojson.
|
||||
const coords = geometry.coordinates
|
||||
let latlng
|
||||
let latlngs
|
||||
|
||||
// Create a default geojson if none is provided
|
||||
if (geojson === undefined) geojson = { type: 'Feature', geometry: geometry }
|
||||
makeFeature(geojson = {}, sync = true, id = null) {
|
||||
// Both Feature and Geometry are valid geojson objects.
|
||||
const geometry = geojson.geometry || geojson
|
||||
let feature
|
||||
|
||||
switch (geometry.type) {
|
||||
case 'Point':
|
||||
try {
|
||||
latlng = GeoJSON.coordsToLatLng(coords)
|
||||
} catch (e) {
|
||||
console.error('Invalid latlng object from', coords)
|
||||
// FIXME: deal with MultiPoint
|
||||
feature = new Point(this, geojson, id)
|
||||
break
|
||||
}
|
||||
if (feature) {
|
||||
feature.setLatLng(latlng)
|
||||
return feature
|
||||
}
|
||||
return this._pointToLayer(geojson, latlng, id)
|
||||
|
||||
case 'MultiLineString':
|
||||
case 'LineString':
|
||||
latlngs = GeoJSON.coordsToLatLngs(
|
||||
coords,
|
||||
geometry.type === 'LineString' ? 0 : 1
|
||||
)
|
||||
if (!latlngs.length) break
|
||||
if (feature) {
|
||||
feature.setLatLngs(latlngs)
|
||||
return feature
|
||||
}
|
||||
return this._lineToLayer(geojson, latlngs, id)
|
||||
|
||||
feature = new LineString(this, geojson, id)
|
||||
break
|
||||
case 'MultiPolygon':
|
||||
case 'Polygon':
|
||||
latlngs = GeoJSON.coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2)
|
||||
if (feature) {
|
||||
feature.setLatLngs(latlngs)
|
||||
return feature
|
||||
}
|
||||
return this._polygonToLayer(geojson, latlngs, id)
|
||||
case 'GeometryCollection':
|
||||
return this.geojsonToFeatures(geometry.geometries)
|
||||
|
||||
feature = new Polygon(this, geojson, id)
|
||||
break
|
||||
default:
|
||||
console.log(geojson)
|
||||
Alert.error(
|
||||
translate('Skipping unknown geometry.type: {type}', {
|
||||
type: geometry.type || 'undefined',
|
||||
})
|
||||
)
|
||||
}
|
||||
if (feature) {
|
||||
this.addFeature(feature)
|
||||
if (sync) feature.onCommit()
|
||||
return feature
|
||||
}
|
||||
|
||||
_pointToLayer(geojson, latlng, id) {
|
||||
return new U.Marker(this.map, latlng, { geojson: geojson, datalayer: this }, id)
|
||||
}
|
||||
|
||||
_lineToLayer(geojson, latlngs, id) {
|
||||
return new U.Polyline(
|
||||
this.map,
|
||||
latlngs,
|
||||
{
|
||||
geojson: geojson,
|
||||
datalayer: this,
|
||||
color: null,
|
||||
},
|
||||
id
|
||||
)
|
||||
}
|
||||
|
||||
_polygonToLayer(geojson, latlngs, id) {
|
||||
// Ensure no empty hole
|
||||
// for (let i = latlngs.length - 1; i > 0; i--) {
|
||||
// if (!latlngs.slice()[i].length) latlngs.splice(i, 1);
|
||||
// }
|
||||
return new U.Polygon(this.map, latlngs, { geojson: geojson, datalayer: this }, id)
|
||||
}
|
||||
|
||||
async importRaw(raw, format) {
|
||||
|
@ -566,7 +476,7 @@ export class DataLayer {
|
|||
}
|
||||
|
||||
importFromFiles(files, type) {
|
||||
for (let i = 0, f; (f = files[i]); i++) {
|
||||
for (const f of files) {
|
||||
this.importFromFile(f, type)
|
||||
}
|
||||
}
|
||||
|
@ -910,9 +820,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]) {
|
||||
|
@ -966,7 +876,7 @@ export class DataLayer {
|
|||
|
||||
featuresToGeoJSON() {
|
||||
const features = []
|
||||
this.eachLayer((layer) => features.push(layer.toGeoJSON()))
|
||||
this.eachFeature((feature) => features.push(feature.toGeoJSON()))
|
||||
return features
|
||||
}
|
||||
|
||||
|
@ -1031,19 +941,21 @@ export class DataLayer {
|
|||
getFeatureByIndex(index) {
|
||||
if (index === -1) index = this._index.length - 1
|
||||
const id = this._index[index]
|
||||
return this._layers[id]
|
||||
return this._features[id]
|
||||
}
|
||||
|
||||
// TODO Add an index
|
||||
// For now, iterate on all the features.
|
||||
getFeatureById(id) {
|
||||
return Object.values(this._layers).find((feature) => feature.id === id)
|
||||
return Object.values(this._features).find((feature) => feature.id === id)
|
||||
}
|
||||
|
||||
getNextFeature(feature) {
|
||||
const id = this._index.indexOf(stamp(feature))
|
||||
const nextId = this._index[id + 1]
|
||||
return nextId ? this._layers[nextId] : this.getNextBrowsable().getFeatureByIndex(0)
|
||||
return nextId
|
||||
? this._features[nextId]
|
||||
: this.getNextBrowsable().getFeatureByIndex(0)
|
||||
}
|
||||
|
||||
getPreviousFeature(feature) {
|
||||
|
@ -1053,7 +965,7 @@ export class DataLayer {
|
|||
const id = this._index.indexOf(stamp(feature))
|
||||
const previousId = this._index[id - 1]
|
||||
return previousId
|
||||
? this._layers[previousId]
|
||||
? this._features[previousId]
|
||||
: this.getPreviousBrowsable().getFeatureByIndex(-1)
|
||||
}
|
||||
|
||||
|
@ -1187,7 +1099,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'
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -28,6 +28,8 @@ import URLs from './urls.js'
|
|||
import * as Utils from './utils.js'
|
||||
import { DataLayer, LAYER_TYPES } from './data/layer.js'
|
||||
import { DataLayerPermissions, MapPermissions } from './permissions.js'
|
||||
import { Point, LineString, Polygon } from './data/features.js'
|
||||
import { LeafletMarker, LeafletPolyline, LeafletPolygon } from './rendering/ui.js'
|
||||
|
||||
// Import modules and export them to the global scope.
|
||||
// For the not yet module-compatible JS out there.
|
||||
|
@ -52,10 +54,16 @@ window.U = {
|
|||
HTTPError,
|
||||
Importer,
|
||||
LAYER_TYPES,
|
||||
LeafletMarker,
|
||||
LeafletPolygon,
|
||||
LeafletPolyline,
|
||||
LineString,
|
||||
MapPermissions,
|
||||
NOKError,
|
||||
Orderable,
|
||||
Panel,
|
||||
Point,
|
||||
Polygon,
|
||||
Request,
|
||||
RequestError,
|
||||
Rules,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
// Uses global L.HeatLayer, not exposed as ESM
|
||||
import { Marker, LatLng, latLngBounds, Bounds, point } from '../../../../vendors/leaflet/leaflet-src.esm.js'
|
||||
import {
|
||||
Marker,
|
||||
LatLng,
|
||||
latLngBounds,
|
||||
Bounds,
|
||||
point,
|
||||
} from '../../../../vendors/leaflet/leaflet-src.esm.js'
|
||||
import { LayerMixin } from './base.js'
|
||||
import * as Utils from '../../utils.js'
|
||||
import { translate } from '../../i18n.js'
|
||||
|
@ -27,7 +33,7 @@ export const Heat = L.HeatLayer.extend({
|
|||
let alt
|
||||
if (this.datalayer.options.heat?.intensityProperty) {
|
||||
alt = Number.parseFloat(
|
||||
layer.properties[this.datalayer.options.heat.intensityProperty || 0]
|
||||
layer.feature.properties[this.datalayer.options.heat.intensityProperty || 0]
|
||||
)
|
||||
latlng = new LatLng(latlng.lat, latlng.lng, alt)
|
||||
}
|
||||
|
|
|
@ -56,8 +56,8 @@ const RelativeColorLayerMixin = {
|
|||
|
||||
getValues: function () {
|
||||
const values = []
|
||||
this.datalayer.eachLayer((layer) => {
|
||||
const value = this._getValue(layer)
|
||||
this.datalayer.eachFeature((feature) => {
|
||||
const value = this._getValue(feature)
|
||||
if (value !== undefined) values.push(value)
|
||||
})
|
||||
return values
|
||||
|
|
484
umap/static/umap/js/modules/rendering/ui.js
Normal file
484
umap/static/umap/js/modules/rendering/ui.js
Normal file
|
@ -0,0 +1,484 @@
|
|||
// 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) {
|
||||
this.feature = feature
|
||||
this.parentClass.prototype.initialize.call(this, this.feature.coordinates)
|
||||
},
|
||||
|
||||
onAdd: function (map) {
|
||||
this.addInteractions()
|
||||
return this.parentClass.prototype.onAdd.call(this, map)
|
||||
},
|
||||
|
||||
onRemove: function (map) {
|
||||
this.parentClass.prototype.onRemove.call(this, map)
|
||||
if (map.editedFeature === this.feature) {
|
||||
this.feature._marked_for_deletion = true
|
||||
this.feature.endEdit()
|
||||
map.editPanel.close()
|
||||
}
|
||||
},
|
||||
|
||||
addInteractions: function () {
|
||||
this.on('contextmenu editable:vertex:contextmenu', this._showContextMenu)
|
||||
this.on('click', this.onClick)
|
||||
},
|
||||
|
||||
onClick: function (event) {
|
||||
if (this._map.measureTools?.enabled()) return
|
||||
this._popupHandlersAdded = true // Prevent leaflet from managing event
|
||||
if (!this._map.editEnabled) {
|
||||
this.feature.view(event)
|
||||
} else if (!this.feature.isReadOnly()) {
|
||||
if (event.originalEvent.shiftKey) {
|
||||
if (event.originalEvent.ctrlKey || event.originalEvent.metaKey) {
|
||||
this.feature.datalayer.edit(event)
|
||||
} else {
|
||||
if (this.feature._toggleEditing) this.feature._toggleEditing(event)
|
||||
else this.feature.edit(event)
|
||||
}
|
||||
} else if (!this._map.editTools?.drawing()) {
|
||||
new L.Toolbar.Popup(event.latlng, {
|
||||
className: 'leaflet-inplace-toolbar',
|
||||
anchor: this.getPopupToolbarAnchor(),
|
||||
actions: this.feature.getInplaceToolbarActions(event),
|
||||
}).addTo(this._map, this.feature, event.latlng)
|
||||
}
|
||||
}
|
||||
L.DomEvent.stop(event)
|
||||
},
|
||||
|
||||
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(false)
|
||||
this.feature.onCommit()
|
||||
},
|
||||
|
||||
getPopupToolbarAnchor: () => [0, 0],
|
||||
}
|
||||
|
||||
export const LeafletMarker = Marker.extend({
|
||||
parentClass: Marker,
|
||||
includes: [FeatureMixin],
|
||||
|
||||
initialize: function (feature) {
|
||||
FeatureMixin.initialize.call(this, feature)
|
||||
this.setIcon(this.getIcon())
|
||||
},
|
||||
|
||||
geometryChanged: function (sync = true) {
|
||||
this.feature.coordinates = this._latlng
|
||||
if (sync) {
|
||||
this.feature.sync.update('geometry', this.feature.geometry)
|
||||
}
|
||||
},
|
||||
|
||||
addInteractions() {
|
||||
FeatureMixin.addInteractions.call(this)
|
||||
this.on('dragend', (event) => {
|
||||
this.isDirty = true
|
||||
this.feature.edit(event)
|
||||
this.geometryChanged()
|
||||
})
|
||||
this.on('editable:drawing:commit', this.onCommit)
|
||||
if (!this.feature.isReadOnly()) this.on('mouseover', this._enableDragging)
|
||||
this.on('mouseout', this._onMouseOut)
|
||||
this._popupHandlersAdded = true // prevent Leaflet from binding event on bindPopup
|
||||
this.on('popupopen', this.highlight)
|
||||
this.on('popupclose', this.resetHighlight)
|
||||
},
|
||||
|
||||
_onMouseOut: function () {
|
||||
if (this.dragging?._draggable && !this.dragging._draggable._moving) {
|
||||
// Do not disable if the mouse went out while dragging
|
||||
this._disableDragging()
|
||||
}
|
||||
},
|
||||
|
||||
_enableDragging: function () {
|
||||
// TODO: start dragging after 1 second on mouse down
|
||||
if (this._map.editEnabled) {
|
||||
if (!this.editEnabled()) this.enableEdit()
|
||||
// Enabling dragging on the marker override the Draggable._OnDown
|
||||
// event, which, as it stopPropagation, refrain the call of
|
||||
// _onDown with map-pane element, which is responsible to
|
||||
// set the _moved to false, and thus to enable the click.
|
||||
// We should find a cleaner way to handle this.
|
||||
this._map.dragging._draggable._moved = false
|
||||
}
|
||||
},
|
||||
|
||||
_disableDragging: function () {
|
||||
if (this._map.editEnabled) {
|
||||
if (this.editor?.drawing) return // when creating a new marker, the mouse can trigger the mouseover/mouseout event
|
||||
// do not listen to them
|
||||
this.disableEdit()
|
||||
}
|
||||
},
|
||||
|
||||
_initIcon: function () {
|
||||
this.options.icon = this.getIcon()
|
||||
Marker.prototype._initIcon.call(this)
|
||||
// Allow to run code when icon is actually part of the DOM
|
||||
this.options.icon.onAdd()
|
||||
this.resetTooltip()
|
||||
},
|
||||
|
||||
getIconClass: function () {
|
||||
return this.feature.getOption('iconClass')
|
||||
},
|
||||
|
||||
getIcon: function () {
|
||||
const Class = U.Icon[this.getIconClass()] || U.Icon.Default
|
||||
return new Class({ feature: this.feature })
|
||||
},
|
||||
|
||||
_getTooltipAnchor: function () {
|
||||
const anchor = this.options.icon.options.tooltipAnchor.clone()
|
||||
const direction = this.feature.getOption('labelDirection')
|
||||
if (direction === 'left') {
|
||||
anchor.x *= -1
|
||||
} else if (direction === 'bottom') {
|
||||
anchor.x = 0
|
||||
anchor.y = 0
|
||||
} else if (direction === 'top') {
|
||||
anchor.x = 0
|
||||
}
|
||||
return anchor
|
||||
},
|
||||
|
||||
_redraw: function () {
|
||||
// May no be on the map when in a cluster.
|
||||
if (this._map) {
|
||||
this._initIcon()
|
||||
this.update()
|
||||
}
|
||||
},
|
||||
|
||||
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')
|
||||
},
|
||||
|
||||
getPopupToolbarAnchor: function () {
|
||||
return this.options.icon.options.popupAnchor
|
||||
},
|
||||
})
|
||||
|
||||
const PathMixin = {
|
||||
_onMouseOver: function () {
|
||||
if (this._map.measureTools?.enabled()) {
|
||||
this._map.tooltip.open({ content: this.feature.getMeasure(), anchor: this })
|
||||
} else if (this._map.editEnabled && !this._map.editedFeature) {
|
||||
this._map.tooltip.open({ content: translate('Click to edit'), anchor: this })
|
||||
}
|
||||
},
|
||||
|
||||
geometryChanged: function () {
|
||||
this.feature.coordinates = this._latlngs
|
||||
},
|
||||
|
||||
addInteractions: function () {
|
||||
FeatureMixin.addInteractions.call(this)
|
||||
this.on('editable:disable', this.onCommit)
|
||||
this.on('mouseover', this._onMouseOver)
|
||||
this.on('drag editable:drag', this._onDrag)
|
||||
this.on('popupopen', this.highlightPath)
|
||||
this.on('popupclose', this._redraw)
|
||||
},
|
||||
|
||||
highlightPath: function () {
|
||||
this.parentClass.prototype.setStyle.call(this, {
|
||||
fillOpacity: Math.sqrt(this.feature.getDynamicOption('fillOpacity', 1.0)),
|
||||
opacity: 1.0,
|
||||
weight: 1.3 * this.feature.getDynamicOption('weight'),
|
||||
})
|
||||
},
|
||||
|
||||
_onDrag: function () {
|
||||
if (this._tooltip) this._tooltip.setLatLng(this.getCenter())
|
||||
},
|
||||
|
||||
onAdd: function (map) {
|
||||
this._container = null
|
||||
this.setStyle()
|
||||
FeatureMixin.onAdd.call(this, map)
|
||||
if (this.editing?.enabled()) this.editing.addHooks()
|
||||
this.resetTooltip()
|
||||
this._path.dataset.feature = this.feature.id
|
||||
},
|
||||
|
||||
onRemove: function (map) {
|
||||
if (this.editing?.enabled()) this.editing.removeHooks()
|
||||
FeatureMixin.onRemove.call(this, map)
|
||||
},
|
||||
|
||||
setStyle: function (options = {}) {
|
||||
for (const option of this.feature.getStyleOptions()) {
|
||||
options[option] = this.feature.getDynamicOption(option)
|
||||
}
|
||||
options.pointerEvents = options.interactive ? 'visiblePainted' : 'stroke'
|
||||
this.parentClass.prototype.setStyle.call(this, options)
|
||||
},
|
||||
|
||||
_redraw: function () {
|
||||
this.setStyle()
|
||||
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.isolateShape(event.latlng)
|
||||
},
|
||||
})
|
||||
}
|
||||
return items
|
||||
},
|
||||
|
||||
isolateShape: function(atLatLng) {
|
||||
if (!this.feature.isMulti()) return
|
||||
const shape = this.enableEdit().deleteShapeAt(atLatLng)
|
||||
this.geometryChanged()
|
||||
this.disableEdit()
|
||||
if (!shape) return
|
||||
return this.feature.isolateShape(shape)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
})
|
|
@ -71,15 +71,13 @@ export class FeatureUpdater extends BaseUpdater {
|
|||
upsert({ metadata, value }) {
|
||||
const { id, layerId } = metadata
|
||||
const datalayer = this.getDataLayerFromID(layerId)
|
||||
let feature = this.getFeatureFromMetadata(metadata, value)
|
||||
const feature = this.getFeatureFromMetadata(metadata, value)
|
||||
|
||||
feature = datalayer.geoJSONToLeaflet({
|
||||
geometry: value.geometry,
|
||||
geojson: value,
|
||||
id,
|
||||
feature,
|
||||
})
|
||||
datalayer.addLayer(feature)
|
||||
if (feature) {
|
||||
feature.geometry = value.geometry
|
||||
} else {
|
||||
datalayer.makeFeature(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Update a property of an object
|
||||
|
@ -90,7 +88,8 @@ export class FeatureUpdater extends BaseUpdater {
|
|||
}
|
||||
if (key === 'geometry') {
|
||||
const datalayer = this.getDataLayerFromID(metadata.layerId)
|
||||
datalayer.geoJSONToLeaflet({ geometry: value, id: metadata.id, feature })
|
||||
const feature = this.getFeatureFromMetadata(metadata, value)
|
||||
feature.geometry = value
|
||||
} else {
|
||||
this.updateObjectValue(feature, key, value)
|
||||
feature.datalayer.indexProperties(feature)
|
||||
|
|
|
@ -89,15 +89,15 @@ export default class TableEditor extends WithTemplate {
|
|||
const bounds = this.map.getBounds()
|
||||
const inBbox = this.map.browser.options.inBbox
|
||||
let html = ''
|
||||
for (const feature of Object.values(this.datalayer._layers)) {
|
||||
if (feature.isFiltered()) continue
|
||||
if (inBbox && !feature.isOnScreen(bounds)) continue
|
||||
this.datalayer.eachFeature((feature) => {
|
||||
if (feature.isFiltered()) return
|
||||
if (inBbox && !feature.isOnScreen(bounds)) return
|
||||
const tds = this.properties.map(
|
||||
(prop) =>
|
||||
`<td tabindex="0" data-property="${prop}">${feature.properties[prop] || ''}</td>`
|
||||
)
|
||||
html += `<tr data-feature="${feature.id}"><th><input type="checkbox" /></th>${tds.join('')}</tr>`
|
||||
}
|
||||
})
|
||||
this.elements.body.innerHTML = html
|
||||
}
|
||||
|
||||
|
@ -125,7 +125,7 @@ export default class TableEditor extends WithTemplate {
|
|||
.prompt(translate('Please enter the new name of this property'))
|
||||
.then(({ prompt }) => {
|
||||
if (!prompt || !this.validateName(prompt)) return
|
||||
this.datalayer.eachLayer((feature) => {
|
||||
this.datalayer.eachFeature((feature) => {
|
||||
feature.renameProperty(property, prompt)
|
||||
})
|
||||
this.datalayer.deindexProperty(property)
|
||||
|
@ -140,7 +140,7 @@ export default class TableEditor extends WithTemplate {
|
|||
translate('Are you sure you want to delete this property on all the features?')
|
||||
)
|
||||
.then(() => {
|
||||
this.datalayer.eachLayer((feature) => {
|
||||
this.datalayer.eachFeature((feature) => {
|
||||
feature.deleteProperty(property)
|
||||
})
|
||||
this.datalayer.deindexProperty(property)
|
||||
|
|
|
@ -302,6 +302,10 @@ export function flattenCoordinates(coords) {
|
|||
return coords
|
||||
}
|
||||
|
||||
export function polygonMustBeFlattened(coords) {
|
||||
return coords.length === 1 && typeof coords?.[0]?.[0]?.[0] !== 'number'
|
||||
}
|
||||
|
||||
export function buildQueryString(params) {
|
||||
const query_string = []
|
||||
for (const key in params) {
|
||||
|
|
|
@ -142,7 +142,7 @@ U.AddPolylineShapeAction = U.BaseAction.extend({
|
|||
},
|
||||
|
||||
addHooks: function () {
|
||||
this.map.editedFeature.editor.newShape()
|
||||
this.map.editedFeature.ui.editor.newShape()
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -182,8 +182,8 @@ U.CreateHoleAction = U.BaseFeatureAction.extend({
|
|||
},
|
||||
},
|
||||
|
||||
onClick: function (e) {
|
||||
this.feature.startHole(e)
|
||||
onClick: function (event) {
|
||||
this.feature.ui.startHole(event)
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -195,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
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -244,7 +244,7 @@ U.ExtractShapeFromMultiAction = U.BaseFeatureAction.extend({
|
|||
},
|
||||
|
||||
onClick: function (e) {
|
||||
this.feature.isolateShape(e.latlng)
|
||||
this.feature.ui.isolateShape(e.latlng)
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -310,7 +310,7 @@ U.DrawToolbar = L.Toolbar.Control.extend({
|
|||
}
|
||||
if (this.map.options.enablePolylineDraw) {
|
||||
this.options.actions.push(U.DrawPolylineAction)
|
||||
if (this.map.editedFeature && this.map.editedFeature instanceof U.Polyline) {
|
||||
if (this.map.editedFeature && this.map.editedFeature instanceof U.LineString) {
|
||||
this.options.actions.push(U.AddPolylineShapeAction)
|
||||
}
|
||||
}
|
||||
|
@ -1022,7 +1022,7 @@ U.Search = L.PhotonSearch.extend({
|
|||
L.DomEvent.on(edit, 'mousedown', (e) => {
|
||||
L.DomEvent.stop(e)
|
||||
const datalayer = this.map.defaultEditDataLayer()
|
||||
const layer = datalayer.geojsonToFeatures(feature)
|
||||
const layer = datalayer.makeFeature(feature)
|
||||
layer.isDirty = true
|
||||
layer.edit()
|
||||
})
|
||||
|
@ -1145,56 +1145,79 @@ U.Editable = L.Editable.extend({
|
|||
initialize: function (map, options) {
|
||||
L.Editable.prototype.initialize.call(this, map, options)
|
||||
this.on('editable:drawing:click editable:drawing:move', this.drawingTooltip)
|
||||
this.on('editable:drawing:end', (e) => {
|
||||
this.on('editable:drawing:end', (event) => {
|
||||
this.map.tooltip.close()
|
||||
// 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
|
||||
if (!e.layer.hasGeom()) e.layer.del()
|
||||
else e.layer.edit()
|
||||
})
|
||||
// Layer for items added by users
|
||||
this.on('editable:drawing:cancel', (e) => {
|
||||
if (e.layer instanceof U.Marker) e.layer.del()
|
||||
})
|
||||
this.on('editable:drawing:commit', function (e) {
|
||||
e.layer.isDirty = true
|
||||
if (this.map.editedFeature !== e.layer) e.layer.edit(e)
|
||||
})
|
||||
this.on('editable:editing', (e) => {
|
||||
const layer = e.layer
|
||||
layer.isDirty = true
|
||||
if (layer._tooltip && layer.isTooltipOpen()) {
|
||||
layer._tooltip.setLatLng(layer.getCenter())
|
||||
layer._tooltip.update()
|
||||
if (!event.layer.feature.hasGeom()) {
|
||||
event.layer.feature.del()
|
||||
} else {
|
||||
event.layer.feature.edit()
|
||||
}
|
||||
})
|
||||
this.on('editable:vertex:ctrlclick', (e) => {
|
||||
const index = e.vertex.getIndex()
|
||||
if (index === 0 || (index === e.vertex.getLastIndex() && e.vertex.continue))
|
||||
e.vertex.continue()
|
||||
// Layer for items added by users
|
||||
this.on('editable:drawing:cancel', (event) => {
|
||||
if (event.layer instanceof U.LeafletMarker) event.layer.feature.del()
|
||||
})
|
||||
this.on('editable:vertex:altclick', (e) => {
|
||||
if (e.vertex.editor.vertexCanBeDeleted(e.vertex)) e.vertex.delete()
|
||||
this.on('editable:drawing:commit', function (event) {
|
||||
event.layer.feature.isDirty = true
|
||||
if (this.map.editedFeature !== event.layer) event.layer.feature.edit(event)
|
||||
})
|
||||
this.on('editable:editing', (event) => {
|
||||
const layer = event.layer
|
||||
layer.feature.isDirty = true
|
||||
if (layer instanceof L.Marker) {
|
||||
layer.feature.coordinates = layer._latlng
|
||||
} else {
|
||||
layer.feature.coordinates = layer._latlngs
|
||||
}
|
||||
// if (layer._tooltip && layer.isTooltipOpen()) {
|
||||
// layer._tooltip.setLatLng(layer.getCenter())
|
||||
// layer._tooltip.update()
|
||||
// }
|
||||
})
|
||||
this.on('editable:vertex:ctrlclick', (event) => {
|
||||
const index = event.vertex.getIndex()
|
||||
if (
|
||||
index === 0 ||
|
||||
(index === event.vertex.getLastIndex() && event.vertex.continue)
|
||||
)
|
||||
event.vertex.continue()
|
||||
})
|
||||
this.on('editable:vertex:altclick', (event) => {
|
||||
if (event.vertex.editor.vertexCanBeDeleted(event.vertex)) event.vertex.delete()
|
||||
})
|
||||
this.on('editable:vertex:rawclick', this.onVertexRawClick)
|
||||
},
|
||||
|
||||
createPolyline: function (latlngs) {
|
||||
return new U.Polyline(this.map, latlngs, this._getDefaultProperties())
|
||||
const datalayer = this.map.defaultEditDataLayer()
|
||||
const point = new U.LineString(datalayer, {
|
||||
geometry: { type: 'LineString', coordinates: [] },
|
||||
})
|
||||
return point.ui
|
||||
},
|
||||
|
||||
createPolygon: function (latlngs) {
|
||||
return new U.Polygon(this.map, latlngs, this._getDefaultProperties())
|
||||
const datalayer = this.map.defaultEditDataLayer()
|
||||
const point = new U.Polygon(datalayer, {
|
||||
geometry: { type: 'Polygon', coordinates: [] },
|
||||
})
|
||||
return point.ui
|
||||
},
|
||||
|
||||
createMarker: function (latlng) {
|
||||
return new U.Marker(this.map, latlng, this._getDefaultProperties())
|
||||
const datalayer = this.map.defaultEditDataLayer()
|
||||
const point = new U.Point(datalayer, {
|
||||
geometry: { type: 'Point', coordinates: [latlng.lng, latlng.lat] },
|
||||
})
|
||||
return point.ui
|
||||
},
|
||||
|
||||
_getDefaultProperties: function () {
|
||||
const result = {}
|
||||
if (this.map.options.featuresHaveOwner && this.map.options.hasOwnProperty('user')) {
|
||||
if (this.map.options.featuresHaveOwner?.user) {
|
||||
result.geojson = { properties: { owner: this.map.options.user.id } }
|
||||
}
|
||||
return result
|
||||
|
@ -1203,7 +1226,7 @@ U.Editable = L.Editable.extend({
|
|||
connectCreatedToMap: function (layer) {
|
||||
// Overrided from Leaflet.Editable
|
||||
const datalayer = this.map.defaultEditDataLayer()
|
||||
datalayer.addLayer(layer)
|
||||
datalayer.addFeature(layer.feature)
|
||||
layer.isDirty = true
|
||||
return layer
|
||||
},
|
||||
|
@ -1231,7 +1254,7 @@ U.Editable = L.Editable.extend({
|
|||
} else {
|
||||
const tmpLatLngs = e.layer.editor._drawnLatLngs.slice()
|
||||
tmpLatLngs.push(e.latlng)
|
||||
measure = e.layer.getMeasure(tmpLatLngs)
|
||||
measure = e.layer.feature.getMeasure(tmpLatLngs)
|
||||
|
||||
if (e.layer.editor._drawnLatLngs.length < e.layer.editor.MIN_VERTEX) {
|
||||
// when drawing second point
|
||||
|
@ -1243,7 +1266,7 @@ U.Editable = L.Editable.extend({
|
|||
}
|
||||
} else {
|
||||
// when moving an existing point
|
||||
measure = e.layer.getMeasure()
|
||||
measure = e.layer.feature.getMeasure()
|
||||
}
|
||||
if (measure) {
|
||||
if (e.layer instanceof L.Polygon) {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -360,7 +360,6 @@ L.FormBuilder.DataLayerSwitcher = L.FormBuilder.Select.extend({
|
|||
getOptions: function () {
|
||||
const options = []
|
||||
this.builder.map.eachDataLayerReverse((datalayer) => {
|
||||
console.log(datalayer.isLoaded(), datalayer.isDataReadOnly(), datalayer.isBrowsable())
|
||||
if (
|
||||
datalayer.isLoaded() &&
|
||||
!datalayer.isDataReadOnly() &&
|
||||
|
|
|
@ -2,11 +2,10 @@ U.Icon = L.DivIcon.extend({
|
|||
statics: {
|
||||
RECENT: [],
|
||||
},
|
||||
initialize: function (map, options) {
|
||||
this.map = map
|
||||
initialize: function (options) {
|
||||
const default_options = {
|
||||
iconSize: null, // Made in css
|
||||
iconUrl: this.map.getDefaultOption('iconUrl'),
|
||||
iconUrl: U.SCHEMA.iconUrl.default,
|
||||
feature: null,
|
||||
}
|
||||
options = L.Util.extend({}, default_options, options)
|
||||
|
@ -40,13 +39,13 @@ U.Icon = L.DivIcon.extend({
|
|||
let color
|
||||
if (this.feature) color = this.feature.getDynamicOption('color')
|
||||
else if (this.options.color) color = this.options.color
|
||||
else color = this.map.getDefaultOption('color')
|
||||
else color = U.SCHEMA.color.default
|
||||
return color
|
||||
},
|
||||
|
||||
_getOpacity: function () {
|
||||
if (this.feature) return this.feature.getOption('iconOpacity')
|
||||
return this.map.getDefaultOption('iconOpacity')
|
||||
return U.SCHEMA.iconOpacity.default
|
||||
},
|
||||
|
||||
formatUrl: (url, feature) =>
|
||||
|
@ -63,9 +62,9 @@ U.Icon.Default = U.Icon.extend({
|
|||
className: 'umap-div-icon',
|
||||
},
|
||||
|
||||
initialize: function (map, options) {
|
||||
initialize: function (options) {
|
||||
options = L.Util.extend({}, this.default_options, options)
|
||||
U.Icon.prototype.initialize.call(this, map, options)
|
||||
U.Icon.prototype.initialize.call(this, options)
|
||||
},
|
||||
|
||||
_setIconStyles: function (img, name) {
|
||||
|
@ -92,6 +91,7 @@ U.Icon.Default = U.Icon.extend({
|
|||
'icon_container',
|
||||
this.elements.main
|
||||
)
|
||||
this.elements.main.dataset.feature = this.feature?.id
|
||||
this.elements.arrow = L.DomUtil.create('div', 'icon_arrow', this.elements.main)
|
||||
const src = this._getIconUrl('icon')
|
||||
if (src) {
|
||||
|
@ -103,14 +103,14 @@ U.Icon.Default = U.Icon.extend({
|
|||
})
|
||||
|
||||
U.Icon.Circle = U.Icon.extend({
|
||||
initialize: function (map, options) {
|
||||
initialize: function (options) {
|
||||
const default_options = {
|
||||
popupAnchor: new L.Point(0, -6),
|
||||
tooltipAnchor: new L.Point(6, 0),
|
||||
className: 'umap-circle-icon',
|
||||
}
|
||||
options = L.Util.extend({}, default_options, options)
|
||||
U.Icon.prototype.initialize.call(this, map, options)
|
||||
U.Icon.prototype.initialize.call(this, options)
|
||||
},
|
||||
|
||||
_setIconStyles: function (img, name) {
|
||||
|
@ -124,6 +124,7 @@ U.Icon.Circle = U.Icon.extend({
|
|||
this.elements.main = L.DomUtil.create('div')
|
||||
this.elements.main.innerHTML = ' '
|
||||
this._setIconStyles(this.elements.main, 'icon')
|
||||
this.elements.main.dataset.feature = this.feature?.id
|
||||
return this.elements.main
|
||||
},
|
||||
})
|
||||
|
@ -153,6 +154,7 @@ U.Icon.Ball = U.Icon.Default.extend({
|
|||
'icon_container',
|
||||
this.elements.main
|
||||
)
|
||||
this.elements.main.dataset.feature = this.feature?.id
|
||||
this.elements.arrow = L.DomUtil.create('div', 'icon_arrow', this.elements.main)
|
||||
this._setIconStyles(this.elements.main, 'icon')
|
||||
return this.elements.main
|
||||
|
|
|
@ -193,7 +193,7 @@ U.Map = L.Map.extend({
|
|||
window.onbeforeunload = () => (this.editEnabled && this.isDirty) || null
|
||||
this.backup()
|
||||
this.initContextMenu()
|
||||
this.on('click contextmenu.show', this.closeInplaceToolbar)
|
||||
this.on('click', this.closeInplaceToolbar)
|
||||
},
|
||||
|
||||
initSyncEngine: async function () {
|
||||
|
@ -863,7 +863,7 @@ U.Map = L.Map.extend({
|
|||
},
|
||||
|
||||
eachFeature: function (callback, context) {
|
||||
this.eachDataLayer((datalayer) => {
|
||||
this.eachBrowsableDataLayer((datalayer) => {
|
||||
if (datalayer.isVisible()) datalayer.eachFeature(callback, context)
|
||||
})
|
||||
},
|
||||
|
@ -1555,6 +1555,7 @@ U.Map = L.Map.extend({
|
|||
this.editPanel.close()
|
||||
this.fullPanel.close()
|
||||
this.sync.stop()
|
||||
this.closeInplaceToolbar()
|
||||
},
|
||||
|
||||
hasEditMode: function () {
|
||||
|
@ -1832,6 +1833,14 @@ U.Map = L.Map.extend({
|
|||
return url
|
||||
},
|
||||
|
||||
getFeatureById: function (id) {
|
||||
let feature
|
||||
for (const datalayer of Object.values(this.datalayers)) {
|
||||
feature = datalayer.getFeatureById(id)
|
||||
if (feature) return feature
|
||||
}
|
||||
},
|
||||
|
||||
closeInplaceToolbar: function () {
|
||||
const toolbar = this._toolbars[L.Toolbar.Popup._toolbar_class_id]
|
||||
if (toolbar) toolbar.remove()
|
||||
|
|
|
@ -479,6 +479,113 @@ describe('Utils', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('#polygonMustBeFlattened', () => {
|
||||
it('should return false for simple polygon', () => {
|
||||
const coords = [
|
||||
[
|
||||
[100.0, 0.0],
|
||||
[101.0, 0.0],
|
||||
[101.0, 1.0],
|
||||
[100.0, 1.0],
|
||||
[100.0, 0.0],
|
||||
],
|
||||
]
|
||||
assert.notOk(Utils.polygonMustBeFlattened(coords))
|
||||
})
|
||||
|
||||
it('should return false for simple polygon with hole', () => {
|
||||
const coords = [
|
||||
[
|
||||
[100.0, 0.0],
|
||||
[101.0, 0.0],
|
||||
[101.0, 1.0],
|
||||
[100.0, 1.0],
|
||||
[100.0, 0.0],
|
||||
],
|
||||
[
|
||||
[100.8, 0.8],
|
||||
[100.8, 0.2],
|
||||
[100.2, 0.2],
|
||||
[100.2, 0.8],
|
||||
[100.8, 0.8],
|
||||
],
|
||||
]
|
||||
assert.notOk(Utils.polygonMustBeFlattened(coords))
|
||||
})
|
||||
|
||||
it('should return false for multipolygon', () => {
|
||||
const coords = [
|
||||
[
|
||||
[
|
||||
[102.0, 2.0],
|
||||
[103.0, 2.0],
|
||||
[103.0, 3.0],
|
||||
[102.0, 3.0],
|
||||
[102.0, 2.0],
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
[100.0, 0.0],
|
||||
[101.0, 0.0],
|
||||
[101.0, 1.0],
|
||||
[100.0, 1.0],
|
||||
[100.0, 0.0],
|
||||
],
|
||||
[
|
||||
[100.2, 0.2],
|
||||
[100.2, 0.8],
|
||||
[100.8, 0.8],
|
||||
[100.8, 0.2],
|
||||
[100.2, 0.2],
|
||||
],
|
||||
],
|
||||
]
|
||||
assert.notOk(Utils.polygonMustBeFlattened(coords))
|
||||
})
|
||||
|
||||
it('should return true for false multi polygon', () => {
|
||||
const coords = [
|
||||
[
|
||||
[
|
||||
[100.0, 0.0],
|
||||
[101.0, 0.0],
|
||||
[101.0, 1.0],
|
||||
[100.0, 1.0],
|
||||
[100.0, 0.0],
|
||||
],
|
||||
],
|
||||
]
|
||||
assert.ok(Utils.polygonMustBeFlattened(coords))
|
||||
})
|
||||
|
||||
it('should return true for false multi polygon with hole', () => {
|
||||
const coords = [
|
||||
[
|
||||
[
|
||||
[100.0, 0.0],
|
||||
[101.0, 0.0],
|
||||
[101.0, 1.0],
|
||||
[100.0, 1.0],
|
||||
[100.0, 0.0],
|
||||
],
|
||||
[
|
||||
[100.8, 0.8],
|
||||
[100.8, 0.2],
|
||||
[100.2, 0.2],
|
||||
[100.2, 0.8],
|
||||
[100.8, 0.8],
|
||||
],
|
||||
],
|
||||
]
|
||||
assert.ok(Utils.polygonMustBeFlattened(coords))
|
||||
})
|
||||
|
||||
it('should return false for empty coords', () => {
|
||||
assert.notOk(Utils.polygonMustBeFlattened([]))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#usableOption()', () => {
|
||||
it('should consider false', () => {
|
||||
assert.ok(Utils.usableOption({ key: false }, 'key'))
|
||||
|
|
|
@ -46,7 +46,6 @@
|
|||
<script src="{% static 'umap/js/umap.popup.js' %}" defer></script>
|
||||
<script src="{% static 'umap/js/umap.forms.js' %}" defer></script>
|
||||
<script src="{% static 'umap/js/umap.icon.js' %}" defer></script>
|
||||
<script src="{% static 'umap/js/umap.features.js' %}" defer></script>
|
||||
<script src="{% static 'umap/js/umap.controls.js' %}" defer></script>
|
||||
<script src="{% static 'umap/js/umap.js' %}" defer></script>
|
||||
<script src="{% static 'umap/js/components/fragment.js' %}" defer></script>
|
||||
|
|
|
@ -27,11 +27,25 @@ def mock_osm_tiles(page):
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def page(context):
|
||||
def new_page(context):
|
||||
def make_page(prefix="console"):
|
||||
page = context.new_page()
|
||||
page.on("console", lambda msg: print(msg.text) if msg.type != "warning" else None)
|
||||
page.on(
|
||||
"console",
|
||||
lambda msg: print(f"{prefix}: {msg.text}")
|
||||
if msg.type != "warning"
|
||||
else None,
|
||||
)
|
||||
page.on("pageerror", lambda exc: print(f"{prefix} uncaught exception: {exc}"))
|
||||
return page
|
||||
|
||||
yield make_page
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def page(new_page):
|
||||
return new_page()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def login(context, settings, live_server):
|
||||
|
|
|
@ -243,7 +243,8 @@ def test_can_transfer_shape_from_simple_polygon(live_server, page, tilelayer):
|
|||
expect(polygons).to_have_count(1)
|
||||
|
||||
|
||||
def test_can_extract_shape(live_server, page, tilelayer):
|
||||
def test_can_extract_shape(live_server, page, tilelayer, settings):
|
||||
settings.UMAP_ALLOW_ANONYMOUS = True
|
||||
page.goto(f"{live_server.url}/en/map/new/")
|
||||
polygons = page.locator(".leaflet-overlay-pane path")
|
||||
expect(polygons).to_have_count(0)
|
||||
|
@ -269,6 +270,58 @@ def test_can_extract_shape(live_server, page, tilelayer):
|
|||
polygons.first.click(position={"x": 20, "y": 20}, button="right")
|
||||
extract_button.click()
|
||||
expect(polygons).to_have_count(2)
|
||||
data = save_and_get_json(page)
|
||||
assert len(data["features"]) == 2
|
||||
assert data["features"][0]["geometry"]["type"] == "Polygon"
|
||||
assert data["features"][1]["geometry"]["type"] == "Polygon"
|
||||
assert data["features"][0]["geometry"]["coordinates"] == [
|
||||
[
|
||||
[
|
||||
-6.569824,
|
||||
53.159947,
|
||||
],
|
||||
[
|
||||
-6.569824,
|
||||
52.49616,
|
||||
],
|
||||
[
|
||||
-7.668457,
|
||||
52.49616,
|
||||
],
|
||||
[
|
||||
-7.668457,
|
||||
53.159947,
|
||||
],
|
||||
[
|
||||
-6.569824,
|
||||
53.159947,
|
||||
],
|
||||
],
|
||||
]
|
||||
assert data["features"][1]["geometry"]["coordinates"] == [
|
||||
[
|
||||
[
|
||||
-8.76709,
|
||||
54.457267,
|
||||
],
|
||||
[
|
||||
-8.76709,
|
||||
53.813626,
|
||||
],
|
||||
[
|
||||
-9.865723,
|
||||
53.813626,
|
||||
],
|
||||
[
|
||||
-9.865723,
|
||||
54.457267,
|
||||
],
|
||||
[
|
||||
-8.76709,
|
||||
54.457267,
|
||||
],
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
def test_cannot_transfer_shape_to_line(live_server, page, tilelayer):
|
||||
|
|
|
@ -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,19 @@ 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": [
|
||||
|
|
|
@ -12,7 +12,7 @@ DATALAYER_UPDATE = re.compile(r".*/datalayer/update/.*")
|
|||
|
||||
@pytest.mark.xdist_group(name="websockets")
|
||||
def test_websocket_connection_can_sync_markers(
|
||||
context, live_server, websocket_server, tilelayer
|
||||
new_page, live_server, websocket_server, tilelayer
|
||||
):
|
||||
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
|
||||
map.settings["properties"]["syncEnabled"] = True
|
||||
|
@ -20,9 +20,9 @@ def test_websocket_connection_can_sync_markers(
|
|||
DataLayerFactory(map=map, data={})
|
||||
|
||||
# Create two tabs
|
||||
peerA = context.new_page()
|
||||
peerA = new_page("Page A")
|
||||
peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
|
||||
peerB = context.new_page()
|
||||
peerB = new_page("Page B")
|
||||
peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
|
||||
|
||||
a_marker_pane = peerA.locator(".leaflet-marker-pane > div")
|
||||
|
@ -60,12 +60,12 @@ def test_websocket_connection_can_sync_markers(
|
|||
expect(b_marker_pane).to_have_count(2)
|
||||
|
||||
# Drag a marker on peer B and check that it moved on peer A
|
||||
a_first_marker.bounding_box() == b_first_marker.bounding_box()
|
||||
assert a_first_marker.bounding_box() == b_first_marker.bounding_box()
|
||||
b_old_bbox = b_first_marker.bounding_box()
|
||||
b_first_marker.drag_to(b_map_el, target_position={"x": 250, "y": 250})
|
||||
|
||||
assert b_old_bbox is not b_first_marker.bounding_box()
|
||||
a_first_marker.bounding_box() == b_first_marker.bounding_box()
|
||||
assert a_first_marker.bounding_box() == b_first_marker.bounding_box()
|
||||
|
||||
# Delete a marker from peer A and check it's been deleted on peer B
|
||||
a_first_marker.click(button="right")
|
||||
|
|
Loading…
Reference in a new issue