Compare commits

...

23 commits

Author SHA1 Message Date
Yohan Boniface
1a9c325f16
fix: setting center and zoom manually should set dirty status (#2676)
Some checks failed
Release Charts / release (push) Has been cancelled
Test & Docs / tests (postgresql, 3.10) (push) Has been cancelled
Test & Docs / tests (postgresql, 3.12) (push) Has been cancelled
Test & Docs / lint (push) Has been cancelled
2025-04-23 18:51:36 +02:00
Yohan Boniface
19d16ac01b fix: setting center and zoom manually should set dirty status
Co-authored-by: David Larlet <david@larlet.fr>
2025-04-23 18:00:48 +02:00
Yohan Boniface
efaa765b82
chore: cleaning core utils and removing DomUtil/DomEvent from modules (#2671) 2025-04-23 17:51:45 +02:00
Yohan Boniface
92ca581fb6
fix: allow to save a remote data with unloaded data (#2657)
When loading remote data fails, the layer is in state "unloaded", and
thus it was not possible to save it.
This "isLoaded" check is made for non remote layer so that we don't save
it by mistake with no data.

Also, let's allow to edit non loaded layers, this check was there when
settings was not on db, so not all loaded at map init, again not to
override with incomplete metadata.
2025-04-23 17:49:12 +02:00
Yohan Boniface
6687cd53ba
feat: add a back button in rules form (#2673)
To go back in rules list.

fix #2631


![image](https://github.com/user-attachments/assets/d0ad67fe-90c1-45c5-8bca-5ac30ef8366a)
2025-04-23 17:43:22 +02:00
Yohan Boniface
4997ee8860
feat: naive support for GeometryCollection as Feature geometry (#2658)
Currently, we just skip those, but sometimes togeojson produces this
output from a KML.
This will create one feature per geometry, while in an ideal world this
should be a multi, but we lack reliable geometry tools to merge the
geometries without risking to create mistakes.
So let's say it's a first improvement.
2025-04-23 17:43:05 +02:00
Yohan Boniface
a708316604
fix: make rules reordering syncable, savable and undoable (#2672) 2025-04-23 17:42:47 +02:00
Yohan Boniface
71d92aa610
fix: show an error message if saving layer failed (#2670)
This could occur when server is down or there is no network.
2025-04-23 17:40:19 +02:00
Yohan Boniface
41752bd0c6
fix: do not display "saved" message if some request failed (#2669) 2025-04-23 17:39:59 +02:00
Yohan Boniface
5b6138a210
chore: move pure Leaflet controls to modules (#2668)
pure == inheriting from Leaflet itself, not from some plugin (which
aren't ESM ready…)
2025-04-23 17:39:33 +02:00
Yohan Boniface
07363fa5fe chore: remove unused DomEvent.once 2025-04-23 16:52:55 +02:00
Yohan Boniface
7991d6cdbe chore: remove DomUtil.after/before 2025-04-23 16:50:13 +02:00
Yohan Boniface
60ac4b35f2 chore: remove most of DomUtil/DomEvent from browser.js 2025-04-23 16:24:15 +02:00
Yohan Boniface
bf2e9dc175 chore: remove call to DomUtil.createIcon in umap.js 2025-04-23 11:33:12 +02:00
Yohan Boniface
67ed6d5b44 feat: add a back button in rules form
To go back in rules list.

fix #2631
2025-04-23 10:55:39 +02:00
Yohan Boniface
fad182c5f3 fix: make rules reordering syncable, savable and undoable 2025-04-23 10:19:19 +02:00
Yohan Boniface
3f5d282477 chore: remove DomUtil and DomEvent dependency from rules.js 2025-04-23 10:14:22 +02:00
Yohan Boniface
49496348d2 chore: remove unused L.DomUtil.createLink method 2025-04-23 09:21:35 +02:00
Yohan Boniface
f8b13639aa fix: show an error message if saving layer failed
This could occur when server is down or there is no network.
2025-04-23 09:02:25 +02:00
Yohan Boniface
4c71710641 fix: do not display "saved" message if some request failed 2025-04-23 08:58:17 +02:00
Yohan Boniface
92d5c47844 chore: move pure Leaflet controls to modules
pure == inheriting from Leaflet itself, not from some plugin
(which aren't ESM ready…)
2025-04-22 20:14:06 +02:00
Yohan Boniface
b0fe95dbd9 feat: naive support for GeometryCollection as Feature geometry
Currently, we just skip those, but sometimes togeojson produces this
output from a KML.
This will create one feature per geometry, while in an ideal world this
should be a multi, but we lack reliable geometry tools to merge the
geometries without risking to create mistakes.
So let's say it's a first improvement.
2025-04-16 18:28:23 +02:00
Yohan Boniface
169be73488 fix: allow to save a remote data with unloaded data
When loading remote data fails, the layer is in state "unloaded",
and thus it was not possible to save it.
This "isLoaded" check is made for non remote layer so that we don't
save it by mistake with no data.

Also, let's allow to edit non loaded layers, this check was there
when settings was not on db, so not all loaded at map init, again
not to override with incomplete metadata.
2025-04-16 16:00:26 +02:00
18 changed files with 588 additions and 529 deletions

View file

@ -59,3 +59,16 @@
.tooltip-accent li:last-of-type {
margin-bottom: 0;
}
.umap-tooltip-container.tooltip-right:before {
right: 100%;
top: calc(50% - var(--arrow-size));
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-right-color: var(--tooltip-color);
border-width: var(--arrow-size);
}

View file

@ -7,6 +7,7 @@ import {
import { translate } from './i18n.js'
import { Request, ServerRequest } from './request.js'
import { escapeHTML, generateId } from './utils.js'
import * as Utils from './utils.js'
export class BaseAutocomplete {
constructor(parent, options) {
@ -291,10 +292,9 @@ class BaseServerAjax extends BaseAjax {
export const SingleMixin = (Base) =>
class extends Base {
initSelectedContainer() {
return DomUtil.after(
this.input,
DomUtil.element({ tagName: 'div', className: 'umap-singleresult' })
)
const el = Utils.loadTemplate('<div class="umap-singleresult"></div>')
this.input.parentNode.insertBefore(el, this.input.nextSibling)
return el
}
displaySelected(result) {
@ -322,10 +322,9 @@ export const SingleMixin = (Base) =>
export const MultipleMixin = (Base) =>
class extends Base {
initSelectedContainer() {
return DomUtil.after(
this.input,
DomUtil.element({ tagName: 'ul', className: 'umap-multiresult' })
)
const el = Utils.loadTemplate('<ul class="umap-multiresult"></ul>')
this.input.parentNode.insertBefore(el, this.input.nextSibling)
return el
}
displaySelected(result) {

View file

@ -5,6 +5,7 @@ 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) {
@ -21,35 +22,24 @@ export default class Browser {
addFeature(feature, parent) {
if (feature.isFiltered()) return
if (this.options.inBbox && !feature.isOnScreen(this.bounds)) return
const row = DomUtil.create('li', `${feature.getClassName()} feature`)
const zoom_to = DomUtil.createButtonIcon(
row,
'icon-zoom',
translate('Bring feature to center')
)
const edit = DomUtil.createButtonIcon(
row,
'show-on-edit icon-edit',
translate('Edit this feature')
)
const del = DomUtil.createButtonIcon(
row,
'show-on-edit icon-delete',
translate('Delete this feature')
)
const colorBox = DomUtil.create(
'i',
`icon icon-16 icon-${feature.getClassName()} feature-color`,
row
)
const title = DomUtil.create('span', 'feature-title', row)
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
title.textContent = title.title = feature.getDisplayName() || '—'
const bgcolor = feature.getPreviewColor()
colorBox.style.backgroundColor = bgcolor
if (symbol && symbol !== U.SCHEMA.iconUrl.default) {
if (symbol && symbol !== SCHEMA.iconUrl.default) {
const icon = Icon.makeElement(symbol, colorBox)
Icon.setContrast(icon, colorBox, symbol, bgcolor)
} else if (DomUtil.contrastedColor(colorBox, bgcolor)) {
@ -58,10 +48,10 @@ export default class Browser {
const viewFeature = (e) => {
feature.zoomTo({ ...e, callback: () => feature.view() })
}
DomEvent.on(zoom_to, 'click', viewFeature)
DomEvent.on(title, 'click', viewFeature)
DomEvent.on(edit, 'click', feature.edit, feature)
DomEvent.on(del, 'click', feature.del, feature)
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)
@ -75,45 +65,51 @@ export default class Browser {
addDataLayer(datalayer, parent) {
let className = `datalayer ${datalayer.getHidableClass()}`
if (this.mode !== 'layers') className += ' show-list'
const container = DomUtil.create('div', className, parent)
const headline = DomUtil.create('h5', '', container)
container.id = this.datalayerId(datalayer)
const ul = DomUtil.create('ul', '', container)
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 parent = DomUtil.get(this.datalayerId(datalayer))
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')
const headline = parent.querySelector('h5')
const toggleList = () => parent.classList.toggle('show-list')
headline.innerHTML = ''
const toggle = DomUtil.create('i', 'icon icon-16 datalayer-toggle-list', headline)
DomEvent.on(toggle, 'click', toggleList)
datalayer.renderToolbox(headline)
const name = DomUtil.create('span', 'datalayer-name', headline)
name.textContent = name.title = datalayer.options.name
DomEvent.on(name, 'click', toggleList)
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 = DomUtil.create('span', 'datalayer-counter', headline)
const counter = parent.querySelector('.datalayer-counter')
counter.textContent = `(${count})`
counter.title = translate(`Features in this layer: ${count}`)
}
toggleBadge() {
U.Utils.toggleBadge(this.filtersTitle, this.hasFilters())
U.Utils.toggleBadge('.umap-control-browse', this.hasFilters())
Utils.toggleBadge(this.filtersTitle, this.hasFilters())
Utils.toggleBadge('.umap-control-browse', this.hasFilters())
}
onFormChange() {
@ -157,21 +153,51 @@ export default class Browser {
open(mode) {
// Force only if mode is known, otherwise keep current mode.
if (mode) this.mode = mode
const container = DomUtil.create('div')
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
DomUtil.createTitle(container, translate('Data browser'), 'icon-layers')
this.formContainer = DomUtil.createFieldset(container, L._('Filters'), {
on: this.mode === 'filters',
className: 'filters',
icon: 'icon-filters',
})
this.filtersTitle = container.querySelector('summary')
this.filtersTitle = filtersTitle
this.dataContainer = dataContainer
this.formContainer = formContainer
this.toggleBadge()
this.addMainToolbox(container)
this.dataContainer = DomUtil.create('div', '', container)
let fields = [
[
@ -184,27 +210,19 @@ export default class Browser {
builder.on('set', () => this.onFormChange())
let filtersBuilder
this.formContainer.appendChild(builder.build())
DomEvent.on(builder.form, 'reset', () => {
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())
DomEvent.on(filtersBuilder.form, 'reset', () => {
filtersBuilder.form.addEventListener('reset', () => {
window.setTimeout(filtersBuilder.syncAll.bind(filtersBuilder))
})
this.formContainer.appendChild(filtersBuilder.build())
}
const reset = DomUtil.createButton('flat', this.formContainer, '', () =>
this.resetFilters()
)
DomUtil.createIcon(reset, 'icon-restore')
DomUtil.element({
tagName: 'span',
parent: reset,
textContent: translate('Reset all'),
})
reset.addEventListener('click', () => this.resetFilters())
this._umap.panel.open({
content: container,
@ -220,21 +238,6 @@ export default class Browser {
}
}
addMainToolbox(container) {
const [toolbox, { toggle, fitBounds, download }] = Utils.loadTemplateWithRefs(`
<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>
`)
container.appendChild(toolbox)
toggle.addEventListener('click', () => this.toggleLayers())
fitBounds.addEventListener('click', () => this._umap.fitDataBounds())
download.addEventListener('click', () => this.downloadVisible(download))
download.hidden = this._umap.getProperty('embedControl') === false
}
downloadVisible(element) {
const menu = new ContextMenu({ fixed: true })
const items = []
@ -265,15 +268,13 @@ export default class Browser {
}
static backButton(umap) {
const button = DomUtil.createButtonIcon(
DomUtil.create('li', '', undefined),
'icon-back',
translate('Back to browser')
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)
DomEvent.on(button, 'click', () => umap.openBrowser())
button.addEventListener('click', () => umap.openBrowser())
return button
}
}

View file

@ -130,6 +130,10 @@ export class DataLayer {
}
render(fields, builder) {
// Propagate will remove the fields it has already
// processed
fields = this.propagate(fields)
const impacts = Utils.getImpactsFromSchema(fields)
for (const impact of impacts) {
@ -153,6 +157,29 @@ export class DataLayer {
}
}
// 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('.datalayer-name', (el) => {
if (el.dataset.id === this.id) {
el.textContent = this.getName()
el.title = this.getName()
}
})
},
}
for (const [field, impact] of Object.entries(impacts)) {
if (!fields.length || fields.includes(field)) {
impact()
fields = fields.filter((item) => item !== field)
}
}
return fields
}
showAtLoad() {
return this.autoLoaded() && this.showAtZoom()
}
@ -492,8 +519,19 @@ export class DataLayer {
const features = []
this.sortFeatures(collection)
for (const featureJson of collection) {
const feature = this.makeFeature(featureJson, sync)
if (feature) features.push(feature)
if (featureJson.geometry?.type === 'GeometryCollection') {
for (const geometry of featureJson.geometry.geometries) {
const feature = this.makeFeature({
type: 'Feature',
geometry,
properties: featureJson.properties,
})
if (feature) features.push(feature)
}
} else {
const feature = this.makeFeature(featureJson, sync)
if (feature) features.push(feature)
}
}
return features
}
@ -645,7 +683,7 @@ export class DataLayer {
}
edit() {
if (!this._umap.editEnabled || !this.isLoaded()) {
if (!this._umap.editEnabled) {
return
}
const container = DomUtil.create('div', 'umap-layer-properties-container')
@ -1105,7 +1143,7 @@ export class DataLayer {
async save() {
if (this.isDeleted) return await this.saveDelete()
if (!this.isLoaded()) return
if (!this.isRemoteLayer() && !this.isLoaded()) return
const geojson = this.umapGeoJSON()
const formData = new FormData()
formData.append('name', this.options.name)
@ -1146,6 +1184,9 @@ export class DataLayer {
await this._umap.saveAll()
}
)
} else {
console.debug(error)
Alert.error(translate('Cannot save layer, please try again in a few minutes.'))
}
} else {
// Response contains geojson only if save has conflicted and conflicts have

View file

@ -0,0 +1,251 @@
import { Control } from '../../../vendors/leaflet/leaflet-src.esm.js'
import * as Utils from '../utils.js'
import { translate } from '../i18n.js'
export const HomeControl = Control.extend({
options: {
position: 'topleft',
},
onAdd: (map) => {
const path = map._umap.getStaticPathFor('home.svg')
const container = Utils.loadTemplate(
`<a href="/" class="home-button" title="${translate('Back to home')}"><img src="${path}" alt="${translate('Home logo')}" width="38px" height="38px" /></a>`
)
return container
},
})
export const EditControl = Control.extend({
options: {
position: 'topright',
},
onAdd: (map) => {
const template = `
<div class="edit-enable">
<button type="button" data-ref="button">${translate('Edit')}</button>
</div>
`
const [container, { button }] = Utils.loadTemplateWithRefs(template)
button.addEventListener('click', () => map._umap.enableEdit())
button.addEventListener('mouseover', () => {
map._umap.tooltip.open({
content: map._umap.help.displayLabel('TOGGLE_EDIT'),
anchor: button,
position: 'bottom',
delay: 750,
duration: 5000,
})
})
return container
},
})
export const MoreControl = Control.extend({
options: {
position: 'topleft',
},
onAdd: function (map) {
const pos = this.getPosition()
const corner = map._controlCorners[pos]
const className = 'umap-more-controls'
const template = `
<div class="umap-control-text">
<button class="umap-control-more" type="button" data-ref="button"></button>
</div>
`
const [container, { button }] = Utils.loadTemplateWithRefs(template)
button.addEventListener('click', () => corner.classList.toggle(className))
button.addEventListener('mouseover', () => {
const extended = corner.classList.contains(className)
map._umap.tooltip.open({
content: extended ? translate('Hide controls') : translate('More controls'),
anchor: button,
position: 'right',
delay: 750,
})
})
return container
},
})
export const PermanentCreditsControl = Control.extend({
options: {
position: 'bottomleft',
},
onAdd: (map) => {
const container = Utils.loadTemplate(
`<div class="umap-permanent-credits-container text">${Utils.toHTML(map.options.permanentCredit)}</div>`
)
const background = map.options.permanentCreditBackground ? '#FFFFFFB0' : ''
container.style.backgroundColor = background
return container
},
})
const BaseButton = Control.extend({
initialize: function (umap, options) {
this._umap = umap
Control.prototype.initialize.call(this, options)
},
onAdd: function (map) {
const template = `
<div class="${this.options.className} umap-control">
<button type="button" title="${this.options.title}" data-ref="button"></button>
</div>
`
const [container, { button }] = Utils.loadTemplateWithRefs(template)
button.addEventListener('click', (event) => {
event.stopPropagation()
this.onClick()
})
button.addEventListener('dblclick', (event) => {
event.stopPropagation()
})
this.afterAdd(container)
return container
},
afterAdd: (container) => {},
})
export const DataLayersControl = BaseButton.extend({
options: {
position: 'topleft',
className: 'umap-control-browse',
title: translate('Open browser'),
},
afterAdd: function (container) {
Utils.toggleBadge(container, this._umap.browser?.hasFilters())
},
onClick: function () {
this._umap.openBrowser()
},
})
export const CaptionControl = BaseButton.extend({
options: {
position: 'topleft',
className: 'umap-control-caption',
title: translate('About'),
},
onClick: function () {
this._umap.openCaption()
},
})
export const EmbedControl = BaseButton.extend({
options: {
position: 'topleft',
title: translate('Share and download'),
className: 'leaflet-control-embed',
},
onClick: function () {
this._umap.share.open()
},
})
export const AttributionControl = Control.Attribution.extend({
options: {
prefix: '',
},
_update: function () {
// Layer is no more on the map
if (!this._map) return
Control.Attribution.prototype._update.call(this)
const shortCredit = this._map._umap.getProperty('shortCredit')
const captionMenus = this._map._umap.getProperty('captionMenus')
// Use our own container, so we can hide/show on small screens
const originalCredits = this._container.innerHTML
this._container.innerHTML = ''
const template = `
<div class="attribution-container">
${originalCredits}
<span data-ref="short"> ${Utils.toHTML(shortCredit)}</span>
<a href="#" data-ref="caption"> ${translate('Open caption')}</a>
<a href="/" data-ref="home"> ${translate('Home')}</a>
<a href="https://umap-project.org/" data-ref="site"> ${translate('Powered by uMap')}</a>
<a href="#" class="attribution-toggle"></a>
</div>
`
const [container, { short, caption, home, site }] =
Utils.loadTemplateWithRefs(template)
caption.addEventListener('click', () => this._map._umap.openCaption())
this._container.appendChild(container)
short.hidden = !shortCredit
caption.hidden = !captionMenus
site.hidden = !captionMenus
home.hidden = this._map._umap.isEmbed || !captionMenus
},
})
/* Used in edit mode to define the default tilelayer */
export const TileLayerChooser = BaseButton.extend({
options: {
position: 'topleft',
},
onClick: function () {
this.openSwitcher({ edit: true })
},
openSwitcher: function (options = {}) {
const template = `
<div class="umap-edit-tilelayers">
<h3><i class="icon icon-16 icon-tilelayer" title=""></i><span class="">${translate('Change tilelayers')}</span></h3>
<ul data-ref="tileContainer"></ul>
</div>
`
const [container, { tileContainer }] = Utils.loadTemplateWithRefs(template)
this.buildList(tileContainer, options)
const panel = options.edit ? this._umap.editPanel : this._umap.panel
panel.open({ content: container, highlight: 'tilelayers' })
},
buildList: function (container, options) {
this._umap._leafletMap.eachTileLayer((tilelayer) => {
const browserIsHttps = window.location.protocol === 'https:'
const tileLayerIsHttp = tilelayer.options.url_template.indexOf('http:') === 0
if (browserIsHttps && tileLayerIsHttp) return
container.appendChild(this.addTileLayerElement(tilelayer, options))
})
},
addTileLayerElement: function (tilelayer, options) {
const selectedClass = this._umap._leafletMap.hasLayer(tilelayer) ? 'selected' : ''
const src = Utils.template(
tilelayer.options.url_template,
this._umap._leafletMap.options.demoTileInfos
)
const template = `
<li>
<img src="${src}" loading="lazy" />
<div>${tilelayer.options.name}</div>
</li>
`
const li = Utils.loadTemplate(template)
li.addEventListener('click', () => {
const oldTileLayer = this._umap.properties.tilelayer
this._umap._leafletMap.selectTileLayer(tilelayer)
this._umap._leafletMap._controls.tilelayers.setLayers()
if (options?.edit) {
this._umap.properties.tilelayer = tilelayer.toJSON()
this._umap.sync.update(
'properties.tilelayer',
this._umap.properties.tilelayer,
oldTileLayer
)
}
})
return li
},
})

View file

@ -11,6 +11,17 @@ import {
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
import DropControl from '../drop.js'
import { translate } from '../i18n.js'
import {
AttributionControl,
CaptionControl,
DataLayersControl,
EmbedControl,
EditControl,
HomeControl,
MoreControl,
PermanentCreditsControl,
TileLayerChooser,
} from './controls.js'
import * as Utils from '../utils.js'
import * as Icon from './icon.js'
@ -40,15 +51,15 @@ const ControlsMixin = {
this._controls = {}
if (this._umap.hasEditMode() && !this.options.noControl) {
new U.EditControl(this).addTo(this)
new EditControl(this).addTo(this)
}
this._controls.home = new U.HomeControl(this._umap)
this._controls.home = new HomeControl(this._umap)
this._controls.zoom = new Control.Zoom({
zoomInTitle: translate('Zoom in'),
zoomOutTitle: translate('Zoom out'),
})
this._controls.datalayers = new U.DataLayersControl(this._umap)
this._controls.caption = new U.CaptionControl(this._umap)
this._controls.datalayers = new DataLayersControl(this._umap)
this._controls.caption = new CaptionControl(this._umap)
this._controls.locate = new U.Locate(this, {
strings: {
title: translate('Center map on your location'),
@ -69,8 +80,8 @@ const ControlsMixin = {
},
})
this._controls.search = new U.SearchControl()
this._controls.embed = new Control.Embed(this._umap)
this._controls.tilelayersChooser = new U.TileLayerChooser(this)
this._controls.embed = new EmbedControl(this._umap)
this._controls.tilelayersChooser = new TileLayerChooser(this._umap)
this._controls.editinosm = new Control.EditInOSM({
position: 'topleft',
widgetOptions: {
@ -80,9 +91,9 @@ const ControlsMixin = {
},
})
this._controls.measure = new L.MeasureControl().initHandler(this)
this._controls.more = new U.MoreControls()
this._controls.more = new MoreControl()
this._controls.scale = L.control.scale()
this._controls.permanentCredit = new U.PermanentCreditsControl(this)
this._controls.permanentCredit = new PermanentCreditsControl(this)
this._umap.drop = new DropControl(this._umap, this, this._container)
this._controls.tilelayers = new U.TileLayerControl(this)
},
@ -93,7 +104,7 @@ const ControlsMixin = {
}
if (this.options.noControl) return
this._controls.attribution = new U.AttributionControl().addTo(this)
this._controls.attribution = new AttributionControl().addTo(this)
if (this.options.miniMap) {
this.whenReady(function () {
if (this.selectedTilelayer) {

View file

@ -1,4 +1,4 @@
import { DomEvent, DomUtil, stamp } from '../../vendors/leaflet/leaflet-src.esm.js'
import { stamp } from '../../vendors/leaflet/leaflet-src.esm.js'
import { AutocompleteDatalist } from './autocomplete.js'
import { MutatingForm } from './form/builder.js'
import { translate } from './i18n.js'
@ -119,10 +119,9 @@ class Rule {
'options.smoothFactor',
'options.dashArray',
]
const container = DomUtil.create('div')
const builder = new MutatingForm(this, options)
const defaultShapeProperties = DomUtil.add('div', '', container)
defaultShapeProperties.appendChild(builder.build())
const container = document.createElement('div')
container.appendChild(builder.build())
const autocomplete = new AutocompleteDatalist(builder.helpers.condition.input)
const properties = this._umap.allProperties()
autocomplete.suggestions = properties
@ -137,43 +136,45 @@ class Rule {
.map((str) => `${value}${str || ''}`)
}
})
this._umap.editPanel.open({ content: container, highlight: 'settings' })
const backButton = Utils.loadTemplate(`
<button class="flat" type="button" data-ref="add">
<i class="icon icon-16 icon-back" title="${translate('Back to list')}"></i>
</button>`)
backButton.addEventListener('click', () =>
this._umap.edit().then(() => {
this._umap.editPanel.container.querySelector('details#rules').open = true
})
)
this._umap.editPanel.open({
content: container,
highlight: 'settings',
actions: [backButton],
})
}
renderToolbox(row) {
row.classList.toggle('off', !this.active)
const toggle = DomUtil.createButtonIcon(
row,
'icon-eye',
translate('Show/hide layer')
)
const edit = DomUtil.createButtonIcon(
row,
'icon-edit show-on-edit',
translate('Edit')
)
const remove = DomUtil.createButtonIcon(
row,
'icon-delete show-on-edit',
translate('Delete layer')
)
DomEvent.on(edit, 'click', this.edit, this)
DomEvent.on(
remove,
'click',
function () {
if (!confirm(translate('Are you sure you want to delete this rule?'))) return
this._delete()
this._umap.editPanel.close()
},
this
)
DomUtil.add('span', '', row, this.condition || translate('empty rule'))
DomUtil.createIcon(row, 'icon-drag', translate('Drag to reorder'))
row.dataset.id = stamp(this)
DomEvent.on(toggle, 'click', () => {
renderToolbox(ul) {
const template = `
<li data-id="${stamp(this)}" class="orderable">
<button class="icon icon-16 icon-eye" title="${translate('Toggle rule')}" data-ref=toggle></button>
<button class="icon icon-16 icon-edit show-on-edit" title="${translate('Edit')}" data-ref=edit></button>
<button class="icon icon-16 icon-delete show-on-edit" title="${translate('Delete rule')}" data-ref=remove></button>
<span>${this.condition || translate('empty rule')}</span>
<i class="icon icon-16 icon-drag" title="${translate('Drag to reorder')}"></i>
</li>
`
const [li, { toggle, edit, remove }] = Utils.loadTemplateWithRefs(template)
ul.appendChild(li)
li.classList.toggle('off', !this.active)
edit.addEventListener('click', () => this.edit())
remove.addEventListener('click', () => {
if (!confirm(translate('Are you sure you want to delete this rule?'))) return
this._delete()
this._umap.editPanel.close()
})
toggle.addEventListener('click', () => {
this.active = !this.active
row.classList.toggle('off', !this.active)
li.classList.toggle('off', !this.active)
this._umap.render(['rules'])
})
}
@ -207,8 +208,9 @@ export default class Rules {
}
onReorder(src, dst, initialIndex, finalIndex) {
const moved = this.rules.find((rule) => stamp(rule) === src.dataset.id)
const reference = this.rules.find((rule) => stamp(rule) === dst.dataset.id)
const oldRules = Utils.CopyJSON(this._umap.properties.rules || {})
const moved = this.rules.find((rule) => stamp(rule) === +src.dataset.id)
const reference = this.rules.find((rule) => stamp(rule) === +dst.dataset.id)
const movedIdx = this.rules.indexOf(moved)
let referenceIdx = this.rules.indexOf(reference)
const minIndex = Math.min(movedIdx, referenceIdx)
@ -222,20 +224,28 @@ export default class Rules {
this.rules.splice(newIdx, 0, moved)
this._umap.render(['rules'])
this.commit()
this._umap.sync.update('properties.rules', this._umap.properties.rules, oldRules)
}
edit(container) {
const body = DomUtil.createFieldset(container, translate('Conditional style rules'))
const template = `
<details id="rules">
<summary>${translate('Conditional style rules')}</summary>
<fieldset>
<ul data-ref=ul></ul>
<button class="umap-add" type="button" data-ref=add>${translate('Add rule')}</button>
</fieldset>
</details>
`
const [body, { ul, add }] = Utils.loadTemplateWithRefs(template)
if (this.rules.length) {
const ul = DomUtil.create('ul', '', body)
for (const rule of this.rules) {
rule.renderToolbox(DomUtil.create('li', 'orderable', ul))
rule.renderToolbox(ul)
}
const orderable = new Orderable(ul, this.onReorder.bind(this))
}
DomUtil.createButton('umap-add', body, translate('Add rule'), this.addRule, this)
add.addEventListener('click', () => this.addRule())
container.appendChild(body)
}
addRule() {

View file

@ -225,13 +225,14 @@ export class SyncEngine {
}
for (const [obj, operations] of needSave.entries()) {
const ok = await obj.save()
if (!ok) break
if (!ok) return false
for (const operation of operations) {
operation.dirty = false
}
}
this.saved()
this._undoManager.toggleState()
return true
}
saved() {

View file

@ -4,6 +4,8 @@ export class Positioned {
this.anchorTop(anchor)
} else if (anchor && position === 'bottom') {
this.anchorBottom(anchor)
} else if (anchor && position === 'right') {
this.anchorRight(anchor)
} else {
this.anchorAbsolute()
}
@ -12,6 +14,7 @@ export class Positioned {
toggleClassPosition(position) {
this.container.classList.toggle('tooltip-bottom', position === 'bottom')
this.container.classList.toggle('tooltip-top', position === 'top')
this.container.classList.toggle('tooltip-right', position === 'right')
}
anchorTop(el) {
@ -33,6 +36,16 @@ export class Positioned {
})
}
anchorRight(el) {
this.toggleClassPosition('right')
const coords = this.getPosition(el)
console.log(coords)
this.setPosition({
left: coords.right + 11,
top: coords.top,
})
}
anchorAbsolute() {
const left =
this.parent.offsetLeft +

View file

@ -107,7 +107,7 @@ export default class Umap {
if (geojson.properties.schema) this.overrideSchema(geojson.properties.schema)
// Do not display in an iframe.
if (window.self !== window.top) {
if (this.isEmbed) {
this.properties.homeControl = false
}
@ -258,6 +258,10 @@ export default class Umap {
}
}
get isEmbed() {
return window.self !== window.top
}
setPropertiesFromQueryString() {
const asBoolean = (key) => {
const value = this.searchParams.get(key)
@ -691,7 +695,8 @@ export default class Umap {
if (!this.isDirty) return
if (this._defaultExtent) this._setCenterAndZoom()
this.backup()
await this.sync.save()
const status = await this.sync.save()
if (!status) return
// 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
@ -819,7 +824,7 @@ export default class Umap {
`<button type="button">${translate('Use current center and zoom')}</button>`
)
button.addEventListener('click', () => {
this._setCenterAndZoom()
this.setCenterAndZoom()
builder.fetchAll()
})
container.appendChild(form)
@ -1177,7 +1182,7 @@ export default class Umap {
}
this._advancedActions(container)
this.editPanel.open({
return this.editPanel.open({
content: container,
className: 'dark',
highlight: 'settings',
@ -1508,12 +1513,17 @@ export default class Umap {
editDatalayers() {
if (!this.editEnabled) return
const container = DomUtil.create('div')
DomUtil.createTitle(container, translate('Manage layers'), 'icon-layers')
const ul = DomUtil.create('ul', '', container)
const template = `
<div>
<h3><i class="icon icon-16 icon-layers"></i>${translate('Manage layers')}</h3>
<ul data-ref=ul></ul>
</div>
`
const [container, { ul }] = Utils.loadTemplateWithRefs(template)
this.eachDataLayerReverse((datalayer) => {
const row = DomUtil.create('li', 'orderable', ul)
DomUtil.createIcon(row, 'icon-drag', translate('Drag to reorder'))
const row = Utils.loadTemplate(
`<li class="orderable"><i class="icon icon-16 icon-drag" title="${translate('Drag to reorder')}"></i></li>`
)
datalayer.renderToolbox(row)
const builder = new MutatingForm(
datalayer,
@ -1524,6 +1534,7 @@ export default class Umap {
row.appendChild(form)
row.classList.toggle('off', !datalayer.isVisible())
row.dataset.id = datalayer.id
ul.appendChild(row)
})
const onReorder = (src, dst, initialIndex, finalIndex) => {
const movedLayer = this.datalayers[src.dataset.id]

View file

@ -1,183 +1,3 @@
U.HomeControl = L.Control.extend({
options: {
position: 'topleft',
},
onAdd: (map) => {
const path = map._umap.getStaticPathFor('home.svg')
const container = U.Utils.loadTemplate(
`<a href="/" class="home-button" title="${L._('Back to home')}"><img src="${path}" alt="${L._('Home logo')}" width="38px" height="38px" /></a>`
)
return container
},
})
U.EditControl = L.Control.extend({
options: {
position: 'topright',
},
onAdd: function (map) {
const container = L.DomUtil.create('div', 'edit-enable')
const enableEditing = L.DomUtil.createButton(
'',
container,
L._('Edit'),
map._umap.enableEdit,
map._umap
)
L.DomEvent.on(
enableEditing,
'mouseover',
() => {
map._umap.tooltip.open({
content: map._umap.help.displayLabel('TOGGLE_EDIT'),
anchor: enableEditing,
position: 'bottom',
delay: 750,
duration: 5000,
})
},
this
)
return container
},
})
U.MoreControls = L.Control.extend({
options: {
position: 'topleft',
},
onAdd: function () {
const container = L.DomUtil.create('div', 'umap-control-text')
const moreButton = L.DomUtil.createButton(
'umap-control-more',
container,
L._('More controls'),
this.toggle,
this
)
const lessButton = L.DomUtil.createButton(
'umap-control-less',
container,
L._('Hide controls'),
this.toggle,
this
)
return container
},
toggle: function () {
const pos = this.getPosition()
const corner = this._map._controlCorners[pos]
const className = 'umap-more-controls'
if (L.DomUtil.hasClass(corner, className)) L.DomUtil.removeClass(corner, className)
else L.DomUtil.addClass(corner, className)
},
})
U.PermanentCreditsControl = L.Control.extend({
options: {
position: 'bottomleft',
},
initialize: function (map, options) {
this.map = map
L.Control.prototype.initialize.call(this, options)
},
onAdd: function () {
this.paragraphContainer = L.DomUtil.create(
'div',
'umap-permanent-credits-container text'
)
this.setCredits()
this.setBackground()
return this.paragraphContainer
},
setCredits: function () {
this.paragraphContainer.innerHTML = U.Utils.toHTML(this.map.options.permanentCredit)
},
setBackground: function () {
if (this.map.options.permanentCreditBackground) {
this.paragraphContainer.style.backgroundColor = '#FFFFFFB0'
} else {
this.paragraphContainer.style.backgroundColor = ''
}
},
})
L.Control.Button = L.Control.extend({
initialize: function (umap, options) {
this._umap = umap
L.Control.prototype.initialize.call(this, options)
},
getClassName: function () {
return this.options.className
},
onAdd: function (map) {
const container = L.DomUtil.create('div', `${this.getClassName()} umap-control`)
const button = L.DomUtil.createButton(
'',
container,
this.options.title,
this.onClick,
this
)
L.DomEvent.on(button, 'dblclick', L.DomEvent.stopPropagation)
this.afterAdd(container)
return container
},
afterAdd: (container) => {},
})
U.DataLayersControl = L.Control.Button.extend({
options: {
position: 'topleft',
className: 'umap-control-browse',
title: L._('Open browser'),
},
afterAdd: function (container) {
U.Utils.toggleBadge(container, this._umap.browser?.hasFilters())
},
onClick: function () {
this._umap.openBrowser()
},
})
U.CaptionControl = L.Control.Button.extend({
options: {
position: 'topleft',
className: 'umap-control-caption',
title: L._('About'),
},
onClick: function () {
this._umap.openCaption()
},
})
L.Control.Embed = L.Control.Button.extend({
options: {
position: 'topleft',
title: L._('Share and download'),
className: 'leaflet-control-embed umap-control',
},
onClick: function () {
this._umap.share.open()
},
})
/* Used in view mode to define the current tilelayer */
U.TileLayerControl = L.Control.IconLayers.extend({
initialize: function (map, options) {
@ -238,128 +58,6 @@ U.TileLayerControl = L.Control.IconLayers.extend({
},
})
/* Used in edit mode to define the default tilelayer */
U.TileLayerChooser = L.Control.extend({
options: {
position: 'topleft',
},
initialize: function (map, options = {}) {
this.map = map
L.Control.prototype.initialize.call(this, options)
},
onAdd: function () {
const container = L.DomUtil.create('div', 'leaflet-control-tilelayers umap-control')
const changeMapBackgroundButton = L.DomUtil.createButton(
'',
container,
L._('Change map background'),
this.openSwitcher,
this
)
L.DomEvent.on(changeMapBackgroundButton, 'dblclick', L.DomEvent.stopPropagation)
return container
},
openSwitcher: function (options = {}) {
const container = L.DomUtil.create('div', 'umap-edit-tilelayers')
L.DomUtil.createTitle(container, L._('Change tilelayers'), 'icon-tilelayer')
this._tilelayers_container = L.DomUtil.create('ul', '', container)
this.buildList(options)
const panel = options.edit ? this.map._umap.editPanel : this.map._umap.panel
panel.open({ content: container, highlight: 'tilelayers' })
},
buildList: function (options) {
this.map.eachTileLayer(function (tilelayer) {
if (
window.location.protocol === 'https:' &&
tilelayer.options.url_template.indexOf('http:') === 0
)
return
this.addTileLayerElement(tilelayer, options)
}, this)
},
addTileLayerElement: function (tilelayer, options) {
const selectedClass = this.map.hasLayer(tilelayer) ? 'selected' : ''
const el = L.DomUtil.create('li', selectedClass, this._tilelayers_container)
const img = L.DomUtil.create('img', '', el)
const name = L.DomUtil.create('div', '', el)
img.src = U.Utils.template(
tilelayer.options.url_template,
this.map.options.demoTileInfos
)
img.loading = 'lazy'
name.textContent = tilelayer.options.name
L.DomEvent.on(
el,
'click',
() => {
const oldTileLayer = this.map._umap.properties.tilelayer
this.map.selectTileLayer(tilelayer)
this.map._controls.tilelayers.setLayers()
if (options?.edit) {
this.map._umap.properties.tilelayer = tilelayer.toJSON()
this.map._umap.isDirty = true
this.map._umap.sync.update(
'properties.tilelayer',
this.map._umap.properties.tilelayer,
oldTileLayer
)
}
},
this
)
},
})
U.AttributionControl = L.Control.Attribution.extend({
options: {
prefix: '',
},
_update: function () {
// Layer is no more on the map
if (!this._map) return
L.Control.Attribution.prototype._update.call(this)
// Use our own container, so we can hide/show on small screens
const credits = this._container.innerHTML
this._container.innerHTML = ''
const container = L.DomUtil.create('div', 'attribution-container', this._container)
container.innerHTML = credits
const shortCredit = this._map._umap.getProperty('shortCredit')
const captionMenus = this._map._umap.getProperty('captionMenus')
if (shortCredit) {
L.DomUtil.element({
tagName: 'span',
parent: container,
safeHTML: `${U.Utils.toHTML(shortCredit)}`,
})
}
if (captionMenus) {
const link = L.DomUtil.add('a', '', container, `${L._('Open caption')}`)
L.DomEvent.on(link, 'click', L.DomEvent.stop)
.on(link, 'click', () => this._map._umap.openCaption())
.on(link, 'dblclick', L.DomEvent.stop)
}
if (window.top === window.self && captionMenus) {
// We are not in iframe mode
L.DomUtil.createLink('', container, `${L._('Home')}`, '/')
}
if (captionMenus) {
L.DomUtil.createLink(
'',
container,
`${L._('Powered by uMap')}`,
'https://umap-project.org/'
)
}
L.DomUtil.createLink('attribution-toggle', this._container, '')
},
})
/*
* Take control over L.Control.Locate to be able to
* call start() before adding the control (and thus the button) to the map.

View file

@ -65,18 +65,6 @@ L.DomUtil.createButton = (className, container, content, callback, context) => {
return el
}
L.DomUtil.createLink = (className, container, content, url, target, title) => {
const el = L.DomUtil.add('a', className, container, content)
el.href = url
if (target) {
el.target = target
}
if (title) {
el.title = title
}
return el
}
L.DomUtil.createIcon = (parent, className, title, size = 16) => {
return L.DomUtil.element({
tagName: 'i',
@ -140,16 +128,6 @@ L.DomUtil.element = ({ tagName, parent, ...attrs }) => {
return el
}
L.DomUtil.before = (target, el) => {
target.parentNode.insertBefore(el, target)
return el
}
L.DomUtil.after = (target, el) => {
target.parentNode.insertBefore(el, target.nextSibling)
return el
}
// From https://gist.github.com/Accudio/b9cb16e0e3df858cef0d31e38f1fe46f
// convert colour in range 0-255 to the modifier used within luminance calculation
L.DomUtil.colourMod = (colour) => {
@ -214,24 +192,6 @@ L.DomUtil.contrastedColor = (el, bgcolor) => {
if (bgcolor) _CACHE_CONSTRAST[bgcolor] = out
return out
}
L.DomEvent.once = (el, types, fn, context) => {
// cf https://github.com/Leaflet/Leaflet/pull/3528#issuecomment-134551575
if (typeof types === 'object') {
for (const type in types) {
L.DomEvent.once(el, type, types[type], fn)
}
return L.DomEvent
}
const handler = L.bind(() => {
L.DomEvent.off(el, types, fn, context).off(el, types, handler, context)
}, L.DomEvent)
// add a listener that's executed once and removed after that
return L.DomEvent.on(el, types, fn, context).on(el, types, handler, context)
}
L.LatLng.prototype.isValid = function () {
return (
Number.isFinite(this.lat) &&

View file

@ -96,27 +96,20 @@ html[dir="rtl"] .leaflet-tooltip-pane > * {
background-color: white;
min-height: initial;
}
.leaflet-control.display-on-more,
.umap-control-less {
.leaflet-control.display-on-more {
display: none;
}
.umap-control-more,
.umap-control-less {
.umap-control-more {
background-image: url('./img/24-white.svg');
background-position: -72px -402px;
text-indent: -9999px;
margin-bottom: 0;
}
.umap-control-less {
.umap-more-controls .umap-control-more {
background-position: -108px -402px;
}
.umap-more-controls .display-on-more,
.umap-more-controls .umap-control-less {
.umap-more-controls .display-on-more {
display: block;
}
.umap-more-controls .umap-control-more {
display: none;
}
.leaflet-control-embed [type="button"] {
background-position: 0 -180px;
}

View file

@ -465,19 +465,19 @@ def test_main_toolbox_toggle_all_layers(live_server, map, page):
expect(page.locator(".datalayer.off")).to_have_count(1)
# Click on button
page.locator(".umap-browser [data-ref=toggle]").click()
page.locator(".umap-browser").get_by_title("Show/hide all layers").click()
# Should have hidden the two other layers
expect(page.locator(".datalayer.off")).to_have_count(3)
expect(markers).to_have_count(0)
# Click again
page.locator(".umap-browser [data-ref=toggle]").click()
page.locator(".umap-browser").get_by_title("Show/hide all layers").click()
# Should shown all layers
expect(page.locator(".datalayer.off")).to_have_count(0)
expect(markers).to_have_count(3)
# Click again
page.locator(".umap-browser [data-ref=toggle]").click()
page.locator(".umap-browser").get_by_title("Show/hide all layers").click()
# Should hidden again all layers
expect(page.locator(".datalayer.off")).to_have_count(3)
expect(markers).to_have_count(0)

View file

@ -281,10 +281,10 @@ def test_can_deactive_rule_from_list(live_server, page, openmap):
page.get_by_role("button", name="Edit").click()
page.get_by_role("button", name="Map advanced properties").click()
page.get_by_text("Conditional style rules").click()
page.get_by_role("button", name="Show/hide layer").click()
page.get_by_role("button", name="Toggle rule").click()
colors = getColors(markers)
assert colors.count("rgb(240, 248, 255)") == 0
page.get_by_role("button", name="Show/hide layer").click()
page.get_by_role("button", name="Toggle rule").click()
colors = getColors(markers)
assert colors.count("rgb(240, 248, 255)") == 3

View file

@ -180,9 +180,9 @@ def test_sortkey_impacts_datalayerindex(map, live_server, page):
first_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(0)
second_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(1)
third_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(2)
assert "X Third" == first_listed_feature.text_content()
assert "Y Second" == second_listed_feature.text_content()
assert "Z First" == third_listed_feature.text_content()
assert "X Third" == first_listed_feature.text_content().strip()
assert "Y Second" == second_listed_feature.text_content().strip()
assert "Z First" == third_listed_feature.text_content().strip()
# Change the default sortkey to be "key"
page.get_by_role("button", name="Edit").click()
@ -201,9 +201,9 @@ def test_sortkey_impacts_datalayerindex(map, live_server, page):
first_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(0)
second_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(1)
third_listed_feature = page.locator(".umap-browser .datalayer ul > li").nth(2)
assert "Z First" == first_listed_feature.text_content()
assert "Y Second" == second_listed_feature.text_content()
assert "X Third" == third_listed_feature.text_content()
assert "Z First" == first_listed_feature.text_content().strip()
assert "Y Second" == second_listed_feature.text_content().strip()
assert "X Third" == third_listed_feature.text_content().strip()
def test_hover_tooltip_setting_should_be_persistent(live_server, map, page):

View file

@ -435,6 +435,63 @@ def test_import_geometry_collection(live_server, page, tilelayer):
expect(paths).to_have_count(2)
def test_import_geometry_collection_in_feature(live_server, page, tilelayer):
data = {
"type": "Feature",
"properties": {"name": "foobar"},
"geometry": {
"type": "GeometryCollection",
"geometries": [
{"type": "Point", "coordinates": [-80.6608, 35.0493]},
{
"type": "Polygon",
"coordinates": [
[
[-80.6645, 35.0449],
[-80.6634, 35.0460],
[-80.6625, 35.0455],
[-80.6638, 35.0442],
[-80.6645, 35.0449],
]
],
},
{
"type": "LineString",
"coordinates": [
[-80.66237, 35.05950],
[-80.66269, 35.05926],
[-80.66284, 35.05893],
[-80.66308, 35.05833],
[-80.66385, 35.04387],
[-80.66303, 35.04371],
],
},
],
},
}
page.goto(f"{live_server.url}/map/new/")
page.get_by_title("Open browser").click()
layers = page.locator(".umap-browser .datalayer")
markers = page.locator(".leaflet-marker-icon")
paths = page.locator("path")
expect(markers).to_have_count(0)
expect(paths).to_have_count(0)
expect(layers).to_have_count(0)
button = page.get_by_title("Import data")
expect(button).to_be_visible()
button.click()
textarea = page.locator(".umap-import textarea")
textarea.fill(json.dumps(data))
page.locator('select[name="format"]').select_option("geojson")
page.get_by_role("button", name="Import data", exact=True).click()
# A layer has been created
expect(layers).to_have_count(1)
expect(markers).to_have_count(1)
expect(paths).to_have_count(2)
# Geometries are treated as separate features.
expect(page.get_by_text("foobar")).to_have_count(3)
def test_import_multipolygon(live_server, page, tilelayer):
data = {
"type": "Feature",

View file

@ -63,5 +63,5 @@ def test_zoom_control(map, live_server, datalayer, page):
expect(control).to_be_visible()
page.goto(f"{live_server.url}{map.get_absolute_url()}?zoomControl=null")
expect(control).to_be_hidden()
page.get_by_title("More controls").click()
page.locator(".umap-control-more").click()
expect(control).to_be_visible()