mirror of
https://github.com/umap-project/umap.git
synced 2025-04-29 03:42:37 +02:00
2050 lines
59 KiB
JavaScript
2050 lines
59 KiB
JavaScript
L.Map.mergeOptions({
|
|
overlay: null,
|
|
datalayers: [],
|
|
hash: true,
|
|
default_color: 'DarkBlue',
|
|
default_smoothFactor: 1.0,
|
|
default_opacity: 0.5,
|
|
default_fillOpacity: 0.3,
|
|
default_stroke: true,
|
|
default_fill: true,
|
|
default_weight: 3,
|
|
default_iconOpacity: 1,
|
|
default_iconClass: 'Default',
|
|
default_popupContentTemplate: '# {name}\n{description}',
|
|
default_interactive: true,
|
|
default_labelDirection: 'auto',
|
|
maxZoomLimit: 24,
|
|
attributionControl: false,
|
|
editMode: 'advanced',
|
|
embedControl: true,
|
|
zoomControl: true,
|
|
datalayersControl: true,
|
|
searchControl: true,
|
|
editInOSMControl: false,
|
|
editInOSMControlOptions: false,
|
|
scaleControl: true,
|
|
noControl: false, // Do not render any control.
|
|
miniMap: false,
|
|
name: '',
|
|
description: '',
|
|
displayPopupFooter: false,
|
|
demoTileInfos: { s: 'a', z: 9, x: 265, y: 181, r: '' },
|
|
licences: [],
|
|
licence: '',
|
|
enableMarkerDraw: true,
|
|
enablePolygonDraw: true,
|
|
enablePolylineDraw: true,
|
|
limitBounds: {},
|
|
importPresets: [
|
|
// {url: 'http://localhost:8019/en/datalayer/1502/', label: 'Simplified World Countries', format: 'geojson'}
|
|
],
|
|
moreControl: true,
|
|
captionBar: false,
|
|
captionMenus: true,
|
|
slideshow: {},
|
|
clickable: true,
|
|
easing: false,
|
|
permissions: {},
|
|
permanentCreditBackground: true,
|
|
featuresHaveOwner: false,
|
|
})
|
|
|
|
L.U.Map.include({
|
|
HIDDABLE_CONTROLS: [
|
|
'zoom',
|
|
'search',
|
|
'fullscreen',
|
|
'embed',
|
|
'locate',
|
|
'measure',
|
|
'editinosm',
|
|
'datalayers',
|
|
'tilelayers',
|
|
'star',
|
|
],
|
|
|
|
initialize: function (el, geojson) {
|
|
// Locale name (pt_PT, en_US…)
|
|
// To be used for Django localization
|
|
if (geojson.properties.locale) L.setLocale(geojson.properties.locale)
|
|
|
|
// Language code (pt-pt, en-us…)
|
|
// To be used in javascript APIs
|
|
if (geojson.properties.lang) L.lang = geojson.properties.lang
|
|
|
|
// Don't let default autocreation of controls
|
|
const zoomControl =
|
|
typeof geojson.properties.zoomControl !== 'undefined'
|
|
? geojson.properties.zoomControl
|
|
: true
|
|
geojson.properties.zoomControl = false
|
|
const fullscreenControl =
|
|
typeof geojson.properties.fullscreenControl !== 'undefined'
|
|
? geojson.properties.fullscreenControl
|
|
: true
|
|
geojson.properties.fullscreenControl = false
|
|
L.Util.setBooleanFromQueryString(geojson.properties, 'scrollWheelZoom')
|
|
|
|
L.Map.prototype.initialize.call(this, el, geojson.properties)
|
|
|
|
// After calling parent initialize, as we are doing initCenter our-selves
|
|
if (geojson.geometry) this.options.center = this.latLng(geojson.geometry)
|
|
|
|
this.ui = new L.U.UI(this._container)
|
|
this.xhr = new L.U.Xhr(this.ui)
|
|
this.xhr.on('dataloading', (e) => this.fire('dataloading', e))
|
|
this.xhr.on('dataload', (e) => this.fire('dataload', e))
|
|
|
|
this.initLoader()
|
|
this.name = this.options.name
|
|
this.description = this.options.description
|
|
this.demoTileInfos = this.options.demoTileInfos
|
|
this.options.zoomControl = zoomControl
|
|
this.options.fullscreenControl = fullscreenControl
|
|
L.Util.setBooleanFromQueryString(this.options, 'moreControl')
|
|
L.Util.setBooleanFromQueryString(this.options, 'scaleControl')
|
|
L.Util.setBooleanFromQueryString(this.options, 'miniMap')
|
|
L.Util.setFromQueryString(this.options, 'editMode')
|
|
L.Util.setBooleanFromQueryString(this.options, 'displayDataBrowserOnLoad')
|
|
L.Util.setBooleanFromQueryString(this.options, 'displayCaptionOnLoad')
|
|
L.Util.setBooleanFromQueryString(this.options, 'captionBar')
|
|
L.Util.setBooleanFromQueryString(this.options, 'captionMenus')
|
|
for (let i = 0; i < this.HIDDABLE_CONTROLS.length; i++) {
|
|
L.Util.setNullableBooleanFromQueryString(
|
|
this.options,
|
|
`${this.HIDDABLE_CONTROLS[i]}Control`
|
|
)
|
|
}
|
|
this.datalayersOnLoad = L.Util.queryString('datalayers')
|
|
this.options.onLoadPanel = L.Util.queryString(
|
|
'onLoadPanel',
|
|
this.options.onLoadPanel
|
|
)
|
|
if (this.datalayersOnLoad)
|
|
this.datalayersOnLoad = this.datalayersOnLoad.toString().split(',')
|
|
|
|
if (L.Browser.ielt9) this.options.editMode = 'disabled' // TODO include ie9
|
|
|
|
let editedFeature = null
|
|
const self = this
|
|
try {
|
|
Object.defineProperty(this, 'editedFeature', {
|
|
get: function () {
|
|
return editedFeature
|
|
},
|
|
set: function (feature) {
|
|
if (editedFeature && editedFeature !== feature) {
|
|
editedFeature.endEdit()
|
|
}
|
|
editedFeature = feature
|
|
self.fire('seteditedfeature')
|
|
},
|
|
})
|
|
} catch (e) {
|
|
// Certainly IE8, which has a limited version of defineProperty
|
|
}
|
|
|
|
// Retrocompat
|
|
if (
|
|
this.options.slideshow &&
|
|
this.options.slideshow.delay &&
|
|
this.options.slideshow.active === undefined
|
|
)
|
|
this.options.slideshow.active = true
|
|
if (this.options.advancedFilterKey)
|
|
this.options.facetKey = this.options.advancedFilterKey
|
|
|
|
// Global storage for retrieving datalayers and features
|
|
this.datalayers = {}
|
|
this.datalayers_index = []
|
|
this.dirty_datalayers = []
|
|
this.features_index = {}
|
|
this.facets = {}
|
|
|
|
if (this.options.hash) this.addHash()
|
|
this.initTileLayers(this.options.tilelayers)
|
|
// Needs tilelayer to exist for minimap
|
|
this.initControls()
|
|
// Needs locate control and hash to exist
|
|
this.initCenter()
|
|
this.handleLimitBounds()
|
|
this.initDatalayers()
|
|
|
|
if (this.options.displayCaptionOnLoad) {
|
|
// Retrocompat
|
|
if (!this.options.onLoadPanel) {
|
|
this.options.onLoadPanel = 'caption'
|
|
}
|
|
delete this.options.displayCaptionOnLoad
|
|
}
|
|
if (this.options.displayDataBrowserOnLoad) {
|
|
// Retrocompat
|
|
if (!this.options.onLoadPanel) {
|
|
this.options.onLoadPanel = 'databrowser'
|
|
}
|
|
delete this.options.displayDataBrowserOnLoad
|
|
}
|
|
|
|
this.ui.on(
|
|
'panel:closed',
|
|
function () {
|
|
this.invalidateSize({ pan: false })
|
|
},
|
|
this
|
|
)
|
|
|
|
let isDirty = false // self status
|
|
try {
|
|
Object.defineProperty(this, 'isDirty', {
|
|
get: function () {
|
|
return isDirty
|
|
},
|
|
set: function (status) {
|
|
isDirty = status
|
|
this.checkDirty()
|
|
},
|
|
})
|
|
} catch (e) {
|
|
// Certainly IE8, which has a limited version of defineProperty
|
|
}
|
|
this.on(
|
|
'baselayerchange',
|
|
function (e) {
|
|
if (this._controls.miniMap) this._controls.miniMap.onMainMapBaseLayerChange(e)
|
|
},
|
|
this
|
|
)
|
|
|
|
// Creation mode
|
|
if (!this.options.umap_id) {
|
|
this.isDirty = true
|
|
this._default_extent = true
|
|
this.options.name = L._('Untitled map')
|
|
this.options.editMode = 'advanced'
|
|
const datalayer = this.createDataLayer()
|
|
datalayer.connectToMap()
|
|
this.enableEdit()
|
|
let dataUrl = L.Util.queryString('dataUrl', null)
|
|
const dataFormat = L.Util.queryString('dataFormat', 'geojson')
|
|
if (dataUrl) {
|
|
dataUrl = decodeURIComponent(dataUrl)
|
|
dataUrl = this.localizeUrl(dataUrl)
|
|
dataUrl = this.proxyUrl(dataUrl)
|
|
datalayer.importFromUrl(dataUrl, dataFormat)
|
|
}
|
|
}
|
|
|
|
this.help = new L.U.Help(this)
|
|
this.slideshow = new L.U.Slideshow(this, this.options.slideshow)
|
|
this.permissions = new L.U.MapPermissions(this)
|
|
this.initCaptionBar()
|
|
if (this.hasEditMode()) {
|
|
this.editTools = new L.U.Editable(this)
|
|
this.ui.on(
|
|
'panel:closed panel:open',
|
|
function () {
|
|
this.editedFeature = null
|
|
},
|
|
this
|
|
)
|
|
this.renderEditToolbar()
|
|
}
|
|
this.initShortcuts()
|
|
this.onceDatalayersLoaded(function () {
|
|
if (L.Util.queryString('share')) this.renderShareBox()
|
|
else if (this.options.onLoadPanel === 'databrowser') this.openBrowser()
|
|
else if (this.options.onLoadPanel === 'caption') this.displayCaption()
|
|
else if (
|
|
this.options.onLoadPanel === 'facet' ||
|
|
this.options.onLoadPanel === 'datafilters'
|
|
)
|
|
this.openFacet()
|
|
})
|
|
this.onceDataLoaded(function () {
|
|
const slug = L.Util.queryString('feature')
|
|
if (slug && this.features_index[slug]) this.features_index[slug].view()
|
|
if (L.Util.queryString('edit')) {
|
|
if (this.hasEditMode()) this.enableEdit()
|
|
// Sometimes users share the ?edit link by mistake, let's remove
|
|
// this search parameter from URL to prevent this
|
|
const url = new URL(window.location)
|
|
url.searchParams.delete('edit')
|
|
history.pushState({}, '', url)
|
|
}
|
|
if (L.Util.queryString('download')) this.download()
|
|
})
|
|
|
|
window.onbeforeunload = () => this.isDirty || null
|
|
this.backup()
|
|
this.initContextMenu()
|
|
this.on('click contextmenu.show', this.closeInplaceToolbar)
|
|
},
|
|
|
|
initControls: function () {
|
|
this.helpMenuActions = {}
|
|
this._controls = {}
|
|
|
|
if (this.hasEditMode() && !this.options.noControl) {
|
|
new L.U.EditControl(this).addTo(this)
|
|
|
|
new L.U.DrawToolbar({ map: this }).addTo(this)
|
|
|
|
const editActions = [
|
|
L.U.ImportAction,
|
|
L.U.EditPropertiesAction,
|
|
L.U.ManageDatalayersAction,
|
|
L.U.ChangeTileLayerAction,
|
|
L.U.UpdateExtentAction,
|
|
L.U.UpdatePermsAction,
|
|
]
|
|
new L.U.SettingsToolbar({ actions: editActions }).addTo(this)
|
|
}
|
|
this._controls.zoom = new L.Control.Zoom({
|
|
zoomInTitle: L._('Zoom in'),
|
|
zoomOutTitle: L._('Zoom out'),
|
|
})
|
|
this._controls.datalayers = new L.U.DataLayersControl(this)
|
|
this._controls.locate = L.control.locate({
|
|
strings: {
|
|
title: L._('Center map on your location'),
|
|
},
|
|
showPopup: false,
|
|
// We style this control in our own CSS for consistency with other controls,
|
|
// but the control breaks if we don't specify a class here, so a fake class
|
|
// will do.
|
|
icon: 'umap-fake-class',
|
|
iconLoading: 'umap-fake-class',
|
|
flyTo: this.options.easing,
|
|
onLocationError: (err) => this.ui.alert({ content: err.message }),
|
|
})
|
|
this._controls.fullscreen = new L.Control.Fullscreen({
|
|
title: { false: L._('View Fullscreen'), true: L._('Exit Fullscreen') },
|
|
})
|
|
this._controls.search = new L.U.SearchControl()
|
|
this._controls.embed = new L.Control.Embed(this, this.options.embedOptions)
|
|
this._controls.tilelayers = new L.U.TileLayerControl(this)
|
|
this._controls.star = new L.U.StarControl(this)
|
|
this._controls.editinosm = new L.Control.EditInOSM({
|
|
position: 'topleft',
|
|
widgetOptions: {
|
|
helpText: L._(
|
|
'Open this map extent in a map editor to provide more accurate data to OpenStreetMap'
|
|
),
|
|
},
|
|
})
|
|
this._controls.measure = new L.MeasureControl().initHandler(this)
|
|
this._controls.more = new L.U.MoreControls()
|
|
this._controls.scale = L.control.scale()
|
|
this._controls.permanentCredit = new L.U.PermanentCreditsControl(this)
|
|
if (this.options.scrollWheelZoom) this.scrollWheelZoom.enable()
|
|
else this.scrollWheelZoom.disable()
|
|
this.browser = new L.U.Browser(this)
|
|
this.drop = new L.U.DropControl(this)
|
|
this.renderControls()
|
|
},
|
|
|
|
renderControls: function () {
|
|
L.DomUtil.classIf(
|
|
document.body,
|
|
'umap-caption-bar-enabled',
|
|
this.options.captionBar ||
|
|
(this.options.slideshow && this.options.slideshow.active)
|
|
)
|
|
L.DomUtil.classIf(
|
|
document.body,
|
|
'umap-slideshow-enabled',
|
|
this.options.slideshow && this.options.slideshow.active
|
|
)
|
|
for (const i in this._controls) {
|
|
this.removeControl(this._controls[i])
|
|
}
|
|
if (this.options.noControl) return
|
|
|
|
this._controls.attribution = new L.U.AttributionControl().addTo(this)
|
|
if (this.options.miniMap && !this.options.noControl) {
|
|
this.whenReady(function () {
|
|
if (this.selected_tilelayer) {
|
|
this._controls.miniMap = new L.Control.MiniMap(this.selected_tilelayer).addTo(
|
|
this
|
|
)
|
|
this._controls.miniMap._miniMap.invalidateSize()
|
|
}
|
|
})
|
|
}
|
|
let name, status, control
|
|
for (let i = 0; i < this.HIDDABLE_CONTROLS.length; i++) {
|
|
name = this.HIDDABLE_CONTROLS[i]
|
|
status = this.options[`${name}Control`]
|
|
if (status === false) continue
|
|
control = this._controls[name]
|
|
control.addTo(this)
|
|
if (status === undefined || status === null)
|
|
L.DomUtil.addClass(control._container, 'display-on-more')
|
|
else L.DomUtil.removeClass(control._container, 'display-on-more')
|
|
}
|
|
if (this.options.permanentCredit) this._controls.permanentCredit.addTo(this)
|
|
if (this.options.moreControl) this._controls.more.addTo(this)
|
|
if (this.options.scaleControl) this._controls.scale.addTo(this)
|
|
},
|
|
|
|
initDatalayers: function () {
|
|
for (let j = 0; j < this.options.datalayers.length; j++) {
|
|
this.createDataLayer(this.options.datalayers[j])
|
|
}
|
|
this.loadDatalayers()
|
|
},
|
|
|
|
loadDatalayers: function (force) {
|
|
force = force || L.Util.queryString('download') // In case we are in download mode, let's go strait to loading all data
|
|
const total = this.datalayers_index.length
|
|
// toload => datalayer metadata remaining to load (synchronous)
|
|
// dataToload => datalayer data remaining to load (asynchronous)
|
|
let toload = total,
|
|
dataToload = total
|
|
let datalayer
|
|
const loaded = () => {
|
|
this.datalayersLoaded = true
|
|
this.fire('datalayersloaded')
|
|
}
|
|
const decrementToLoad = () => {
|
|
toload--
|
|
if (toload === 0) loaded()
|
|
}
|
|
const dataLoaded = () => {
|
|
this.dataLoaded = true
|
|
this.fire('dataloaded')
|
|
}
|
|
const decrementDataToLoad = () => {
|
|
dataToload--
|
|
if (dataToload === 0) dataLoaded()
|
|
}
|
|
this.eachDataLayer(function (datalayer) {
|
|
if (force && !datalayer.hasDataLoaded()) {
|
|
datalayer.show()
|
|
}
|
|
if (datalayer.showAtLoad() || force) {
|
|
datalayer.onceLoaded(decrementToLoad)
|
|
} else {
|
|
decrementToLoad()
|
|
}
|
|
if (datalayer.showAtLoad() || force) {
|
|
datalayer.onceDataLoaded(decrementDataToLoad)
|
|
} else {
|
|
decrementDataToLoad({ sourceTarget: datalayer })
|
|
}
|
|
})
|
|
if (total === 0) {
|
|
// no datalayer
|
|
loaded()
|
|
dataLoaded()
|
|
}
|
|
},
|
|
|
|
indexDatalayers: function () {
|
|
const panes = this.getPane('overlayPane')
|
|
let pane
|
|
this.datalayers_index = []
|
|
for (let i = 0; i < panes.children.length; i++) {
|
|
pane = panes.children[i]
|
|
if (!pane.dataset || !pane.dataset.id) continue
|
|
this.datalayers_index.push(this.datalayers[pane.dataset.id])
|
|
}
|
|
this.updateDatalayersControl()
|
|
},
|
|
|
|
ensurePanesOrder: function () {
|
|
this.eachDataLayer((datalayer) => {
|
|
datalayer.bringToTop()
|
|
})
|
|
},
|
|
|
|
onceDatalayersLoaded: function (callback, context) {
|
|
// Once datalayers **metadata** have been loaded
|
|
if (this.datalayersLoaded) {
|
|
callback.call(context || this, this)
|
|
} else {
|
|
this.once('datalayersloaded', callback, context)
|
|
}
|
|
return this
|
|
},
|
|
|
|
onceDataLoaded: function (callback, context) {
|
|
// Once datalayers **data** have been loaded
|
|
if (this.dataLoaded) {
|
|
callback.call(context || this, this)
|
|
} else {
|
|
this.once('dataloaded', callback, context)
|
|
}
|
|
return this
|
|
},
|
|
|
|
updateDatalayersControl: function () {
|
|
if (this._controls.datalayers) this._controls.datalayers.update()
|
|
},
|
|
|
|
backupOptions: function () {
|
|
this._backupOptions = L.extend({}, this.options)
|
|
this._backupOptions.tilelayer = L.extend({}, this.options.tilelayer)
|
|
this._backupOptions.limitBounds = L.extend({}, this.options.limitBounds)
|
|
this._backupOptions.permissions = L.extend({}, this.permissions.options)
|
|
},
|
|
|
|
resetOptions: function () {
|
|
this.options = L.extend({}, this._backupOptions)
|
|
this.options.tilelayer = L.extend({}, this._backupOptions.tilelayer)
|
|
this.permissions.options = L.extend({}, this._backupOptions.permissions)
|
|
},
|
|
|
|
initShortcuts: function () {
|
|
const globalShortcuts = function (e) {
|
|
const key = e.keyCode,
|
|
modifierKey = e.ctrlKey || e.metaKey
|
|
|
|
/* Generic shortcuts */
|
|
if (key === L.U.Keys.F && modifierKey) {
|
|
L.DomEvent.stop(e)
|
|
this.search()
|
|
} else if (e.keyCode === L.U.Keys.ESC) {
|
|
if (this.help.visible()) this.help.hide()
|
|
else this.ui.closePanel()
|
|
}
|
|
|
|
if (!this.hasEditMode()) return
|
|
|
|
/* Edit mode only shortcuts */
|
|
if (key === L.U.Keys.E && modifierKey && !this.editEnabled) {
|
|
L.DomEvent.stop(e)
|
|
this.enableEdit()
|
|
} else if (
|
|
key === L.U.Keys.E &&
|
|
modifierKey &&
|
|
this.editEnabled &&
|
|
!this.isDirty
|
|
) {
|
|
L.DomEvent.stop(e)
|
|
this.disableEdit()
|
|
this.ui.closePanel()
|
|
}
|
|
if (key === L.U.Keys.S && modifierKey) {
|
|
L.DomEvent.stop(e)
|
|
if (this.isDirty) {
|
|
this.save()
|
|
}
|
|
}
|
|
if (key === L.U.Keys.Z && modifierKey && this.isDirty) {
|
|
L.DomEvent.stop(e)
|
|
this.askForReset()
|
|
}
|
|
if (key === L.U.Keys.M && modifierKey && this.editEnabled) {
|
|
L.DomEvent.stop(e)
|
|
this.editTools.startMarker()
|
|
}
|
|
if (key === L.U.Keys.P && modifierKey && this.editEnabled) {
|
|
L.DomEvent.stop(e)
|
|
this.editTools.startPolygon()
|
|
}
|
|
if (key === L.U.Keys.L && modifierKey && this.editEnabled) {
|
|
L.DomEvent.stop(e)
|
|
this.editTools.startPolyline()
|
|
}
|
|
if (key === L.U.Keys.I && modifierKey && this.editEnabled) {
|
|
L.DomEvent.stop(e)
|
|
this.importPanel()
|
|
}
|
|
if (key === L.U.Keys.H && modifierKey && this.editEnabled) {
|
|
L.DomEvent.stop(e)
|
|
this.help.show('edit')
|
|
}
|
|
if (e.keyCode === L.U.Keys.ESC) {
|
|
if (this.editEnabled) this.editTools.stopDrawing()
|
|
if (this.measureTools.enabled()) this.measureTools.stopDrawing()
|
|
}
|
|
}
|
|
L.DomEvent.addListener(document, 'keydown', globalShortcuts, this)
|
|
},
|
|
|
|
initTileLayers: function () {
|
|
this.tilelayers = []
|
|
for (const i in this.options.tilelayers) {
|
|
if (this.options.tilelayers.hasOwnProperty(i)) {
|
|
this.tilelayers.push(this.createTileLayer(this.options.tilelayers[i]))
|
|
if (
|
|
this.options.tilelayer &&
|
|
this.options.tilelayer.url_template ===
|
|
this.options.tilelayers[i].url_template
|
|
) {
|
|
// Keep control over the displayed attribution for non custom tilelayers
|
|
this.options.tilelayer.attribution = this.options.tilelayers[i].attribution
|
|
}
|
|
}
|
|
}
|
|
if (
|
|
this.options.tilelayer &&
|
|
this.options.tilelayer.url_template &&
|
|
this.options.tilelayer.attribution
|
|
) {
|
|
this.customTilelayer = this.createTileLayer(this.options.tilelayer)
|
|
this.selectTileLayer(this.customTilelayer)
|
|
} else {
|
|
this.selectTileLayer(this.tilelayers[0])
|
|
}
|
|
},
|
|
|
|
createTileLayer: function (tilelayer) {
|
|
return new L.TileLayer(tilelayer.url_template, tilelayer)
|
|
},
|
|
|
|
selectTileLayer: function (tilelayer) {
|
|
if (tilelayer === this.selected_tilelayer) {
|
|
return
|
|
}
|
|
try {
|
|
this.addLayer(tilelayer)
|
|
this.fire('baselayerchange', { layer: tilelayer })
|
|
if (this.selected_tilelayer) {
|
|
this.removeLayer(this.selected_tilelayer)
|
|
}
|
|
this.selected_tilelayer = tilelayer
|
|
if (
|
|
!isNaN(this.selected_tilelayer.options.minZoom) &&
|
|
this.getZoom() < this.selected_tilelayer.options.minZoom
|
|
) {
|
|
this.setZoom(this.selected_tilelayer.options.minZoom)
|
|
}
|
|
if (
|
|
!isNaN(this.selected_tilelayer.options.maxZoom) &&
|
|
this.getZoom() > this.selected_tilelayer.options.maxZoom
|
|
) {
|
|
this.setZoom(this.selected_tilelayer.options.maxZoom)
|
|
}
|
|
} catch (e) {
|
|
this.removeLayer(tilelayer)
|
|
this.ui.alert({
|
|
content: `${L._('Error in the tilelayer URL')}: ${tilelayer._url}`,
|
|
level: 'error',
|
|
})
|
|
// Users can put tilelayer URLs by hand, and if they add wrong {variable},
|
|
// Leaflet throw an error, and then the map is no more editable
|
|
}
|
|
this.setOverlay()
|
|
},
|
|
|
|
eachTileLayer: function (method, context) {
|
|
const urls = []
|
|
for (const i in this.tilelayers) {
|
|
if (this.tilelayers.hasOwnProperty(i)) {
|
|
method.call(context, this.tilelayers[i])
|
|
urls.push(this.tilelayers[i]._url)
|
|
}
|
|
}
|
|
if (
|
|
this.customTilelayer &&
|
|
Array.prototype.indexOf &&
|
|
urls.indexOf(this.customTilelayer._url) === -1
|
|
) {
|
|
method.call(context || this, this.customTilelayer)
|
|
}
|
|
},
|
|
|
|
setOverlay: function () {
|
|
if (!this.options.overlay || !this.options.overlay.url_template) return
|
|
const overlay = this.createTileLayer(this.options.overlay)
|
|
try {
|
|
this.addLayer(overlay)
|
|
if (this.overlay) this.removeLayer(this.overlay)
|
|
this.overlay = overlay
|
|
} catch (e) {
|
|
this.removeLayer(overlay)
|
|
console.error(e)
|
|
this.ui.alert({
|
|
content: `${L._('Error in the overlay URL')}: ${overlay._url}`,
|
|
level: 'error',
|
|
})
|
|
}
|
|
},
|
|
|
|
_setDefaultCenter: function () {
|
|
this.options.center = this.latLng(this.options.center)
|
|
this.setView(this.options.center, this.options.zoom)
|
|
},
|
|
|
|
hasData: function () {
|
|
for (const datalayer of this.datalayers_index) {
|
|
if (datalayer.hasData()) return true
|
|
}
|
|
},
|
|
|
|
fitDataBounds: function () {
|
|
const bounds = this.getLayersBounds()
|
|
if (!this.hasData() || !bounds.isValid()) return false
|
|
this.fitBounds(bounds)
|
|
},
|
|
|
|
initCenter: function () {
|
|
if (this.options.hash && this._hash.parseHash(location.hash)) {
|
|
// FIXME An invalid hash will cause the load to fail
|
|
this._hash.update()
|
|
} else if (this.options.defaultView === 'locate' && !this.options.noControl) {
|
|
this.once('locationerror', this._setDefaultCenter)
|
|
this._controls.locate.start()
|
|
} else if (this.options.defaultView === 'data') {
|
|
this.onceDataLoaded(() => {
|
|
if (!this.fitDataBounds()) return this._setDefaultCenter()
|
|
})
|
|
} else if (this.options.defaultView === 'latest') {
|
|
this.onceDataLoaded(() => {
|
|
if (!this.hasData()) {
|
|
this._setDefaultCenter()
|
|
return
|
|
}
|
|
const datalayer = this.defaultDataLayer(),
|
|
feature = datalayer.getFeatureByIndex(-1)
|
|
if (feature) feature.zoomTo()
|
|
else this._setDefaultCenter()
|
|
})
|
|
} else {
|
|
this._setDefaultCenter()
|
|
}
|
|
},
|
|
|
|
latLng: function (a, b, c) {
|
|
// manage geojson case and call original method
|
|
if (!(a instanceof L.LatLng) && a.coordinates) {
|
|
// Guess it's a geojson
|
|
a = [a.coordinates[1], a.coordinates[0]]
|
|
}
|
|
return L.latLng(a, b, c)
|
|
},
|
|
|
|
handleLimitBounds: function () {
|
|
const south = parseFloat(this.options.limitBounds.south),
|
|
west = parseFloat(this.options.limitBounds.west),
|
|
north = parseFloat(this.options.limitBounds.north),
|
|
east = parseFloat(this.options.limitBounds.east)
|
|
if (!isNaN(south) && !isNaN(west) && !isNaN(north) && !isNaN(east)) {
|
|
const bounds = L.latLngBounds([
|
|
[south, west],
|
|
[north, east],
|
|
])
|
|
this.options.minZoom = this.getBoundsZoom(bounds, false)
|
|
try {
|
|
this.setMaxBounds(bounds)
|
|
} catch (e) {
|
|
// Unusable bounds, like -2 -2 -2 -2?
|
|
console.error('Error limiting bounds', e)
|
|
}
|
|
} else {
|
|
this.options.minZoom = 0
|
|
this.setMaxBounds()
|
|
}
|
|
},
|
|
|
|
setMaxBounds: function (bounds) {
|
|
// Hack. Remove me when fix is released:
|
|
// https://github.com/Leaflet/Leaflet/pull/4494
|
|
bounds = L.latLngBounds(bounds)
|
|
|
|
if (!bounds.isValid()) {
|
|
this.options.maxBounds = null
|
|
return this.off('moveend', this._panInsideMaxBounds)
|
|
}
|
|
return L.Map.prototype.setMaxBounds.call(this, bounds)
|
|
},
|
|
|
|
createDataLayer: function (datalayer) {
|
|
datalayer = datalayer || {
|
|
name: `${L._('Layer')} ${this.datalayers_index.length + 1}`,
|
|
}
|
|
return new L.U.DataLayer(this, datalayer)
|
|
},
|
|
|
|
getDefaultOption: function (option) {
|
|
return this.options[`default_${option}`]
|
|
},
|
|
|
|
getOption: function (option) {
|
|
if (L.Util.usableOption(this.options, option)) return this.options[option]
|
|
return this.getDefaultOption(option)
|
|
},
|
|
|
|
updateExtent: function () {
|
|
this.options.center = this.getCenter()
|
|
this.options.zoom = this.getZoom()
|
|
this.isDirty = true
|
|
this._default_extent = false
|
|
if (this.options.umap_id) {
|
|
// We do not want an extra message during the map creation
|
|
// to avoid the double notification/alert.
|
|
this.ui.alert({
|
|
content: L._('The zoom and center have been modified.'),
|
|
level: 'info',
|
|
})
|
|
}
|
|
},
|
|
|
|
updateTileLayers: function () {
|
|
const self = this,
|
|
callback = (tilelayer) => {
|
|
self.options.tilelayer = tilelayer.toJSON()
|
|
self.isDirty = true
|
|
}
|
|
if (this._controls.tilelayers)
|
|
this._controls.tilelayers.openSwitcher({ callback: callback, className: 'dark' })
|
|
},
|
|
|
|
manageDatalayers: function () {
|
|
if (this._controls.datalayers) this._controls.datalayers.openPanel()
|
|
},
|
|
|
|
toGeoJSON: function () {
|
|
let features = []
|
|
this.eachDataLayer((datalayer) => {
|
|
if (datalayer.isVisible()) {
|
|
features = features.concat(datalayer.featuresToGeoJSON())
|
|
}
|
|
})
|
|
const geojson = {
|
|
type: 'FeatureCollection',
|
|
features: features,
|
|
}
|
|
return geojson
|
|
},
|
|
|
|
eachFeature: function (callback, context) {
|
|
this.eachDataLayer((datalayer) => {
|
|
if (datalayer.isVisible()) datalayer.eachFeature(callback, context)
|
|
})
|
|
},
|
|
|
|
fullDownload: function () {
|
|
// Make sure all data is loaded before downloading
|
|
this.once('dataloaded', () => this.download())
|
|
this.loadDatalayers(true) // Force load
|
|
},
|
|
|
|
format: function (mode) {
|
|
const type = this.EXPORT_TYPES[mode || 'umap']
|
|
const content = type.formatter(this)
|
|
let name = this.options.name || 'data'
|
|
name = name.replace(/[^a-z0-9]/gi, '_').toLowerCase()
|
|
const filename = name + type.ext
|
|
return { content, filetype: type.filetype, filename }
|
|
},
|
|
|
|
download: function (mode) {
|
|
const { content, filetype, filename } = this.format(mode)
|
|
const blob = new Blob([content], { type: filetype })
|
|
window.URL = window.URL || window.webkitURL
|
|
const el = document.createElement('a')
|
|
el.download = filename
|
|
el.href = window.URL.createObjectURL(blob)
|
|
el.style.display = 'none'
|
|
document.body.appendChild(el)
|
|
el.click()
|
|
document.body.removeChild(el)
|
|
},
|
|
|
|
processFileToImport: function (file, layer, type) {
|
|
type = type || L.Util.detectFileType(file)
|
|
if (!type) {
|
|
this.ui.alert({
|
|
content: L._('Unable to detect format of file {filename}', {
|
|
filename: file.name,
|
|
}),
|
|
level: 'error',
|
|
})
|
|
return
|
|
}
|
|
if (type === 'umap') {
|
|
this.importFromFile(file, 'umap')
|
|
} else {
|
|
if (!layer) layer = this.createDataLayer({ name: file.name })
|
|
layer.importFromFile(file, type)
|
|
}
|
|
},
|
|
|
|
importRaw: function (rawData) {
|
|
const importedData = JSON.parse(rawData)
|
|
|
|
let mustReindex = false
|
|
|
|
for (let i = 0; i < this.editableOptions.length; i++) {
|
|
const option = this.editableOptions[i]
|
|
if (typeof importedData.properties[option] !== 'undefined') {
|
|
this.options[option] = importedData.properties[option]
|
|
if (option === 'sortKey') mustReindex = true
|
|
}
|
|
}
|
|
|
|
if (importedData.geometry) this.options.center = this.latLng(importedData.geometry)
|
|
const self = this
|
|
importedData.layers.forEach((geojson) => {
|
|
delete geojson._umap_options['id'] // Never trust an id at this stage
|
|
const dataLayer = self.createDataLayer(geojson._umap_options)
|
|
dataLayer.fromUmapGeoJSON(geojson)
|
|
})
|
|
|
|
this.initTileLayers()
|
|
this.renderControls()
|
|
this.handleLimitBounds()
|
|
this.eachDataLayer((datalayer) => {
|
|
if (mustReindex) datalayer.reindex()
|
|
datalayer.redraw()
|
|
})
|
|
this.fire('postsync')
|
|
this.isDirty = true
|
|
},
|
|
|
|
importFromFile: function (file) {
|
|
const reader = new FileReader()
|
|
reader.readAsText(file)
|
|
const self = this
|
|
reader.onload = (e) => {
|
|
const rawData = e.target.result
|
|
try {
|
|
self.importRaw(rawData)
|
|
} catch (e) {
|
|
console.error('Error importing data', e)
|
|
self.ui.alert({
|
|
content: L._('Invalid umap data in {filename}', { filename: file.name }),
|
|
level: 'error',
|
|
})
|
|
}
|
|
}
|
|
},
|
|
|
|
openBrowser: function () {
|
|
this.onceDatalayersLoaded(function () {
|
|
this.browser.open()
|
|
})
|
|
},
|
|
|
|
openFacet: function () {
|
|
this.onceDatalayersLoaded(function () {
|
|
this._openFacet()
|
|
})
|
|
},
|
|
|
|
eachDataLayer: function (method, context) {
|
|
for (let i = 0; i < this.datalayers_index.length; i++) {
|
|
method.call(context, this.datalayers_index[i])
|
|
}
|
|
},
|
|
|
|
eachDataLayerReverse: function (method, context, filter) {
|
|
for (let i = this.datalayers_index.length - 1; i >= 0; i--) {
|
|
if (filter && !filter.call(context, this.datalayers_index[i])) continue
|
|
method.call(context, this.datalayers_index[i])
|
|
}
|
|
},
|
|
|
|
eachBrowsableDataLayer: function (method, context) {
|
|
this.eachDataLayerReverse(method, context, (d) => d.allowBrowse())
|
|
},
|
|
|
|
eachVisibleDataLayer: function (method, context) {
|
|
this.eachDataLayerReverse(method, context, (d) => d.isVisible())
|
|
},
|
|
|
|
findDataLayer: function (method, context) {
|
|
for (let i = this.datalayers_index.length - 1; i >= 0; i--) {
|
|
if (method.call(context, this.datalayers_index[i]))
|
|
return this.datalayers_index[i]
|
|
}
|
|
},
|
|
|
|
backup: function () {
|
|
this.backupOptions()
|
|
this._datalayers_index_bk = [].concat(this.datalayers_index)
|
|
},
|
|
|
|
reset: function () {
|
|
if (this.editTools) this.editTools.stopDrawing()
|
|
this.resetOptions()
|
|
this.datalayers_index = [].concat(this._datalayers_index_bk)
|
|
this.dirty_datalayers.slice().forEach((datalayer) => {
|
|
if (datalayer.isDeleted) datalayer.connectToMap()
|
|
datalayer.reset()
|
|
})
|
|
this.ensurePanesOrder()
|
|
this.dirty_datalayers = []
|
|
this.updateDatalayersControl()
|
|
this.initTileLayers()
|
|
this.isDirty = false
|
|
},
|
|
|
|
checkDirty: function () {
|
|
L.DomUtil.classIf(this._container, 'umap-is-dirty', this.isDirty)
|
|
},
|
|
|
|
addDirtyDatalayer: function (datalayer) {
|
|
if (this.dirty_datalayers.indexOf(datalayer) === -1) {
|
|
this.dirty_datalayers.push(datalayer)
|
|
this.isDirty = true
|
|
}
|
|
},
|
|
|
|
removeDirtyDatalayer: function (datalayer) {
|
|
if (this.dirty_datalayers.indexOf(datalayer) !== -1) {
|
|
this.dirty_datalayers.splice(this.dirty_datalayers.indexOf(datalayer), 1)
|
|
this.checkDirty()
|
|
}
|
|
},
|
|
|
|
continueSaving: function () {
|
|
if (this.dirty_datalayers.length) this.dirty_datalayers[0].save()
|
|
else this.fire('saved')
|
|
},
|
|
|
|
editableOptions: [
|
|
'zoom',
|
|
'scrollWheelZoom',
|
|
'scaleControl',
|
|
'moreControl',
|
|
'miniMap',
|
|
'displayPopupFooter',
|
|
'onLoadPanel',
|
|
'defaultView',
|
|
'tilelayersControl',
|
|
'name',
|
|
'description',
|
|
'licence',
|
|
'tilelayer',
|
|
'overlay',
|
|
'limitBounds',
|
|
'color',
|
|
'iconClass',
|
|
'iconUrl',
|
|
'smoothFactor',
|
|
'iconOpacity',
|
|
'opacity',
|
|
'weight',
|
|
'fill',
|
|
'fillColor',
|
|
'fillOpacity',
|
|
'dashArray',
|
|
'popupShape',
|
|
'popupTemplate',
|
|
'popupContentTemplate',
|
|
'zoomTo',
|
|
'captionBar',
|
|
'captionMenus',
|
|
'slideshow',
|
|
'sortKey',
|
|
'labelKey',
|
|
'filterKey',
|
|
'facetKey',
|
|
'slugKey',
|
|
'showLabel',
|
|
'labelDirection',
|
|
'labelInteractive',
|
|
'outlinkTarget',
|
|
'shortCredit',
|
|
'longCredit',
|
|
'permanentCredit',
|
|
'permanentCreditBackground',
|
|
'zoomControl',
|
|
'datalayersControl',
|
|
'searchControl',
|
|
'locateControl',
|
|
'fullscreenControl',
|
|
'editinosmControl',
|
|
'embedControl',
|
|
'measureControl',
|
|
'tilelayersControl',
|
|
'starControl',
|
|
'easing',
|
|
],
|
|
|
|
exportOptions: function () {
|
|
const properties = {}
|
|
for (let i = this.editableOptions.length - 1; i >= 0; i--) {
|
|
if (typeof this.options[this.editableOptions[i]] !== 'undefined') {
|
|
properties[this.editableOptions[i]] = this.options[this.editableOptions[i]]
|
|
}
|
|
}
|
|
return properties
|
|
},
|
|
|
|
serialize: function () {
|
|
// Do not use local path during unit tests
|
|
const uri = window.location.protocol === 'file:' ? null : window.location.href
|
|
const umapfile = {
|
|
type: 'umap',
|
|
uri: uri,
|
|
properties: this.exportOptions(),
|
|
geometry: this.geometry(),
|
|
layers: [],
|
|
}
|
|
|
|
this.eachDataLayer((datalayer) => {
|
|
umapfile.layers.push(datalayer.umapGeoJSON())
|
|
})
|
|
|
|
return JSON.stringify(umapfile, null, 2)
|
|
},
|
|
|
|
saveSelf: function () {
|
|
const geojson = {
|
|
type: 'Feature',
|
|
geometry: this.geometry(),
|
|
properties: this.exportOptions(),
|
|
}
|
|
const formData = new FormData()
|
|
formData.append('name', this.options.name)
|
|
formData.append('center', JSON.stringify(this.geometry()))
|
|
formData.append('settings', JSON.stringify(geojson))
|
|
this.post(this.getSaveUrl(), {
|
|
data: formData,
|
|
context: this,
|
|
callback: function (data) {
|
|
let duration = 3000,
|
|
alert = { content: L._('Map has been saved!'), level: 'info' }
|
|
if (!this.options.umap_id) {
|
|
alert.content = L._('Congratulations, your map has been created!')
|
|
this.options.umap_id = data.id
|
|
this.permissions.setOptions(data.permissions)
|
|
this.permissions.commit()
|
|
if (
|
|
data.permissions &&
|
|
data.permissions.anonymous_edit_url &&
|
|
this.options.urls.map_send_edit_link
|
|
) {
|
|
alert.duration = Infinity
|
|
alert.content =
|
|
L._(
|
|
'Your map has been created! As you are not logged in, here is your secret link to edit the map, please keep it safe:'
|
|
) + `<br>${data.permissions.anonymous_edit_url}`
|
|
|
|
alert.actions = [
|
|
{
|
|
label: L._('Send me the link'),
|
|
input: L._('Email'),
|
|
callback: this.sendEditLink,
|
|
callbackContext: this,
|
|
},
|
|
{
|
|
label: L._('Copy link'),
|
|
callback: () => {
|
|
L.Util.copyToClipboard(data.permissions.anonymous_edit_url)
|
|
this.ui.alert({
|
|
content: L._('Secret edit link copied to clipboard!'),
|
|
level: 'info',
|
|
})
|
|
},
|
|
callbackContext: this,
|
|
},
|
|
]
|
|
}
|
|
} else if (!this.permissions.isDirty) {
|
|
// Do not override local changes to permissions,
|
|
// but update in case some other editors changed them in the meantime.
|
|
this.permissions.setOptions(data.permissions)
|
|
this.permissions.commit()
|
|
}
|
|
// Update URL in case the name has changed.
|
|
if (history && history.pushState)
|
|
history.pushState({}, this.options.name, data.url)
|
|
else window.location = data.url
|
|
alert.content = data.info || alert.content
|
|
this.once('saved', () => this.ui.alert(alert))
|
|
this.ui.closePanel()
|
|
this.permissions.save()
|
|
},
|
|
})
|
|
},
|
|
|
|
save: function () {
|
|
if (!this.isDirty) return
|
|
if (this._default_extent) this.updateExtent()
|
|
this.backup()
|
|
this.once('saved', () => {
|
|
this.isDirty = false
|
|
})
|
|
if (this.options.editMode === 'advanced') {
|
|
// Only save the map if the user has the rights to do so.
|
|
this.saveSelf()
|
|
} else {
|
|
this.permissions.save()
|
|
}
|
|
},
|
|
|
|
sendEditLink: function () {
|
|
const url = L.Util.template(this.options.urls.map_send_edit_link, {
|
|
map_id: this.options.umap_id,
|
|
}),
|
|
input = this.ui._alert.querySelector('input'),
|
|
email = input.value
|
|
|
|
const formData = new FormData()
|
|
formData.append('email', email)
|
|
this.post(url, {
|
|
data: formData,
|
|
})
|
|
},
|
|
|
|
getEditUrl: function () {
|
|
return L.Util.template(this.options.urls.map_update, {
|
|
map_id: this.options.umap_id,
|
|
})
|
|
},
|
|
|
|
getCreateUrl: function () {
|
|
return L.Util.template(this.options.urls.map_create)
|
|
},
|
|
|
|
getSaveUrl: function () {
|
|
return (this.options.umap_id && this.getEditUrl()) || this.getCreateUrl()
|
|
},
|
|
|
|
star: function () {
|
|
if (!this.options.umap_id)
|
|
return this.ui.alert({
|
|
content: L._('Please save the map first'),
|
|
level: 'error',
|
|
})
|
|
let url = L.Util.template(this.options.urls.map_star, {
|
|
map_id: this.options.umap_id,
|
|
})
|
|
this.post(url, {
|
|
context: this,
|
|
callback: function (data) {
|
|
this.options.starred = data.starred
|
|
let msg = data.starred
|
|
? L._('Map has been starred')
|
|
: L._('Map has been unstarred')
|
|
this.ui.alert({ content: msg, level: 'info' })
|
|
this.renderControls()
|
|
},
|
|
})
|
|
},
|
|
|
|
geometry: function () {
|
|
/* Return a GeoJSON geometry Object */
|
|
const latlng = this.latLng(this.options.center || this.getCenter())
|
|
return {
|
|
type: 'Point',
|
|
coordinates: [latlng.lng, latlng.lat],
|
|
}
|
|
},
|
|
|
|
// TODO: allow to control the default datalayer
|
|
// (edit and viewing)
|
|
// cf https://github.com/umap-project/umap/issues/585
|
|
defaultDataLayer: function () {
|
|
let datalayer, fallback
|
|
datalayer = this.lastUsedDataLayer
|
|
if (
|
|
datalayer &&
|
|
!datalayer.isDataReadOnly() &&
|
|
datalayer.canBrowse() &&
|
|
datalayer.isVisible()
|
|
) {
|
|
return datalayer
|
|
}
|
|
datalayer = this.findDataLayer((datalayer) => {
|
|
if (!datalayer.isDataReadOnly() && datalayer.canBrowse()) {
|
|
fallback = datalayer
|
|
if (datalayer.isVisible()) return true
|
|
}
|
|
})
|
|
if (datalayer) return datalayer
|
|
if (fallback) {
|
|
// No datalayer visible, let's force one
|
|
this.addLayer(fallback.layer)
|
|
return fallback
|
|
}
|
|
return this.createDataLayer()
|
|
},
|
|
|
|
getDataLayerByUmapId: function (umap_id) {
|
|
return this.findDataLayer((d) => d.umap_id == umap_id)
|
|
},
|
|
|
|
_editControls: function (container) {
|
|
let UIFields = []
|
|
for (let i = 0; i < this.HIDDABLE_CONTROLS.length; i++) {
|
|
UIFields.push(`options.${this.HIDDABLE_CONTROLS[i]}Control`)
|
|
}
|
|
UIFields = UIFields.concat([
|
|
'options.moreControl',
|
|
'options.scrollWheelZoom',
|
|
'options.miniMap',
|
|
'options.scaleControl',
|
|
'options.onLoadPanel',
|
|
'options.defaultView',
|
|
'options.displayPopupFooter',
|
|
'options.captionBar',
|
|
'options.captionMenus',
|
|
])
|
|
builder = new L.U.FormBuilder(this, UIFields, {
|
|
callback: function () {
|
|
this.renderControls()
|
|
this.initCaptionBar()
|
|
},
|
|
callbackContext: this,
|
|
})
|
|
const controlsOptions = L.DomUtil.createFieldset(
|
|
container,
|
|
L._('User interface options')
|
|
)
|
|
controlsOptions.appendChild(builder.build())
|
|
},
|
|
|
|
_editShapeProperties: function (container) {
|
|
const shapeOptions = [
|
|
'options.color',
|
|
'options.iconClass',
|
|
'options.iconUrl',
|
|
'options.iconOpacity',
|
|
'options.opacity',
|
|
'options.weight',
|
|
'options.fill',
|
|
'options.fillColor',
|
|
'options.fillOpacity',
|
|
]
|
|
|
|
builder = new L.U.FormBuilder(this, shapeOptions, {
|
|
callback: function (e) {
|
|
this.eachDataLayer((datalayer) => {
|
|
datalayer.redraw()
|
|
})
|
|
},
|
|
})
|
|
const defaultShapeProperties = L.DomUtil.createFieldset(
|
|
container,
|
|
L._('Default shape properties')
|
|
)
|
|
defaultShapeProperties.appendChild(builder.build())
|
|
},
|
|
|
|
_editDefaultProperties: function (container) {
|
|
const optionsFields = [
|
|
'options.smoothFactor',
|
|
'options.dashArray',
|
|
'options.zoomTo',
|
|
['options.easing', { handler: 'Switch', label: L._('Animated transitions') }],
|
|
'options.labelKey',
|
|
[
|
|
'options.sortKey',
|
|
{
|
|
handler: 'BlurInput',
|
|
helpEntries: 'sortKey',
|
|
placeholder: L._('Default: name'),
|
|
label: L._('Sort key'),
|
|
inheritable: true,
|
|
},
|
|
],
|
|
[
|
|
'options.filterKey',
|
|
{
|
|
handler: 'Input',
|
|
helpEntries: 'filterKey',
|
|
placeholder: L._('Default: name'),
|
|
label: L._('Filter keys'),
|
|
inheritable: true,
|
|
},
|
|
],
|
|
[
|
|
'options.facetKey',
|
|
{
|
|
handler: 'Input',
|
|
helpEntries: 'facetKey',
|
|
placeholder: L._('Example: key1,key2,key3'),
|
|
label: L._('Facet keys'),
|
|
},
|
|
],
|
|
[
|
|
'options.slugKey',
|
|
{
|
|
handler: 'BlurInput',
|
|
helpEntries: 'slugKey',
|
|
placeholder: L._('Default: name'),
|
|
label: L._('Feature identifier key'),
|
|
},
|
|
],
|
|
]
|
|
|
|
builder = new L.U.FormBuilder(this, optionsFields, {
|
|
callback: function (e) {
|
|
this.initCaptionBar()
|
|
this.eachDataLayer((datalayer) => {
|
|
if (e.helper.field === 'options.sortKey') datalayer.reindex()
|
|
datalayer.redraw()
|
|
})
|
|
},
|
|
})
|
|
const defaultProperties = L.DomUtil.createFieldset(
|
|
container,
|
|
L._('Default properties')
|
|
)
|
|
defaultProperties.appendChild(builder.build())
|
|
},
|
|
|
|
_editInteractionsProperties: function (container) {
|
|
const popupFields = [
|
|
'options.popupShape',
|
|
'options.popupTemplate',
|
|
'options.popupContentTemplate',
|
|
'options.showLabel',
|
|
'options.labelDirection',
|
|
'options.labelInteractive',
|
|
'options.outlinkTarget',
|
|
]
|
|
builder = new L.U.FormBuilder(this, popupFields, {
|
|
callback: function (e) {
|
|
if (
|
|
e.helper.field === 'options.popupTemplate' ||
|
|
e.helper.field === 'options.popupContentTemplate' ||
|
|
e.helper.field === 'options.popupShape'
|
|
)
|
|
return
|
|
this.eachDataLayer((datalayer) => {
|
|
datalayer.redraw()
|
|
})
|
|
},
|
|
})
|
|
const popupFieldset = L.DomUtil.createFieldset(
|
|
container,
|
|
L._('Default interaction options')
|
|
)
|
|
popupFieldset.appendChild(builder.build())
|
|
},
|
|
|
|
_editTilelayer: function (container) {
|
|
if (!L.Util.isObject(this.options.tilelayer)) {
|
|
this.options.tilelayer = {}
|
|
}
|
|
const tilelayerFields = [
|
|
[
|
|
'options.tilelayer.name',
|
|
{ handler: 'BlurInput', placeholder: L._('display name') },
|
|
],
|
|
[
|
|
'options.tilelayer.url_template',
|
|
{
|
|
handler: 'BlurInput',
|
|
helpText: `${L._('Supported scheme')}: http://{s}.domain.com/{z}/{x}/{y}.png`,
|
|
placeholder: 'url',
|
|
type: 'url',
|
|
},
|
|
],
|
|
[
|
|
'options.tilelayer.maxZoom',
|
|
{
|
|
handler: 'BlurIntInput',
|
|
placeholder: L._('max zoom'),
|
|
min: 0,
|
|
max: this.options.maxZoomLimit,
|
|
},
|
|
],
|
|
[
|
|
'options.tilelayer.minZoom',
|
|
{
|
|
handler: 'BlurIntInput',
|
|
placeholder: L._('min zoom'),
|
|
min: 0,
|
|
max: this.options.maxZoomLimit,
|
|
},
|
|
],
|
|
[
|
|
'options.tilelayer.attribution',
|
|
{ handler: 'BlurInput', placeholder: L._('attribution') },
|
|
],
|
|
['options.tilelayer.tms', { handler: 'Switch', label: L._('TMS format') }],
|
|
]
|
|
const customTilelayer = L.DomUtil.createFieldset(
|
|
container,
|
|
L._('Custom background')
|
|
)
|
|
builder = new L.U.FormBuilder(this, tilelayerFields, {
|
|
callback: this.initTileLayers,
|
|
callbackContext: this,
|
|
})
|
|
customTilelayer.appendChild(builder.build())
|
|
},
|
|
|
|
_editOverlay: function (container) {
|
|
if (!L.Util.isObject(this.options.overlay)) {
|
|
this.options.overlay = {}
|
|
}
|
|
const overlayFields = [
|
|
[
|
|
'options.overlay.url_template',
|
|
{
|
|
handler: 'BlurInput',
|
|
helpText: `${L._('Supported scheme')}: http://{s}.domain.com/{z}/{x}/{y}.png`,
|
|
placeholder: 'url',
|
|
helpText: L._('Background overlay url'),
|
|
type: 'url',
|
|
},
|
|
],
|
|
[
|
|
'options.overlay.maxZoom',
|
|
{
|
|
handler: 'BlurIntInput',
|
|
placeholder: L._('max zoom'),
|
|
min: 0,
|
|
max: this.options.maxZoomLimit,
|
|
},
|
|
],
|
|
[
|
|
'options.overlay.minZoom',
|
|
{
|
|
handler: 'BlurIntInput',
|
|
placeholder: L._('min zoom'),
|
|
min: 0,
|
|
max: this.options.maxZoomLimit,
|
|
},
|
|
],
|
|
[
|
|
'options.overlay.attribution',
|
|
{ handler: 'BlurInput', placeholder: L._('attribution') },
|
|
],
|
|
[
|
|
'options.overlay.opacity',
|
|
{ handler: 'Range', min: 0, max: 1, step: 0.1, label: L._('Opacity') },
|
|
],
|
|
['options.overlay.tms', { handler: 'Switch', label: L._('TMS format') }],
|
|
]
|
|
const overlay = L.DomUtil.createFieldset(container, L._('Custom overlay'))
|
|
builder = new L.U.FormBuilder(this, overlayFields, {
|
|
callback: this.initTileLayers,
|
|
callbackContext: this,
|
|
})
|
|
overlay.appendChild(builder.build())
|
|
},
|
|
|
|
_editBounds: function (container) {
|
|
if (!L.Util.isObject(this.options.limitBounds)) {
|
|
this.options.limitBounds = {}
|
|
}
|
|
const limitBounds = L.DomUtil.createFieldset(container, L._('Limit bounds'))
|
|
const boundsFields = [
|
|
[
|
|
'options.limitBounds.south',
|
|
{ handler: 'BlurFloatInput', placeholder: L._('max South') },
|
|
],
|
|
[
|
|
'options.limitBounds.west',
|
|
{ handler: 'BlurFloatInput', placeholder: L._('max West') },
|
|
],
|
|
[
|
|
'options.limitBounds.north',
|
|
{ handler: 'BlurFloatInput', placeholder: L._('max North') },
|
|
],
|
|
[
|
|
'options.limitBounds.east',
|
|
{ handler: 'BlurFloatInput', placeholder: L._('max East') },
|
|
],
|
|
]
|
|
const boundsBuilder = new L.U.FormBuilder(this, boundsFields, {
|
|
callback: this.handleLimitBounds,
|
|
callbackContext: this,
|
|
})
|
|
limitBounds.appendChild(boundsBuilder.build())
|
|
const boundsButtons = L.DomUtil.create('div', 'button-bar half', limitBounds)
|
|
const setCurrentButton = L.DomUtil.add(
|
|
'a',
|
|
'button',
|
|
boundsButtons,
|
|
L._('Use current bounds')
|
|
)
|
|
setCurrentButton.href = '#'
|
|
L.DomEvent.on(
|
|
setCurrentButton,
|
|
'click',
|
|
function () {
|
|
const bounds = this.getBounds()
|
|
this.options.limitBounds.south = L.Util.formatNum(bounds.getSouth())
|
|
this.options.limitBounds.west = L.Util.formatNum(bounds.getWest())
|
|
this.options.limitBounds.north = L.Util.formatNum(bounds.getNorth())
|
|
this.options.limitBounds.east = L.Util.formatNum(bounds.getEast())
|
|
boundsBuilder.fetchAll()
|
|
this.isDirty = true
|
|
this.handleLimitBounds()
|
|
},
|
|
this
|
|
)
|
|
const emptyBounds = L.DomUtil.add('a', 'button', boundsButtons, L._('Empty'))
|
|
emptyBounds.href = '#'
|
|
L.DomEvent.on(
|
|
emptyBounds,
|
|
'click',
|
|
function () {
|
|
this.options.limitBounds.south = null
|
|
this.options.limitBounds.west = null
|
|
this.options.limitBounds.north = null
|
|
this.options.limitBounds.east = null
|
|
boundsBuilder.fetchAll()
|
|
this.isDirty = true
|
|
this.handleLimitBounds()
|
|
},
|
|
this
|
|
)
|
|
},
|
|
|
|
_editSlideshow: function (container) {
|
|
const slideshow = L.DomUtil.createFieldset(container, L._('Slideshow'))
|
|
const slideshowFields = [
|
|
[
|
|
'options.slideshow.active',
|
|
{ handler: 'Switch', label: L._('Activate slideshow mode') },
|
|
],
|
|
[
|
|
'options.slideshow.delay',
|
|
{
|
|
handler: 'SlideshowDelay',
|
|
helpText: L._('Delay between two transitions when in play mode'),
|
|
},
|
|
],
|
|
[
|
|
'options.slideshow.easing',
|
|
{ handler: 'Switch', label: L._('Animated transitions'), inheritable: true },
|
|
],
|
|
[
|
|
'options.slideshow.autoplay',
|
|
{ handler: 'Switch', label: L._('Autostart when map is loaded') },
|
|
],
|
|
]
|
|
const slideshowHandler = function () {
|
|
this.slideshow.setOptions(this.options.slideshow)
|
|
this.renderControls()
|
|
}
|
|
const slideshowBuilder = new L.U.FormBuilder(this, slideshowFields, {
|
|
callback: slideshowHandler,
|
|
callbackContext: this,
|
|
})
|
|
slideshow.appendChild(slideshowBuilder.build())
|
|
},
|
|
|
|
_editCredits: function (container) {
|
|
const credits = L.DomUtil.createFieldset(container, L._('Credits'))
|
|
const creditsFields = [
|
|
['options.licence', { handler: 'LicenceChooser', label: L._('licence') }],
|
|
[
|
|
'options.shortCredit',
|
|
{
|
|
handler: 'Input',
|
|
label: L._('Short credits'),
|
|
helpEntries: ['shortCredit', 'textFormatting'],
|
|
},
|
|
],
|
|
[
|
|
'options.longCredit',
|
|
{
|
|
handler: 'Textarea',
|
|
label: L._('Long credits'),
|
|
helpEntries: ['longCredit', 'textFormatting'],
|
|
},
|
|
],
|
|
[
|
|
'options.permanentCredit',
|
|
{
|
|
handler: 'Textarea',
|
|
label: L._('Permanent credits'),
|
|
helpEntries: ['permanentCredit', 'textFormatting'],
|
|
},
|
|
],
|
|
[
|
|
'options.permanentCreditBackground',
|
|
{ handler: 'Switch', label: L._('Permanent credits background') },
|
|
],
|
|
]
|
|
const creditsBuilder = new L.U.FormBuilder(this, creditsFields, {
|
|
callback: this.renderControls,
|
|
callbackContext: this,
|
|
})
|
|
credits.appendChild(creditsBuilder.build())
|
|
},
|
|
|
|
_advancedActions: function (container) {
|
|
const advancedActions = L.DomUtil.createFieldset(container, L._('Advanced actions'))
|
|
const advancedButtons = L.DomUtil.create('div', 'button-bar half', advancedActions)
|
|
if (this.permissions.isOwner()) {
|
|
const del = L.DomUtil.create('a', 'button umap-delete', advancedButtons)
|
|
del.href = '#'
|
|
del.title = L._('Delete map')
|
|
del.textContent = L._('Delete')
|
|
L.DomEvent.on(del, 'click', L.DomEvent.stop).on(del, 'click', this.del, this)
|
|
const empty = L.DomUtil.create('a', 'button umap-empty', advancedButtons)
|
|
empty.href = '#'
|
|
empty.textContent = L._('Empty')
|
|
empty.title = L._('Delete all layers')
|
|
L.DomEvent.on(empty, 'click', L.DomEvent.stop).on(
|
|
empty,
|
|
'click',
|
|
this.empty,
|
|
this
|
|
)
|
|
}
|
|
const clone = L.DomUtil.create('a', 'button umap-clone', advancedButtons)
|
|
clone.href = '#'
|
|
clone.textContent = L._('Clone')
|
|
clone.title = L._('Clone this map')
|
|
L.DomEvent.on(clone, 'click', L.DomEvent.stop).on(clone, 'click', this.clone, this)
|
|
const download = L.DomUtil.create('a', 'button umap-download', advancedButtons)
|
|
download.href = '#'
|
|
download.textContent = L._('Download')
|
|
download.title = L._('Open download panel')
|
|
L.DomEvent.on(download, 'click', L.DomEvent.stop).on(
|
|
download,
|
|
'click',
|
|
this.renderShareBox,
|
|
this
|
|
)
|
|
},
|
|
|
|
edit: function () {
|
|
if (!this.editEnabled) return
|
|
if (this.options.editMode !== 'advanced') return
|
|
const container = L.DomUtil.create('div', 'umap-edit-container'),
|
|
metadataFields = ['options.name', 'options.description'],
|
|
title = L.DomUtil.create('h3', '', container)
|
|
title.textContent = L._('Edit map properties')
|
|
const builder = new L.U.FormBuilder(this, metadataFields)
|
|
const form = builder.build()
|
|
container.appendChild(form)
|
|
this._editControls(container)
|
|
this._editShapeProperties(container)
|
|
this._editDefaultProperties(container)
|
|
this._editInteractionsProperties(container)
|
|
this._editTilelayer(container)
|
|
this._editOverlay(container)
|
|
this._editBounds(container)
|
|
this._editSlideshow(container)
|
|
this._editCredits(container)
|
|
this._advancedActions(container)
|
|
|
|
this.ui.openPanel({ data: { html: container }, className: 'dark' })
|
|
},
|
|
|
|
enableEdit: function () {
|
|
L.DomUtil.addClass(document.body, 'umap-edit-enabled')
|
|
this.editEnabled = true
|
|
this.drop.enable()
|
|
this.fire('edit:enabled')
|
|
},
|
|
|
|
disableEdit: function () {
|
|
if (this.isDirty) return
|
|
this.drop.disable()
|
|
L.DomUtil.removeClass(document.body, 'umap-edit-enabled')
|
|
this.editedFeature = null
|
|
this.editEnabled = false
|
|
this.fire('edit:disabled')
|
|
},
|
|
|
|
hasEditMode: function () {
|
|
return this.options.editMode === 'simple' || this.options.editMode === 'advanced'
|
|
},
|
|
|
|
getDisplayName: function () {
|
|
return this.options.name || L._('Untitled map')
|
|
},
|
|
|
|
initCaptionBar: function () {
|
|
const container = L.DomUtil.create(
|
|
'div',
|
|
'umap-caption-bar',
|
|
this._controlContainer
|
|
),
|
|
name = L.DomUtil.create('h3', '', container)
|
|
L.DomEvent.disableClickPropagation(container)
|
|
this.permissions.addOwnerLink('span', container)
|
|
if (this.options.captionMenus) {
|
|
const about = L.DomUtil.add(
|
|
'a',
|
|
'umap-about-link',
|
|
container,
|
|
` — ${L._('About')}`
|
|
)
|
|
about.href = '#'
|
|
L.DomEvent.on(about, 'click', this.displayCaption, this)
|
|
const browser = L.DomUtil.add(
|
|
'a',
|
|
'umap-open-browser-link',
|
|
container,
|
|
` | ${L._('Browse data')}`
|
|
)
|
|
browser.href = '#'
|
|
L.DomEvent.on(browser, 'click', L.DomEvent.stop).on(
|
|
browser,
|
|
'click',
|
|
this.openBrowser,
|
|
this
|
|
)
|
|
if (this.options.facetKey) {
|
|
const filter = L.DomUtil.add(
|
|
'a',
|
|
'umap-open-filter-link',
|
|
container,
|
|
` | ${L._('Select data')}`
|
|
)
|
|
filter.href = '#'
|
|
L.DomEvent.on(filter, 'click', L.DomEvent.stop).on(
|
|
filter,
|
|
'click',
|
|
this.openFacet,
|
|
this
|
|
)
|
|
}
|
|
}
|
|
const setName = function () {
|
|
name.textContent = this.getDisplayName()
|
|
}
|
|
L.bind(setName, this)()
|
|
this.on('postsync', L.bind(setName, this))
|
|
this.onceDatalayersLoaded(function () {
|
|
this.slideshow.renderToolbox(container)
|
|
})
|
|
},
|
|
|
|
askForReset: function (e) {
|
|
if (!confirm(L._('Are you sure you want to cancel your changes?'))) return
|
|
this.reset()
|
|
this.disableEdit(e)
|
|
this.ui.closePanel()
|
|
},
|
|
|
|
startMarker: function () {
|
|
return this.editTools.startMarker()
|
|
},
|
|
|
|
startPolyline: function () {
|
|
return this.editTools.startPolyline()
|
|
},
|
|
|
|
startPolygon: function () {
|
|
return this.editTools.startPolygon()
|
|
},
|
|
|
|
del: function () {
|
|
if (confirm(L._('Are you sure you want to delete this map?'))) {
|
|
const url = L.Util.template(this.options.urls.map_delete, {
|
|
map_id: this.options.umap_id,
|
|
})
|
|
this.post(url)
|
|
}
|
|
},
|
|
|
|
clone: function () {
|
|
if (
|
|
confirm(L._('Are you sure you want to clone this map and all its datalayers?'))
|
|
) {
|
|
const url = L.Util.template(this.options.urls.map_clone, {
|
|
map_id: this.options.umap_id,
|
|
})
|
|
this.post(url)
|
|
}
|
|
},
|
|
|
|
empty: function () {
|
|
this.eachDataLayerReverse((datalayer) => {
|
|
datalayer._delete()
|
|
})
|
|
},
|
|
|
|
initLoader: function () {
|
|
this.loader = new L.Control.Loading()
|
|
this.loader.onAdd(this)
|
|
},
|
|
|
|
post: function (url, options) {
|
|
options = options || {}
|
|
options.listener = this
|
|
this.xhr.post(url, options)
|
|
},
|
|
|
|
get: function (url, options) {
|
|
options = options || {}
|
|
options.listener = this
|
|
this.xhr.get(url, options)
|
|
},
|
|
|
|
ajax: function (options) {
|
|
options.listener = this
|
|
this.xhr._ajax(options)
|
|
},
|
|
|
|
initContextMenu: function () {
|
|
this.contextmenu = new L.U.ContextMenu(this)
|
|
this.contextmenu.enable()
|
|
},
|
|
|
|
setContextMenuItems: function (e) {
|
|
let items = []
|
|
if (this._zoom !== this.getMaxZoom()) {
|
|
items.push({
|
|
text: L._('Zoom in'),
|
|
callback: function () {
|
|
this.zoomIn()
|
|
},
|
|
})
|
|
}
|
|
if (this._zoom !== this.getMinZoom()) {
|
|
items.push({
|
|
text: L._('Zoom out'),
|
|
callback: function () {
|
|
this.zoomOut()
|
|
},
|
|
})
|
|
}
|
|
if (e && e.relatedTarget) {
|
|
if (e.relatedTarget.getContextMenuItems) {
|
|
items = items.concat(e.relatedTarget.getContextMenuItems(e))
|
|
}
|
|
}
|
|
if (this.hasEditMode()) {
|
|
items.push('-')
|
|
if (this.editEnabled) {
|
|
if (!this.isDirty) {
|
|
items.push({
|
|
text: `${L._('Stop editing')} (Ctrl+E)`,
|
|
callback: this.disableEdit,
|
|
})
|
|
}
|
|
if (this.options.enableMarkerDraw) {
|
|
items.push({
|
|
text: `${L._('Draw a marker')} (Ctrl+M)`,
|
|
callback: this.startMarker,
|
|
context: this,
|
|
})
|
|
}
|
|
if (this.options.enablePolylineDraw) {
|
|
items.push({
|
|
text: `${L._('Draw a polygon')} (Ctrl+P)`,
|
|
callback: this.startPolygon,
|
|
context: this,
|
|
})
|
|
}
|
|
if (this.options.enablePolygonDraw) {
|
|
items.push({
|
|
text: `${L._('Draw a line')} (Ctrl+L)`,
|
|
callback: this.startPolyline,
|
|
context: this,
|
|
})
|
|
}
|
|
items.push('-')
|
|
items.push({
|
|
text: L._('Help'),
|
|
callback: function () {
|
|
this.help.show('edit')
|
|
},
|
|
})
|
|
} else {
|
|
items.push({
|
|
text: `${L._('Start editing')} (Ctrl+E)`,
|
|
callback: this.enableEdit,
|
|
})
|
|
}
|
|
}
|
|
items.push('-', {
|
|
text: L._('Browse data'),
|
|
callback: this.openBrowser,
|
|
})
|
|
if (this.options.facetKey) {
|
|
items.push({
|
|
text: L._('Facet search'),
|
|
callback: this.openFacet,
|
|
})
|
|
}
|
|
items.push(
|
|
{
|
|
text: L._('About'),
|
|
callback: this.displayCaption,
|
|
},
|
|
{
|
|
text: L._('Search location'),
|
|
callback: this.search,
|
|
}
|
|
)
|
|
if (this.options.urls.routing) {
|
|
items.push('-', {
|
|
text: L._('Directions from here'),
|
|
callback: this.openExternalRouting,
|
|
})
|
|
}
|
|
this.options.contextmenuItems = items
|
|
},
|
|
|
|
openExternalRouting: function (e) {
|
|
const url = this.options.urls.routing
|
|
if (url) {
|
|
const params = {
|
|
lat: e.latlng.lat,
|
|
lng: e.latlng.lng,
|
|
locale: L.locale,
|
|
zoom: this.getZoom(),
|
|
}
|
|
window.open(L.Util.template(url, params))
|
|
}
|
|
return
|
|
},
|
|
|
|
getMap: function () {
|
|
return this
|
|
},
|
|
|
|
getGeoContext: function () {
|
|
const context = {
|
|
bbox: this.getBounds().toBBoxString(),
|
|
north: this.getBounds().getNorthEast().lat,
|
|
east: this.getBounds().getNorthEast().lng,
|
|
south: this.getBounds().getSouthWest().lat,
|
|
west: this.getBounds().getSouthWest().lng,
|
|
lat: this.getCenter().lat,
|
|
lng: this.getCenter().lng,
|
|
zoom: this.getZoom(),
|
|
}
|
|
context.left = context.west
|
|
context.bottom = context.south
|
|
context.right = context.east
|
|
context.top = context.north
|
|
return context
|
|
},
|
|
|
|
localizeUrl: function (url) {
|
|
return L.Util.greedyTemplate(url, this.getGeoContext(), true)
|
|
},
|
|
|
|
proxyUrl: function (url, ttl) {
|
|
if (this.options.urls.ajax_proxy) {
|
|
url = L.Util.greedyTemplate(this.options.urls.ajax_proxy, {
|
|
url: encodeURIComponent(url),
|
|
ttl: ttl,
|
|
})
|
|
}
|
|
return url
|
|
},
|
|
|
|
closeInplaceToolbar: function () {
|
|
const toolbar = this._toolbars[L.Toolbar.Popup._toolbar_class_id]
|
|
if (toolbar) toolbar.remove()
|
|
},
|
|
|
|
search: function () {
|
|
if (this._controls.search) this._controls.search.openPanel(this)
|
|
},
|
|
|
|
getFilterKeys: function () {
|
|
return (this.options.filterKey || this.options.sortKey || 'name').split(',')
|
|
},
|
|
|
|
getFacetKeys: function () {
|
|
return (this.options.facetKey || '').split(',').reduce((acc, curr) => {
|
|
const els = curr.split('|')
|
|
acc[els[0]] = els[1] || els[0]
|
|
return acc
|
|
}, {})
|
|
},
|
|
|
|
getLayersBounds: function () {
|
|
const bounds = new L.latLngBounds()
|
|
this.eachBrowsableDataLayer((d) => {
|
|
if (d.isVisible()) bounds.extend(d.layer.getBounds())
|
|
})
|
|
return bounds
|
|
},
|
|
})
|