umap/umap/static/umap/js/modules/browser.js

280 lines
9.8 KiB
JavaScript

import { DomEvent, DomUtil, stamp } from '../../vendors/leaflet/leaflet-src.esm.js'
import { Form } from './form/builder.js'
import { EXPORT_FORMATS } from './formatter.js'
import { translate } from './i18n.js'
import * as Icon from './rendering/icon.js'
import ContextMenu from './ui/contextmenu.js'
import * as Utils from './utils.js'
import { SCHEMA } from './schema.js'
export default class Browser {
constructor(umap, leafletMap) {
this._umap = umap
this._leafletMap = leafletMap
this._leafletMap.on('moveend', this.onMoveEnd, this)
this.options = {
filter: '',
inBbox: false,
}
this.mode = 'layers'
}
addFeature(feature, parent) {
if (feature.isFiltered()) return
if (this.options.inBbox && !feature.isOnScreen(this.bounds)) return
const template = `
<li class="feature ${feature.getClassName()}">
<button class="icon icon-16 icon-zoom" title="${translate('Bring feature to center')}" data-ref=zoom></button>
<button class="icon icon-16 show-on-edit icon-edit" title="${translate('Edit this feature')}" data-ref=edit></button>
<button class="icon icon-16 show-on-edit icon-delete" title="${translate('Delete this feature')}" data-ref=remove></button>
<i class="icon icon-16 icon-${feature.getClassName()} feature-color" data-ref=colorBox></i>
<span class="feature-title" data-ref=label></span>
</li>
`
const [row, { zoom, edit, remove, colorBox, label }] =
Utils.loadTemplateWithRefs(template)
label.textContent = label.title = feature.getDisplayName() || '—'
const symbol = feature._getIconUrl
? Icon.formatUrl(feature._getIconUrl(), feature)
: null
const bgcolor = feature.getPreviewColor()
colorBox.style.backgroundColor = bgcolor
if (symbol && symbol !== SCHEMA.iconUrl.default) {
const icon = Icon.makeElement(symbol, colorBox)
Icon.setContrast(icon, colorBox, symbol, bgcolor)
} else if (DomUtil.contrastedColor(colorBox, bgcolor)) {
colorBox.classList.add('icon-white')
}
const viewFeature = (e) => {
feature.zoomTo({ ...e, callback: () => feature.view() })
}
zoom.addEventListener('click', viewFeature)
label.addEventListener('click', viewFeature)
edit.addEventListener('click', () => feature.edit())
remove.addEventListener('click', () => feature.del())
// HOTFIX. Remove when this is released:
// https://github.com/Leaflet/Leaflet/pull/9052
DomEvent.disableClickPropagation(row)
parent.appendChild(row)
}
datalayerId(datalayer) {
return `browse_data_datalayer_${stamp(datalayer)}`
}
addDataLayer(datalayer, parent) {
let className = `datalayer ${datalayer.getHidableClass()}`
if (this.mode !== 'layers') className += ' show-list'
const [container, { headline, toolbox, toggle, label }] =
Utils.loadTemplateWithRefs(`
<div class="${className}" id="${this.datalayerId(datalayer)}">
<h5 data-ref=headline>
<i class="icon icon-16 datalayer-toggle-list" data-ref=toggle></i>
<span data-ref=toolbox></span>
<span class="datalayer-name" data-id="${datalayer.id}" data-ref=label></span>
<span class="datalayer-counter"></span>
</h5>
<ul></ul>
</div>
`)
datalayer.renderToolbox(toolbox)
parent.appendChild(container)
const toggleList = () => parent.classList.toggle('show-list')
toggle.addEventListener('click', toggleList)
label.addEventListener('click', toggleList)
this.updateDatalayer(datalayer)
}
updateDatalayer(datalayer) {
// Compute once, but use it for each feature later.
this.bounds = this._leafletMap.getBounds()
const id = this.datalayerId(datalayer)
const parent = document.getElementById(id)
// Panel is not open
if (!parent) return
parent.classList.toggle('off', !datalayer.isVisible())
const label = parent.querySelector('.datalayer-name')
const container = parent.querySelector('ul')
container.innerHTML = ''
datalayer.eachFeature((feature) => this.addFeature(feature, container))
datalayer.propagate(['properties.name'])
const total = datalayer.count()
if (!total) return
const current = container.querySelectorAll('li').length
const count = total === current ? total : `${current}/${total}`
const counter = parent.querySelector('.datalayer-counter')
counter.textContent = `(${count})`
counter.title = translate(`Features in this layer: ${count}`)
}
toggleBadge() {
Utils.toggleBadge(this.filtersTitle, this.hasFilters())
Utils.toggleBadge('.umap-control-browse', this.hasFilters())
}
onFormChange() {
this._umap.eachBrowsableDataLayer((datalayer) => {
datalayer.resetLayer(true)
this.updateDatalayer(datalayer)
if (this._umap.fullPanel?.isOpen()) datalayer.tableEdit()
})
this.toggleBadge()
}
redraw() {
if (this.isOpen()) this.open()
}
isOpen() {
return !!document.querySelector('.on .umap-browser')
}
hasFilters() {
return !!this.options.filter || this._umap.facets.isActive()
}
onMoveEnd() {
if (!this.isOpen()) return
const isListDynamic = this.options.inBbox
this._umap.eachBrowsableDataLayer((datalayer) => {
if (!isListDynamic && !datalayer.hasDynamicData()) return
this.updateDatalayer(datalayer)
})
}
update() {
if (!this.isOpen()) return
this.dataContainer.innerHTML = ''
this._umap.eachBrowsableDataLayer((datalayer) => {
this.addDataLayer(datalayer, this.dataContainer)
})
}
open(mode) {
// Force only if mode is known, otherwise keep current mode.
if (mode) this.mode = mode
const template = `
<div>
<h3><i class="icon icon-16 icon-layers"></i>${translate('Data browser')}</h3>
<details class="filters" data-ref="details">
<summary data-ref=filtersTitle><i class="icon icon-16 icon-filters"></i>${translate('Filters')}</summary>
<fieldset>
<div data-ref=formContainer>
</div>
<button class="flat" type="button" data-ref=reset><i class="icon icon-16 icon-restore" title=""></i>${translate('Reset all')}</button>
</fieldset>
</details>
<div class="main-toolbox">
<i class="icon icon-16 icon-eye" title="${translate('show/hide all layers')}" data-ref="toggle"></i>
<i class="icon icon-16 icon-zoom" title="${translate('zoom to data extent')}" data-ref="fitBounds"></i>
<i class="icon icon-16 icon-download" title="${translate('download visible data')}" data-ref="download"></i>
</div>
<div data-ref=dataContainer></div>
</div>
`
const [
container,
{
details,
filtersTitle,
toggle,
fitBounds,
download,
dataContainer,
formContainer,
reset,
},
] = Utils.loadTemplateWithRefs(template)
// HOTFIX. Remove when this is released:
// https://github.com/Leaflet/Leaflet/pull/9052
DomEvent.disableClickPropagation(container)
details.open = this.mode === 'filters'
toggle.addEventListener('click', () => this.toggleLayers())
fitBounds.addEventListener('click', () => this._umap.fitDataBounds())
download.addEventListener('click', () => this.downloadVisible(download))
download.hidden = this._umap.getProperty('embedControl') === false
this.filtersTitle = filtersTitle
this.dataContainer = dataContainer
this.formContainer = formContainer
this.toggleBadge()
let fields = [
[
'options.filter',
{ handler: 'Input', placeholder: translate('Search map features…') },
],
['options.inBbox', { handler: 'Switch', label: translate('Current map view') }],
]
const builder = new Form(this, fields)
builder.on('set', () => this.onFormChange())
let filtersBuilder
this.formContainer.appendChild(builder.build())
builder.form.addEventListener('reset', () => {
window.setTimeout(builder.syncAll.bind(builder))
})
if (this._umap.properties.facetKey) {
fields = this._umap.facets.build()
filtersBuilder = new Form(this._umap.facets, fields)
filtersBuilder.on('set', () => this.onFormChange())
filtersBuilder.form.addEventListener('reset', () => {
window.setTimeout(filtersBuilder.syncAll.bind(filtersBuilder))
})
this.formContainer.appendChild(filtersBuilder.build())
}
reset.addEventListener('click', () => this.resetFilters())
this._umap.panel.open({
content: container,
className: 'umap-browser',
})
this.update()
}
resetFilters() {
for (const form of this.formContainer?.querySelectorAll('form') || []) {
form.reset()
}
}
downloadVisible(element) {
const menu = new ContextMenu({ fixed: true })
const items = []
for (const format of Object.keys(EXPORT_FORMATS)) {
items.push({
label: format,
action: () => this._umap.share.download(format),
})
}
menu.openBelow(element, items)
}
toggleLayers() {
// If at least one layer is shown, hide it
// otherwise show all
let allHidden = true
this._umap.eachBrowsableDataLayer((datalayer) => {
if (datalayer.isVisible()) allHidden = false
})
this._umap.eachBrowsableDataLayer((datalayer) => {
datalayer._forcedVisibility = true
if (allHidden) {
datalayer.show()
} else {
if (datalayer.isVisible()) datalayer.hide()
}
})
}
static backButton(umap) {
const button = Utils.loadTemplate(
`<button class="icon icon-16 icon-back" title="${translate('Back to browser')}"></button>`
)
// Fixme: remove me when this is merged and released
// https://github.com/Leaflet/Leaflet/pull/9052
DomEvent.disableClickPropagation(button)
button.addEventListener('click', () => umap.openBrowser())
return button
}
}