mirror of
https://github.com/umap-project/umap.git
synced 2025-04-28 11:32:38 +02:00
Compare commits
35 commits
e3c6dcf3fa
...
2b2098410b
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2b2098410b | ||
![]() |
f01f737e7b | ||
![]() |
164b5a01bd | ||
![]() |
71c6974c3c | ||
![]() |
23f7337a98 | ||
![]() |
b4fcbd1541 | ||
![]() |
efaa765b82 | ||
![]() |
92ca581fb6 | ||
![]() |
6687cd53ba | ||
![]() |
4997ee8860 | ||
![]() |
a708316604 | ||
![]() |
71d92aa610 | ||
![]() |
41752bd0c6 | ||
![]() |
5b6138a210 | ||
![]() |
07363fa5fe | ||
![]() |
7991d6cdbe | ||
![]() |
60ac4b35f2 | ||
![]() |
bf2e9dc175 | ||
![]() |
67ed6d5b44 | ||
![]() |
fad182c5f3 | ||
![]() |
3f5d282477 | ||
![]() |
49496348d2 | ||
![]() |
f8b13639aa | ||
![]() |
4c71710641 | ||
![]() |
92d5c47844 | ||
![]() |
05493d8a48 | ||
![]() |
fd8f403a69 | ||
![]() |
a6412c9539 | ||
![]() |
fe65a5351b | ||
![]() |
2ede25f541 | ||
![]() |
b7acdcfd93 | ||
![]() |
a3d2330661 | ||
![]() |
20ae374173 | ||
![]() |
b0fe95dbd9 | ||
![]() |
169be73488 |
37 changed files with 890 additions and 567 deletions
|
@ -1,5 +1,5 @@
|
|||
# Force rtfd to use a recent version of mkdocs
|
||||
mkdocs==1.6.1
|
||||
pymdown-extensions==10.14.3
|
||||
mkdocs-material==9.6.11
|
||||
mkdocs-material==9.6.12
|
||||
mkdocs-static-i18n==1.3.0
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Force rtfd to use a recent version of mkdocs
|
||||
mkdocs==1.6.1
|
||||
pymdown-extensions==10.14.3
|
||||
mkdocs-material==9.6.11
|
||||
mkdocs-material==9.6.12
|
||||
mkdocs-static-i18n==1.3.0
|
||||
|
|
|
@ -44,10 +44,10 @@ dependencies = [
|
|||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"hatch==1.14.1",
|
||||
"ruff==0.11.4",
|
||||
"ruff==0.11.6",
|
||||
"djlint==1.36.4",
|
||||
"mkdocs==1.6.1",
|
||||
"mkdocs-material==9.6.11",
|
||||
"mkdocs-material==9.6.12",
|
||||
"mkdocs-static-i18n==1.3.0",
|
||||
"vermin==1.6.0",
|
||||
"pymdown-extensions==10.14.3",
|
||||
|
@ -62,10 +62,10 @@ test = [
|
|||
"pytest-playwright==0.7.0",
|
||||
"pytest-rerunfailures==15.0",
|
||||
"pytest-xdist>=3.5.0,<4",
|
||||
"moto[s3]==5.1.3"
|
||||
"moto[s3]==5.1.4"
|
||||
]
|
||||
docker = [
|
||||
"uvicorn==0.34.1",
|
||||
"uvicorn==0.34.2",
|
||||
]
|
||||
s3 = [
|
||||
"django-storages[s3]==1.14.6",
|
||||
|
|
|
@ -91,7 +91,7 @@ class MapSettingsForm(forms.ModelForm):
|
|||
return self.cleaned_data["center"]
|
||||
|
||||
class Meta:
|
||||
fields = ("settings", "name", "center", "slug", "tags")
|
||||
fields = ("settings", "name", "center", "slug", "tags", "is_template")
|
||||
model = Map
|
||||
|
||||
|
||||
|
|
|
@ -1,10 +1,37 @@
|
|||
from django.db.models import Manager
|
||||
from django.db import models
|
||||
|
||||
|
||||
class PublicManager(Manager):
|
||||
class PublicManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super(PublicManager, self)
|
||||
.get_queryset()
|
||||
.filter(share_status=self.model.PUBLIC)
|
||||
)
|
||||
|
||||
def starred_by_staff(self):
|
||||
from .models import Star, User
|
||||
|
||||
staff = User.objects.filter(is_staff=True)
|
||||
stars = Star.objects.filter(by__in=staff).values("map")
|
||||
return self.get_queryset().filter(pk__in=stars)
|
||||
|
||||
|
||||
class PrivateQuerySet(models.QuerySet):
|
||||
def for_user(self, user):
|
||||
qs = self.exclude(share_status__in=[self.model.DELETED, self.model.BLOCKED])
|
||||
teams = user.teams.all()
|
||||
qs = (
|
||||
qs.filter(owner=user)
|
||||
.union(qs.filter(editors=user))
|
||||
.union(qs.filter(team__in=teams))
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class PrivateManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return PrivateQuerySet(self.model, using=self._db)
|
||||
|
||||
def for_user(self, user):
|
||||
return self.get_queryset().for_user(user)
|
||||
|
|
21
umap/migrations/0028_map_is_template.py
Normal file
21
umap/migrations/0028_map_is_template.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 5.1.7 on 2025-04-17 09:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("umap", "0027_map_tags"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="map",
|
||||
name="is_template",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="This map is a template map.",
|
||||
verbose_name="save as template",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -12,7 +12,7 @@ from django.urls import reverse
|
|||
from django.utils.functional import classproperty
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .managers import PublicManager
|
||||
from .managers import PrivateManager, PublicManager
|
||||
from .utils import _urls_for_js
|
||||
|
||||
|
||||
|
@ -238,9 +238,15 @@ class Map(NamedModel):
|
|||
blank=True, null=True, verbose_name=_("settings"), default=dict
|
||||
)
|
||||
tags = ArrayField(models.CharField(max_length=200), blank=True, default=list)
|
||||
is_template = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("save as template"),
|
||||
help_text=_("This map is a template map."),
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
public = PublicManager()
|
||||
private = PrivateManager()
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
|
@ -289,14 +295,17 @@ class Map(NamedModel):
|
|||
datalayer.delete()
|
||||
return super().delete(**kwargs)
|
||||
|
||||
def generate_umapjson(self, request):
|
||||
def generate_umapjson(self, request, include_data=True):
|
||||
umapjson = self.settings
|
||||
umapjson["type"] = "umap"
|
||||
umapjson["properties"].pop("is_template", None)
|
||||
umapjson["uri"] = request.build_absolute_uri(self.get_absolute_url())
|
||||
datalayers = []
|
||||
for datalayer in self.datalayers:
|
||||
with datalayer.geojson.open("rb") as f:
|
||||
layer = json.loads(f.read())
|
||||
layer = {}
|
||||
if include_data:
|
||||
with datalayer.geojson.open("rb") as f:
|
||||
layer = json.loads(f.read())
|
||||
if datalayer.settings:
|
||||
datalayer.settings.pop("id", None)
|
||||
layer["_umap_options"] = datalayer.settings
|
||||
|
|
|
@ -165,7 +165,6 @@ h2.tabs a:hover {
|
|||
min-height: var(--map-fragment-height);
|
||||
}
|
||||
.tag-list {
|
||||
margin-top: var(--text-margin);
|
||||
margin-bottom: var(--text-margin);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -205,6 +204,7 @@ h2.tabs a:hover {
|
|||
margin-bottom: 0;
|
||||
flex-grow: 1;
|
||||
gap: var(--gutter);
|
||||
margin-top: var(--text-margin);
|
||||
}
|
||||
.card h3 {
|
||||
margin-bottom: 0;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
margin-top: 100px;
|
||||
width: var(--dialog-width);
|
||||
max-width: 100vw;
|
||||
max-height: 50vh;
|
||||
max-height: 80vh;
|
||||
padding: 20px;
|
||||
border: 1px solid #222;
|
||||
background-color: var(--background-color);
|
||||
|
@ -12,11 +12,14 @@
|
|||
border-radius: 5px;
|
||||
overflow-y: auto;
|
||||
height: fit-content;
|
||||
max-height: 90vh;
|
||||
}
|
||||
.umap-dialog ul + h4 {
|
||||
margin-top: var(--box-margin);
|
||||
}
|
||||
.umap-dialog .body {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
:where([data-component="no-dialog"]:not([hidden])) {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -725,7 +725,7 @@ Fields.IconUrl = class extends Fields.BlurInput {
|
|||
<button class="flat tab-url" data-ref=url>${translate('URL')}</button>
|
||||
</div>
|
||||
`)
|
||||
this.tabs.appendChild(root)
|
||||
;[recent, symbols, chars, url].forEach((node) => this.tabs.appendChild(node))
|
||||
if (Icon.RECENT.length) {
|
||||
recent.addEventListener('click', (event) => {
|
||||
event.stopPropagation()
|
||||
|
|
|
@ -97,6 +97,9 @@ export default class Importer extends Utils.WithTemplate {
|
|||
case 'banfr':
|
||||
import('./importers/banfr.js').then(register)
|
||||
break
|
||||
case 'templates':
|
||||
import('./importers/templates.js').then(register)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
95
umap/static/umap/js/modules/importers/templates.js
Normal file
95
umap/static/umap/js/modules/importers/templates.js
Normal file
|
@ -0,0 +1,95 @@
|
|||
import { DomEvent, DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
|
||||
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
|
||||
import { BaseAjax, SingleMixin } from '../autocomplete.js'
|
||||
import { translate } from '../i18n.js'
|
||||
import * as Utils from '../utils.js'
|
||||
|
||||
const TEMPLATE = `
|
||||
<div>
|
||||
<h3>${translate('Load map template')}</h3>
|
||||
<p>${translate('Use a template to initialize your map')}.</p>
|
||||
<div class="formbox">
|
||||
<div class="flat-tabs" data-ref="tabs">
|
||||
<button type="button" class="flat" data-value="mine" data-ref="mine">${translate('My templates')}</button>
|
||||
<button type="button" class="flat" data-value="staff">${translate('Staff templates')}</button>
|
||||
<button type="button" class="flat" data-value="community">${translate('Community templates')}</button>
|
||||
</div>
|
||||
<div data-ref="body" class="body"></div>
|
||||
<label>
|
||||
<input type="checkbox" name="include_data" />
|
||||
${translate('Include template data, if any')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
export class Importer {
|
||||
constructor(umap, options = {}) {
|
||||
this.umap = umap
|
||||
this.name = options.name || 'Templates'
|
||||
this.id = 'templates'
|
||||
}
|
||||
|
||||
async open(importer) {
|
||||
const [root, { tabs, include_data, body, mine }] =
|
||||
Utils.loadTemplateWithRefs(TEMPLATE)
|
||||
const uri = this.umap.urls.get('template_list')
|
||||
const userIsAuth = Boolean(this.umap.properties.user?.id)
|
||||
const defaultTab = userIsAuth ? 'mine' : 'staff'
|
||||
mine.hidden = !userIsAuth
|
||||
|
||||
const loadTemplates = async (source) => {
|
||||
const [data, response, error] = await this.umap.server.get(
|
||||
`${uri}?source=${source}`
|
||||
)
|
||||
if (!error) {
|
||||
body.innerHTML = ''
|
||||
for (const template of data.templates) {
|
||||
const item = Utils.loadTemplate(
|
||||
`<dl>
|
||||
<dt><label><input type="radio" value="${template.id}" name="template" />${template.name}</label></dt>
|
||||
<dd>${template.description} <a href="${template.url}" target="_blank">${translate('Open')}</a></dd>
|
||||
</dl>`
|
||||
)
|
||||
body.appendChild(item)
|
||||
}
|
||||
tabs.querySelectorAll('button').forEach((el) => el.classList.remove('on'))
|
||||
tabs.querySelector(`[data-value="${source}"]`).classList.add('on')
|
||||
} else {
|
||||
console.error(response)
|
||||
}
|
||||
}
|
||||
loadTemplates(defaultTab)
|
||||
tabs
|
||||
.querySelectorAll('button')
|
||||
.forEach((el) =>
|
||||
el.addEventListener('click', () => loadTemplates(el.dataset.value))
|
||||
)
|
||||
const confirm = (form) => {
|
||||
console.log(form)
|
||||
if (!form.template) {
|
||||
Alert.error(translate('You must select a template.'))
|
||||
return false
|
||||
}
|
||||
let url = this.umap.urls.get('map_download', {
|
||||
map_id: form.template,
|
||||
})
|
||||
if (!form.include_data) {
|
||||
url = `${url}?include_data=0`
|
||||
}
|
||||
importer.url = url
|
||||
importer.format = 'umap'
|
||||
importer.submit()
|
||||
this.umap.editPanel.close()
|
||||
}
|
||||
|
||||
importer.dialog
|
||||
.open({
|
||||
template: root,
|
||||
className: `${this.id} importer dark`,
|
||||
accept: translate('Use this template'),
|
||||
cancel: false,
|
||||
})
|
||||
.then(confirm)
|
||||
}
|
||||
}
|
251
umap/static/umap/js/modules/rendering/controls.js
vendored
Normal file
251
umap/static/umap/js/modules/rendering/controls.js
vendored
Normal 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
|
||||
},
|
||||
})
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -253,6 +253,12 @@ export const SCHEMA = {
|
|||
inheritable: true,
|
||||
default: true,
|
||||
},
|
||||
is_template: {
|
||||
type: Boolean,
|
||||
impacts: ['ui'],
|
||||
label: translate('This map is a template'),
|
||||
default: false,
|
||||
},
|
||||
labelDirection: {
|
||||
type: String,
|
||||
impacts: ['data'],
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -37,6 +37,7 @@ const TOP_BAR_TEMPLATE = `
|
|||
<i class="icon icon-16 icon-save-disabled"></i>
|
||||
<span hidden data-ref="saveLabel">${translate('Save')}</span>
|
||||
<span hidden data-ref="saveDraftLabel">${translate('Save draft')}</span>
|
||||
<span hidden data-ref="saveTemplateLabel">${translate('Save template')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>`
|
||||
|
@ -167,8 +168,11 @@ export class TopBar extends WithTemplate {
|
|||
const syncEnabled = this._umap.getProperty('syncEnabled')
|
||||
this.elements.peers.hidden = !syncEnabled
|
||||
this.elements.view.disabled = this._umap.sync._undoManager.isDirty()
|
||||
this.elements.saveLabel.hidden = this._umap.permissions.isDraft()
|
||||
this.elements.saveDraftLabel.hidden = !this._umap.permissions.isDraft()
|
||||
const isDraft = this._umap.permissions.isDraft()
|
||||
const isTemplate = this._umap.getProperty('is_template')
|
||||
this.elements.saveLabel.hidden = isDraft || isTemplate
|
||||
this.elements.saveDraftLabel.hidden = !isDraft || isTemplate
|
||||
this.elements.saveTemplateLabel.hidden = !isTemplate
|
||||
this._umap.sync._undoManager.toggleState()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 +
|
||||
|
|
|
@ -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
|
||||
|
@ -763,7 +768,11 @@ export default class Umap {
|
|||
if (!this.editEnabled) return
|
||||
if (this.properties.editMode !== 'advanced') return
|
||||
const container = DomUtil.create('div')
|
||||
const metadataFields = ['properties.name', 'properties.description']
|
||||
const metadataFields = [
|
||||
'properties.name',
|
||||
'properties.description',
|
||||
'properties.is_template',
|
||||
]
|
||||
|
||||
DomUtil.createTitle(container, translate('Edit map details'), 'icon-caption')
|
||||
const builder = new MutatingForm(this, metadataFields, {
|
||||
|
@ -1177,7 +1186,7 @@ export default class Umap {
|
|||
}
|
||||
this._advancedActions(container)
|
||||
|
||||
this.editPanel.open({
|
||||
return this.editPanel.open({
|
||||
content: container,
|
||||
className: 'dark',
|
||||
highlight: 'settings',
|
||||
|
@ -1192,6 +1201,7 @@ export default class Umap {
|
|||
}
|
||||
const formData = new FormData()
|
||||
formData.append('name', this.properties.name)
|
||||
formData.append('is_template', Boolean(this.properties.is_template))
|
||||
formData.append('center', JSON.stringify(this.geometry()))
|
||||
formData.append('tags', this.properties.tags || [])
|
||||
formData.append('settings', JSON.stringify(geojson))
|
||||
|
@ -1508,12 +1518,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 +1539,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]
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) &&
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -301,6 +301,15 @@
|
|||
</form>
|
||||
</fieldset>
|
||||
</details>
|
||||
<details open>
|
||||
<summary>With tabs</summary>
|
||||
<div class="flat-tabs" data-ref="tabs">
|
||||
<button class="flat on" data-ref="recent">Récents</button>
|
||||
<button class="flat" data-ref="symbols">Symbole</button>
|
||||
<button class="flat" data-ref="chars">Emoji & texte</button>
|
||||
<button class="flat" data-ref="url">URL</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<h4>Importers</h4>
|
||||
<div class="umap-dialog window importers dark">
|
||||
|
|
|
@ -12,14 +12,14 @@
|
|||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<h3>{{ map_inst.name }}</h3>
|
||||
<h3>{% if map_inst.is_template %}<mark class="template-map">[{% trans "template" %}]</mark>{% endif %} {{ map_inst.name }}</h3>
|
||||
{% with author=map_inst.get_author %}
|
||||
{% if author %}
|
||||
<p>{% trans "by" %} <a href="{{ author.get_url }}">{{ author }}</a></p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<a class="main" href="{{ map_inst.get_absolute_url }}">{% translate "See the map" %}</a>
|
||||
<a class="main" href="{{ map_inst.get_absolute_url }}">{% if map_inst.is_template %}{% translate "See the template" %}{% else %}{% translate "See the map" %}{% endif %}</a>
|
||||
</hgroup>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -528,3 +528,62 @@ def test_can_find_small_usernames(client):
|
|||
data = json.loads(response.content)["data"]
|
||||
assert len(data) == 1
|
||||
assert data[0]["label"] == "JoeJoe"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_templates_list(client, user, user2):
|
||||
public = MapFactory(
|
||||
owner=user,
|
||||
name="A public template",
|
||||
share_status=Map.PUBLIC,
|
||||
is_template=True,
|
||||
)
|
||||
link_only = MapFactory(
|
||||
owner=user,
|
||||
name="A link-only template",
|
||||
share_status=Map.OPEN,
|
||||
is_template=True,
|
||||
)
|
||||
private = MapFactory(
|
||||
owner=user,
|
||||
name="A link-only template",
|
||||
share_status=Map.PRIVATE,
|
||||
is_template=True,
|
||||
)
|
||||
someone_else = MapFactory(
|
||||
owner=user2,
|
||||
name="A public template from someone else",
|
||||
share_status=Map.PUBLIC,
|
||||
is_template=True,
|
||||
)
|
||||
staff = UserFactory(username="Staff", is_staff=True)
|
||||
Star.objects.create(by=staff, map=someone_else)
|
||||
client.login(username=user.username, password="123123")
|
||||
url = reverse("template_list")
|
||||
|
||||
# Ask for mine
|
||||
response = client.get(f"{url}?source=mine")
|
||||
templates = json.loads(response.content)["templates"]
|
||||
ids = [t["id"] for t in templates]
|
||||
assert public.pk in ids
|
||||
assert link_only.pk in ids
|
||||
assert private.pk in ids
|
||||
assert someone_else.pk not in ids
|
||||
|
||||
# Ask for staff ones
|
||||
response = client.get(f"{url}?source=staff")
|
||||
templates = json.loads(response.content)["templates"]
|
||||
ids = [t["id"] for t in templates]
|
||||
assert public.pk not in ids
|
||||
assert link_only.pk not in ids
|
||||
assert private.pk not in ids
|
||||
assert someone_else.pk in ids
|
||||
|
||||
# Ask for community ones
|
||||
response = client.get(f"{url}?source=community")
|
||||
templates = json.loads(response.content)["templates"]
|
||||
ids = [t["id"] for t in templates]
|
||||
assert public.pk in ids
|
||||
assert link_only.pk not in ids
|
||||
assert private.pk not in ids
|
||||
assert someone_else.pk in ids
|
||||
|
|
|
@ -73,6 +73,11 @@ i18n_urls = [
|
|||
views.PictogramJSONList.as_view(),
|
||||
name="pictogram_list_json",
|
||||
),
|
||||
re_path(
|
||||
r"^templates/json/$",
|
||||
views.TemplateList.as_view(),
|
||||
name="template_list",
|
||||
),
|
||||
]
|
||||
i18n_urls += decorated_patterns(
|
||||
[can_view_map, cache_control(must_revalidate=True)],
|
||||
|
|
|
@ -138,9 +138,7 @@ class PublicMapsMixin(object):
|
|||
return maps
|
||||
|
||||
def get_highlighted_maps(self):
|
||||
staff = User.objects.filter(is_staff=True)
|
||||
stars = Star.objects.filter(by__in=staff).values("map")
|
||||
qs = Map.public.filter(pk__in=stars)
|
||||
qs = Map.public.starred_by_staff()
|
||||
maps = qs.order_by("-modified_at")
|
||||
return maps
|
||||
|
||||
|
@ -332,10 +330,10 @@ class TeamMaps(PaginatorMixin, DetailView):
|
|||
|
||||
|
||||
class SearchMixin:
|
||||
def get_search_queryset(self, **kwargs):
|
||||
def get_search_queryset(self, qs=None, **kwargs):
|
||||
q = self.request.GET.get("q")
|
||||
tags = [t for t in self.request.GET.getlist("tags") if t]
|
||||
qs = Map.objects.all()
|
||||
qs = qs or Map.public.all()
|
||||
if q:
|
||||
vector = SearchVector("name", config=settings.UMAP_SEARCH_CONFIGURATION)
|
||||
query = SearchQuery(
|
||||
|
@ -382,14 +380,8 @@ class UserDashboard(PaginatorMixin, DetailView, SearchMixin):
|
|||
return self.get_queryset().get(pk=self.request.user.pk)
|
||||
|
||||
def get_maps(self):
|
||||
qs = self.get_search_queryset() or Map.objects.all()
|
||||
qs = qs.exclude(share_status__in=[Map.DELETED, Map.BLOCKED])
|
||||
teams = self.object.teams.all()
|
||||
qs = (
|
||||
qs.filter(owner=self.object)
|
||||
.union(qs.filter(editors=self.object))
|
||||
.union(qs.filter(team__in=teams))
|
||||
)
|
||||
qs = Map.private.for_user(self.object)
|
||||
qs = self.get_search_queryset(qs) or qs
|
||||
return qs.order_by("-modified_at")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -408,9 +400,9 @@ class UserDownload(DetailView, SearchMixin):
|
|||
return self.get_queryset().get(pk=self.request.user.pk)
|
||||
|
||||
def get_maps(self):
|
||||
qs = Map.objects.filter(id__in=self.request.GET.getlist("map_id"))
|
||||
qs = qs.filter(owner=self.object).union(qs.filter(editors=self.object))
|
||||
return qs.order_by("-modified_at")
|
||||
qs = Map.private.filter(id__in=self.request.GET.getlist("map_id"))
|
||||
qsu = qs.for_user(self.object)
|
||||
return qsu.order_by("-modified_at")
|
||||
|
||||
def render_to_response(self, context, *args, **kwargs):
|
||||
zip_buffer = io.BytesIO()
|
||||
|
@ -802,7 +794,10 @@ class MapDownload(DetailView):
|
|||
return reverse("map_download", args=(self.object.pk,))
|
||||
|
||||
def render_to_response(self, context, *args, **kwargs):
|
||||
umapjson = self.object.generate_umapjson(self.request)
|
||||
include_data = self.request.GET.get("include_data") != "0"
|
||||
umapjson = self.object.generate_umapjson(
|
||||
self.request, include_data=include_data
|
||||
)
|
||||
response = simple_json_response(**umapjson)
|
||||
response["Content-Disposition"] = (
|
||||
f'attachment; filename="umap_backup_{self.object.slug}.umap"'
|
||||
|
@ -1456,3 +1451,26 @@ class LoginPopupEnd(TemplateView):
|
|||
if backend in settings.DEPRECATED_AUTHENTICATION_BACKENDS:
|
||||
return HttpResponseRedirect(reverse("user_profile"))
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
|
||||
class TemplateList(ListView):
|
||||
model = Map
|
||||
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
source = self.request.GET.get("source")
|
||||
if source == "mine":
|
||||
qs = Map.private.filter(is_template=True).for_user(self.request.user)
|
||||
elif source == "community":
|
||||
qs = Map.public.filter(is_template=True)
|
||||
elif source == "staff":
|
||||
qs = Map.public.starred_by_staff().filter(is_template=True)
|
||||
templates = [
|
||||
{
|
||||
"id": m.id,
|
||||
"name": m.name,
|
||||
"description": m.description,
|
||||
"url": m.get_absolute_url(),
|
||||
}
|
||||
for m in qs.order_by("-modified_at")
|
||||
]
|
||||
return simple_json_response(templates=templates)
|
||||
|
|
Loading…
Reference in a new issue