mirror of
https://github.com/umap-project/umap.git
synced 2025-04-29 03:42:37 +02:00

This commit also introduce a new settings UMAP_SCHEMA, that could be used to easily override schema default, like the default color, the default path weigth and so on. I'm not documenting yet, because I'm not yet totally sure we want this.
1228 lines
34 KiB
JavaScript
1228 lines
34 KiB
JavaScript
U.FeatureMixin = {
|
|
staticOptions: { mainColor: 'color' },
|
|
|
|
initialize: function (map, latlng, options) {
|
|
this.map = map
|
|
if (typeof options === 'undefined') {
|
|
options = {}
|
|
}
|
|
// DataLayer the marker belongs to
|
|
this.datalayer = options.datalayer || null
|
|
this.properties = { _umap_options: {} }
|
|
let geojson_id
|
|
if (options.geojson) {
|
|
this.populate(options.geojson)
|
|
geojson_id = options.geojson.id
|
|
}
|
|
|
|
// Each feature needs an unique identifier
|
|
if (U.Utils.checkId(geojson_id)) {
|
|
this.id = geojson_id
|
|
} else {
|
|
this.id = U.Utils.generateId()
|
|
}
|
|
let isDirty = false
|
|
const self = this
|
|
try {
|
|
Object.defineProperty(this, 'isDirty', {
|
|
get: function () {
|
|
return isDirty
|
|
},
|
|
set: function (status) {
|
|
if (!isDirty && status) {
|
|
self.fire('isdirty')
|
|
}
|
|
isDirty = status
|
|
if (self.datalayer) {
|
|
self.datalayer.isDirty = status
|
|
}
|
|
},
|
|
})
|
|
} catch (e) {
|
|
// Certainly IE8, which has a limited version of defineProperty
|
|
}
|
|
this.preInit()
|
|
this.addInteractions()
|
|
this.parentClass.prototype.initialize.call(this, latlng, options)
|
|
},
|
|
|
|
preInit: function () {},
|
|
|
|
isReadOnly: function () {
|
|
return this.datalayer && this.datalayer.isDataReadOnly()
|
|
},
|
|
|
|
getSlug: function () {
|
|
return this.properties[this.map.getOption('slugKey') || 'name'] || ''
|
|
},
|
|
|
|
getPermalink: function () {
|
|
const slug = this.getSlug()
|
|
if (slug)
|
|
return `${L.Util.getBaseUrl()}?${L.Util.buildQueryString({ feature: slug })}${
|
|
window.location.hash
|
|
}`
|
|
},
|
|
|
|
view: function (e) {
|
|
if (this.map.editEnabled) return
|
|
const outlink = this.getOption('outlink'),
|
|
target = this.getOption('outlinkTarget')
|
|
if (outlink) {
|
|
switch (target) {
|
|
case 'self':
|
|
window.location = outlink
|
|
break
|
|
case 'parent':
|
|
window.top.location = outlink
|
|
break
|
|
default:
|
|
const win = 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.openPopup((e && e.latlng) || this.getCenter())
|
|
},
|
|
|
|
openPopup: function () {
|
|
if (this.map.editEnabled) return
|
|
this.parentClass.prototype.openPopup.apply(this, arguments)
|
|
},
|
|
|
|
edit: function (e) {
|
|
if (!this.map.editEnabled || this.isReadOnly()) return
|
|
const container = L.DomUtil.create('div', 'umap-feature-container')
|
|
L.DomUtil.add(
|
|
'h3',
|
|
`umap-feature-properties ${this.getClassName()}`,
|
|
container,
|
|
L._('Feature properties')
|
|
)
|
|
|
|
let builder = new U.FormBuilder(this, ['datalayer'], {
|
|
callback: function () {
|
|
this.edit(e)
|
|
}, // removeLayer step will close the edit panel, let's reopen it
|
|
})
|
|
container.appendChild(builder.build())
|
|
|
|
const properties = []
|
|
let property
|
|
for (let i = 0; i < this.datalayer._propertiesIndex.length; i++) {
|
|
property = this.datalayer._propertiesIndex[i]
|
|
if (L.Util.indexOf(['name', 'description'], property) !== -1) {
|
|
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',
|
|
callback: this._redraw, // In case we have dynamic options…
|
|
})
|
|
container.appendChild(builder.build())
|
|
this.map.ui.once('panel:ready', () => {
|
|
builder.helpers['properties.name'].input.focus()
|
|
})
|
|
this.appendEditFieldsets(container)
|
|
const advancedActions = L.DomUtil.createFieldset(container, L._('Advanced actions'))
|
|
this.getAdvancedEditActions(advancedActions)
|
|
this.map.ui.openPanel({ data: { html: container }, className: 'dark' })
|
|
this.map.editedFeature = this
|
|
if (!this.isOnScreen()) this.zoomTo(e)
|
|
},
|
|
|
|
getAdvancedEditActions: function (container) {
|
|
const deleteButton = L.DomUtil.createButton(
|
|
'button umap-delete',
|
|
container,
|
|
L._('Delete'),
|
|
function (e) {
|
|
L.DomEvent.stop(e)
|
|
if (this.confirmDelete()) this.map.ui.closePanel()
|
|
},
|
|
this
|
|
)
|
|
},
|
|
|
|
appendEditFieldsets: function (container) {
|
|
const optionsFields = this.getShapeOptions()
|
|
let builder = new U.FormBuilder(this, optionsFields, {
|
|
id: 'umap-feature-shape-properties',
|
|
callback: this._redraw,
|
|
})
|
|
const shapeProperties = L.DomUtil.createFieldset(container, L._('Shape properties'))
|
|
shapeProperties.appendChild(builder.build())
|
|
|
|
const advancedOptions = this.getAdvancedOptions()
|
|
builder = new U.FormBuilder(this, advancedOptions, {
|
|
id: 'umap-feature-advanced-properties',
|
|
callback: this._redraw,
|
|
})
|
|
const advancedProperties = L.DomUtil.createFieldset(
|
|
container,
|
|
L._('Advanced properties')
|
|
)
|
|
advancedProperties.appendChild(builder.build())
|
|
|
|
const interactionOptions = this.getInteractionOptions()
|
|
builder = new U.FormBuilder(this, interactionOptions, {
|
|
callback: this._redraw,
|
|
})
|
|
const popupFieldset = L.DomUtil.createFieldset(
|
|
container,
|
|
L._('Interaction options')
|
|
)
|
|
popupFieldset.appendChild(builder.build())
|
|
},
|
|
|
|
getInteractionOptions: function () {
|
|
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: function () {},
|
|
|
|
getDisplayName: function (fallback) {
|
|
if (fallback === undefined) fallback = this.datalayer.options.name
|
|
const key = this.getOption('labelKey') || 'name'
|
|
// Variables mode.
|
|
if (L.Util.hasVar(key)) return L.Util.greedyTemplate(key, this.extendedProperties())
|
|
// Simple mode.
|
|
return this.properties[key] || this.properties.title || fallback
|
|
},
|
|
|
|
hasPopupFooter: function () {
|
|
if (L.Browser.ielt9) return false
|
|
if (this.datalayer.isRemoteLayer() && this.datalayer.options.remoteData.dynamic)
|
|
return false
|
|
return this.map.getOption('displayPopupFooter')
|
|
},
|
|
|
|
getPopupClass: function () {
|
|
const old = this.getOption('popupTemplate') // Retrocompat.
|
|
return U.Popup[this.getOption('popupShape') || old] || U.Popup
|
|
},
|
|
|
|
attachPopup: function () {
|
|
const Class = this.getPopupClass()
|
|
this.bindPopup(new Class(this))
|
|
},
|
|
|
|
confirmDelete: function () {
|
|
if (confirm(L._('Are you sure you want to delete the feature?'))) {
|
|
this.del()
|
|
return true
|
|
}
|
|
return false
|
|
},
|
|
|
|
del: function () {
|
|
this.isDirty = true
|
|
this.map.closePopup()
|
|
if (this.datalayer) {
|
|
this.datalayer.removeLayer(this)
|
|
this.disconnectFromDataLayer(this.datalayer)
|
|
}
|
|
},
|
|
|
|
connectToDataLayer: function (datalayer) {
|
|
this.datalayer = datalayer
|
|
this.options.renderer = this.datalayer.renderer
|
|
},
|
|
|
|
disconnectFromDataLayer: function (datalayer) {
|
|
if (this.datalayer === datalayer) {
|
|
this.datalayer = null
|
|
}
|
|
},
|
|
|
|
cleanProperty: function ([key, value]) {
|
|
// dot in key will break the dot based property access
|
|
// while editing the feature
|
|
key = key.replace('.', '_')
|
|
return [key, value]
|
|
},
|
|
|
|
populate: function (feature) {
|
|
this.properties = Object.fromEntries(
|
|
Object.entries(feature.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: function (datalayer) {
|
|
if (this.datalayer) {
|
|
this.datalayer.isDirty = true
|
|
this.datalayer.removeLayer(this)
|
|
}
|
|
datalayer.addLayer(this)
|
|
datalayer.isDirty = true
|
|
this._redraw()
|
|
},
|
|
|
|
getOption: function (option, fallback) {
|
|
let value = fallback
|
|
if (typeof this.staticOptions[option] !== 'undefined') {
|
|
value = this.staticOptions[option]
|
|
} else if (L.Util.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: function (option, fallback) {
|
|
let value = this.getOption(option, fallback)
|
|
// There is a variable inside.
|
|
if (L.Util.hasVar(value)) {
|
|
value = L.Util.greedyTemplate(value, this.properties, true)
|
|
if (L.Util.hasVar(value)) value = this.map.getDefaultOption(option)
|
|
}
|
|
return value
|
|
},
|
|
|
|
zoomTo: function (e) {
|
|
e = e || {}
|
|
const easing = e.easing !== undefined ? e.easing : this.map.getOption('easing')
|
|
if (easing) {
|
|
this.map.flyTo(this.getCenter(), this.getBestZoom())
|
|
} else {
|
|
const latlng = e.latlng || this.getCenter()
|
|
this.map.setView(latlng, this.getBestZoom() || this.map.getZoom())
|
|
}
|
|
if (e.callback) e.callback.call(this)
|
|
},
|
|
|
|
getBestZoom: function () {
|
|
return this.getOption('zoomTo')
|
|
},
|
|
|
|
getNext: function () {
|
|
return this.datalayer.getNextFeature(this)
|
|
},
|
|
|
|
getPrevious: function () {
|
|
return this.datalayer.getPreviousFeature(this)
|
|
},
|
|
|
|
cloneProperties: function () {
|
|
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
|
|
}
|
|
return properties
|
|
},
|
|
|
|
deleteProperty: function (property) {
|
|
delete this.properties[property]
|
|
this.makeDirty()
|
|
},
|
|
|
|
renameProperty: function (from, to) {
|
|
this.properties[to] = this.properties[from]
|
|
this.deleteProperty(from)
|
|
},
|
|
|
|
toGeoJSON: function () {
|
|
const geojson = this.parentClass.prototype.toGeoJSON.call(this)
|
|
geojson.properties = this.cloneProperties()
|
|
geojson.id = this.id
|
|
delete geojson.properties._storage_options
|
|
return geojson
|
|
},
|
|
|
|
addInteractions: function () {
|
|
this.on('contextmenu editable:vertex:contextmenu', this._showContextMenu, this)
|
|
this.on('click', this._onClick)
|
|
},
|
|
|
|
_onClick: function (e) {
|
|
if (this.map.measureTools && this.map.measureTools.enabled()) return
|
|
this._popupHandlersAdded = true // Prevent leaflet from managing event
|
|
if (!this.map.editEnabled) {
|
|
this.view(e)
|
|
} else if (!this.isReadOnly()) {
|
|
if (e.originalEvent.shiftKey) {
|
|
if (e.originalEvent.ctrlKey || e.originalEvent.metaKey) {
|
|
this.datalayer.edit(e)
|
|
} else {
|
|
if (this._toggleEditing) this._toggleEditing(e)
|
|
else this.edit(e)
|
|
}
|
|
} else {
|
|
new L.Toolbar.Popup(e.latlng, {
|
|
className: 'leaflet-inplace-toolbar',
|
|
anchor: this.getPopupToolbarAnchor(),
|
|
actions: this.getInplaceToolbarActions(e),
|
|
}).addTo(this.map, this, e.latlng)
|
|
}
|
|
}
|
|
L.DomEvent.stop(e)
|
|
},
|
|
|
|
getPopupToolbarAnchor: function () {
|
|
return [0, 0]
|
|
},
|
|
|
|
getInplaceToolbarActions: function (e) {
|
|
return [U.ToggleEditAction, U.DeleteFeatureAction]
|
|
},
|
|
|
|
_showContextMenu: function (e) {
|
|
L.DomEvent.stop(e)
|
|
const pt = this.map.mouseEventToContainerPoint(e.originalEvent)
|
|
e.relatedTarget = this
|
|
this.map.contextmenu.showAt(pt, e)
|
|
},
|
|
|
|
makeDirty: function () {
|
|
this.isDirty = true
|
|
},
|
|
|
|
getMap: function () {
|
|
return this.map
|
|
},
|
|
|
|
getContextMenuItems: function (e) {
|
|
const permalink = this.getPermalink()
|
|
let items = []
|
|
if (permalink)
|
|
items.push({
|
|
text: L._('Permalink'),
|
|
callback: function () {
|
|
window.open(permalink)
|
|
},
|
|
})
|
|
if (this.map.editEnabled && !this.isReadOnly()) {
|
|
items = items.concat(this.getContextMenuEditItems(e))
|
|
}
|
|
return items
|
|
},
|
|
|
|
getContextMenuEditItems: function () {
|
|
let items = ['-']
|
|
if (this.map.editedFeature !== this) {
|
|
items.push({
|
|
text: L._('Edit this feature') + ' (⇧+Click)',
|
|
callback: this.edit,
|
|
context: this,
|
|
iconCls: 'umap-edit',
|
|
})
|
|
}
|
|
items = items.concat(
|
|
{
|
|
text: L._("Edit feature's layer") + ' (Ctrl+⇧+Click)',
|
|
callback: this.datalayer.edit,
|
|
context: this.datalayer,
|
|
iconCls: 'umap-edit',
|
|
},
|
|
{
|
|
text: L._('Delete this feature'),
|
|
callback: this.confirmDelete,
|
|
context: this,
|
|
iconCls: 'umap-delete',
|
|
},
|
|
{
|
|
text: L._('Clone this feature'),
|
|
callback: this.clone,
|
|
context: this,
|
|
}
|
|
)
|
|
return items
|
|
},
|
|
|
|
onRemove: function (map) {
|
|
this.parentClass.prototype.onRemove.call(this, map)
|
|
if (this.map.editedFeature === this) {
|
|
this.endEdit()
|
|
this.map.ui.closePanel()
|
|
}
|
|
},
|
|
|
|
resetTooltip: function () {
|
|
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(L.Util.escapeHTML(displayName), options)
|
|
},
|
|
|
|
matchFilter: function (filter, keys) {
|
|
filter = filter.toLowerCase()
|
|
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: function () {
|
|
const facets = this.map.facets
|
|
for (const [property, expected] of Object.entries(facets)) {
|
|
if (expected.length) {
|
|
let value = this.properties[property]
|
|
if (!value || !expected.includes(value)) return false
|
|
}
|
|
}
|
|
return true
|
|
},
|
|
|
|
onVertexRawClick: function (e) {
|
|
new L.Toolbar.Popup(e.latlng, {
|
|
className: 'leaflet-inplace-toolbar',
|
|
actions: this.getVertexActions(e),
|
|
}).addTo(this.map, this, e.latlng, e.vertex)
|
|
},
|
|
|
|
getVertexActions: function () {
|
|
return [U.DeleteVertexAction]
|
|
},
|
|
|
|
isMulti: function () {
|
|
return false
|
|
},
|
|
|
|
clone: function () {
|
|
const layer = this.datalayer.geojsonToFeatures(this.toGeoJSON())
|
|
layer.isDirty = true
|
|
layer.edit()
|
|
return layer
|
|
},
|
|
|
|
extendedProperties: function () {
|
|
// Include context properties
|
|
properties = this.map.getGeoContext()
|
|
if (L.locale) properties.locale = L.locale
|
|
if (L.lang) properties.lang = L.lang
|
|
properties.rank = this.getRank() + 1
|
|
if (this.hasGeom()) {
|
|
center = this.getCenter()
|
|
properties.lat = center.lat
|
|
properties.lon = center.lng
|
|
properties.lng = center.lng
|
|
if (typeof this.getMeasure !== 'undefined') {
|
|
properties.measure = this.getMeasure()
|
|
}
|
|
}
|
|
return L.extend(properties, this.properties)
|
|
},
|
|
|
|
getRank: function () {
|
|
return this.datalayer._index.indexOf(L.stamp(this))
|
|
},
|
|
}
|
|
|
|
U.Marker = L.Marker.extend({
|
|
parentClass: L.Marker,
|
|
includes: [U.FeatureMixin],
|
|
|
|
preInit: function () {
|
|
this.setIcon(this.getIcon())
|
|
},
|
|
|
|
highlight: function () {
|
|
L.DomUtil.addClass(this.options.icon.elements.main, 'umap-icon-active')
|
|
},
|
|
|
|
resetHighlight: function () {
|
|
L.DomUtil.removeClass(this.options.icon.elements.main, 'umap-icon-active')
|
|
},
|
|
|
|
addInteractions: function () {
|
|
U.FeatureMixin.addInteractions.call(this)
|
|
this.on(
|
|
'dragend',
|
|
function (e) {
|
|
this.isDirty = true
|
|
this.edit(e)
|
|
},
|
|
this
|
|
)
|
|
if (!this.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)
|
|
},
|
|
|
|
hasGeom: function () {
|
|
return !!this._latlng
|
|
},
|
|
|
|
_onMouseOut: function () {
|
|
if (
|
|
this.dragging &&
|
|
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 && this.editor.drawing) return // when creating a new marker, the mouse can trigger the mouseover/mouseout event
|
|
// do not listen to them
|
|
this.disableEdit()
|
|
}
|
|
},
|
|
|
|
_redraw: function () {
|
|
if (this.datalayer && this.datalayer.isVisible()) {
|
|
this._initIcon()
|
|
this.update()
|
|
}
|
|
},
|
|
|
|
_initIcon: function () {
|
|
this.options.icon = this.getIcon()
|
|
L.Marker.prototype._initIcon.call(this)
|
|
// Allow to run code when icon is actually part of the DOM
|
|
this.options.icon.onAdd()
|
|
this.resetTooltip()
|
|
},
|
|
|
|
_getTooltipAnchor: function () {
|
|
const anchor = this.options.icon.options.tooltipAnchor.clone(),
|
|
direction = this.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
|
|
},
|
|
|
|
disconnectFromDataLayer: function (datalayer) {
|
|
this.options.icon.datalayer = null
|
|
U.FeatureMixin.disconnectFromDataLayer.call(this, datalayer)
|
|
},
|
|
|
|
_getIconUrl: function (name) {
|
|
if (typeof name === 'undefined') name = 'icon'
|
|
return this.getOption(`${name}Url`)
|
|
},
|
|
|
|
getIconClass: function () {
|
|
return this.getOption('iconClass')
|
|
},
|
|
|
|
getIcon: function () {
|
|
const Class = U.Icon[this.getIconClass()] || U.Icon.Default
|
|
return new Class(this.map, { feature: this })
|
|
},
|
|
|
|
getCenter: function () {
|
|
return this._latlng
|
|
},
|
|
|
|
getClassName: function () {
|
|
return 'marker'
|
|
},
|
|
|
|
getShapeOptions: function () {
|
|
return [
|
|
'properties._umap_options.color',
|
|
'properties._umap_options.iconClass',
|
|
'properties._umap_options.iconUrl',
|
|
'properties._umap_options.iconOpacity',
|
|
]
|
|
},
|
|
|
|
getAdvancedOptions: function () {
|
|
return ['properties._umap_options.zoomTo']
|
|
},
|
|
|
|
appendEditFieldsets: function (container) {
|
|
U.FeatureMixin.appendEditFieldsets.call(this, container)
|
|
const coordinatesOptions = [
|
|
['_latlng.lat', { handler: 'FloatInput', label: L._('Latitude') }],
|
|
['_latlng.lng', { handler: 'FloatInput', label: L._('Longitude') }],
|
|
]
|
|
const builder = new U.FormBuilder(this, coordinatesOptions, {
|
|
callback: function () {
|
|
if (!this._latlng.isValid()) {
|
|
this.map.ui.alert({
|
|
content: L._('Invalid latitude or longitude'),
|
|
level: 'error',
|
|
})
|
|
builder.resetField('_latlng.lat')
|
|
builder.resetField('_latlng.lng')
|
|
}
|
|
this._redraw()
|
|
this.zoomTo({ easing: false })
|
|
},
|
|
callbackContext: this,
|
|
})
|
|
const fieldset = L.DomUtil.createFieldset(container, L._('Coordinates'))
|
|
fieldset.appendChild(builder.build())
|
|
},
|
|
|
|
zoomTo: function (e) {
|
|
if (this.datalayer.isClustered() && !this._icon) {
|
|
// callback is mandatory for zoomToShowLayer
|
|
this.datalayer.layer.zoomToShowLayer(this, e.callback || (() => {}))
|
|
} else {
|
|
U.FeatureMixin.zoomTo.call(this, e)
|
|
}
|
|
},
|
|
|
|
isOnScreen: function (bounds) {
|
|
bounds = bounds || this.map.getBounds()
|
|
return bounds.contains(this._latlng)
|
|
},
|
|
|
|
getPopupToolbarAnchor: function () {
|
|
return this.options.icon.options.popupAnchor
|
|
},
|
|
})
|
|
|
|
U.PathMixin = {
|
|
hasGeom: function () {
|
|
return !this.isEmpty()
|
|
},
|
|
|
|
connectToDataLayer: function (datalayer) {
|
|
U.FeatureMixin.connectToDataLayer.call(this, datalayer)
|
|
// We keep markers on their own layer on top of the paths.
|
|
this.options.pane = this.datalayer.pane
|
|
},
|
|
|
|
edit: function (e) {
|
|
if (this.map.editEnabled) {
|
|
if (!this.editEnabled()) this.enableEdit()
|
|
U.FeatureMixin.edit.call(this, e)
|
|
}
|
|
},
|
|
|
|
_toggleEditing: function (e) {
|
|
if (this.map.editEnabled) {
|
|
if (this.editEnabled()) {
|
|
this.endEdit()
|
|
this.map.ui.closePanel()
|
|
} else {
|
|
this.edit(e)
|
|
}
|
|
}
|
|
// FIXME: disable when disabling global edit
|
|
L.DomEvent.stop(e)
|
|
},
|
|
|
|
styleOptions: [
|
|
'smoothFactor',
|
|
'color',
|
|
'opacity',
|
|
'stroke',
|
|
'weight',
|
|
'fill',
|
|
'fillColor',
|
|
'fillOpacity',
|
|
'dashArray',
|
|
'interactive',
|
|
],
|
|
|
|
getShapeOptions: function () {
|
|
return [
|
|
'properties._umap_options.color',
|
|
'properties._umap_options.opacity',
|
|
'properties._umap_options.weight',
|
|
]
|
|
},
|
|
|
|
getAdvancedOptions: function () {
|
|
return [
|
|
'properties._umap_options.smoothFactor',
|
|
'properties._umap_options.dashArray',
|
|
'properties._umap_options.zoomTo',
|
|
]
|
|
},
|
|
|
|
setStyle: function (options) {
|
|
options = options || {}
|
|
let option
|
|
for (const idx in this.styleOptions) {
|
|
option = this.styleOptions[idx]
|
|
options[option] = this.getDynamicOption(option)
|
|
}
|
|
if (options.interactive) this.options.pointerEvents = 'visiblePainted'
|
|
else this.options.pointerEvents = 'stroke'
|
|
this.parentClass.prototype.setStyle.call(this, options)
|
|
},
|
|
|
|
_redraw: function () {
|
|
if (this.datalayer && this.datalayer.isVisible()) {
|
|
this.setStyle()
|
|
this.resetTooltip()
|
|
}
|
|
},
|
|
|
|
onAdd: function (map) {
|
|
this._container = null
|
|
this.setStyle()
|
|
// Show tooltip again when Leaflet.label allow static label on path.
|
|
// cf https://github.com/Leaflet/Leaflet/pull/3952
|
|
// this.map.on('showmeasure', this.showMeasureTooltip, this);
|
|
// this.map.on('hidemeasure', this.removeTooltip, this);
|
|
this.parentClass.prototype.onAdd.call(this, map)
|
|
if (this.editing && this.editing.enabled()) this.editing.addHooks()
|
|
this.resetTooltip()
|
|
},
|
|
|
|
onRemove: function (map) {
|
|
// this.map.off('showmeasure', this.showMeasureTooltip, this);
|
|
// this.map.off('hidemeasure', this.removeTooltip, this);
|
|
if (this.editing && this.editing.enabled()) this.editing.removeHooks()
|
|
U.FeatureMixin.onRemove.call(this, map)
|
|
},
|
|
|
|
getBestZoom: function () {
|
|
return this.getOption('zoomTo') || this.map.getBoundsZoom(this.getBounds(), true)
|
|
},
|
|
|
|
endEdit: function () {
|
|
this.disableEdit()
|
|
U.FeatureMixin.endEdit.call(this)
|
|
},
|
|
|
|
highlightPath: function () {
|
|
this.parentClass.prototype.setStyle.call(this, {
|
|
fillOpacity: Math.sqrt(this.getDynamicOption('fillOpacity', 1.0)),
|
|
opacity: 1.0,
|
|
weight: 1.3 * this.getDynamicOption('weight'),
|
|
})
|
|
},
|
|
|
|
_onMouseOver: function () {
|
|
if (this.map.measureTools && this.map.measureTools.enabled()) {
|
|
this.map.ui.tooltip({ content: this.getMeasure(), anchor: this })
|
|
} else if (this.map.editEnabled && !this.map.editedFeature) {
|
|
this.map.ui.tooltip({ content: L._('Click to edit'), anchor: this })
|
|
}
|
|
},
|
|
|
|
addInteractions: function () {
|
|
U.FeatureMixin.addInteractions.call(this)
|
|
this.on('mouseover', this._onMouseOver)
|
|
this.on('edit', this.makeDirty)
|
|
this.on('drag editable:drag', this._onDrag)
|
|
this.on('popupopen', this.highlightPath)
|
|
this.on('popupclose', this._redraw)
|
|
},
|
|
|
|
_onDrag: function () {
|
|
if (this._tooltip) this._tooltip.setLatLng(this.getCenter())
|
|
},
|
|
|
|
transferShape: function (at, to) {
|
|
const shape = this.enableEdit().deleteShapeAt(at)
|
|
this.disableEdit()
|
|
if (!shape) return
|
|
to.enableEdit().appendShape(shape)
|
|
if (!this._latlngs.length || !this._latlngs[0].length) this.del()
|
|
},
|
|
|
|
isolateShape: function (at) {
|
|
if (!this.isMulti()) return
|
|
const shape = this.enableEdit().deleteShapeAt(at)
|
|
this.disableEdit()
|
|
if (!shape) return
|
|
const properties = this.cloneProperties()
|
|
const other = new (this instanceof U.Polyline ? U.Polyline : U.Polygon)(
|
|
this.map,
|
|
shape,
|
|
{ geojson: { properties: properties } }
|
|
)
|
|
this.datalayer.addLayer(other)
|
|
other.edit()
|
|
return other
|
|
},
|
|
|
|
getContextMenuItems: function (e) {
|
|
let items = U.FeatureMixin.getContextMenuItems.call(this, e)
|
|
items.push({
|
|
text: L._('Display measure'),
|
|
callback: function () {
|
|
this.map.ui.alert({ content: this.getMeasure(), level: 'info' })
|
|
},
|
|
context: this,
|
|
})
|
|
if (this.map.editEnabled && !this.isReadOnly() && this.isMulti()) {
|
|
items = items.concat(this.getContextMenuMultiItems(e))
|
|
}
|
|
return items
|
|
},
|
|
|
|
getContextMenuMultiItems: function (e) {
|
|
const items = [
|
|
'-',
|
|
{
|
|
text: L._('Remove shape from the multi'),
|
|
callback: function () {
|
|
this.enableEdit().deleteShapeAt(e.latlng)
|
|
},
|
|
context: this,
|
|
},
|
|
]
|
|
const shape = this.shapeAt(e.latlng)
|
|
if (this._latlngs.indexOf(shape) > 0) {
|
|
items.push({
|
|
text: L._('Make main shape'),
|
|
callback: function () {
|
|
this.enableEdit().deleteShape(shape)
|
|
this.editor.prependShape(shape)
|
|
},
|
|
context: this,
|
|
})
|
|
}
|
|
return items
|
|
},
|
|
|
|
getContextMenuEditItems: function (e) {
|
|
const items = U.FeatureMixin.getContextMenuEditItems.call(this, e)
|
|
if (
|
|
this.map.editedFeature &&
|
|
this.isSameClass(this.map.editedFeature) &&
|
|
this.map.editedFeature !== this
|
|
) {
|
|
items.push({
|
|
text: L._('Transfer shape to edited feature'),
|
|
callback: function () {
|
|
this.transferShape(e.latlng, this.map.editedFeature)
|
|
},
|
|
context: this,
|
|
})
|
|
}
|
|
if (this.isMulti()) {
|
|
items.push({
|
|
text: L._('Extract shape to separate feature'),
|
|
callback: function () {
|
|
this.isolateShape(e.latlng, this.map.editedFeature)
|
|
},
|
|
context: this,
|
|
})
|
|
}
|
|
return items
|
|
},
|
|
|
|
getInplaceToolbarActions: function (e) {
|
|
const items = U.FeatureMixin.getInplaceToolbarActions.call(this, e)
|
|
if (this.isMulti()) {
|
|
items.push(U.DeleteShapeAction)
|
|
items.push(U.ExtractShapeFromMultiAction)
|
|
}
|
|
return items
|
|
},
|
|
|
|
isOnScreen: function (bounds) {
|
|
bounds = bounds || this.map.getBounds()
|
|
return bounds.overlaps(this.getBounds())
|
|
},
|
|
|
|
zoomTo: function (e) {
|
|
// Use bounds instead of centroid for paths.
|
|
e = e || {}
|
|
const easing = e.easing !== undefined ? e.easing : this.map.getOption('easing')
|
|
if (easing) {
|
|
this.map.flyToBounds(this.getBounds(), this.getBestZoom())
|
|
} else {
|
|
this.map.fitBounds(this.getBounds(), this.getBestZoom() || this.map.getZoom())
|
|
}
|
|
if (e.callback) e.callback.call(this)
|
|
},
|
|
}
|
|
|
|
U.Polyline = L.Polyline.extend({
|
|
parentClass: L.Polyline,
|
|
includes: [U.FeatureMixin, U.PathMixin],
|
|
|
|
staticOptions: {
|
|
stroke: true,
|
|
fill: false,
|
|
mainColor: 'color',
|
|
},
|
|
|
|
isSameClass: function (other) {
|
|
return other instanceof U.Polyline
|
|
},
|
|
|
|
getClassName: function () {
|
|
return 'polyline'
|
|
},
|
|
|
|
getMeasure: function (shape) {
|
|
const length = L.GeoUtil.lineLength(this.map, shape || this._defaultShape())
|
|
return L.GeoUtil.readableDistance(length, this.map.measureTools.getMeasureUnit())
|
|
},
|
|
|
|
getContextMenuEditItems: function (e) {
|
|
const items = U.PathMixin.getContextMenuEditItems.call(this, e)
|
|
const vertexClicked = e.vertex
|
|
let index
|
|
if (!this.isMulti()) {
|
|
items.push({
|
|
text: L._('Transform to polygon'),
|
|
callback: this.toPolygon,
|
|
context: this,
|
|
})
|
|
}
|
|
if (vertexClicked) {
|
|
index = e.vertex.getIndex()
|
|
if (index !== 0 && index !== e.vertex.getLastIndex()) {
|
|
items.push({
|
|
text: L._('Split line'),
|
|
callback: e.vertex.split,
|
|
context: e.vertex,
|
|
})
|
|
} else if (index === 0 || index === e.vertex.getLastIndex()) {
|
|
items.push({
|
|
text: L._('Continue line (Ctrl+Click)'),
|
|
callback: e.vertex.continue,
|
|
context: e.vertex.continue,
|
|
})
|
|
}
|
|
}
|
|
return items
|
|
},
|
|
|
|
getContextMenuMultiItems: function (e) {
|
|
const items = U.PathMixin.getContextMenuMultiItems.call(this, e)
|
|
items.push({
|
|
text: L._('Merge lines'),
|
|
callback: this.mergeShapes,
|
|
context: this,
|
|
})
|
|
return items
|
|
},
|
|
|
|
toPolygon: function () {
|
|
const geojson = this.toGeoJSON()
|
|
geojson.geometry.type = 'Polygon'
|
|
geojson.geometry.coordinates = [
|
|
L.Util.flattenCoordinates(geojson.geometry.coordinates),
|
|
]
|
|
const polygon = this.datalayer.geojsonToFeatures(geojson)
|
|
polygon.edit()
|
|
this.del()
|
|
},
|
|
|
|
getAdvancedEditActions: function (container) {
|
|
U.FeatureMixin.getAdvancedEditActions.call(this, container)
|
|
const toPolygon = L.DomUtil.createButton(
|
|
'button umap-to-polygon',
|
|
container,
|
|
L._('Transform to polygon'),
|
|
this.toPolygon,
|
|
this
|
|
)
|
|
},
|
|
|
|
_mergeShapes: function (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],
|
|
b = toMerge[1],
|
|
p1 = this.map.latLngToContainerPoint(a[a.length - 1]),
|
|
p2 = this.map.latLngToContainerPoint(b[0]),
|
|
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: function () {
|
|
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: function () {
|
|
return !L.LineUtil.isFlat(this._latlngs) && this._latlngs.length > 1
|
|
},
|
|
|
|
getVertexActions: function (e) {
|
|
const actions = U.FeatureMixin.getVertexActions.call(this, e),
|
|
index = e.vertex.getIndex()
|
|
if (index === 0 || index === e.vertex.getLastIndex())
|
|
actions.push(U.ContinueLineAction)
|
|
else actions.push(U.SplitLineAction)
|
|
return actions
|
|
},
|
|
})
|
|
|
|
U.Polygon = L.Polygon.extend({
|
|
parentClass: L.Polygon,
|
|
includes: [U.FeatureMixin, U.PathMixin],
|
|
staticOptions: {
|
|
mainColor: 'fillColor',
|
|
},
|
|
|
|
isSameClass: function (other) {
|
|
return other instanceof U.Polygon
|
|
},
|
|
|
|
getClassName: function () {
|
|
return 'polygon'
|
|
},
|
|
|
|
getShapeOptions: function () {
|
|
const options = U.PathMixin.getShapeOptions()
|
|
options.push(
|
|
'properties._umap_options.stroke',
|
|
'properties._umap_options.fill',
|
|
'properties._umap_options.fillColor',
|
|
'properties._umap_options.fillOpacity'
|
|
)
|
|
return options
|
|
},
|
|
|
|
getInteractionOptions: function () {
|
|
const options = U.FeatureMixin.getInteractionOptions()
|
|
options.push('properties._umap_options.interactive')
|
|
return options
|
|
},
|
|
|
|
getMeasure: function (shape) {
|
|
const area = L.GeoUtil.geodesicArea(shape || this._defaultShape())
|
|
return L.GeoUtil.readableArea(area, this.map.measureTools.getMeasureUnit())
|
|
},
|
|
|
|
getContextMenuEditItems: function (e) {
|
|
const items = U.PathMixin.getContextMenuEditItems.call(this, e),
|
|
shape = this.shapeAt(e.latlng)
|
|
// No multi and no holes.
|
|
if (shape && !this.isMulti() && (L.LineUtil.isFlat(shape) || shape.length === 1)) {
|
|
items.push({
|
|
text: L._('Transform to lines'),
|
|
callback: this.toPolyline,
|
|
context: this,
|
|
})
|
|
}
|
|
items.push({
|
|
text: L._('Start a hole here'),
|
|
callback: this.startHole,
|
|
context: this,
|
|
})
|
|
return items
|
|
},
|
|
|
|
startHole: function (e) {
|
|
this.enableEdit().newHole(e.latlng)
|
|
},
|
|
|
|
toPolyline: function () {
|
|
const geojson = this.toGeoJSON()
|
|
geojson.geometry.type = 'LineString'
|
|
geojson.geometry.coordinates = L.Util.flattenCoordinates(
|
|
geojson.geometry.coordinates
|
|
)
|
|
const polyline = this.datalayer.geojsonToFeatures(geojson)
|
|
polyline.edit()
|
|
this.del()
|
|
},
|
|
|
|
getAdvancedEditActions: function (container) {
|
|
U.FeatureMixin.getAdvancedEditActions.call(this, container)
|
|
const toPolyline = L.DomUtil.createButton(
|
|
'button umap-to-polyline',
|
|
container,
|
|
L._('Transform to lines'),
|
|
this.toPolyline,
|
|
this
|
|
)
|
|
},
|
|
|
|
isMulti: function () {
|
|
// Change me when Leaflet#3279 is merged.
|
|
return (
|
|
!L.LineUtil.isFlat(this._latlngs) &&
|
|
!L.LineUtil.isFlat(this._latlngs[0]) &&
|
|
this._latlngs.length > 1
|
|
)
|
|
},
|
|
|
|
getInplaceToolbarActions: function (e) {
|
|
const items = U.PathMixin.getInplaceToolbarActions.call(this, e)
|
|
items.push(U.CreateHoleAction)
|
|
return items
|
|
},
|
|
})
|