maps: define tiles layers with opacity (#22639)

This commit is contained in:
Lauréline Guérin 2020-02-11 14:58:00 +01:00
parent 9881331665
commit 9a08778110
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
10 changed files with 215 additions and 29 deletions

View File

@ -73,12 +73,25 @@ class MapLayerForm(forms.ModelForm):
class MapLayerOptionsForm(forms.ModelForm):
class Meta:
model = MapLayerOptions
fields = ['map_layer']
fields = ['map_layer', 'opacity']
widgets = {
'opacity': forms.NumberInput(attrs={'step': 0.1, 'min': 0, 'max': 1})
}
def __init__(self, *args, **kwargs):
self.kind = kwargs.pop('kind')
super(MapLayerOptionsForm, self).__init__(*args, **kwargs)
if self.kind == 'geojson':
self.fields['map_layer'].queryset = self.instance.map_cell.get_free_geojson_layers()
# if edition, no possibility to change the layer
if self.instance.pk:
del self.fields['map_layer']
else:
self.fields['map_layer'].queryset = self.instance.map_cell.get_free_tiles_layers()
if self.kind == 'geojson':
self.fields['map_layer'].queryset = self.instance.map_cell.get_free_geojson_layers()
else:
self.fields['map_layer'].queryset = self.instance.map_cell.get_free_tiles_layers()
# init opacity field only for tiles layers
if self.kind == 'geojson':
del self.fields['opacity']
else:
self.fields['opacity'].required = True
self.fields['opacity'].initial = 1

View File

@ -94,6 +94,45 @@ class MapCellAddLayer(CreateView):
map_cell_add_layer = MapCellAddLayer.as_view()
class MapCellEditLayer(UpdateView):
form_class = MapLayerOptionsForm
template_name = 'maps/layer_options_form.html'
def dispatch(self, request, *args, **kwargs):
try:
self.cell = CellBase.get_cell(kwargs['cell_reference'], page=kwargs['page_pk'])
except Map.DoesNotExist:
raise Http404
self.object = get_object_or_404(
MapLayerOptions,
pk=kwargs['layeroptions_pk'],
map_cell=self.cell)
return super(MapCellEditLayer, self).dispatch(request, *args, **kwargs)
def get_object(self, *args, **kwargs):
return self.object
def get_form_kwargs(self):
kwargs = super(MapCellEditLayer, self).get_form_kwargs()
kwargs['kind'] = self.object.map_layer.kind
return kwargs
def form_valid(self, form):
PageSnapshot.take(
self.cell.page,
request=self.request,
comment=_('changed options of layer "%s" in cell "%s"') % (form.instance.map_layer, self.cell))
return super(MapCellEditLayer, self).form_valid(form)
def get_success_url(self):
return '%s#cell-%s' % (
reverse('combo-manager-page-view', kwargs={'pk': self.kwargs.get('page_pk')}),
self.kwargs['cell_reference'])
map_cell_edit_layer = MapCellEditLayer.as_view()
class MapCellDeleteLayer(DeleteView):
template_name = 'combo/generic_confirm_delete.html'

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('maps', '0009_map_layer_kind'),
]
operations = [
migrations.AddField(
model_name='maplayeroptions',
name='opacity',
field=models.FloatField(
help_text='Float value between 0 and 1', null=True,
validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Opacity'),
),
]

View File

@ -17,6 +17,7 @@
import json
from django.core import serializers
from django.core import validators
from django.db import models
from django.utils import six
from django.utils.encoding import python_2_unicode_compatible
@ -333,6 +334,38 @@ class Map(CellBase):
def is_enabled(cls):
return MapLayer.objects.exists()
def get_tiles_layers(self):
tiles_layers = []
options_qs = (
self.maplayeroptions_set
.filter(map_layer__kind='tiles')
.select_related('map_layer')
.order_by('-opacity'))
for options in options_qs:
tiles_layers.append({
'tile_urltemplate': options.map_layer.tiles_template_url,
'map_attribution': options.map_layer.tiles_attribution,
'opacity': options.opacity or 0,
})
# check if at least one layer with opacity set to 1 exists
if any([l['opacity'] == 1 for l in tiles_layers]):
return tiles_layers
# add the default tiles layer
default_tiles_layer = MapLayer.get_default_tiles_layer()
if default_tiles_layer is not None:
tiles_layers.insert(0, {
'tile_urltemplate': default_tiles_layer.tiles_template_url,
'map_attribution': default_tiles_layer.tiles_attribution,
'opacity': 1,
})
else:
tiles_layers.insert(0, {
'tile_urltemplate': settings.COMBO_MAP_TILE_URLTEMPLATE,
'map_attribution': settings.COMBO_MAP_ATTRIBUTION,
'opacity': 1,
})
return tiles_layers
def get_cell_extra_context(self, context):
ctx = super(Map, self).get_cell_extra_context(context)
ctx['title'] = self.title
@ -344,13 +377,7 @@ class Map(CellBase):
ctx['min_zoom'] = self.min_zoom
ctx['max_zoom'] = self.max_zoom
ctx['geojson_url'] = reverse_lazy('mapcell-geojson', kwargs={'cell_id': self.pk})
default_tiles_layer = MapLayer.get_default_tiles_layer()
if default_tiles_layer is not None:
ctx['tile_urltemplate'] = default_tiles_layer.tiles_template_url
ctx['map_attribution'] = default_tiles_layer.tiles_attribution
else:
ctx['tile_urltemplate'] = settings.COMBO_MAP_TILE_URLTEMPLATE
ctx['map_attribution'] = settings.COMBO_MAP_ATTRIBUTION
ctx['tiles_layers'] = self.get_tiles_layers()
ctx['max_bounds'] = settings.COMBO_MAP_MAX_BOUNDS
ctx['group_markers'] = self.group_markers
ctx['marker_behaviour_onclick'] = self.marker_behaviour_onclick
@ -400,6 +427,14 @@ class Map(CellBase):
class MapLayerOptions(models.Model):
map_cell = models.ForeignKey(Map, on_delete=models.CASCADE, db_column='map_id')
map_layer = models.ForeignKey(MapLayer, verbose_name=_('Layer'), on_delete=models.CASCADE, db_column='maplayer_id')
opacity = models.FloatField(
verbose_name=_('Opacity'),
validators=[
validators.MinValueValidator(0),
validators.MaxValueValidator(1)],
null=True,
help_text=_('Float value between 0 and 1'),
)
class Meta:
db_table = 'maps_map_layers'

View File

@ -124,8 +124,6 @@ $(function() {
map_options.zoomControl = false;
var latlng = [$map_widget.data('init-lat'), $map_widget.data('init-lng')];
var geojson_url = $map_widget.data('geojson-url');
var map_tile_url = $map_widget.data('tile-urltemplate');
var map_attribution = $map_widget.data('map-attribution');
if ($map_widget.data('max-bounds-lat1')) {
map_options.maxBounds = L.latLngBounds(
L.latLng($map_widget.data('max-bounds-lat1'), $map_widget.data('max-bounds-lng1')),
@ -175,11 +173,18 @@ $(function() {
});
}
L.tileLayer(map_tile_url,
{
attribution: map_attribution,
maxZoom: map_options.maxZoom
}).addTo(map);
var map_id = $map_widget.data('cell-id');
var tiles_layers = window['tiles_'+map_id];
$.each(tiles_layers, function(idx, layer) {
L.tileLayer(
layer.tile_urltemplate,
{
attribution: layer.map_attribution,
opacity: layer.opacity,
maxZoom: map_options.maxZoom
}
).addTo(map);
});
if (geojson_url) {
map.add_geojson_layer(function(geo_json) {
var bounds = geo_json.getBounds();

View File

@ -6,7 +6,6 @@
data-init-zoom="{{ initial_zoom }}" data-min-zoom="{{ min_zoom }}"
data-max-zoom="{{ max_zoom }}" data-init-lat="{{ init_lat }}"
data-init-lng="{{ init_lng }}" data-geojson-url="{{ geojson_url }}"
data-tile-urltemplate="{{ tile_urltemplate}}" data-map-attribution="{{ map_attribution}}"
data-include-geoloc-button="true"
{% if group_markers %}data-group-markers="1"{% endif %}
data-marker-behaviour-onclick="{{ cell.marker_behaviour_onclick }}"
@ -16,7 +15,18 @@
data-max-bounds-lat2="{{ max_bounds.corner2.lat }}"
data-max-bounds-lng2="{{ max_bounds.corner2.lng }}"
{% endif %}
data-cell-id="{{ cell.pk }}"
>
{% endlocalize %}
<script>
var tiles_{{ cell.pk }} = [];
{% for layer in tiles_layers %}
tiles_{{ cell.pk }}.push({
tile_urltemplate: {{ layer.tile_urltemplate|as_json|safe }},
map_attribution: {{ layer.map_attribution|as_json|safe }},
opacity: {{ layer.opacity|as_json|safe }}
});
{% endfor %}
</script>
</div>
{% endblock %}

View File

@ -11,6 +11,9 @@
{% for option in options %}
<li>
<span>{{ option.map_layer.label }} {% if option.map_layer.kind == 'tiles' %}({{ option.map_layer.get_kind_display }}){% endif %}</span>
{% if option.map_layer.kind == 'tiles' %}
<a rel="popup" title="{% trans "Edit" %}" class="link-action-icon edit" href="{% url 'maps-manager-cell-edit-layer' page_pk=page.pk cell_reference=cell.get_reference layeroptions_pk=option.pk %}">{% trans "Edit" %}</a>
{% endif %}
<a rel="popup" title="{% trans "Delete" %}" class="link-action-icon delete" href="{% url 'maps-manager-cell-delete-layer' page_pk=page.pk cell_reference=cell.get_reference layeroptions_pk=option.pk %}">{% trans "Delete" %}</a>
</li>
{% endfor %}

View File

@ -32,6 +32,9 @@ maps_manager_urls = [
url(r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/add-layer/(?P<kind>geojson|tiles)/$',
manager_views.map_cell_add_layer,
name='maps-manager-cell-add-layer'),
url(r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/layer/(?P<layeroptions_pk>\d+)/edit/$',
manager_views.map_cell_edit_layer,
name='maps-manager-cell-edit-layer'),
url(r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/layer/(?P<layeroptions_pk>\d+)/delete/$',
manager_views.map_cell_delete_layer,
name='maps-manager-cell-delete-layer'),

View File

@ -137,7 +137,7 @@ def test_cell_rendering(app, layer, tiles_layer):
page.save()
cell = Map(page=page, placeholder='content', order=0, title='Map with points')
cell.save()
MapLayerOptions.objects.create(map_cell=cell, map_layer=layer)
options = MapLayerOptions.objects.create(map_cell=cell, map_layer=layer)
context = {'request': RequestFactory().get('/')}
rendered = cell.render(context)
assert 'data-init-zoom="13"' in rendered
@ -147,8 +147,6 @@ def test_cell_rendering(app, layer, tiles_layer):
assert 'data-init-lng="2.3233688436448574"' in rendered
assert 'data-geojson-url="/ajax/mapcell/geojson/1/"' in rendered
assert 'data-group-markers="1"' not in rendered
assert 'data-tile-urltemplate="%s"' % tiles_layer.tiles_template_url in rendered
assert 'data-map-attribution="%s"' % tiles_layer.tiles_attribution in rendered
resp = app.get('/test_map_cell/')
assert 'xstatic/leaflet.js' in resp.text
assert 'js/combo.map.js' in resp.text
@ -160,16 +158,60 @@ def test_cell_rendering(app, layer, tiles_layer):
rendered = cell.render(context)
assert 'data-group-markers="1"' in rendered
def test_cell_tiles_layers(tiles_layer):
page = Page.objects.create(title='xxx', slug='test_map_cell', template_name='standard')
cell = Map.objects.create(page=page, placeholder='content', order=0, title='Map with points')
# no tiles layer for this map, take default tiles layers, tiles_layer
assert cell.get_tiles_layers() == [{
'tile_urltemplate': tiles_layer.tiles_template_url,
'map_attribution': tiles_layer.tiles_attribution,
'opacity': 1,
}]
# tiles_layer is not set as default, fallback on settings
tiles_layer.tiles_default = False
tiles_layer.save()
rendered = cell.render(context)
assert 'data-tile-urltemplate="%s"' % settings.COMBO_MAP_TILE_URLTEMPLATE in rendered
assert 'data-map-attribution="%s"' % escape(settings.COMBO_MAP_ATTRIBUTION) in rendered
assert cell.get_tiles_layers() == [{
'tile_urltemplate': settings.COMBO_MAP_TILE_URLTEMPLATE,
'map_attribution': settings.COMBO_MAP_ATTRIBUTION,
'opacity': 1,
}]
tiles_layer.delete()
rendered = cell.render(context)
assert 'data-tile-urltemplate="%s"' % settings.COMBO_MAP_TILE_URLTEMPLATE in rendered
assert 'data-map-attribution="%s"' % escape(settings.COMBO_MAP_ATTRIBUTION) in rendered
# add a tile layer to the map, with opacity 1
options = MapLayerOptions.objects.create(map_cell=cell, map_layer=tiles_layer, opacity=1)
assert cell.get_tiles_layers() == [{
'tile_urltemplate': tiles_layer.tiles_template_url,
'map_attribution': tiles_layer.tiles_attribution,
'opacity': 1,
}]
# opacity is less than 1 => add default tiles layer, defined in settings
options.opacity = 0.5
options.save()
assert cell.get_tiles_layers() == [{
'tile_urltemplate': settings.COMBO_MAP_TILE_URLTEMPLATE,
'map_attribution': settings.COMBO_MAP_ATTRIBUTION,
'opacity': 1,
}, {
'tile_urltemplate': tiles_layer.tiles_template_url,
'map_attribution': tiles_layer.tiles_attribution,
'opacity': 0.5,
}]
# set tiles_layer as default => add tiles_layer
tiles_layer.tiles_default = True
tiles_layer.save()
assert cell.get_tiles_layers() == [{
'tile_urltemplate': tiles_layer.tiles_template_url,
'map_attribution': tiles_layer.tiles_attribution,
'opacity': 1,
}, {
'tile_urltemplate': tiles_layer.tiles_template_url,
'map_attribution': tiles_layer.tiles_attribution,
'opacity': 0.5,
}]
def test_get_geojson_on_non_public_page(app, layer):

View File

@ -219,6 +219,7 @@ def test_add_delete_layer(app, admin_user, layer, tiles_layer):
assert list(cell.get_free_tiles_layers()) == [tiles_layer]
resp = resp.click(href='.*/add-layer/geojson/$')
assert list(resp.context['form'].fields['map_layer'].queryset) == [layer]
assert 'opacity' not in resp.context['form'].fields
resp.forms[0]['map_layer'] = layer.pk
resp = resp.forms[0].submit()
assert resp.status_int == 302
@ -229,6 +230,7 @@ def test_add_delete_layer(app, admin_user, layer, tiles_layer):
assert options.map_layer == layer
resp = resp.follow()
assert '/layer/%s/edit/' % options.pk not in resp.text
assert list(cell.get_free_geojson_layers()) == []
assert list(cell.get_free_tiles_layers()) == [tiles_layer]
assert '/add-layer/geojson/' not in resp.text
@ -244,6 +246,7 @@ def test_add_delete_layer(app, admin_user, layer, tiles_layer):
resp = resp.click(href='.*/add-layer/tiles/$')
assert list(resp.context['form'].fields['map_layer'].queryset) == [tiles_layer]
resp.forms[0]['map_layer'] = tiles_layer.pk
resp.forms[0]['opacity'] = 1
resp = resp.forms[0].submit()
assert resp.status_int == 302
assert resp.location.endswith('/manage/pages/%s/#cell-%s' % (page.pk, cell.get_reference()))
@ -251,6 +254,17 @@ def test_add_delete_layer(app, admin_user, layer, tiles_layer):
options = MapLayerOptions.objects.get()
assert options.map_cell == cell
assert options.map_layer == tiles_layer
assert options.opacity == 1
resp = resp.follow()
resp = resp.click(href='.*/layer/%s/edit/$' % options.pk)
assert 'map_layer' not in resp.context['form'].fields
resp.forms[0]['opacity'] = 0.5
resp = resp.forms[0].submit()
assert resp.status_int == 302
assert resp.location.endswith('/manage/pages/%s/#cell-%s' % (page.pk, cell.get_reference()))
options.refresh_from_db()
assert options.opacity == 0.5
resp = resp.follow()
assert list(cell.get_free_geojson_layers()) == [layer]