mirror of
https://github.com/umap-project/umap.git
synced 2025-04-28 19:42:36 +02:00
1678 lines
49 KiB
JavaScript
1678 lines
49 KiB
JavaScript
import {
|
|
DomUtil,
|
|
Util as LeafletUtil,
|
|
stamp,
|
|
latLngBounds,
|
|
} from '../../vendors/leaflet/leaflet-src.esm.js'
|
|
import { translate, setLocale, getLocale } from './i18n.js'
|
|
import * as Utils from './utils.js'
|
|
import { ServerStored } from './saving.js'
|
|
import * as SAVEMANAGER from './saving.js'
|
|
import { SyncEngine } from './sync/engine.js'
|
|
import { LeafletMap } from './rendering/map.js'
|
|
import URLs from './urls.js'
|
|
import { Panel, EditPanel, FullPanel } from './ui/panel.js'
|
|
import Dialog from './ui/dialog.js'
|
|
import { BottomBar, TopBar } from './ui/bar.js'
|
|
import Tooltip from './ui/tooltip.js'
|
|
import ContextMenu from './ui/contextmenu.js'
|
|
import { Request, ServerRequest } from './request.js'
|
|
import Help from './help.js'
|
|
import { Formatter } from './formatter.js'
|
|
import Slideshow from './slideshow.js'
|
|
import { MapPermissions } from './permissions.js'
|
|
import { SCHEMA } from './schema.js'
|
|
import { DataLayer } from './data/layer.js'
|
|
import Facets from './facets.js'
|
|
import Browser from './browser.js'
|
|
import Caption from './caption.js'
|
|
import Importer from './importer.js'
|
|
import Rules from './rules.js'
|
|
import Share from './share.js'
|
|
import {
|
|
uMapAlertCreation as AlertCreation,
|
|
uMapAlert as Alert,
|
|
} from '../components/alerts/alert.js'
|
|
import Orderable from './orderable.js'
|
|
|
|
export default class Umap extends ServerStored {
|
|
constructor(element, geojson) {
|
|
super()
|
|
// We need to call async function in the init process,
|
|
// the init itself does not need to be awaited, but some calls
|
|
// in the process must be blocker
|
|
this.init(element, geojson)
|
|
}
|
|
|
|
get id() {
|
|
return this.properties.id
|
|
}
|
|
|
|
async init(element, geojson) {
|
|
this.properties = Object.assign(
|
|
{
|
|
enableMarkerDraw: true,
|
|
enablePolygonDraw: true,
|
|
enablePolylineDraw: true,
|
|
hash: true,
|
|
limitBounds: {},
|
|
},
|
|
geojson.properties
|
|
)
|
|
this.searchParams = new URLSearchParams(window.location.search)
|
|
|
|
this.sync_engine = new SyncEngine(this)
|
|
this.sync = this.sync_engine.proxy(this)
|
|
// Locale name (pt_PT, en_US…)
|
|
// To be used for Django localization
|
|
if (geojson.properties.locale) setLocale(geojson.properties.locale)
|
|
|
|
// Language code (pt-pt, en-us…)
|
|
// To be used in javascript APIs
|
|
if (geojson.properties.lang) U.lang = geojson.properties.lang
|
|
|
|
// Make it available to utils, without needing a reference to `Umap`.
|
|
U.LABEL_KEYS = geojson.properties.defaultLabelKeys || []
|
|
U.DEFAULT_LABEL_KEY = U.LABEL_KEYS[0] || 'name'
|
|
|
|
this.setPropertiesFromQueryString()
|
|
|
|
// Needed for actions labels
|
|
this.help = new Help(this)
|
|
// Prevent default creation of controls
|
|
const zoomControl = this.properties.zoomControl
|
|
const fullscreenControl = this.properties.fullscreenControl
|
|
const center = geojson.geometry
|
|
this.properties.zoomControl = false
|
|
this.properties.fullscreenControl = false
|
|
|
|
this._leafletMap = new LeafletMap(this, element)
|
|
|
|
this.properties.zoomControl = zoomControl !== undefined ? zoomControl : true
|
|
this.properties.fullscreenControl =
|
|
fullscreenControl !== undefined ? fullscreenControl : true
|
|
|
|
if (center) {
|
|
this._leafletMap.options.center = this._leafletMap.latLng(center)
|
|
}
|
|
|
|
// Needed to render controls
|
|
this.permissions = new MapPermissions(this)
|
|
this.urls = new URLs(this.properties.urls)
|
|
this.slideshow = new Slideshow(this, this._leafletMap)
|
|
|
|
this._leafletMap.setup()
|
|
|
|
if (geojson.properties.schema) this.overrideSchema(geojson.properties.schema)
|
|
|
|
this.panel = new Panel(this, this._leafletMap)
|
|
this.dialog = new Dialog({ className: 'dark' })
|
|
this.topBar = new TopBar(this, this._leafletMap._controlContainer)
|
|
this.bottomBar = new BottomBar(
|
|
this,
|
|
this.slideshow,
|
|
this._leafletMap._controlContainer
|
|
)
|
|
this.tooltip = new Tooltip(this._leafletMap._controlContainer)
|
|
this.contextmenu = new ContextMenu()
|
|
this.server = new ServerRequest()
|
|
this.request = new Request()
|
|
this.facets = new Facets(this)
|
|
this.browser = new Browser(this, this._leafletMap)
|
|
this.caption = new Caption(this, this._leafletMap)
|
|
this.importer = new Importer(this)
|
|
this.share = new Share(this)
|
|
this.rules = new Rules(this)
|
|
|
|
if (this.hasEditMode()) {
|
|
this.editPanel = new EditPanel(this, this._leafletMap)
|
|
this.fullPanel = new FullPanel(this, this._leafletMap)
|
|
this._leafletMap.initEditTools()
|
|
this.topBar.setup()
|
|
}
|
|
|
|
this.datalayersFromQueryString = this.searchParams.get('datalayers')
|
|
if (this.datalayersFromQueryString) {
|
|
this.datalayersFromQueryString = this.datalayersFromQueryString
|
|
.toString()
|
|
.split(',')
|
|
}
|
|
|
|
// Retrocompat
|
|
if (
|
|
this.properties.slideshow?.delay &&
|
|
this.properties.slideshow.active === undefined
|
|
) {
|
|
this.properties.slideshow.active = true
|
|
}
|
|
if (this.properties.advancedFilterKey) {
|
|
this.properties.facetKey = this.properties.advancedFilterKey
|
|
delete this.properties.advancedFilterKey
|
|
}
|
|
|
|
// Global storage for retrieving datalayers and features.
|
|
this.datalayers = {} // All datalayers, including deleted.
|
|
this.datalayersIndex = [] // Datalayers actually on the map and ordered.
|
|
this.featuresIndex = {}
|
|
|
|
this.formatter = new Formatter(this)
|
|
|
|
this.initDataLayers()
|
|
|
|
if (this.properties.displayCaptionOnLoad) {
|
|
// Retrocompat
|
|
if (!this.properties.onLoadPanel) {
|
|
this.properties.onLoadPanel = 'caption'
|
|
}
|
|
delete this.properties.displayCaptionOnLoad
|
|
}
|
|
if (this.properties.displayDataBrowserOnLoad) {
|
|
// Retrocompat
|
|
if (!this.properties.onLoadPanel) {
|
|
this.properties.onLoadPanel = 'databrowser'
|
|
}
|
|
delete this.properties.displayDataBrowserOnLoad
|
|
}
|
|
if (this.properties.datalayersControl === 'expanded') {
|
|
if (!this.properties.onLoadPanel) {
|
|
this.properties.onLoadPanel = 'datalayers'
|
|
}
|
|
delete this.properties.datalayersControl
|
|
}
|
|
if (this.properties.onLoadPanel === 'facet') {
|
|
this.properties.onLoadPanel = 'datafilters'
|
|
}
|
|
|
|
// Creation mode
|
|
if (!this.id) {
|
|
if (!this.properties.preview) {
|
|
this.isDirty = true
|
|
this.enableEdit()
|
|
}
|
|
this._defaultExtent = true
|
|
this.properties.name = translate('Untitled map')
|
|
await this.loadDataFromQueryString()
|
|
}
|
|
|
|
if (!this.properties.noControl) {
|
|
this.initShortcuts()
|
|
this._leafletMap.on('contextmenu', (e) => this.onContextMenu(e))
|
|
this.onceDataLoaded(this.setViewFromQueryString)
|
|
this.bottomBar.setup()
|
|
this.propagate()
|
|
}
|
|
|
|
window.onbeforeunload = () => (this.editEnabled && SAVEMANAGER.isDirty) || null
|
|
this.backup()
|
|
}
|
|
|
|
get editedFeature() {
|
|
return this._editedFeature
|
|
}
|
|
|
|
set editedFeature(feature) {
|
|
if (this._editedFeature && this._editedFeature !== feature) {
|
|
this._editedFeature.endEdit()
|
|
}
|
|
this._editedFeature = feature
|
|
this.fire('seteditedfeature')
|
|
}
|
|
|
|
setPropertiesFromQueryString() {
|
|
const asBoolean = (key) => {
|
|
const value = this.searchParams.get(key)
|
|
if (value !== undefined && value !== null) {
|
|
this.properties[key] = value === '1' || value === 'true'
|
|
}
|
|
}
|
|
const asNullableBoolean = (key) => {
|
|
if (this.searchParams.has(key)) {
|
|
let value = this.searchParams.get(key)
|
|
if (value === 'null') value = null
|
|
else if (value === '0' || value === 'false') value = false
|
|
else value = true
|
|
this.properties[key] = value
|
|
}
|
|
}
|
|
const asNumber = (key) => {
|
|
const value = +this.searchParams.get(key)
|
|
if (!Number.isNaN(value)) this.properties[name] = value
|
|
}
|
|
// FIXME retrocompat
|
|
asBoolean('displayDataBrowserOnLoad')
|
|
asBoolean('displayCaptionOnLoad')
|
|
for (const [key, schema] of Object.entries(SCHEMA)) {
|
|
switch (schema.type) {
|
|
case Boolean:
|
|
if (schema.nullable) asNullableBoolean(key)
|
|
else asBoolean(key)
|
|
break
|
|
case Number:
|
|
asNumber(key)
|
|
break
|
|
case String: {
|
|
if (this.searchParams.has(key)) {
|
|
const value = this.searchParams.get(key)
|
|
if (value !== undefined) this.properties[key] = value
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Specific case for datalayersControl
|
|
// which accepts "expanded" value, on top of true/false/null
|
|
if (this.searchParams.get('datalayersControl') === 'expanded') {
|
|
if (!this.properties.onLoadPanel) {
|
|
this.properties.onLoadPanel = 'datalayers'
|
|
}
|
|
}
|
|
}
|
|
|
|
async setViewFromQueryString() {
|
|
if (this.properties.noControl) return
|
|
// TODO: move to a "initPanel" function
|
|
if (this.searchParams.has('share')) {
|
|
this.share.open()
|
|
} else if (this.properties.onLoadPanel === 'databrowser') {
|
|
this.panel.setDefaultMode('expanded')
|
|
this.openBrowser('data')
|
|
} else if (this.properties.onLoadPanel === 'datalayers') {
|
|
this.panel.setDefaultMode('condensed')
|
|
this.openBrowser('layers')
|
|
} else if (this.properties.onLoadPanel === 'datafilters') {
|
|
this.panel.setDefaultMode('expanded')
|
|
this.openBrowser('filters')
|
|
} else if (this.properties.onLoadPanel === 'caption') {
|
|
this.panel.setDefaultMode('condensed')
|
|
this.openCaption()
|
|
}
|
|
// Comes after default panels, so if it opens in a panel it will
|
|
// take precedence.
|
|
const slug = this.searchParams.get('feature')
|
|
if (slug && this.featuresIndex[slug]) this.featuresIndex[slug].view()
|
|
if (this.searchParams.has('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 (this.searchParams.has('download')) {
|
|
const download_url = this.urls.get('map_download', {
|
|
map_id: this.id,
|
|
})
|
|
window.location = download_url
|
|
}
|
|
}
|
|
|
|
async loadDataFromQueryString() {
|
|
let data = this.searchParams.get('data')
|
|
const dataUrls = this.searchParams.getAll('dataUrl')
|
|
const dataFormat = this.searchParams.get('dataFormat') || 'geojson'
|
|
if (dataUrls.length) {
|
|
for (let dataUrl of dataUrls) {
|
|
dataUrl = decodeURIComponent(dataUrl)
|
|
dataUrl = this.renderUrl(dataUrl)
|
|
dataUrl = this.proxyUrl(dataUrl)
|
|
const datalayer = this.createDataLayer()
|
|
await datalayer
|
|
.importFromUrl(dataUrl, dataFormat)
|
|
.then(() => datalayer.zoomTo())
|
|
}
|
|
} else if (data) {
|
|
data = decodeURIComponent(data)
|
|
const datalayer = this.createDataLayer()
|
|
await datalayer.importRaw(data, dataFormat).then(() => datalayer.zoomTo())
|
|
}
|
|
}
|
|
|
|
getOwnContextMenuItems(event) {
|
|
const items = []
|
|
if (this.hasEditMode()) {
|
|
if (this.editEnabled) {
|
|
if (!SAVEMANAGER.isDirty) {
|
|
items.push({
|
|
label: this.help.displayLabel('STOP_EDIT'),
|
|
action: () => this.disableEdit(),
|
|
})
|
|
}
|
|
if (this.properties.enableMarkerDraw) {
|
|
items.push({
|
|
label: this.help.displayLabel('DRAW_MARKER'),
|
|
action: () => this._leafletMap.startMarker(event),
|
|
})
|
|
}
|
|
if (this.properties.enablePolylineDraw) {
|
|
items.push({
|
|
label: this.help.displayLabel('DRAW_POLYGON'),
|
|
action: () => this._leafletMap.startPolygon(event),
|
|
})
|
|
}
|
|
if (this.properties.enablePolygonDraw) {
|
|
items.push({
|
|
label: this.help.displayLabel('DRAW_LINE'),
|
|
action: () => this._leafletMap.startPolyline(event),
|
|
})
|
|
}
|
|
items.push('-')
|
|
items.push({
|
|
label: translate('Help'),
|
|
action: () => this.help.show('edit'),
|
|
})
|
|
} else {
|
|
items.push({
|
|
label: this.help.displayLabel('TOGGLE_EDIT'),
|
|
action: () => this.enableEdit(),
|
|
})
|
|
}
|
|
}
|
|
if (items.length) {
|
|
items.push('-')
|
|
}
|
|
items.push(
|
|
{
|
|
label: translate('Open browser'),
|
|
action: () => this.openBrowser('layers'),
|
|
},
|
|
{
|
|
label: translate('Browse data'),
|
|
action: () => this.openBrowser('data'),
|
|
}
|
|
)
|
|
if (this.properties.facetKey) {
|
|
items.push({
|
|
label: translate('Filter data'),
|
|
action: () => this.openBrowser('filters'),
|
|
})
|
|
}
|
|
items.push(
|
|
{
|
|
label: translate('Open caption'),
|
|
action: () => this.openCaption(),
|
|
},
|
|
{
|
|
label: this.help.displayLabel('SEARCH'),
|
|
action: () => this.search(),
|
|
}
|
|
)
|
|
return items
|
|
}
|
|
|
|
getSharedContextMenuItems(event) {
|
|
const items = []
|
|
if (this.properties.urls.routing) {
|
|
items.push('-', {
|
|
label: translate('Directions from here'),
|
|
action: () => this.openExternalRouting(event),
|
|
})
|
|
}
|
|
if (this.properties.urls.edit_in_osm) {
|
|
items.push('-', {
|
|
label: translate('Edit in OpenStreetMap'),
|
|
action: () => this.editInOSM(event),
|
|
})
|
|
}
|
|
return items
|
|
}
|
|
|
|
onContextMenu(event) {
|
|
const items = this.getOwnContextMenuItems(event).concat(
|
|
this.getSharedContextMenuItems(event)
|
|
)
|
|
this.contextmenu.open(event.originalEvent, items)
|
|
}
|
|
|
|
// Merge the given schema with the default one
|
|
// Missing keys inside the schema are merged with the default ones.
|
|
overrideSchema(schema) {
|
|
for (const [key, extra] of Object.entries(schema)) {
|
|
SCHEMA[key] = Object.assign({}, SCHEMA[key], extra)
|
|
}
|
|
}
|
|
|
|
search() {
|
|
if (this._leafletMap._controls.search) this._leafletMap._controls.search.open()
|
|
}
|
|
|
|
hasEditMode() {
|
|
const editMode = this.properties.editMode
|
|
return editMode === 'simple' || editMode === 'advanced'
|
|
}
|
|
|
|
getProperty(key, feature) {
|
|
if (feature) {
|
|
const value = this.rules.getOption(key, feature)
|
|
if (value !== undefined) return value
|
|
}
|
|
if (Utils.usableOption(this.properties, key)) return this.properties[key]
|
|
return SCHEMA[key]?.default
|
|
}
|
|
|
|
getOption(key, feature) {
|
|
// TODO: remove when umap.forms.js is refactored and does not call blindly
|
|
// obj.getOption anymore
|
|
return this.getProperty(key, feature)
|
|
}
|
|
|
|
getGeoContext() {
|
|
const bounds = this._leafletMap.getBounds()
|
|
const center = this._leafletMap.getCenter()
|
|
const context = {
|
|
bbox: bounds.toBBoxString(),
|
|
north: bounds.getNorthEast().lat,
|
|
east: bounds.getNorthEast().lng,
|
|
south: bounds.getSouthWest().lat,
|
|
west: bounds.getSouthWest().lng,
|
|
lat: center.lat,
|
|
lng: center.lng,
|
|
zoom: this._leafletMap.getZoom(),
|
|
}
|
|
context.left = context.west
|
|
context.bottom = context.south
|
|
context.right = context.east
|
|
context.top = context.north
|
|
return context
|
|
}
|
|
|
|
renderUrl(url) {
|
|
return Utils.greedyTemplate(url, this.getGeoContext(), true)
|
|
}
|
|
|
|
initShortcuts() {
|
|
const globalShortcuts = (event) => {
|
|
if (event.key === 'Escape') {
|
|
if (this.importer.dialog.visible) {
|
|
this.importer.dialog.close()
|
|
} else if (this.editEnabled && this._leafletMap.editTools.drawing()) {
|
|
this._leafletMap.editTools.onEscape()
|
|
} else if (this._leafletMap.measureTools.enabled()) {
|
|
this._leafletMap.measureTools.stopDrawing()
|
|
} else if (this.fullPanel?.isOpen()) {
|
|
this.fullPanel?.close()
|
|
} else if (this.editPanel?.isOpen()) {
|
|
this.editPanel?.close()
|
|
} else if (this.panel.isOpen()) {
|
|
this.panel.close()
|
|
}
|
|
}
|
|
|
|
// From now on, only ctrl/meta shortcut
|
|
if (!(event.ctrlKey || event.metaKey) || event.shiftKey) return
|
|
|
|
if (event.key === 'f') {
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
this.search()
|
|
}
|
|
|
|
/* Edit mode only shortcuts */
|
|
if (!this.hasEditMode()) return
|
|
|
|
// Edit mode Off
|
|
if (!this.editEnabled) {
|
|
switch (event.key) {
|
|
case 'e':
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
this.enableEdit()
|
|
break
|
|
}
|
|
return
|
|
}
|
|
|
|
// Edit mode on
|
|
let used = true
|
|
switch (event.key) {
|
|
case 'e':
|
|
if (!SAVEMANAGER.isDirty) this.disableEdit()
|
|
break
|
|
case 's':
|
|
if (SAVEMANAGER.isDirty) this.saveAll()
|
|
break
|
|
case 'z':
|
|
if (SAVEMANAGER.isDirty) this.askForReset()
|
|
break
|
|
case 'm':
|
|
this._leafletMap.editTools.startMarker()
|
|
break
|
|
case 'p':
|
|
this._leafletMap.editTools.startPolygon()
|
|
break
|
|
case 'l':
|
|
this._leafletMap.editTools.startPolyline()
|
|
break
|
|
case 'i':
|
|
this.importer.open()
|
|
break
|
|
case 'o':
|
|
this.importer.openFiles()
|
|
break
|
|
case 'h':
|
|
this.help.showGetStarted()
|
|
break
|
|
default:
|
|
used = false
|
|
}
|
|
if (used) {
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
}
|
|
}
|
|
document.addEventListener('keydown', globalShortcuts)
|
|
}
|
|
|
|
async initDataLayers(datalayers) {
|
|
datalayers = datalayers || this.properties.datalayers
|
|
for (const options of datalayers) {
|
|
// `false` to not propagate syncing elements served from uMap
|
|
this.createDataLayer(options, false)
|
|
}
|
|
this.datalayersLoaded = true
|
|
this.fire('datalayersloaded')
|
|
const toLoad = []
|
|
for (const datalayer of this.datalayersIndex) {
|
|
if (datalayer.showAtLoad()) toLoad.push(datalayer.show())
|
|
}
|
|
await Promise.all(toLoad)
|
|
this.dataloaded = true
|
|
this.fire('dataloaded')
|
|
}
|
|
|
|
createDataLayer(options = {}, sync = true) {
|
|
options.name =
|
|
options.name || `${translate('Layer')} ${this.datalayersIndex.length + 1}`
|
|
const datalayer = new DataLayer(this, this._leafletMap, options)
|
|
|
|
if (sync !== false) {
|
|
datalayer.sync.upsert(datalayer.options)
|
|
}
|
|
return datalayer
|
|
}
|
|
|
|
newDataLayer() {
|
|
const datalayer = this.createDataLayer({})
|
|
datalayer.edit()
|
|
}
|
|
|
|
reindexDataLayers() {
|
|
this.eachDataLayer((datalayer) => datalayer.reindex())
|
|
this.onDataLayersChanged()
|
|
}
|
|
|
|
indexDatalayers() {
|
|
const panes = this._leafletMap.getPane('overlayPane')
|
|
|
|
this.datalayersIndex = []
|
|
for (const pane of panes.children) {
|
|
if (!pane.dataset || !pane.dataset.id) continue
|
|
this.datalayersIndex.push(this.datalayers[pane.dataset.id])
|
|
}
|
|
this.onDataLayersChanged()
|
|
}
|
|
|
|
onceDatalayersLoaded(callback, context) {
|
|
// Once datalayers **metadata** have been loaded
|
|
if (this.datalayersLoaded) {
|
|
callback.call(context || this, this)
|
|
} else {
|
|
this._leafletMap.once('datalayersloaded', callback, context)
|
|
}
|
|
return this
|
|
}
|
|
|
|
onceDataLoaded(callback, context) {
|
|
// Once datalayers **data** have been loaded
|
|
if (this.dataloaded) {
|
|
callback.call(context || this, this)
|
|
} else {
|
|
this._leafletMap.once('dataloaded', callback, context || this)
|
|
}
|
|
return this
|
|
}
|
|
|
|
onDataLayersChanged() {
|
|
if (this.browser) this.browser.update()
|
|
this.caption.refresh()
|
|
}
|
|
|
|
async saveAll() {
|
|
if (!SAVEMANAGER.isDirty) return
|
|
if (this._defaultExtent) this._setCenterAndZoom()
|
|
this.backup()
|
|
await SAVEMANAGER.save()
|
|
// Do a blind render for now, as we are not sure what could
|
|
// have changed, we'll be more subtil when we'll remove the
|
|
// save action
|
|
this.render(['name', 'user', 'permissions'])
|
|
if (!this._leafletMap.listens('saved')) {
|
|
// When we save only layers, we don't have the map feedback message
|
|
this._leafletMap.on('saved', () => {
|
|
Alert.success(translate('Map has been saved!'))
|
|
})
|
|
}
|
|
this.fire('saved')
|
|
}
|
|
|
|
getDisplayName() {
|
|
return this.properties.name || translate('Untitled map')
|
|
}
|
|
|
|
backup() {
|
|
this.backupProperties()
|
|
this._datalayersIndex_bk = [].concat(this.datalayersIndex)
|
|
}
|
|
|
|
backupProperties() {
|
|
this._backupProperties = Object.assign({}, this.properties)
|
|
this._backupProperties.tilelayer = Object.assign({}, this.properties.tilelayer)
|
|
this._backupProperties.limitBounds = Object.assign({}, this.properties.limitBounds)
|
|
this._backupProperties.permissions = Object.assign({}, this.permissions.properties)
|
|
}
|
|
|
|
resetProperties() {
|
|
this.properties = Object.assign({}, this._backupProperties)
|
|
this.properties.tilelayer = Object.assign({}, this._backupProperties.tilelayer)
|
|
this.permissions.properties = Object.assign({}, this._backupProperties.permissions)
|
|
}
|
|
|
|
setProperties(newProperties) {
|
|
for (const key of Object.keys(SCHEMA)) {
|
|
if (newProperties[key] !== undefined) {
|
|
this.properties[key] = newProperties[key]
|
|
if (key === 'rules') this.rules.load()
|
|
if (key === 'slideshow') this.slideshow.load()
|
|
// TODO: sync ?
|
|
}
|
|
}
|
|
}
|
|
|
|
hasData() {
|
|
for (const datalayer of this.datalayersIndex) {
|
|
if (datalayer.hasData()) return true
|
|
}
|
|
}
|
|
|
|
hasLayers() {
|
|
return Boolean(this.datalayersIndex.length)
|
|
}
|
|
|
|
allProperties() {
|
|
return [].concat(...this.datalayersIndex.map((dl) => dl.allProperties()))
|
|
}
|
|
|
|
sortedValues(property) {
|
|
return []
|
|
.concat(...this.datalayersIndex.map((dl) => dl.sortedValues(property)))
|
|
.filter((val, idx, arr) => arr.indexOf(val) === idx)
|
|
.sort(U.Utils.naturalSort)
|
|
}
|
|
|
|
editCaption() {
|
|
if (!this.editEnabled) return
|
|
if (this.properties.editMode !== 'advanced') return
|
|
const container = DomUtil.create('div', 'umap-edit-container')
|
|
const metadataFields = ['properties.name', 'properties.description']
|
|
|
|
DomUtil.createTitle(container, translate('Edit map details'), 'icon-caption')
|
|
const builder = new U.FormBuilder(this, metadataFields, {
|
|
className: 'map-metadata',
|
|
umap: this,
|
|
})
|
|
const form = builder.build()
|
|
container.appendChild(form)
|
|
|
|
const credits = DomUtil.createFieldset(container, translate('Credits'))
|
|
const creditsFields = [
|
|
'properties.licence',
|
|
'properties.shortCredit',
|
|
'properties.longCredit',
|
|
'properties.permanentCredit',
|
|
'properties.permanentCreditBackground',
|
|
]
|
|
const creditsBuilder = new U.FormBuilder(this, creditsFields, { umap: this })
|
|
credits.appendChild(creditsBuilder.build())
|
|
this.editPanel.open({ content: container })
|
|
}
|
|
|
|
_editControls(container) {
|
|
let UIFields = []
|
|
for (const name of this._leafletMap.HIDDABLE_CONTROLS) {
|
|
UIFields.push(`properties.${name}Control`)
|
|
}
|
|
UIFields = UIFields.concat([
|
|
'properties.moreControl',
|
|
'properties.scrollWheelZoom',
|
|
'properties.miniMap',
|
|
'properties.scaleControl',
|
|
'properties.onLoadPanel',
|
|
'properties.defaultView',
|
|
'properties.displayPopupFooter',
|
|
'properties.captionBar',
|
|
'properties.captionMenus',
|
|
])
|
|
const builder = new U.FormBuilder(this, UIFields, { umap: this })
|
|
const controlsOptions = DomUtil.createFieldset(
|
|
container,
|
|
translate('User interface options')
|
|
)
|
|
controlsOptions.appendChild(builder.build())
|
|
}
|
|
|
|
_editShapeProperties(container) {
|
|
const shapeOptions = [
|
|
'properties.color',
|
|
'properties.iconClass',
|
|
'properties.iconUrl',
|
|
'properties.iconOpacity',
|
|
'properties.opacity',
|
|
'properties.weight',
|
|
'properties.fill',
|
|
'properties.fillColor',
|
|
'properties.fillOpacity',
|
|
'properties.smoothFactor',
|
|
'properties.dashArray',
|
|
]
|
|
|
|
const builder = new U.FormBuilder(this, shapeOptions, { umap: this })
|
|
const defaultShapeProperties = DomUtil.createFieldset(
|
|
container,
|
|
translate('Default shape properties')
|
|
)
|
|
defaultShapeProperties.appendChild(builder.build())
|
|
}
|
|
|
|
_editDefaultProperties(container) {
|
|
const optionsFields = [
|
|
'properties.zoomTo',
|
|
'properties.easing',
|
|
'properties.labelKey',
|
|
'properties.sortKey',
|
|
'properties.filterKey',
|
|
'properties.facetKey',
|
|
'properties.slugKey',
|
|
]
|
|
|
|
const builder = new U.FormBuilder(this, optionsFields, { umap: this })
|
|
const defaultProperties = DomUtil.createFieldset(
|
|
container,
|
|
translate('Default properties')
|
|
)
|
|
defaultProperties.appendChild(builder.build())
|
|
}
|
|
|
|
_editInteractionsProperties(container) {
|
|
const popupFields = [
|
|
'properties.popupShape',
|
|
'properties.popupTemplate',
|
|
'properties.popupContentTemplate',
|
|
'properties.showLabel',
|
|
'properties.labelDirection',
|
|
'properties.labelInteractive',
|
|
'properties.outlinkTarget',
|
|
]
|
|
const builder = new U.FormBuilder(this, popupFields, { umap: this })
|
|
const popupFieldset = DomUtil.createFieldset(
|
|
container,
|
|
translate('Default interaction options')
|
|
)
|
|
popupFieldset.appendChild(builder.build())
|
|
}
|
|
|
|
_editTilelayer(container) {
|
|
if (!Utils.isObject(this.properties.tilelayer)) {
|
|
this.properties.tilelayer = {}
|
|
}
|
|
const tilelayerFields = [
|
|
[
|
|
'properties.tilelayer.name',
|
|
{ handler: 'BlurInput', placeholder: translate('display name') },
|
|
],
|
|
[
|
|
'properties.tilelayer.url_template',
|
|
{
|
|
handler: 'BlurInput',
|
|
helpText: `${translate('Supported scheme')}: http://{s}.domain.com/{z}/{x}/{y}.png`,
|
|
placeholder: 'url',
|
|
type: 'url',
|
|
},
|
|
],
|
|
[
|
|
'properties.tilelayer.maxZoom',
|
|
{
|
|
handler: 'BlurIntInput',
|
|
placeholder: translate('max zoom'),
|
|
min: 0,
|
|
max: this.properties.maxZoomLimit,
|
|
},
|
|
],
|
|
[
|
|
'properties.tilelayer.minZoom',
|
|
{
|
|
handler: 'BlurIntInput',
|
|
placeholder: translate('min zoom'),
|
|
min: 0,
|
|
max: this.properties.maxZoomLimit,
|
|
},
|
|
],
|
|
[
|
|
'properties.tilelayer.attribution',
|
|
{ handler: 'BlurInput', placeholder: translate('attribution') },
|
|
],
|
|
[
|
|
'properties.tilelayer.tms',
|
|
{ handler: 'Switch', label: translate('TMS format') },
|
|
],
|
|
]
|
|
const customTilelayer = DomUtil.createFieldset(
|
|
container,
|
|
translate('Custom background')
|
|
)
|
|
const builder = new U.FormBuilder(this, tilelayerFields, { umap: this })
|
|
customTilelayer.appendChild(builder.build())
|
|
}
|
|
|
|
_editOverlay(container) {
|
|
if (!Utils.isObject(this.properties.overlay)) {
|
|
this.properties.overlay = {}
|
|
}
|
|
const overlayFields = [
|
|
[
|
|
'properties.overlay.url_template',
|
|
{
|
|
handler: 'BlurInput',
|
|
helpText: `${translate('Supported scheme')}: http://{s}.domain.com/{z}/{x}/{y}.png`,
|
|
placeholder: 'url',
|
|
label: translate('Background overlay url'),
|
|
type: 'url',
|
|
},
|
|
],
|
|
[
|
|
'properties.overlay.maxZoom',
|
|
{
|
|
handler: 'BlurIntInput',
|
|
placeholder: translate('max zoom'),
|
|
min: 0,
|
|
max: this.properties.maxZoomLimit,
|
|
},
|
|
],
|
|
[
|
|
'properties.overlay.minZoom',
|
|
{
|
|
handler: 'BlurIntInput',
|
|
placeholder: translate('min zoom'),
|
|
min: 0,
|
|
max: this.properties.maxZoomLimit,
|
|
},
|
|
],
|
|
[
|
|
'properties.overlay.attribution',
|
|
{ handler: 'BlurInput', placeholder: translate('attribution') },
|
|
],
|
|
[
|
|
'properties.overlay.opacity',
|
|
{ handler: 'Range', min: 0, max: 1, step: 0.1, label: translate('Opacity') },
|
|
],
|
|
['properties.overlay.tms', { handler: 'Switch', label: translate('TMS format') }],
|
|
]
|
|
const overlay = DomUtil.createFieldset(container, translate('Custom overlay'))
|
|
const builder = new U.FormBuilder(this, overlayFields, { umap: this })
|
|
overlay.appendChild(builder.build())
|
|
}
|
|
|
|
_editBounds(container) {
|
|
if (!Utils.isObject(this.properties.limitBounds)) {
|
|
this.properties.limitBounds = {}
|
|
}
|
|
const limitBounds = DomUtil.createFieldset(container, translate('Limit bounds'))
|
|
const boundsFields = [
|
|
[
|
|
'properties.limitBounds.south',
|
|
{ handler: 'BlurFloatInput', placeholder: translate('max South') },
|
|
],
|
|
[
|
|
'properties.limitBounds.west',
|
|
{ handler: 'BlurFloatInput', placeholder: translate('max West') },
|
|
],
|
|
[
|
|
'properties.limitBounds.north',
|
|
{ handler: 'BlurFloatInput', placeholder: translate('max North') },
|
|
],
|
|
[
|
|
'properties.limitBounds.east',
|
|
{ handler: 'BlurFloatInput', placeholder: translate('max East') },
|
|
],
|
|
]
|
|
const boundsBuilder = new U.FormBuilder(this, boundsFields, { umap: this })
|
|
limitBounds.appendChild(boundsBuilder.build())
|
|
const boundsButtons = DomUtil.create('div', 'button-bar half', limitBounds)
|
|
DomUtil.createButton(
|
|
'button',
|
|
boundsButtons,
|
|
translate('Use current bounds'),
|
|
function () {
|
|
const bounds = this._leafletMap.getBounds()
|
|
this.properties.limitBounds.south = LeafletUtil.formatNum(bounds.getSouth())
|
|
this.properties.limitBounds.west = LeafletUtil.formatNum(bounds.getWest())
|
|
this.properties.limitBounds.north = LeafletUtil.formatNum(bounds.getNorth())
|
|
this.properties.limitBounds.east = LeafletUtil.formatNum(bounds.getEast())
|
|
boundsBuilder.fetchAll()
|
|
|
|
this.sync.update(this, 'properties.limitBounds', this.properties.limitBounds)
|
|
this.isDirty = true
|
|
this._leafletMap.handleLimitBounds()
|
|
},
|
|
this
|
|
)
|
|
DomUtil.createButton(
|
|
'button',
|
|
boundsButtons,
|
|
translate('Empty'),
|
|
function () {
|
|
this.properties.limitBounds.south = null
|
|
this.properties.limitBounds.west = null
|
|
this.properties.limitBounds.north = null
|
|
this.properties.limitBounds.east = null
|
|
boundsBuilder.fetchAll()
|
|
this.isDirty = true
|
|
this._leafletMap.handleLimitBounds()
|
|
},
|
|
this
|
|
)
|
|
}
|
|
|
|
_editSlideshow(container) {
|
|
const slideshow = DomUtil.createFieldset(container, translate('Slideshow'))
|
|
const slideshowFields = [
|
|
[
|
|
'properties.slideshow.active',
|
|
{ handler: 'Switch', label: translate('Activate slideshow mode') },
|
|
],
|
|
[
|
|
'properties.slideshow.delay',
|
|
{
|
|
handler: 'SlideshowDelay',
|
|
helpText: translate('Delay between two transitions when in play mode'),
|
|
},
|
|
],
|
|
[
|
|
'properties.slideshow.easing',
|
|
{
|
|
handler: 'Switch',
|
|
label: translate('Animated transitions'),
|
|
inheritable: true,
|
|
},
|
|
],
|
|
[
|
|
'properties.slideshow.autoplay',
|
|
{ handler: 'Switch', label: translate('Autostart when map is loaded') },
|
|
],
|
|
]
|
|
const slideshowBuilder = new U.FormBuilder(this, slideshowFields, {
|
|
callback: () => {
|
|
this.slideshow.load()
|
|
// FIXME when we refactor formbuilder: this callback is called in a 'postsync'
|
|
// event, which comes after the call of `setter` method, which will call the
|
|
// map.render method, which should do this redraw.
|
|
this.bottomBar.redraw()
|
|
},
|
|
umap: this,
|
|
})
|
|
slideshow.appendChild(slideshowBuilder.build())
|
|
}
|
|
|
|
_editSync(container) {
|
|
const sync = DomUtil.createFieldset(container, translate('Real-time collaboration'))
|
|
const builder = new U.FormBuilder(this, ['properties.syncEnabled'], { umap: this })
|
|
sync.appendChild(builder.build())
|
|
}
|
|
|
|
_advancedActions(container) {
|
|
const advancedActions = DomUtil.createFieldset(
|
|
container,
|
|
translate('Advanced actions')
|
|
)
|
|
const advancedButtons = DomUtil.create('div', 'button-bar half', advancedActions)
|
|
if (this.permissions.isOwner()) {
|
|
const deleteButton = Utils.loadTemplate(`
|
|
<button class="button" type="button">
|
|
<i class="icon icon-24 icon-delete"></i>${translate('Delete')}
|
|
</button>`)
|
|
deleteButton.addEventListener('click', () => this.del())
|
|
advancedButtons.appendChild(deleteButton)
|
|
|
|
DomUtil.createButton(
|
|
'button umap-empty',
|
|
advancedButtons,
|
|
translate('Clear data'),
|
|
this.emptyDataLayers,
|
|
this
|
|
)
|
|
DomUtil.createButton(
|
|
'button umap-empty',
|
|
advancedButtons,
|
|
translate('Remove layers'),
|
|
this.removeDataLayers,
|
|
this
|
|
)
|
|
}
|
|
DomUtil.createButton(
|
|
'button umap-clone',
|
|
advancedButtons,
|
|
translate('Clone this map'),
|
|
this.clone,
|
|
this
|
|
)
|
|
DomUtil.createButton(
|
|
'button umap-download',
|
|
advancedButtons,
|
|
translate('Open share & download panel'),
|
|
this.share.open,
|
|
this.share
|
|
)
|
|
}
|
|
|
|
edit() {
|
|
if (!this.editEnabled) return
|
|
if (this.properties.editMode !== 'advanced') return
|
|
const container = DomUtil.create('div')
|
|
DomUtil.createTitle(
|
|
container,
|
|
translate('Map advanced properties'),
|
|
'icon-settings'
|
|
)
|
|
this._editControls(container)
|
|
this._editShapeProperties(container)
|
|
this._editDefaultProperties(container)
|
|
this._editInteractionsProperties(container)
|
|
this.rules.edit(container)
|
|
this._editTilelayer(container)
|
|
this._editOverlay(container)
|
|
this._editBounds(container)
|
|
this._editSlideshow(container)
|
|
if (this.properties.websocketEnabled) {
|
|
this._editSync(container)
|
|
}
|
|
this._advancedActions(container)
|
|
|
|
this.editPanel.open({ content: container, className: 'dark' })
|
|
}
|
|
|
|
reset() {
|
|
if (this._leafletMap.editTools) this._leafletMap.editTools.stopDrawing()
|
|
this.resetProperties()
|
|
this.datalayersIndex = [].concat(this._datalayersIndex_bk)
|
|
// Iter over all datalayers, including deleted if any.
|
|
for (const datalayer of Object.values(this.datalayers)) {
|
|
if (datalayer.isDeleted) datalayer.connectToMap()
|
|
if (datalayer.isDirty) datalayer.reset()
|
|
}
|
|
this.ensurePanesOrder()
|
|
this._leafletMap.initTileLayers()
|
|
this.isDirty = false
|
|
this.onDataLayersChanged()
|
|
}
|
|
|
|
async save() {
|
|
this.rules.commit()
|
|
const geojson = {
|
|
type: 'Feature',
|
|
geometry: this.geometry(),
|
|
properties: this.exportProperties(),
|
|
}
|
|
const formData = new FormData()
|
|
formData.append('name', this.properties.name)
|
|
formData.append('center', JSON.stringify(this.geometry()))
|
|
formData.append('settings', JSON.stringify(geojson))
|
|
const uri = this.urls.get('map_save', { map_id: this.id })
|
|
const [data, _, error] = await this.server.post(uri, {}, formData)
|
|
// FIXME: login_required response will not be an error, so it will not
|
|
// stop code while it should
|
|
if (error) {
|
|
return
|
|
}
|
|
// TOOD: map.save may not always be the first call during save process
|
|
// since SAVEMANAGER refactor
|
|
if (data.login_required) {
|
|
window.onLogin = () => this.saveAll()
|
|
window.open(data.login_required)
|
|
return
|
|
}
|
|
this.properties.user = data.user
|
|
if (!this.id) {
|
|
this.properties.id = data.id
|
|
this.permissions.setProperties(data.permissions)
|
|
this.permissions.commit()
|
|
if (data.permissions?.anonymous_edit_url) {
|
|
this._leafletMap.once('saved', () => {
|
|
AlertCreation.info(
|
|
translate('Your map has been created with an anonymous account!'),
|
|
Number.Infinity,
|
|
data.permissions.anonymous_edit_url,
|
|
this.properties.urls.map_send_edit_link
|
|
? this.sendEditLinkEmail.bind(this)
|
|
: null
|
|
)
|
|
})
|
|
} else {
|
|
this._leafletMap.once('saved', () => {
|
|
Alert.success(translate('Congratulations, your map has been created!'))
|
|
})
|
|
}
|
|
} 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.setProperties(data.permissions)
|
|
this.permissions.commit()
|
|
}
|
|
this._leafletMap.once('saved', () => {
|
|
Alert.success(data.info || translate('Map has been saved!'))
|
|
})
|
|
}
|
|
// Update URL in case the name has changed.
|
|
if (history?.pushState) {
|
|
history.pushState({}, this.properties.name, data.url)
|
|
} else {
|
|
window.location = data.url
|
|
}
|
|
return true
|
|
}
|
|
|
|
exportProperties() {
|
|
const properties = {}
|
|
for (const key of Object.keys(SCHEMA)) {
|
|
if (this.properties[key] !== undefined) {
|
|
properties[key] = this.properties[key]
|
|
}
|
|
}
|
|
return properties
|
|
}
|
|
|
|
geometry() {
|
|
/* Return a GeoJSON geometry Object */
|
|
const latlng = this._leafletMap.latLng(
|
|
this.properties.center || this._leafletMap.getCenter()
|
|
)
|
|
return {
|
|
type: 'Point',
|
|
coordinates: [latlng.lng, latlng.lat],
|
|
}
|
|
}
|
|
|
|
toGeoJSON() {
|
|
let features = []
|
|
this.eachDataLayer((datalayer) => {
|
|
if (datalayer.isVisible()) {
|
|
features = features.concat(datalayer.featuresToGeoJSON())
|
|
}
|
|
})
|
|
const geojson = {
|
|
type: 'FeatureCollection',
|
|
features: features,
|
|
}
|
|
return geojson
|
|
}
|
|
|
|
enableEdit() {
|
|
document.body.classList.add('umap-edit-enabled')
|
|
this.editEnabled = true
|
|
this.drop.enable()
|
|
this.fire('edit:enabled')
|
|
this.initSyncEngine()
|
|
}
|
|
|
|
disableEdit() {
|
|
if (this.isDirty) return
|
|
this.drop.disable()
|
|
document.body.classList.remove('umap-edit-enabled')
|
|
this.editedFeature = null
|
|
this.editEnabled = false
|
|
this.fire('edit:disabled')
|
|
this.editPanel.close()
|
|
this.fullPanel.close()
|
|
this.sync.stop()
|
|
this._leafletMap.closeInplaceToolbar()
|
|
}
|
|
|
|
fire(name) {
|
|
this._leafletMap.fire(name)
|
|
}
|
|
|
|
askForReset(e) {
|
|
this.dialog
|
|
.confirm(translate('Are you sure you want to cancel your changes?'))
|
|
.then(() => {
|
|
this.reset()
|
|
this.disableEdit()
|
|
})
|
|
}
|
|
|
|
async initSyncEngine() {
|
|
if (this.properties.websocketEnabled === false) return
|
|
if (this.properties.syncEnabled !== true) {
|
|
this.sync.stop()
|
|
} else {
|
|
const ws_token_uri = this.urls.get('map_websocket_auth_token', {
|
|
map_id: this.id,
|
|
})
|
|
await this.sync.authenticate(
|
|
ws_token_uri,
|
|
this.properties.websocketURI,
|
|
this.server
|
|
)
|
|
}
|
|
}
|
|
|
|
getSyncMetadata() {
|
|
return {
|
|
engine: this.sync,
|
|
subject: 'map',
|
|
}
|
|
}
|
|
|
|
render(fields = []) {
|
|
// Propagate will remove the fields it has already
|
|
// processed
|
|
fields = this.propagate(fields)
|
|
|
|
const impacts = Utils.getImpactsFromSchema(fields)
|
|
for (const impact of impacts) {
|
|
switch (impact) {
|
|
case 'ui':
|
|
this._leafletMap.renderUI()
|
|
this.browser.redraw()
|
|
this.topBar.redraw()
|
|
this.bottomBar.redraw()
|
|
break
|
|
case 'data':
|
|
this.eachVisibleDataLayer((datalayer) => {
|
|
datalayer.redraw()
|
|
})
|
|
break
|
|
case 'datalayer-index':
|
|
this.reindexDataLayers()
|
|
break
|
|
case 'background':
|
|
this._leafletMap.initTileLayers()
|
|
break
|
|
case 'bounds':
|
|
this._leafletMap.handleLimitBounds()
|
|
break
|
|
case 'sync':
|
|
this.initSyncEngine()
|
|
}
|
|
}
|
|
}
|
|
|
|
// This method does a targeted update of the UI,
|
|
// it whould be merged with `render`` method and the
|
|
// SCHEMA at some point
|
|
propagate(fields = []) {
|
|
const impacts = {
|
|
'properties.name': () => {
|
|
Utils.eachElement('.map-name', (el) => {
|
|
el.textContent = this.getDisplayName()
|
|
})
|
|
},
|
|
user: () => {
|
|
Utils.eachElement('.umap-user .username', (el) => {
|
|
if (this.properties.user?.id) {
|
|
el.textContent = this.properties.user.name
|
|
}
|
|
})
|
|
},
|
|
'properties.permissions': () => {
|
|
const status = this.permissions.getShareStatusDisplay()
|
|
if (status) {
|
|
Utils.eachElement('.share-status', (el) => {
|
|
el.textContent = translate('Visibility: {status}', {
|
|
status: status,
|
|
})
|
|
})
|
|
}
|
|
this.topBar.redraw()
|
|
},
|
|
numberOfConnectedPeers: () => {
|
|
Utils.eachElement('.connected-peers span', (el) => {
|
|
el.textContent = this.sync.getNumberOfConnectedPeers()
|
|
})
|
|
},
|
|
}
|
|
for (const [field, impact] of Object.entries(impacts)) {
|
|
if (!fields.length || fields.includes(field)) {
|
|
impact()
|
|
fields = fields.filter((item) => item !== field)
|
|
}
|
|
}
|
|
return fields
|
|
}
|
|
|
|
// TODO: allow to control the default datalayer
|
|
// (edit and viewing)
|
|
// cf https://github.com/umap-project/umap/issues/585
|
|
defaultEditDataLayer() {
|
|
let datalayer
|
|
let fallback
|
|
datalayer = this.lastUsedDataLayer
|
|
if (
|
|
datalayer &&
|
|
!datalayer.isDataReadOnly() &&
|
|
datalayer.isBrowsable() &&
|
|
datalayer.isVisible()
|
|
) {
|
|
return datalayer
|
|
}
|
|
datalayer = this.findDataLayer((datalayer) => {
|
|
if (!datalayer.isDataReadOnly() && datalayer.isBrowsable()) {
|
|
fallback = datalayer
|
|
if (datalayer.isVisible()) return true
|
|
}
|
|
})
|
|
if (datalayer) return datalayer
|
|
if (fallback) {
|
|
// No datalayer visible, let's force one
|
|
fallback.show()
|
|
return fallback
|
|
}
|
|
return this.createDataLayer()
|
|
}
|
|
|
|
findDataLayer(method, context) {
|
|
for (let i = this.datalayersIndex.length - 1; i >= 0; i--) {
|
|
if (method.call(context, this.datalayersIndex[i])) {
|
|
return this.datalayersIndex[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
eachDataLayer(method, context) {
|
|
for (let i = 0; i < this.datalayersIndex.length; i++) {
|
|
method.call(context, this.datalayersIndex[i])
|
|
}
|
|
}
|
|
|
|
eachDataLayerReverse(method, context, filter) {
|
|
for (let i = this.datalayersIndex.length - 1; i >= 0; i--) {
|
|
if (filter && !filter.call(context, this.datalayersIndex[i])) continue
|
|
method.call(context, this.datalayersIndex[i])
|
|
}
|
|
}
|
|
|
|
eachBrowsableDataLayer(method, context) {
|
|
this.eachDataLayerReverse(method, context, (d) => d.allowBrowse())
|
|
}
|
|
|
|
eachVisibleDataLayer(method, context) {
|
|
this.eachDataLayerReverse(method, context, (d) => d.isVisible())
|
|
}
|
|
|
|
eachFeature(callback, context) {
|
|
this.eachBrowsableDataLayer((datalayer) => {
|
|
if (datalayer.isVisible()) datalayer.eachFeature(callback, context)
|
|
})
|
|
}
|
|
|
|
removeDataLayers() {
|
|
this.eachDataLayerReverse((datalayer) => {
|
|
datalayer._delete()
|
|
})
|
|
}
|
|
|
|
emptyDataLayers() {
|
|
this.eachDataLayerReverse((datalayer) => {
|
|
datalayer.empty()
|
|
})
|
|
}
|
|
|
|
editDatalayers() {
|
|
if (!this.editEnabled) return
|
|
const container = DomUtil.create('div')
|
|
DomUtil.createTitle(container, translate('Manage layers'), 'icon-layers')
|
|
const ul = DomUtil.create('ul', '', container)
|
|
this.eachDataLayerReverse((datalayer) => {
|
|
const row = DomUtil.create('li', 'orderable', ul)
|
|
DomUtil.createIcon(row, 'icon-drag', translate('Drag to reorder'))
|
|
datalayer.renderToolbox(row)
|
|
const title = DomUtil.add('span', '', row, datalayer.options.name)
|
|
row.classList.toggle('off', !datalayer.isVisible())
|
|
title.textContent = datalayer.options.name
|
|
row.dataset.id = stamp(datalayer)
|
|
})
|
|
const onReorder = (src, dst, initialIndex, finalIndex) => {
|
|
const movedLayer = this.datalayers[src.dataset.id]
|
|
const targetLayer = this.datalayers[dst.dataset.id]
|
|
const minIndex = Math.min(movedLayer.getRank(), targetLayer.getRank())
|
|
const maxIndex = Math.max(movedLayer.getRank(), targetLayer.getRank())
|
|
if (finalIndex === 0) movedLayer.bringToTop()
|
|
else if (finalIndex > initialIndex) movedLayer.insertBefore(targetLayer)
|
|
else movedLayer.insertAfter(targetLayer)
|
|
this.eachDataLayerReverse((datalayer) => {
|
|
if (datalayer.getRank() >= minIndex && datalayer.getRank() <= maxIndex)
|
|
datalayer.isDirty = true
|
|
})
|
|
this.indexDatalayers()
|
|
}
|
|
const orderable = new Orderable(ul, onReorder)
|
|
|
|
const bar = DomUtil.create('div', 'button-bar', container)
|
|
DomUtil.createButton(
|
|
'show-on-edit block add-datalayer button',
|
|
bar,
|
|
translate('Add a layer'),
|
|
this.newDataLayer,
|
|
this
|
|
)
|
|
|
|
this.editPanel.open({ content: container })
|
|
}
|
|
|
|
getDataLayerByUmapId(id) {
|
|
const datalayer = this.findDataLayer((d) => d.id === id)
|
|
if (!datalayer) throw new Error(`Can't find datalayer with id ${id}`)
|
|
return datalayer
|
|
}
|
|
|
|
firstVisibleDatalayer() {
|
|
return this.findDataLayer((datalayer) => {
|
|
if (datalayer.isVisible()) return true
|
|
})
|
|
}
|
|
|
|
ensurePanesOrder() {
|
|
this.eachDataLayer((datalayer) => {
|
|
datalayer.bringToTop()
|
|
})
|
|
}
|
|
|
|
openBrowser(mode) {
|
|
this.onceDatalayersLoaded(() => this.browser.open(mode))
|
|
}
|
|
|
|
openCaption() {
|
|
this.onceDatalayersLoaded(() => this.caption.open())
|
|
}
|
|
|
|
addAuthorLink(container) {
|
|
const author = this.properties.author
|
|
if (author?.name) {
|
|
const el = Utils.loadTemplate(
|
|
`<span class="umap-map-author"> ${translate('by')} <a href="${author.url}">${author.name}</a></span>`
|
|
)
|
|
container.appendChild(el)
|
|
}
|
|
}
|
|
|
|
async star() {
|
|
if (!this.id) {
|
|
return Alert.error(translate('Please save the map first'))
|
|
}
|
|
const url = this.urls.get('map_star', { map_id: this.id })
|
|
const [data, response, error] = await this.server.post(url)
|
|
if (error) {
|
|
return
|
|
}
|
|
this.properties.starred = data.starred
|
|
Alert.success(
|
|
data.starred
|
|
? translate('Map has been starred')
|
|
: translate('Map has been unstarred')
|
|
)
|
|
this.render(['starred'])
|
|
}
|
|
|
|
processFileToImport(file, layer, type) {
|
|
type = type || Utils.detectFileType(file)
|
|
if (!type) {
|
|
Alert.error(
|
|
translate('Unable to detect format of file {filename}', {
|
|
filename: file.name,
|
|
})
|
|
)
|
|
return
|
|
}
|
|
if (type === 'umap') {
|
|
this.importUmapFile(file, 'umap')
|
|
} else {
|
|
if (!layer) layer = this.createDataLayer({ name: file.name })
|
|
layer.importFromFile(file, type)
|
|
}
|
|
}
|
|
|
|
async importFromUrl(uri) {
|
|
const response = await this.request.get(uri)
|
|
if (response?.ok) {
|
|
this.importRaw(await response.text())
|
|
}
|
|
}
|
|
|
|
importRaw(rawData) {
|
|
const importedData = JSON.parse(rawData)
|
|
|
|
this.setProperties(importedData.properties)
|
|
|
|
if (importedData.geometry) {
|
|
this.properties.center = this._leafletMap.latLng(importedData.geometry)
|
|
}
|
|
for (const geojson of importedData.layers) {
|
|
if (!geojson._umap_options && geojson._storage) {
|
|
geojson._umap_options = geojson._storage
|
|
delete geojson._storage
|
|
}
|
|
delete geojson._umap_options?.id // Never trust an id at this stage
|
|
const dataLayer = this.createDataLayer(geojson._umap_options)
|
|
dataLayer.fromUmapGeoJSON(geojson)
|
|
}
|
|
|
|
// For now render->propagate expect a `properties.` prefix.
|
|
// Remove this when we have refactored schema and render.
|
|
const fields = Object.keys(importedData.properties).map(
|
|
(field) => `properties.${field}`
|
|
)
|
|
this.render(fields)
|
|
this._leafletMap._setDefaultCenter()
|
|
this.isDirty = true
|
|
}
|
|
|
|
importUmapFile(file) {
|
|
const reader = new FileReader()
|
|
reader.readAsText(file)
|
|
reader.onload = (e) => {
|
|
const rawData = e.target.result
|
|
try {
|
|
this.importRaw(rawData)
|
|
} catch (e) {
|
|
console.error('Error importing data', e)
|
|
U.Alert.error(
|
|
translate('Invalid umap data in {filename}', { filename: file.name })
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
async del() {
|
|
this.dialog
|
|
.confirm(translate('Are you sure you want to delete this map?'))
|
|
.then(async () => {
|
|
const url = this.urls.get('map_delete', { map_id: this.id })
|
|
const [data, response, error] = await this.server.post(url)
|
|
if (data.redirect) window.location = data.redirect
|
|
})
|
|
}
|
|
|
|
async clone() {
|
|
this.dialog
|
|
.confirm(
|
|
translate('Are you sure you want to clone this map and all its datalayers?')
|
|
)
|
|
.then(async () => {
|
|
const url = this.urls.get('map_clone', { map_id: this.id })
|
|
const [data, response, error] = await this.server.post(url)
|
|
if (data.redirect) window.location = data.redirect
|
|
})
|
|
}
|
|
|
|
async sendEditLinkEmail(formData) {
|
|
const sendLink =
|
|
this.properties.urls.map_send_edit_link &&
|
|
this.urls.get('map_send_edit_link', {
|
|
map_id: this.id,
|
|
})
|
|
await this.server.post(sendLink, {}, formData)
|
|
}
|
|
|
|
getLayersBounds() {
|
|
const bounds = new latLngBounds()
|
|
this.eachBrowsableDataLayer((d) => {
|
|
if (d.isVisible()) bounds.extend(d.layer.getBounds())
|
|
})
|
|
return bounds
|
|
}
|
|
|
|
fitDataBounds() {
|
|
const bounds = this.getLayersBounds()
|
|
if (!this.hasData() || !bounds.isValid()) return false
|
|
this._leafletMap.fitBounds(bounds)
|
|
}
|
|
|
|
proxyUrl(url, ttl) {
|
|
if (this.properties.urls.ajax_proxy) {
|
|
url = Utils.greedyTemplate(this.properties.urls.ajax_proxy, {
|
|
url: encodeURIComponent(url),
|
|
ttl: ttl,
|
|
})
|
|
}
|
|
return url
|
|
}
|
|
|
|
openExternalRouting(event) {
|
|
const url = this.urls.get('routing', {
|
|
lat: event.latlng.lat,
|
|
lng: event.latlng.lng,
|
|
locale: getLocale(),
|
|
zoom: this._leafletMap.getZoom(),
|
|
})
|
|
if (url) window.open(url)
|
|
}
|
|
|
|
editInOSM(event) {
|
|
const url = this.urls.get('edit_in_osm', {
|
|
lat: event.latlng.lat,
|
|
lng: event.latlng.lng,
|
|
zoom: Math.max(this._leafletMap.getZoom(), 16),
|
|
})
|
|
if (url) window.open(url)
|
|
}
|
|
|
|
setCenterAndZoom() {
|
|
this._setCenterAndZoom()
|
|
Alert.success(translate('The zoom and center have been modified.'))
|
|
}
|
|
|
|
_setCenterAndZoom() {
|
|
this.properties.center = this._leafletMap.getCenter()
|
|
this.properties.zoom = this._leafletMap.getZoom()
|
|
this.isDirty = true
|
|
this._defaultExtent = false
|
|
}
|
|
}
|