mirror of
https://github.com/umap-project/umap.git
synced 2025-05-06 14:31:50 +02:00
Compare commits
52 commits
ba3a1ccd33
...
0b818e8b75
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0b818e8b75 | ||
![]() |
b692cb0cd1 | ||
![]() |
1f13c6ef00 | ||
![]() |
a9b13c709d | ||
![]() |
50efbc25be | ||
![]() |
d99fe70e36 | ||
![]() |
d64cdae987 | ||
![]() |
da86020a0b | ||
![]() |
7f3726ddd1 | ||
![]() |
2c601e483d | ||
![]() |
171aba6676 | ||
![]() |
b7c77b9c9f | ||
![]() |
82853f7ade | ||
![]() |
e012d57e5c | ||
![]() |
6a0bc9443b | ||
![]() |
4949fcb4a4 | ||
![]() |
430dede0dd | ||
![]() |
d9c04c7d0a | ||
![]() |
8f7e5c7252 | ||
![]() |
c9d532508d | ||
![]() |
6221b709f4 | ||
![]() |
2b2580fa22 | ||
![]() |
26ff82e838 | ||
![]() |
8fa26a02a2 | ||
![]() |
9a900319af | ||
![]() |
e01a526935 | ||
![]() |
8569b827ca | ||
![]() |
2dab2f23b5 | ||
![]() |
f4ff1048bd | ||
![]() |
6310cde28b | ||
![]() |
666a92ec44 | ||
![]() |
2f776dab59 | ||
![]() |
995052d83e | ||
![]() |
7e53e50b9c | ||
![]() |
8e5f46eb95 | ||
![]() |
0f8ebcdf9a | ||
![]() |
177a4edc1d | ||
![]() |
31c8bf95ba | ||
![]() |
d9998efc0f | ||
![]() |
30d9e43cd4 | ||
![]() |
c29df404c8 | ||
![]() |
3f24563a05 | ||
![]() |
64f0926e2d | ||
![]() |
3aa0c8fc82 | ||
![]() |
1a88c11289 | ||
![]() |
78d6699a81 | ||
![]() |
9e2b207dfd | ||
![]() |
35b541f200 | ||
![]() |
8624209e1b | ||
![]() |
998bf87a0b | ||
![]() |
4acb20f3bb | ||
![]() |
4bd7bd7d48 |
29 changed files with 312 additions and 85 deletions
|
@ -47,7 +47,7 @@ Voici un bref passage en revu des différents imports proposés et pour finir l
|
|||
|
||||
## 1. Importer le contour d’une commune
|
||||
|
||||
Cliquez sur l’outil d’importation en bas de la barre de droite, puis descendez jusqu’au cadre « Assistants d’import ».
|
||||
Cliquez sur l’outil d’importation en bas de la barre de droite, puis cliquez sur le lien « Assistants d’import ».
|
||||
|
||||
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
|
||||
|
||||
Cliquez sur l’outil d’importation en bas de la barre de droite, puis descendez jusqu’au cadre « Assistants d’import ».
|
||||
Cliquez sur l’outil d’importation en bas de la barre de droite, puis cliquez sur le lien « Assistants d’import ».
|
||||
|
||||
Cliquez sur « Contours nationaux » puis soit départements, soit régions et enfin le type de calque (voir supra l’explication). 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 d’intérêt issu de GeoDataMine
|
||||
|
||||
Cliquez sur l’outil d’importation en bas de la barre de droite, puis descendez jusqu’au cadre « Assistants d’import ».
|
||||
Cliquez sur l’outil d’importation en bas de la barre de droite, puis cliquez sur le lien « Assistants d’import ».
|
||||
|
||||
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 d’eau potable de la CA du Grand Avignon, puis « Copier dans un calque »
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Force rtfd to use a recent version of mkdocs
|
||||
mkdocs==1.6.1
|
||||
pymdown-extensions==10.12
|
||||
mkdocs-material==9.5.44
|
||||
mkdocs-material==9.5.47
|
||||
mkdocs-static-i18n==1.2.3
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.4 MiB After Width: | Height: | Size: 734 KiB |
|
@ -175,11 +175,6 @@ UMAP_EXTRA_URLS = {
|
|||
}
|
||||
```
|
||||
|
||||
#### UMAP_KEEP_VERSIONS
|
||||
|
||||
How many datalayer versions to keep. 10 by default.
|
||||
|
||||
|
||||
#### UMAP_DEFAULT_EDIT_STATUS
|
||||
|
||||
Define the map default edit status.
|
||||
|
@ -270,6 +265,16 @@ Available importers:
|
|||
- `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`
|
||||
|
||||
#### 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
|
||||
|
||||
How many maps to show in maps list, like search or home page.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Force rtfd to use a recent version of mkdocs
|
||||
mkdocs==1.6.1
|
||||
pymdown-extensions==10.12
|
||||
mkdocs-material==9.5.44
|
||||
mkdocs-material==9.5.47
|
||||
mkdocs-static-i18n==1.2.3
|
||||
|
|
|
@ -44,10 +44,10 @@ dependencies = [
|
|||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"hatch==1.13.0",
|
||||
"ruff==0.7.4",
|
||||
"djlint==1.36.1",
|
||||
"ruff==0.8.1",
|
||||
"djlint==1.36.3",
|
||||
"mkdocs==1.6.1",
|
||||
"mkdocs-material==9.5.44",
|
||||
"mkdocs-material==9.5.47",
|
||||
"mkdocs-static-i18n==1.2.3",
|
||||
"vermin==1.6.0",
|
||||
"pymdown-extensions==10.12",
|
||||
|
@ -56,10 +56,10 @@ dev = [
|
|||
test = [
|
||||
"factory-boy==3.3.1",
|
||||
"playwright>=1.39",
|
||||
"pytest==8.3.3",
|
||||
"pytest==8.3.4",
|
||||
"pytest-django==4.9.0",
|
||||
"pytest-playwright==0.5.2",
|
||||
"pytest-rerunfailures==14.0",
|
||||
"pytest-playwright==0.6.2",
|
||||
"pytest-rerunfailures==15.0",
|
||||
"pytest-xdist>=3.5.0,<4",
|
||||
]
|
||||
docker = [
|
||||
|
@ -68,7 +68,7 @@ docker = [
|
|||
sync = [
|
||||
"channels==4.2.0",
|
||||
"daphne==4.1.2",
|
||||
"pydantic==2.9.2",
|
||||
"pydantic==2.10.2",
|
||||
"websockets==13.1",
|
||||
]
|
||||
|
||||
|
|
|
@ -273,6 +273,7 @@ UMAP_HOME_FEED = "latest"
|
|||
UMAP_IMPORTERS = {}
|
||||
UMAP_HOST_INFOS = {}
|
||||
UMAP_PURGATORY_ROOT = "/tmp/umappurgatory"
|
||||
UMAP_LABEL_KEYS = ["name", "title"]
|
||||
|
||||
UMAP_READONLY = env("UMAP_READONLY", default=False)
|
||||
UMAP_GZIP = True
|
||||
|
|
|
@ -65,7 +65,9 @@ body.login header {
|
|||
.login-grid .login-openstreetmap-oauth2 {
|
||||
background-image: url("./openstreetmap.png");
|
||||
}
|
||||
|
||||
.login-grid .login-keycloak {
|
||||
background-image: url("./keycloak.png");
|
||||
}
|
||||
|
||||
/* **************************** */
|
||||
/* home */
|
||||
|
|
|
@ -78,8 +78,7 @@ input[type="submit"] {
|
|||
border-radius: 2px;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
padding: 7px;
|
||||
width: 100%;
|
||||
padding: 7px 14px;
|
||||
min-height: 32px;
|
||||
line-height: 32px;
|
||||
border: none;
|
||||
|
@ -92,6 +91,12 @@ input[type="submit"] {
|
|||
color: var(--text-color);
|
||||
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 [type="button"]:hover,
|
||||
.dark input[type="submit"]:hover {
|
||||
|
@ -100,6 +105,11 @@ input[type="submit"] {
|
|||
.dark a {
|
||||
color: var(--text-color);
|
||||
}
|
||||
.dark [type="button"][disabled],
|
||||
.dark input[type="submit"][disabled] {
|
||||
background-color: var(--color-mediumGray);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
button.flat,
|
||||
[type="button"].flat,
|
||||
.dark [type="button"].flat {
|
||||
|
|
|
@ -165,7 +165,9 @@ class Feature {
|
|||
}
|
||||
|
||||
getSlug() {
|
||||
return this.properties[this._umap.getProperty('slugKey') || 'name'] || ''
|
||||
return (
|
||||
this.properties[this._umap.getProperty('slugKey') || U.DEFAULT_LABEL_KEY] || ''
|
||||
)
|
||||
}
|
||||
|
||||
getPermalink() {
|
||||
|
@ -234,15 +236,23 @@ class Feature {
|
|||
container.appendChild(builder.build())
|
||||
|
||||
const properties = []
|
||||
let labelKeyFound = undefined
|
||||
for (const property of this.datalayer._propertiesIndex) {
|
||||
if (['name', 'description'].includes(property)) {
|
||||
if (!labelKeyFound && U.LABEL_KEYS.includes(property)) {
|
||||
labelKeyFound = property
|
||||
continue
|
||||
}
|
||||
if (property === 'description') {
|
||||
continue
|
||||
}
|
||||
properties.push([`properties.${property}`, { label: property }])
|
||||
}
|
||||
// We always want name and description for now (properties management to come)
|
||||
properties.unshift('properties.description')
|
||||
properties.unshift('properties.name')
|
||||
if (!labelKeyFound) {
|
||||
labelKeyFound = U.DEFAULT_LABEL_KEY
|
||||
}
|
||||
properties.unshift([`properties.${labelKeyFound}`, { label: labelKeyFound }])
|
||||
builder = new U.FormBuilder(this, properties, {
|
||||
id: 'umap-feature-properties',
|
||||
})
|
||||
|
@ -255,7 +265,7 @@ class Feature {
|
|||
this.getAdvancedEditActions(advancedActions)
|
||||
const onLoad = this._umap.editPanel.open({ content: container })
|
||||
onLoad.then(() => {
|
||||
builder.helpers['properties.name'].input.focus()
|
||||
builder.helpers[`properties.${labelKeyFound}`].input.focus()
|
||||
})
|
||||
this._umap.editedFeature = this
|
||||
if (!this.ui.isOnScreen(this._umap._leafletMap.getBounds())) this.zoomTo(event)
|
||||
|
@ -316,19 +326,21 @@ class Feature {
|
|||
|
||||
endEdit() {}
|
||||
|
||||
getDisplayName(fallback) {
|
||||
const key = this.getOption('labelKey') || 'name'
|
||||
getDisplayName() {
|
||||
const keys = U.LABEL_KEYS.slice() // Copy.
|
||||
const labelKey = this.getOption('labelKey')
|
||||
// Variables mode.
|
||||
if (Utils.hasVar(key)) {
|
||||
return Utils.greedyTemplate(key, this.extendedProperties())
|
||||
if (labelKey) {
|
||||
if (Utils.hasVar(labelKey)) {
|
||||
return Utils.greedyTemplate(labelKey, this.extendedProperties())
|
||||
}
|
||||
keys.unshift(labelKey)
|
||||
}
|
||||
// Simple mode.
|
||||
return (
|
||||
this.properties[key] ||
|
||||
this.properties.title ||
|
||||
fallback ||
|
||||
this.datalayer.getName()
|
||||
)
|
||||
for (const key of keys) {
|
||||
const value = this.properties[key]
|
||||
if (value) return value
|
||||
}
|
||||
return this.datalayer.getName()
|
||||
}
|
||||
|
||||
hasPopupFooter() {
|
||||
|
|
|
@ -289,7 +289,7 @@ export class DataLayer extends ServerStored {
|
|||
|
||||
reindex() {
|
||||
const features = Object.values(this._features)
|
||||
Utils.sortFeatures(features, this._umap.getProperty('sortKey'), U.lang)
|
||||
this.sortFeatures(features)
|
||||
this._index = features.map((feature) => stamp(feature))
|
||||
}
|
||||
|
||||
|
@ -428,6 +428,10 @@ export class DataLayer extends ServerStored {
|
|||
if (idx !== -1) this._propertiesIndex.splice(idx, 1)
|
||||
}
|
||||
|
||||
allProperties() {
|
||||
return this._propertiesIndex
|
||||
}
|
||||
|
||||
sortedValues(property) {
|
||||
return Object.values(this._features)
|
||||
.map((feature) => feature.properties[property])
|
||||
|
@ -446,6 +450,11 @@ 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) {
|
||||
if (geojson.type === 'Feature' || geojson.coordinates) {
|
||||
geojson = [geojson]
|
||||
|
@ -454,7 +463,7 @@ export class DataLayer extends ServerStored {
|
|||
? geojson
|
||||
: geojson.features || geojson.geometries
|
||||
if (!collection) return
|
||||
Utils.sortFeatures(collection, this._umap.getProperty('sortKey'), U.lang)
|
||||
this.sortFeatures(collection)
|
||||
for (const feature of collection) {
|
||||
this.makeFeature(feature, sync)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { uMapAlert as Alert } from '../components/alerts/alert.js'
|
||||
import { AjaxAutocomplete, AjaxAutocompleteMultiple } from './autocomplete.js'
|
||||
import { AjaxAutocomplete, AjaxAutocompleteMultiple, AutocompleteDatalist } from './autocomplete.js'
|
||||
import Help from './help.js'
|
||||
import { ServerRequest } from './request.js'
|
||||
import { SCHEMA } from './schema.js'
|
||||
|
@ -17,6 +17,7 @@ window.U = {
|
|||
Alert,
|
||||
AjaxAutocomplete,
|
||||
AjaxAutocompleteMultiple,
|
||||
AutocompleteDatalist,
|
||||
Help,
|
||||
Icon,
|
||||
LAYER_TYPES,
|
||||
|
|
|
@ -33,15 +33,15 @@ const TEMPLATE = `
|
|||
<fieldset id="import-mode" class="formbox">
|
||||
<legend class="counter" data-help="importMode">${translate('Choose import mode')}</legend>
|
||||
<label>
|
||||
<input type="radio" name="action" value="copy" />
|
||||
<input type="radio" name="action" value="copy" checked onchange />
|
||||
${translate('Copy into the layer')}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="action" value="link" />
|
||||
<input type="radio" name="action" value="link" onchange />
|
||||
${translate('Link to the layer as remote data')}
|
||||
</label>
|
||||
</fieldset>
|
||||
<input type="button" class="button" name="submit" value="${translate('Import data')}" />
|
||||
<input type="button" class="button primary" name="submit" value="${translate('Import data')}" disabled />
|
||||
</div>
|
||||
`
|
||||
|
||||
|
@ -121,6 +121,11 @@ export default class Importer extends Utils.WithTemplate {
|
|||
return this.qs('textarea').value
|
||||
}
|
||||
|
||||
set raw(value) {
|
||||
this.qs('textarea').value = value
|
||||
this.onChange()
|
||||
}
|
||||
|
||||
get clear() {
|
||||
return Boolean(this.qs('[name=clear]').checked)
|
||||
}
|
||||
|
@ -198,6 +203,7 @@ export default class Importer extends Utils.WithTemplate {
|
|||
)
|
||||
this.qs('[name=layer-name]').toggleAttribute('hidden', Boolean(this.layerId))
|
||||
this.qs('#clear').toggleAttribute('hidden', !this.layerId)
|
||||
this.qs('[name=submit').toggleAttribute('disabled', !this.canSubmit())
|
||||
}
|
||||
|
||||
onFileChange(e) {
|
||||
|
@ -219,6 +225,7 @@ export default class Importer extends Utils.WithTemplate {
|
|||
this.url = null
|
||||
this.format = undefined
|
||||
this.layerName = null
|
||||
this.raw = null
|
||||
const layerSelect = this.qs('[name="layer-id"]')
|
||||
layerSelect.innerHTML = ''
|
||||
this._umap.eachDataLayerReverse((datalayer) => {
|
||||
|
@ -251,6 +258,17 @@ export default class Importer extends Utils.WithTemplate {
|
|||
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() {
|
||||
let hasErrors
|
||||
if (this.format === 'umap') {
|
||||
|
|
|
@ -2,6 +2,8 @@ import { DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
|
|||
import { BaseAjax, SingleMixin } from '../autocomplete.js'
|
||||
import * as Util from '../utils.js'
|
||||
import { AutocompleteCommunes } from './communesfr.js'
|
||||
import { translate } from '../i18n.js'
|
||||
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
|
||||
|
||||
const TEMPLATE = `
|
||||
<h3>Cadastre</h3>
|
||||
|
@ -56,6 +58,8 @@ export class Importer {
|
|||
.open({
|
||||
template: container,
|
||||
className: `${this.id} importer dark`,
|
||||
cancel: false,
|
||||
accept: translate('Choose this data'),
|
||||
})
|
||||
.then(confirm)
|
||||
}
|
||||
|
|
|
@ -37,8 +37,8 @@ class Autocomplete extends SingleMixin(BaseAjax) {
|
|||
}
|
||||
|
||||
export class Importer {
|
||||
constructor(map, options = {}) {
|
||||
this.map = map
|
||||
constructor(umap, options = {}) {
|
||||
this.umap = umap
|
||||
this.name = options.name || 'GeoDataMine'
|
||||
this.baseUrl = options?.url || 'https://geodatamine.fr'
|
||||
this.id = 'geodatamine'
|
||||
|
@ -49,7 +49,7 @@ export class Importer {
|
|||
let boundaryName = null
|
||||
const container = DomUtil.create('div')
|
||||
container.innerHTML = TEMPLATE
|
||||
const response = await importer.map.request.get(`${this.baseUrl}/themes`)
|
||||
const response = await this.umap.request.get(`${this.baseUrl}/themes`)
|
||||
const select = container.querySelector('select')
|
||||
if (response?.ok) {
|
||||
const { themes } = await response.json()
|
||||
|
|
|
@ -53,11 +53,15 @@ export class Importer {
|
|||
'https://photon.komoot.io/api?q={q}&layer=county&layer=city&layer=state'
|
||||
this.id = 'overpass'
|
||||
this.boundaryChoice = null
|
||||
this.expression = null
|
||||
}
|
||||
|
||||
async open(importer) {
|
||||
const container = DomUtil.create('div')
|
||||
container.innerHTML = TEMPLATE
|
||||
if (this.expression) {
|
||||
container.querySelector('[name=tags]').value = this.expression
|
||||
}
|
||||
this.autocomplete = new Autocomplete(container.querySelector('#area'), {
|
||||
url: this.searchUrl,
|
||||
placeholder: translate(
|
||||
|
@ -80,6 +84,7 @@ export class Importer {
|
|||
Alert.error(translate('Expression is empty'))
|
||||
return
|
||||
}
|
||||
this.expression = form.tags
|
||||
let tags = form.tags
|
||||
if (!tags.startsWith('[')) tags = `[${tags}]`
|
||||
let area = '{south},{west},{north},{east}'
|
||||
|
|
|
@ -44,12 +44,12 @@ const ControlsMixin = {
|
|||
new U.DrawToolbar({ map: this }).addTo(this)
|
||||
const editActions = [
|
||||
U.EditCaptionAction,
|
||||
U.EditPropertiesAction,
|
||||
U.ImportAction,
|
||||
U.EditLayersAction,
|
||||
U.ChangeTileLayerAction,
|
||||
U.UpdateExtentAction,
|
||||
U.UpdatePermsAction,
|
||||
U.ImportAction,
|
||||
U.EditPropertiesAction,
|
||||
]
|
||||
if (this.options.editMode === 'advanced') {
|
||||
new U.SettingsToolbar({ actions: editActions }).addTo(this)
|
||||
|
|
|
@ -115,7 +115,9 @@ class Table extends TitleMixin(PopupTemplate) {
|
|||
const table = document.createElement('table')
|
||||
|
||||
for (const key in feature.properties) {
|
||||
if (typeof feature.properties[key] === 'object' || key === 'name') continue
|
||||
if (typeof feature.properties[key] === 'object' || U.LABEL_KEYS.includes(key)) {
|
||||
continue
|
||||
}
|
||||
table.appendChild(this.makeRow(feature, key))
|
||||
}
|
||||
return table
|
||||
|
@ -150,6 +152,19 @@ class GeoRSSLink extends 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) {
|
||||
const props = feature.properties
|
||||
const locale = getLocale()
|
||||
|
@ -160,15 +175,6 @@ class OSM extends TitleMixin(PopupTemplate) {
|
|||
renderBody(feature) {
|
||||
const props = feature.properties
|
||||
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']
|
||||
if (street) {
|
||||
const row = DomUtil.add('address', 'address', body)
|
||||
|
@ -205,6 +211,13 @@ class OSM extends TitleMixin(PopupTemplate) {
|
|||
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
|
||||
if (id) {
|
||||
body.appendChild(
|
||||
|
|
|
@ -75,7 +75,7 @@ const FeatureMixin = {
|
|||
|
||||
resetTooltip: function () {
|
||||
if (!this.feature.hasGeom()) return
|
||||
const displayName = this.feature.getDisplayName(null)
|
||||
const displayName = this.feature.getDisplayName()
|
||||
let showLabel = this.feature.getOption('showLabel')
|
||||
const oldLabelHover = this.feature.getOption('labelHover')
|
||||
|
||||
|
@ -255,7 +255,10 @@ const PathMixin = {
|
|||
if (this._map.measureTools?.enabled()) {
|
||||
this._map._umap.tooltip.open({ content: this.getMeasure(), anchor: this })
|
||||
} else if (this._map._umap.editEnabled && !this._map._umap.editedFeature) {
|
||||
this._map._umap.tooltip.open({ content: translate('Click to edit'), anchor: this })
|
||||
this._map._umap.tooltip.open({
|
||||
content: translate('Click to edit'),
|
||||
anchor: this,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -267,7 +270,9 @@ const PathMixin = {
|
|||
this._map.once('moveend', this.makeGeometryEditable, this)
|
||||
const pointsCount = this._parts.reduce((acc, part) => acc + part.length, 0)
|
||||
if (pointsCount > 100 && this._map.getZoom() < this._map.getMaxZoom()) {
|
||||
this._map._umap.tooltip.open({ content: L._('Please zoom in to edit the geometry') })
|
||||
this._map._umap.tooltip.open({
|
||||
content: L._('Please zoom in to edit the geometry'),
|
||||
})
|
||||
this.disableEdit()
|
||||
} else {
|
||||
this.enableEdit()
|
||||
|
@ -380,8 +385,19 @@ export const LeafletPolyline = Polyline.extend({
|
|||
},
|
||||
|
||||
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)
|
||||
const length = L.GeoUtil.lineLength(this._map, shape || this._defaultShape())
|
||||
const length = shapes.reduce(
|
||||
(acc, shape) => acc + L.GeoUtil.lineLength(this._map, shape),
|
||||
0
|
||||
)
|
||||
return L.GeoUtil.readableDistance(length, this._map.measureTools.getMeasureUnit())
|
||||
},
|
||||
})
|
||||
|
|
|
@ -105,7 +105,7 @@ export default class TableEditor extends WithTemplate {
|
|||
resetProperties() {
|
||||
this.properties = this.datalayer._propertiesIndex
|
||||
if (this.properties.length === 0) {
|
||||
this.properties = ['name', 'description']
|
||||
this.properties = [U.DEFAULT_LABEL_KEY, 'description']
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -71,6 +71,10 @@ export default class Umap extends ServerStored {
|
|||
// To be used in javascript APIs
|
||||
if (geojson.properties.lang) U.lang = geojson.properties.lang
|
||||
|
||||
// Make it available to utils, without needing a reference to `Umap`.
|
||||
U.LABEL_KEYS = geojson.properties.defaultLabelKeys || []
|
||||
U.DEFAULT_LABEL_KEY = U.LABEL_KEYS[0] || 'name'
|
||||
|
||||
this.setPropertiesFromQueryString()
|
||||
|
||||
// Needed for actions labels
|
||||
|
@ -537,10 +541,10 @@ export default class Umap extends ServerStored {
|
|||
this._leafletMap.editTools.startPolyline()
|
||||
break
|
||||
case 'i':
|
||||
this._leafletMap.importer.open()
|
||||
this.importer.open()
|
||||
break
|
||||
case 'o':
|
||||
this._leafletMap.importer.openFiles()
|
||||
this.importer.openFiles()
|
||||
break
|
||||
case 'h':
|
||||
this.help.showGetStarted()
|
||||
|
@ -596,7 +600,7 @@ export default class Umap extends ServerStored {
|
|||
const panes = this._leafletMap.getPane('overlayPane')
|
||||
|
||||
this.datalayersIndex = []
|
||||
for (const pane of panes) {
|
||||
for (const pane of panes.children) {
|
||||
if (!pane.dataset || !pane.dataset.id) continue
|
||||
this.datalayersIndex.push(this.datalayers[pane.dataset.id])
|
||||
}
|
||||
|
@ -684,7 +688,7 @@ export default class Umap extends ServerStored {
|
|||
}
|
||||
|
||||
allProperties() {
|
||||
return [].concat(...this.datalayersIndex.map((dl) => dl._propertiesIndex))
|
||||
return [].concat(...this.datalayersIndex.map((dl) => dl.allProperties()))
|
||||
}
|
||||
|
||||
sortedValues(property) {
|
||||
|
@ -1425,13 +1429,13 @@ export default class Umap extends ServerStored {
|
|||
row.dataset.id = stamp(datalayer)
|
||||
})
|
||||
const onReorder = (src, dst, initialIndex, finalIndex) => {
|
||||
const layer = this.datalayers[src.dataset.id]
|
||||
const other = this.datalayers[dst.dataset.id]
|
||||
const minIndex = Math.min(layer.getRank(), other.getRank())
|
||||
const maxIndex = Math.max(layer.getRank(), other.getRank())
|
||||
if (finalIndex === 0) layer.bringToTop()
|
||||
else if (finalIndex > initialIndex) layer.insertBefore(other)
|
||||
else layer.insertAfter(other)
|
||||
const movedLayer = this.datalayers[src.dataset.id]
|
||||
const targetLayer = this.datalayers[dst.dataset.id]
|
||||
const minIndex = Math.min(movedLayer.getRank(), targetLayer.getRank())
|
||||
const maxIndex = Math.max(movedLayer.getRank(), targetLayer.getRank())
|
||||
if (finalIndex === 0) movedLayer.bringToTop()
|
||||
else if (finalIndex > initialIndex) movedLayer.insertBefore(targetLayer)
|
||||
else movedLayer.insertAfter(targetLayer)
|
||||
this.eachDataLayerReverse((datalayer) => {
|
||||
if (datalayer.getRank() >= minIndex && datalayer.getRank() <= maxIndex)
|
||||
datalayer.isDirty = true
|
||||
|
|
|
@ -292,7 +292,7 @@ export function naturalSort(a, b, lang) {
|
|||
}
|
||||
|
||||
export function sortFeatures(features, sortKey, lang) {
|
||||
const sortKeys = (sortKey || 'name').split(',')
|
||||
const sortKeys = sortKey.split(',')
|
||||
|
||||
const sort = (a, b, i) => {
|
||||
let sortKey = sortKeys[i]
|
||||
|
|
|
@ -170,22 +170,43 @@ L.DomUtil.contrastWCAG21 = (rgb) => {
|
|||
const contrast = (whiteLum + 0.05) / (lum + 0.05)
|
||||
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 = {}
|
||||
L.DomUtil.contrastedColor = (el, bgcolor) => {
|
||||
// Return 0 for black and 1 for white
|
||||
// bgcolor is a human color, it can be a any keyword (purple…)
|
||||
if (typeof _CACHE_CONSTRAST[bgcolor] !== 'undefined') return _CACHE_CONSTRAST[bgcolor]
|
||||
let out = 0
|
||||
let rgb = window.getComputedStyle(el).getPropertyValue('background-color')
|
||||
rgb = L.DomUtil.RGBRegex.exec(rgb)
|
||||
if (!rgb || rgb.length !== 4) return out
|
||||
rgb = [
|
||||
Number.parseInt(rgb[1], 10),
|
||||
Number.parseInt(rgb[2], 10),
|
||||
Number.parseInt(rgb[3], 10),
|
||||
]
|
||||
out = L.DomUtil.contrastWCAG21(rgb)
|
||||
if (rgb && rgb.length === 4) {
|
||||
rgb = [
|
||||
Number.parseInt(rgb[1], 10),
|
||||
Number.parseInt(rgb[2], 10),
|
||||
Number.parseInt(rgb[3], 10),
|
||||
]
|
||||
} else {
|
||||
// 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
|
||||
return out
|
||||
}
|
||||
|
|
|
@ -448,6 +448,17 @@ 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({
|
||||
type: () => 'hidden',
|
||||
|
||||
|
@ -1110,10 +1121,11 @@ U.FormBuilder = L.FormBuilder.extend({
|
|||
},
|
||||
|
||||
customHandlers: {
|
||||
sortKey: 'BlurInput',
|
||||
sortKey: 'PropertyInput',
|
||||
easing: 'Switch',
|
||||
facetKey: 'BlurInput',
|
||||
slugKey: 'BlurInput',
|
||||
facetKey: 'PropertyInput',
|
||||
slugKey: 'PropertyInput',
|
||||
labelKey: 'PropertyInput',
|
||||
},
|
||||
|
||||
computeDefaultOptions: function () {
|
||||
|
|
BIN
umap/static/umap/keycloak.png
Normal file
BIN
umap/static/umap/keycloak.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -15,7 +15,12 @@ DATALAYER_DATA = {
|
|||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {"name": "one point in france", "foo": "point", "bar": "one"},
|
||||
"properties": {
|
||||
"name": "one point in france",
|
||||
"foo": "point",
|
||||
"bar": "one",
|
||||
"label": "this is label one",
|
||||
},
|
||||
"geometry": {"type": "Point", "coordinates": [3.339844, 46.920255]},
|
||||
},
|
||||
{
|
||||
|
@ -24,6 +29,7 @@ DATALAYER_DATA = {
|
|||
"name": "one polygon in greenland",
|
||||
"foo": "polygon",
|
||||
"bar": "two",
|
||||
"label": "this is label two",
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
|
@ -44,6 +50,7 @@ DATALAYER_DATA = {
|
|||
"name": "one line in new zeland",
|
||||
"foo": "line",
|
||||
"bar": "three",
|
||||
"label": "this is label three",
|
||||
},
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
|
@ -476,3 +483,11 @@ def test_main_toolbox_toggle_all_layers(live_server, map, page):
|
|||
# Should hidden again all layers
|
||||
expect(page.locator(".datalayer.off")).to_have_count(3)
|
||||
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()
|
||||
|
|
44
umap/tests/integration/test_popup.py
Normal file
44
umap/tests/integration/test_popup.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
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",
|
||||
)
|
|
@ -49,3 +49,37 @@ def test_should_open_popup_on_click(live_server, map, page, bootstrap):
|
|||
# Close popup
|
||||
page.locator("#map").click()
|
||||
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()
|
||||
|
|
|
@ -609,6 +609,7 @@ class MapDetailMixin(SessionMixin):
|
|||
"websocketEnabled": settings.WEBSOCKET_ENABLED,
|
||||
"websocketURI": settings.WEBSOCKET_FRONT_URI,
|
||||
"importers": settings.UMAP_IMPORTERS,
|
||||
"defaultLabelKeys": settings.UMAP_LABEL_KEYS,
|
||||
}
|
||||
created = bool(getattr(self, "object", None))
|
||||
if (created and self.object.owner) or (not created and not user.is_anonymous):
|
||||
|
|
Loading…
Reference in a new issue