mirror of
https://github.com/umap-project/umap.git
synced 2025-04-28 19:42:36 +02:00
280 lines
9.8 KiB
JavaScript
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
|
|
}
|
|
}
|