Compare commits

..

3 commits

Author SHA1 Message Date
Yohan Boniface
ba3a1ccd33 feat: display importers in a dialog instead of direclty in the form
The goal is to keep the form smaller, specifically to keep the
submit button visible as much as possible.
2024-12-02 19:50:46 +01:00
Yohan Boniface
88ee836b34 chore: make that Importer extends WithTemplate
Another step is needed to use this.elements instead of this.qs
2024-12-02 19:23:47 +01:00
Yohan Boniface
4d1286e0ef feat: add icon-magic in 16-white.svg 2024-12-02 19:04:30 +01:00
29 changed files with 85 additions and 312 deletions

View file

@ -47,7 +47,7 @@ Voici un bref passage en revu des différents imports proposés et pour finir l
## 1. Importer le contour dune commune ## 1. Importer le contour dune commune
Cliquez sur loutil dimportation en bas de la barre de droite, puis cliquez sur le lien « Assistants dimport ». Cliquez sur loutil dimportation en bas de la barre de droite, puis descendez jusquau cadre « Assistants dimport ».
Cliquez sur « Communes France » et sélectionnez la commune souhaitée dans une liste déroulante. Une fois la commune sélectionnée, le format est reconnu automatiquement (geojson) puis le type de calque (cliquer sur « ? » pour savoir quel choix opérer) Cliquez sur « Communes France » et sélectionnez la commune souhaitée dans une liste déroulante. Une fois la commune sélectionnée, le format est reconnu automatiquement (geojson) puis le type de calque (cliquer sur « ? » pour savoir quel choix opérer)
@ -64,7 +64,7 @@ Une fois cet import réalisé, tout est réglable : couleur de contour, de fond,
## 2. Importer les contours des départements ou des régions ## 2. Importer les contours des départements ou des régions
Cliquez sur loutil dimportation en bas de la barre de droite, puis cliquez sur le lien « Assistants dimport ». Cliquez sur loutil dimportation en bas de la barre de droite, puis descendez jusquau cadre « Assistants dimport ».
Cliquez sur « Contours nationaux » puis soit départements, soit régions et enfin le type de calque (voir supra lexplication). Tous les départements sont importés : Cliquez sur « Contours nationaux » puis soit départements, soit régions et enfin le type de calque (voir supra lexplication). Tous les départements sont importés :
@ -72,7 +72,7 @@ Cliquez sur « Contours nationaux » puis soit départements, soit régions et
## 3. Importer un point dintérêt issu de GeoDataMine ## 3. Importer un point dintérêt issu de GeoDataMine
Cliquez sur loutil dimportation en bas de la barre de droite, puis cliquez sur le lien « Assistants dimport ». Cliquez sur loutil dimportation en bas de la barre de droite, puis descendez jusquau cadre « Assistants dimport ».
Cliquez sur « GeoDataMine (thèmes OSM) » et sélectionnez les informations souhaitées, routes, bâtiments, commerces, services publics, … Cliquez sur « GeoDataMine (thèmes OSM) » et sélectionnez les informations souhaitées, routes, bâtiments, commerces, services publics, …
Par exemple, en sélectionnant les points deau potable de la CA du Grand Avignon, puis « Copier dans un calque » Par exemple, en sélectionnant les points deau potable de la CA du Grand Avignon, puis « Copier dans un calque »

View file

@ -1,5 +1,5 @@
# Force rtfd to use a recent version of mkdocs # Force rtfd to use a recent version of mkdocs
mkdocs==1.6.1 mkdocs==1.6.1
pymdown-extensions==10.12 pymdown-extensions==10.12
mkdocs-material==9.5.47 mkdocs-material==9.5.44
mkdocs-static-i18n==1.2.3 mkdocs-static-i18n==1.2.3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 734 KiB

After

Width:  |  Height:  |  Size: 3.4 MiB

View file

@ -175,6 +175,11 @@ UMAP_EXTRA_URLS = {
} }
``` ```
#### UMAP_KEEP_VERSIONS
How many datalayer versions to keep. 10 by default.
#### UMAP_DEFAULT_EDIT_STATUS #### UMAP_DEFAULT_EDIT_STATUS
Define the map default edit status. Define the map default edit status.
@ -265,16 +270,6 @@ Available importers:
- `communesfr`: download French communes boundaries, from https://geo.api.gouv.fr/ - `communesfr`: download French communes boundaries, from https://geo.api.gouv.fr/
- `datasets`: define URLs you want to promote to users, with a `name` and a `format` - `datasets`: define URLs you want to promote to users, with a `name` and a `format`
#### UMAP_KEEP_VERSIONS
How many datalayer versions to keep. 10 by default.
#### UMAP_LABEL_KEYS
List of properties to consider as "Feature label" (to show in popup or in browser).
UMAP_LABEL_KEYS = ["name", "title"]
#### UMAP_MAPS_PER_PAGE #### UMAP_MAPS_PER_PAGE
How many maps to show in maps list, like search or home page. How many maps to show in maps list, like search or home page.

View file

@ -1,5 +1,5 @@
# Force rtfd to use a recent version of mkdocs # Force rtfd to use a recent version of mkdocs
mkdocs==1.6.1 mkdocs==1.6.1
pymdown-extensions==10.12 pymdown-extensions==10.12
mkdocs-material==9.5.47 mkdocs-material==9.5.44
mkdocs-static-i18n==1.2.3 mkdocs-static-i18n==1.2.3

View file

@ -44,10 +44,10 @@ dependencies = [
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"hatch==1.13.0", "hatch==1.13.0",
"ruff==0.8.1", "ruff==0.7.4",
"djlint==1.36.3", "djlint==1.36.1",
"mkdocs==1.6.1", "mkdocs==1.6.1",
"mkdocs-material==9.5.47", "mkdocs-material==9.5.44",
"mkdocs-static-i18n==1.2.3", "mkdocs-static-i18n==1.2.3",
"vermin==1.6.0", "vermin==1.6.0",
"pymdown-extensions==10.12", "pymdown-extensions==10.12",
@ -56,10 +56,10 @@ dev = [
test = [ test = [
"factory-boy==3.3.1", "factory-boy==3.3.1",
"playwright>=1.39", "playwright>=1.39",
"pytest==8.3.4", "pytest==8.3.3",
"pytest-django==4.9.0", "pytest-django==4.9.0",
"pytest-playwright==0.6.2", "pytest-playwright==0.5.2",
"pytest-rerunfailures==15.0", "pytest-rerunfailures==14.0",
"pytest-xdist>=3.5.0,<4", "pytest-xdist>=3.5.0,<4",
] ]
docker = [ docker = [
@ -68,7 +68,7 @@ docker = [
sync = [ sync = [
"channels==4.2.0", "channels==4.2.0",
"daphne==4.1.2", "daphne==4.1.2",
"pydantic==2.10.2", "pydantic==2.9.2",
"websockets==13.1", "websockets==13.1",
] ]

View file

@ -273,7 +273,6 @@ UMAP_HOME_FEED = "latest"
UMAP_IMPORTERS = {} UMAP_IMPORTERS = {}
UMAP_HOST_INFOS = {} UMAP_HOST_INFOS = {}
UMAP_PURGATORY_ROOT = "/tmp/umappurgatory" UMAP_PURGATORY_ROOT = "/tmp/umappurgatory"
UMAP_LABEL_KEYS = ["name", "title"]
UMAP_READONLY = env("UMAP_READONLY", default=False) UMAP_READONLY = env("UMAP_READONLY", default=False)
UMAP_GZIP = True UMAP_GZIP = True

View file

@ -65,9 +65,7 @@ body.login header {
.login-grid .login-openstreetmap-oauth2 { .login-grid .login-openstreetmap-oauth2 {
background-image: url("./openstreetmap.png"); background-image: url("./openstreetmap.png");
} }
.login-grid .login-keycloak {
background-image: url("./keycloak.png");
}
/* **************************** */ /* **************************** */
/* home */ /* home */

View file

@ -78,7 +78,8 @@ input[type="submit"] {
border-radius: 2px; border-radius: 2px;
font-weight: normal; font-weight: normal;
cursor: pointer; cursor: pointer;
padding: 7px 14px; padding: 7px;
width: 100%;
min-height: 32px; min-height: 32px;
line-height: 32px; line-height: 32px;
border: none; border: none;
@ -91,12 +92,6 @@ input[type="submit"] {
color: var(--text-color); color: var(--text-color);
border: 1px solid #1b1f20; border: 1px solid #1b1f20;
} }
.dark .button.primary:not([disabled]),
.dark [type="button"].primary:not([disabled]) {
background-color: var(--color-brightCyan);
color: var(--color-dark);
border: 1px solid #1b1f20;
}
.dark .button:hover, .dark .button:hover,
.dark [type="button"]:hover, .dark [type="button"]:hover,
.dark input[type="submit"]:hover { .dark input[type="submit"]:hover {
@ -105,11 +100,6 @@ input[type="submit"] {
.dark a { .dark a {
color: var(--text-color); color: var(--text-color);
} }
.dark [type="button"][disabled],
.dark input[type="submit"][disabled] {
background-color: var(--color-mediumGray);
cursor: not-allowed;
}
button.flat, button.flat,
[type="button"].flat, [type="button"].flat,
.dark [type="button"].flat { .dark [type="button"].flat {

View file

@ -165,9 +165,7 @@ class Feature {
} }
getSlug() { getSlug() {
return ( return this.properties[this._umap.getProperty('slugKey') || 'name'] || ''
this.properties[this._umap.getProperty('slugKey') || U.DEFAULT_LABEL_KEY] || ''
)
} }
getPermalink() { getPermalink() {
@ -236,23 +234,15 @@ class Feature {
container.appendChild(builder.build()) container.appendChild(builder.build())
const properties = [] const properties = []
let labelKeyFound = undefined
for (const property of this.datalayer._propertiesIndex) { for (const property of this.datalayer._propertiesIndex) {
if (!labelKeyFound && U.LABEL_KEYS.includes(property)) { if (['name', 'description'].includes(property)) {
labelKeyFound = property
continue
}
if (property === 'description') {
continue continue
} }
properties.push([`properties.${property}`, { label: property }]) properties.push([`properties.${property}`, { label: property }])
} }
// We always want name and description for now (properties management to come) // We always want name and description for now (properties management to come)
properties.unshift('properties.description') properties.unshift('properties.description')
if (!labelKeyFound) { properties.unshift('properties.name')
labelKeyFound = U.DEFAULT_LABEL_KEY
}
properties.unshift([`properties.${labelKeyFound}`, { label: labelKeyFound }])
builder = new U.FormBuilder(this, properties, { builder = new U.FormBuilder(this, properties, {
id: 'umap-feature-properties', id: 'umap-feature-properties',
}) })
@ -265,7 +255,7 @@ class Feature {
this.getAdvancedEditActions(advancedActions) this.getAdvancedEditActions(advancedActions)
const onLoad = this._umap.editPanel.open({ content: container }) const onLoad = this._umap.editPanel.open({ content: container })
onLoad.then(() => { onLoad.then(() => {
builder.helpers[`properties.${labelKeyFound}`].input.focus() builder.helpers['properties.name'].input.focus()
}) })
this._umap.editedFeature = this this._umap.editedFeature = this
if (!this.ui.isOnScreen(this._umap._leafletMap.getBounds())) this.zoomTo(event) if (!this.ui.isOnScreen(this._umap._leafletMap.getBounds())) this.zoomTo(event)
@ -326,21 +316,19 @@ class Feature {
endEdit() {} endEdit() {}
getDisplayName() { getDisplayName(fallback) {
const keys = U.LABEL_KEYS.slice() // Copy. const key = this.getOption('labelKey') || 'name'
const labelKey = this.getOption('labelKey')
// Variables mode. // Variables mode.
if (labelKey) { if (Utils.hasVar(key)) {
if (Utils.hasVar(labelKey)) { return Utils.greedyTemplate(key, this.extendedProperties())
return Utils.greedyTemplate(labelKey, this.extendedProperties())
} }
keys.unshift(labelKey) // Simple mode.
} return (
for (const key of keys) { this.properties[key] ||
const value = this.properties[key] this.properties.title ||
if (value) return value fallback ||
} this.datalayer.getName()
return this.datalayer.getName() )
} }
hasPopupFooter() { hasPopupFooter() {

View file

@ -289,7 +289,7 @@ export class DataLayer extends ServerStored {
reindex() { reindex() {
const features = Object.values(this._features) const features = Object.values(this._features)
this.sortFeatures(features) Utils.sortFeatures(features, this._umap.getProperty('sortKey'), U.lang)
this._index = features.map((feature) => stamp(feature)) this._index = features.map((feature) => stamp(feature))
} }
@ -428,10 +428,6 @@ export class DataLayer extends ServerStored {
if (idx !== -1) this._propertiesIndex.splice(idx, 1) if (idx !== -1) this._propertiesIndex.splice(idx, 1)
} }
allProperties() {
return this._propertiesIndex
}
sortedValues(property) { sortedValues(property) {
return Object.values(this._features) return Object.values(this._features)
.map((feature) => feature.properties[property]) .map((feature) => feature.properties[property])
@ -450,11 +446,6 @@ export class DataLayer extends ServerStored {
} }
} }
sortFeatures(collection) {
const sortKeys = this._umap.getProperty('sortKey') || U.DEFAULT_LABEL_KEY
return Utils.sortFeatures(collection, sortKeys, U.lang)
}
makeFeatures(geojson = {}, sync = true) { makeFeatures(geojson = {}, sync = true) {
if (geojson.type === 'Feature' || geojson.coordinates) { if (geojson.type === 'Feature' || geojson.coordinates) {
geojson = [geojson] geojson = [geojson]
@ -463,7 +454,7 @@ export class DataLayer extends ServerStored {
? geojson ? geojson
: geojson.features || geojson.geometries : geojson.features || geojson.geometries
if (!collection) return if (!collection) return
this.sortFeatures(collection) Utils.sortFeatures(collection, this._umap.getProperty('sortKey'), U.lang)
for (const feature of collection) { for (const feature of collection) {
this.makeFeature(feature, sync) this.makeFeature(feature, sync)
} }

View file

@ -1,5 +1,5 @@
import { uMapAlert as Alert } from '../components/alerts/alert.js' import { uMapAlert as Alert } from '../components/alerts/alert.js'
import { AjaxAutocomplete, AjaxAutocompleteMultiple, AutocompleteDatalist } from './autocomplete.js' import { AjaxAutocomplete, AjaxAutocompleteMultiple } from './autocomplete.js'
import Help from './help.js' import Help from './help.js'
import { ServerRequest } from './request.js' import { ServerRequest } from './request.js'
import { SCHEMA } from './schema.js' import { SCHEMA } from './schema.js'
@ -17,7 +17,6 @@ window.U = {
Alert, Alert,
AjaxAutocomplete, AjaxAutocomplete,
AjaxAutocompleteMultiple, AjaxAutocompleteMultiple,
AutocompleteDatalist,
Help, Help,
Icon, Icon,
LAYER_TYPES, LAYER_TYPES,

View file

@ -33,15 +33,15 @@ const TEMPLATE = `
<fieldset id="import-mode" class="formbox"> <fieldset id="import-mode" class="formbox">
<legend class="counter" data-help="importMode">${translate('Choose import mode')}</legend> <legend class="counter" data-help="importMode">${translate('Choose import mode')}</legend>
<label> <label>
<input type="radio" name="action" value="copy" checked onchange /> <input type="radio" name="action" value="copy" />
${translate('Copy into the layer')} ${translate('Copy into the layer')}
</label> </label>
<label> <label>
<input type="radio" name="action" value="link" onchange /> <input type="radio" name="action" value="link" />
${translate('Link to the layer as remote data')} ${translate('Link to the layer as remote data')}
</label> </label>
</fieldset> </fieldset>
<input type="button" class="button primary" name="submit" value="${translate('Import data')}" disabled /> <input type="button" class="button" name="submit" value="${translate('Import data')}" />
</div> </div>
` `
@ -121,11 +121,6 @@ export default class Importer extends Utils.WithTemplate {
return this.qs('textarea').value return this.qs('textarea').value
} }
set raw(value) {
this.qs('textarea').value = value
this.onChange()
}
get clear() { get clear() {
return Boolean(this.qs('[name=clear]').checked) return Boolean(this.qs('[name=clear]').checked)
} }
@ -203,7 +198,6 @@ export default class Importer extends Utils.WithTemplate {
) )
this.qs('[name=layer-name]').toggleAttribute('hidden', Boolean(this.layerId)) this.qs('[name=layer-name]').toggleAttribute('hidden', Boolean(this.layerId))
this.qs('#clear').toggleAttribute('hidden', !this.layerId) this.qs('#clear').toggleAttribute('hidden', !this.layerId)
this.qs('[name=submit').toggleAttribute('disabled', !this.canSubmit())
} }
onFileChange(e) { onFileChange(e) {
@ -225,7 +219,6 @@ export default class Importer extends Utils.WithTemplate {
this.url = null this.url = null
this.format = undefined this.format = undefined
this.layerName = null this.layerName = null
this.raw = null
const layerSelect = this.qs('[name="layer-id"]') const layerSelect = this.qs('[name="layer-id"]')
layerSelect.innerHTML = '' layerSelect.innerHTML = ''
this._umap.eachDataLayerReverse((datalayer) => { this._umap.eachDataLayerReverse((datalayer) => {
@ -258,17 +251,6 @@ export default class Importer extends Utils.WithTemplate {
this.qs('[type=file]').showPicker() this.qs('[type=file]').showPicker()
} }
canSubmit() {
if (!this.format) return false
const hasFiles = Boolean(this.files.length)
const hasRaw = Boolean(this.raw)
const hasUrl = Boolean(this.url)
const hasAction = Boolean(this.action)
if (!hasFiles && !hasRaw && !hasUrl) return false
if (this.url) return hasAction
return true
}
submit() { submit() {
let hasErrors let hasErrors
if (this.format === 'umap') { if (this.format === 'umap') {

View file

@ -2,8 +2,6 @@ import { DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { BaseAjax, SingleMixin } from '../autocomplete.js' import { BaseAjax, SingleMixin } from '../autocomplete.js'
import * as Util from '../utils.js' import * as Util from '../utils.js'
import { AutocompleteCommunes } from './communesfr.js' import { AutocompleteCommunes } from './communesfr.js'
import { translate } from '../i18n.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
const TEMPLATE = ` const TEMPLATE = `
<h3>Cadastre</h3> <h3>Cadastre</h3>
@ -58,8 +56,6 @@ export class Importer {
.open({ .open({
template: container, template: container,
className: `${this.id} importer dark`, className: `${this.id} importer dark`,
cancel: false,
accept: translate('Choose this data'),
}) })
.then(confirm) .then(confirm)
} }

View file

@ -37,8 +37,8 @@ class Autocomplete extends SingleMixin(BaseAjax) {
} }
export class Importer { export class Importer {
constructor(umap, options = {}) { constructor(map, options = {}) {
this.umap = umap this.map = map
this.name = options.name || 'GeoDataMine' this.name = options.name || 'GeoDataMine'
this.baseUrl = options?.url || 'https://geodatamine.fr' this.baseUrl = options?.url || 'https://geodatamine.fr'
this.id = 'geodatamine' this.id = 'geodatamine'
@ -49,7 +49,7 @@ export class Importer {
let boundaryName = null let boundaryName = null
const container = DomUtil.create('div') const container = DomUtil.create('div')
container.innerHTML = TEMPLATE container.innerHTML = TEMPLATE
const response = await this.umap.request.get(`${this.baseUrl}/themes`) const response = await importer.map.request.get(`${this.baseUrl}/themes`)
const select = container.querySelector('select') const select = container.querySelector('select')
if (response?.ok) { if (response?.ok) {
const { themes } = await response.json() const { themes } = await response.json()

View file

@ -53,15 +53,11 @@ export class Importer {
'https://photon.komoot.io/api?q={q}&layer=county&layer=city&layer=state' 'https://photon.komoot.io/api?q={q}&layer=county&layer=city&layer=state'
this.id = 'overpass' this.id = 'overpass'
this.boundaryChoice = null this.boundaryChoice = null
this.expression = null
} }
async open(importer) { async open(importer) {
const container = DomUtil.create('div') const container = DomUtil.create('div')
container.innerHTML = TEMPLATE container.innerHTML = TEMPLATE
if (this.expression) {
container.querySelector('[name=tags]').value = this.expression
}
this.autocomplete = new Autocomplete(container.querySelector('#area'), { this.autocomplete = new Autocomplete(container.querySelector('#area'), {
url: this.searchUrl, url: this.searchUrl,
placeholder: translate( placeholder: translate(
@ -84,7 +80,6 @@ export class Importer {
Alert.error(translate('Expression is empty')) Alert.error(translate('Expression is empty'))
return return
} }
this.expression = form.tags
let tags = form.tags let tags = form.tags
if (!tags.startsWith('[')) tags = `[${tags}]` if (!tags.startsWith('[')) tags = `[${tags}]`
let area = '{south},{west},{north},{east}' let area = '{south},{west},{north},{east}'

View file

@ -44,12 +44,12 @@ const ControlsMixin = {
new U.DrawToolbar({ map: this }).addTo(this) new U.DrawToolbar({ map: this }).addTo(this)
const editActions = [ const editActions = [
U.EditCaptionAction, U.EditCaptionAction,
U.ImportAction, U.EditPropertiesAction,
U.EditLayersAction, U.EditLayersAction,
U.ChangeTileLayerAction, U.ChangeTileLayerAction,
U.UpdateExtentAction, U.UpdateExtentAction,
U.UpdatePermsAction, U.UpdatePermsAction,
U.EditPropertiesAction, U.ImportAction,
] ]
if (this.options.editMode === 'advanced') { if (this.options.editMode === 'advanced') {
new U.SettingsToolbar({ actions: editActions }).addTo(this) new U.SettingsToolbar({ actions: editActions }).addTo(this)

View file

@ -115,9 +115,7 @@ class Table extends TitleMixin(PopupTemplate) {
const table = document.createElement('table') const table = document.createElement('table')
for (const key in feature.properties) { for (const key in feature.properties) {
if (typeof feature.properties[key] === 'object' || U.LABEL_KEYS.includes(key)) { if (typeof feature.properties[key] === 'object' || key === 'name') continue
continue
}
table.appendChild(this.makeRow(feature, key)) table.appendChild(this.makeRow(feature, key))
} }
return table return table
@ -152,19 +150,6 @@ class GeoRSSLink extends PopupTemplate {
} }
class OSM extends TitleMixin(PopupTemplate) { class OSM extends TitleMixin(PopupTemplate) {
renderTitle(feature) {
const title = DomUtil.add('h3', 'popup-title')
const color = feature.getPreviewColor()
title.style.backgroundColor = color
const iconUrl = feature.getDynamicOption('iconUrl')
const icon = Icon.makeElement(iconUrl, title)
DomUtil.addClass(icon, 'icon')
Icon.setContrast(icon, title, iconUrl, color)
if (DomUtil.contrastedColor(title, color)) title.style.color = 'white'
DomUtil.add('span', '', title, this.getName(feature))
return title
}
getName(feature) { getName(feature) {
const props = feature.properties const props = feature.properties
const locale = getLocale() const locale = getLocale()
@ -175,6 +160,15 @@ class OSM extends TitleMixin(PopupTemplate) {
renderBody(feature) { renderBody(feature) {
const props = feature.properties const props = feature.properties
const body = document.createElement('div') const body = document.createElement('div')
const title = DomUtil.add('h3', 'popup-title', container)
const color = feature.getPreviewColor()
title.style.backgroundColor = color
const iconUrl = feature.getDynamicOption('iconUrl')
const icon = Icon.makeElement(iconUrl, title)
DomUtil.addClass(icon, 'icon')
Icon.setContrast(icon, title, iconUrl, color)
if (DomUtil.contrastedColor(title, color)) title.style.color = 'white'
DomUtil.add('span', '', title, this.getName(feature))
const street = props['addr:street'] const street = props['addr:street']
if (street) { if (street) {
const row = DomUtil.add('address', 'address', body) const row = DomUtil.add('address', 'address', body)
@ -211,13 +205,6 @@ class OSM extends TitleMixin(PopupTemplate) {
Utils.loadTemplate(`<div><a href="mailto:${email}">${email}</a></div>`) Utils.loadTemplate(`<div><a href="mailto:${email}">${email}</a></div>`)
) )
} }
if (props.panoramax) {
body.appendChild(
Utils.loadTemplate(
`<div><img src="https://api.panoramax.xyz/api/pictures/${props.panoramax}/sd.jpg" /></div>`
)
)
}
const id = props['@id'] || props.id const id = props['@id'] || props.id
if (id) { if (id) {
body.appendChild( body.appendChild(

View file

@ -75,7 +75,7 @@ const FeatureMixin = {
resetTooltip: function () { resetTooltip: function () {
if (!this.feature.hasGeom()) return if (!this.feature.hasGeom()) return
const displayName = this.feature.getDisplayName() const displayName = this.feature.getDisplayName(null)
let showLabel = this.feature.getOption('showLabel') let showLabel = this.feature.getOption('showLabel')
const oldLabelHover = this.feature.getOption('labelHover') const oldLabelHover = this.feature.getOption('labelHover')
@ -255,10 +255,7 @@ const PathMixin = {
if (this._map.measureTools?.enabled()) { if (this._map.measureTools?.enabled()) {
this._map._umap.tooltip.open({ content: this.getMeasure(), anchor: this }) this._map._umap.tooltip.open({ content: this.getMeasure(), anchor: this })
} else if (this._map._umap.editEnabled && !this._map._umap.editedFeature) { } else if (this._map._umap.editEnabled && !this._map._umap.editedFeature) {
this._map._umap.tooltip.open({ this._map._umap.tooltip.open({ content: translate('Click to edit'), anchor: this })
content: translate('Click to edit'),
anchor: this,
})
} }
}, },
@ -270,9 +267,7 @@ const PathMixin = {
this._map.once('moveend', this.makeGeometryEditable, this) this._map.once('moveend', this.makeGeometryEditable, this)
const pointsCount = this._parts.reduce((acc, part) => acc + part.length, 0) const pointsCount = this._parts.reduce((acc, part) => acc + part.length, 0)
if (pointsCount > 100 && this._map.getZoom() < this._map.getMaxZoom()) { if (pointsCount > 100 && this._map.getZoom() < this._map.getMaxZoom()) {
this._map._umap.tooltip.open({ this._map._umap.tooltip.open({ content: L._('Please zoom in to edit the geometry') })
content: L._('Please zoom in to edit the geometry'),
})
this.disableEdit() this.disableEdit()
} else { } else {
this.enableEdit() this.enableEdit()
@ -385,19 +380,8 @@ export const LeafletPolyline = Polyline.extend({
}, },
getMeasure: function (shape) { getMeasure: function (shape) {
let shapes
if (shape) {
shapes = [shape]
} else if (LineUtil.isFlat(this._latlngs)) {
shapes = [this._latlngs]
} else {
shapes = this._latlngs
}
// FIXME: compute from data in feature (with TurfJS) // FIXME: compute from data in feature (with TurfJS)
const length = shapes.reduce( const length = L.GeoUtil.lineLength(this._map, shape || this._defaultShape())
(acc, shape) => acc + L.GeoUtil.lineLength(this._map, shape),
0
)
return L.GeoUtil.readableDistance(length, this._map.measureTools.getMeasureUnit()) return L.GeoUtil.readableDistance(length, this._map.measureTools.getMeasureUnit())
}, },
}) })

View file

@ -105,7 +105,7 @@ export default class TableEditor extends WithTemplate {
resetProperties() { resetProperties() {
this.properties = this.datalayer._propertiesIndex this.properties = this.datalayer._propertiesIndex
if (this.properties.length === 0) { if (this.properties.length === 0) {
this.properties = [U.DEFAULT_LABEL_KEY, 'description'] this.properties = ['name', 'description']
} }
} }

View file

@ -71,10 +71,6 @@ export default class Umap extends ServerStored {
// To be used in javascript APIs // To be used in javascript APIs
if (geojson.properties.lang) U.lang = geojson.properties.lang 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() this.setPropertiesFromQueryString()
// Needed for actions labels // Needed for actions labels
@ -541,10 +537,10 @@ export default class Umap extends ServerStored {
this._leafletMap.editTools.startPolyline() this._leafletMap.editTools.startPolyline()
break break
case 'i': case 'i':
this.importer.open() this._leafletMap.importer.open()
break break
case 'o': case 'o':
this.importer.openFiles() this._leafletMap.importer.openFiles()
break break
case 'h': case 'h':
this.help.showGetStarted() this.help.showGetStarted()
@ -600,7 +596,7 @@ export default class Umap extends ServerStored {
const panes = this._leafletMap.getPane('overlayPane') const panes = this._leafletMap.getPane('overlayPane')
this.datalayersIndex = [] this.datalayersIndex = []
for (const pane of panes.children) { for (const pane of panes) {
if (!pane.dataset || !pane.dataset.id) continue if (!pane.dataset || !pane.dataset.id) continue
this.datalayersIndex.push(this.datalayers[pane.dataset.id]) this.datalayersIndex.push(this.datalayers[pane.dataset.id])
} }
@ -688,7 +684,7 @@ export default class Umap extends ServerStored {
} }
allProperties() { allProperties() {
return [].concat(...this.datalayersIndex.map((dl) => dl.allProperties())) return [].concat(...this.datalayersIndex.map((dl) => dl._propertiesIndex))
} }
sortedValues(property) { sortedValues(property) {
@ -1429,13 +1425,13 @@ export default class Umap extends ServerStored {
row.dataset.id = stamp(datalayer) row.dataset.id = stamp(datalayer)
}) })
const onReorder = (src, dst, initialIndex, finalIndex) => { const onReorder = (src, dst, initialIndex, finalIndex) => {
const movedLayer = this.datalayers[src.dataset.id] const layer = this.datalayers[src.dataset.id]
const targetLayer = this.datalayers[dst.dataset.id] const other = this.datalayers[dst.dataset.id]
const minIndex = Math.min(movedLayer.getRank(), targetLayer.getRank()) const minIndex = Math.min(layer.getRank(), other.getRank())
const maxIndex = Math.max(movedLayer.getRank(), targetLayer.getRank()) const maxIndex = Math.max(layer.getRank(), other.getRank())
if (finalIndex === 0) movedLayer.bringToTop() if (finalIndex === 0) layer.bringToTop()
else if (finalIndex > initialIndex) movedLayer.insertBefore(targetLayer) else if (finalIndex > initialIndex) layer.insertBefore(other)
else movedLayer.insertAfter(targetLayer) else layer.insertAfter(other)
this.eachDataLayerReverse((datalayer) => { this.eachDataLayerReverse((datalayer) => {
if (datalayer.getRank() >= minIndex && datalayer.getRank() <= maxIndex) if (datalayer.getRank() >= minIndex && datalayer.getRank() <= maxIndex)
datalayer.isDirty = true datalayer.isDirty = true

View file

@ -292,7 +292,7 @@ export function naturalSort(a, b, lang) {
} }
export function sortFeatures(features, sortKey, lang) { export function sortFeatures(features, sortKey, lang) {
const sortKeys = sortKey.split(',') const sortKeys = (sortKey || 'name').split(',')
const sort = (a, b, i) => { const sort = (a, b, i) => {
let sortKey = sortKeys[i] let sortKey = sortKeys[i]

View file

@ -170,43 +170,22 @@ L.DomUtil.contrastWCAG21 = (rgb) => {
const contrast = (whiteLum + 0.05) / (lum + 0.05) const contrast = (whiteLum + 0.05) / (lum + 0.05)
return contrast > 3 ? 1 : 0 return contrast > 3 ? 1 : 0
} }
L.DomUtil.colorNameToHex = (str) => {
const ctx = document.createElement('canvas').getContext('2d')
ctx.fillStyle = str
return ctx.fillStyle
}
L.DomUtil.hexToRGB = (hex) => {
return hex
.replace(
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
(m, r, g, b) => `#${r}${r}${g}${g}${b}${b}`
)
.substring(1)
.match(/.{2}/g)
.map((x) => Number.parseInt(x, 16))
}
const _CACHE_CONSTRAST = {} const _CACHE_CONSTRAST = {}
L.DomUtil.contrastedColor = (el, bgcolor) => { L.DomUtil.contrastedColor = (el, bgcolor) => {
// Return 0 for black and 1 for white // Return 0 for black and 1 for white
// bgcolor is a human color, it can be a any keyword (purple…) // bgcolor is a human color, it can be a any keyword (purple…)
if (typeof _CACHE_CONSTRAST[bgcolor] !== 'undefined') return _CACHE_CONSTRAST[bgcolor] if (typeof _CACHE_CONSTRAST[bgcolor] !== 'undefined') return _CACHE_CONSTRAST[bgcolor]
let out = 0
let rgb = window.getComputedStyle(el).getPropertyValue('background-color') let rgb = window.getComputedStyle(el).getPropertyValue('background-color')
rgb = L.DomUtil.RGBRegex.exec(rgb) rgb = L.DomUtil.RGBRegex.exec(rgb)
if (rgb && rgb.length === 4) { if (!rgb || rgb.length !== 4) return out
rgb = [ rgb = [
Number.parseInt(rgb[1], 10), Number.parseInt(rgb[1], 10),
Number.parseInt(rgb[2], 10), Number.parseInt(rgb[2], 10),
Number.parseInt(rgb[3], 10), Number.parseInt(rgb[3], 10),
] ]
} else { out = L.DomUtil.contrastWCAG21(rgb)
// The element may not yet be added to the DOM, so let's try
// another way
const hex = L.DomUtil.colorNameToHex(bgcolor)
rgb = L.DomUtil.hexToRGB(hex)
}
if (!rgb) return 1
const out = L.DomUtil.contrastWCAG21(rgb)
if (bgcolor) _CACHE_CONSTRAST[bgcolor] = out if (bgcolor) _CACHE_CONSTRAST[bgcolor] = out
return out return out
} }

View file

@ -448,17 +448,6 @@ L.FormBuilder.BlurInput.include({
}, },
}) })
// Adds an autocomplete using all available user defined properties
L.FormBuilder.PropertyInput = L.FormBuilder.BlurInput.extend({
build: function () {
L.FormBuilder.BlurInput.prototype.build.call(this)
const autocomplete = new U.AutocompleteDatalist(this.input)
// Will be used on Umap and DataLayer
const properties = this.builder.obj.allProperties()
autocomplete.suggestions = properties
},
})
L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
type: () => 'hidden', type: () => 'hidden',
@ -1121,11 +1110,10 @@ U.FormBuilder = L.FormBuilder.extend({
}, },
customHandlers: { customHandlers: {
sortKey: 'PropertyInput', sortKey: 'BlurInput',
easing: 'Switch', easing: 'Switch',
facetKey: 'PropertyInput', facetKey: 'BlurInput',
slugKey: 'PropertyInput', slugKey: 'BlurInput',
labelKey: 'PropertyInput',
}, },
computeDefaultOptions: function () { computeDefaultOptions: function () {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View file

@ -15,12 +15,7 @@ DATALAYER_DATA = {
"features": [ "features": [
{ {
"type": "Feature", "type": "Feature",
"properties": { "properties": {"name": "one point in france", "foo": "point", "bar": "one"},
"name": "one point in france",
"foo": "point",
"bar": "one",
"label": "this is label one",
},
"geometry": {"type": "Point", "coordinates": [3.339844, 46.920255]}, "geometry": {"type": "Point", "coordinates": [3.339844, 46.920255]},
}, },
{ {
@ -29,7 +24,6 @@ DATALAYER_DATA = {
"name": "one polygon in greenland", "name": "one polygon in greenland",
"foo": "polygon", "foo": "polygon",
"bar": "two", "bar": "two",
"label": "this is label two",
}, },
"geometry": { "geometry": {
"type": "Polygon", "type": "Polygon",
@ -50,7 +44,6 @@ DATALAYER_DATA = {
"name": "one line in new zeland", "name": "one line in new zeland",
"foo": "line", "foo": "line",
"bar": "three", "bar": "three",
"label": "this is label three",
}, },
"geometry": { "geometry": {
"type": "LineString", "type": "LineString",
@ -483,11 +476,3 @@ def test_main_toolbox_toggle_all_layers(live_server, map, page):
# Should hidden again all layers # Should hidden again all layers
expect(page.locator(".datalayer.off")).to_have_count(3) expect(page.locator(".datalayer.off")).to_have_count(3)
expect(markers).to_have_count(0) expect(markers).to_have_count(0)
def test_honour_the_label_fields_settings(live_server, map, page, bootstrap, settings):
settings.UMAP_LABEL_KEYS = ["label", "name"]
page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.locator(".panel").get_by_text("this is label one")).to_be_visible()
expect(page.locator(".panel").get_by_text("this is label two")).to_be_visible()
expect(page.locator(".panel").get_by_text("this is label three")).to_be_visible()

View file

@ -1,44 +0,0 @@
import pytest
from playwright.sync_api import expect
from ..base import DataLayerFactory
pytestmark = pytest.mark.django_db
OSM_DATA = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [2.49, 48.79]},
"properties": {
"amenity": "restaurant",
"cuisine": "italian",
"name": "A Casa di Nonna",
"panoramax": "d811b398-d930-4cf8-95a2-0c29c34d9fca",
"phone": "+33 1 48 89 54 12",
"takeaway:covid19": "yes",
"wheelchair": "no",
"id": "node/1130849864",
},
"id": "AzMjk",
},
],
"_umap_options": {
"popupTemplate": "OSM",
},
}
def test_openstreetmap_popup(live_server, map, page):
DataLayerFactory(map=map, data=OSM_DATA)
page.goto(f"{live_server.url}{map.get_absolute_url()}#18/48.79/2.49")
expect(page.locator(".umap-icon-active")).to_be_hidden()
page.locator(".leaflet-marker-icon").click()
expect(page.get_by_role("heading", name="A Casa di Nonna")).to_be_visible()
expect(page.get_by_text("+33 1 48 89 54 12")).to_be_visible()
img = page.locator(".umap-popup-content img")
expect(img).to_have_attribute(
"src",
"https://api.panoramax.xyz/api/pictures/d811b398-d930-4cf8-95a2-0c29c34d9fca/sd.jpg",
)

View file

@ -49,37 +49,3 @@ def test_should_open_popup_on_click(live_server, map, page, bootstrap):
# Close popup # Close popup
page.locator("#map").click() page.locator("#map").click()
expect(line).to_have_attribute("stroke-opacity", "0.5") expect(line).to_have_attribute("stroke-opacity", "0.5")
def test_can_use_measure_on_name(live_server, map, page):
data = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {"name": "linestring"},
"geometry": {
"type": "LineString",
"coordinates": [
[11.25, 53.585984],
[10.151367, 52.975108],
],
},
},
{
"type": "Feature",
"properties": {"name": "multilinestring"},
"geometry": {
"type": "MultiLineString",
"coordinates": [[[8, 53], [13, 52]], [[12, 51], [15, 52]]],
},
},
],
}
map.settings["properties"]["labelKey"] = "{name} ({measure})"
map.settings["properties"]["onLoadPanel"] = "databrowser"
map.save()
DataLayerFactory(map=map, data=data)
page.goto(f"{live_server.url}{map.get_absolute_url()}#6/10/50")
expect(page.get_by_text("linestring (99.7 km)")).to_be_visible()
expect(page.get_by_text("multilinestring (592 km)")).to_be_visible()

View file

@ -609,7 +609,6 @@ class MapDetailMixin(SessionMixin):
"websocketEnabled": settings.WEBSOCKET_ENABLED, "websocketEnabled": settings.WEBSOCKET_ENABLED,
"websocketURI": settings.WEBSOCKET_FRONT_URI, "websocketURI": settings.WEBSOCKET_FRONT_URI,
"importers": settings.UMAP_IMPORTERS, "importers": settings.UMAP_IMPORTERS,
"defaultLabelKeys": settings.UMAP_LABEL_KEYS,
} }
created = bool(getattr(self, "object", None)) created = bool(getattr(self, "object", None))
if (created and self.object.owner) or (not created and not user.is_anonymous): if (created and self.object.owner) or (not created and not user.is_anonymous):