0005-maps-define-tiles-layers-with-opacity-22639.patch
combo/apps/maps/forms.py | ||
---|---|---|
73 | 73 |
class MapLayerOptionsForm(forms.ModelForm): |
74 | 74 |
class Meta: |
75 | 75 |
model = MapLayerOptions |
76 |
fields = ['map_layer'] |
|
76 |
fields = ['map_layer', 'opacity'] |
|
77 |
widgets = { |
|
78 |
'opacity': forms.NumberInput(attrs={'step': 0.1, 'min': 0, 'max': 1}) |
|
79 |
} |
|
77 | 80 | |
78 | 81 |
def __init__(self, *args, **kwargs): |
79 | 82 |
self.kind = kwargs.pop('kind') |
80 | 83 |
super(MapLayerOptionsForm, self).__init__(*args, **kwargs) |
84 |
# if edition, no possibility to change the layer |
|
85 |
if self.instance.pk: |
|
86 |
del self.fields['map_layer'] |
|
87 |
else: |
|
88 |
if self.kind == 'geojson': |
|
89 |
self.fields['map_layer'].queryset = self.instance.map_cell.get_free_geojson_layers() |
|
90 |
else: |
|
91 |
self.fields['map_layer'].queryset = self.instance.map_cell.get_free_tiles_layers() |
|
92 |
# init opacity field only for tiles layers |
|
81 | 93 |
if self.kind == 'geojson': |
82 |
self.fields['map_layer'].queryset = self.instance.map_cell.get_free_geojson_layers()
|
|
94 |
del self.fields['opacity']
|
|
83 | 95 |
else: |
84 |
self.fields['map_layer'].queryset = self.instance.map_cell.get_free_tiles_layers() |
|
96 |
self.fields['opacity'].required = True |
|
97 |
self.fields['opacity'].initial = 1 |
combo/apps/maps/manager_views.py | ||
---|---|---|
94 | 94 |
map_cell_add_layer = MapCellAddLayer.as_view() |
95 | 95 | |
96 | 96 | |
97 |
class MapCellEditLayer(UpdateView): |
|
98 |
form_class = MapLayerOptionsForm |
|
99 |
template_name = 'maps/layer_options_form.html' |
|
100 | ||
101 |
def dispatch(self, request, *args, **kwargs): |
|
102 |
try: |
|
103 |
self.cell = CellBase.get_cell(kwargs['cell_reference'], page=kwargs['page_pk']) |
|
104 |
except Map.DoesNotExist: |
|
105 |
raise Http404 |
|
106 |
self.object = get_object_or_404( |
|
107 |
MapLayerOptions, |
|
108 |
pk=kwargs['layeroptions_pk'], |
|
109 |
map_cell=self.cell) |
|
110 |
return super(MapCellEditLayer, self).dispatch(request, *args, **kwargs) |
|
111 | ||
112 |
def get_object(self, *args, **kwargs): |
|
113 |
return self.object |
|
114 | ||
115 |
def get_form_kwargs(self): |
|
116 |
kwargs = super(MapCellEditLayer, self).get_form_kwargs() |
|
117 |
kwargs['kind'] = self.object.map_layer.kind |
|
118 |
return kwargs |
|
119 | ||
120 |
def form_valid(self, form): |
|
121 |
PageSnapshot.take( |
|
122 |
self.cell.page, |
|
123 |
request=self.request, |
|
124 |
comment=_('changed options of layer "%s" in cell "%s"') % (form.instance.map_layer, self.cell)) |
|
125 |
return super(MapCellEditLayer, self).form_valid(form) |
|
126 | ||
127 |
def get_success_url(self): |
|
128 |
return '%s#cell-%s' % ( |
|
129 |
reverse('combo-manager-page-view', kwargs={'pk': self.kwargs.get('page_pk')}), |
|
130 |
self.kwargs['cell_reference']) |
|
131 | ||
132 | ||
133 |
map_cell_edit_layer = MapCellEditLayer.as_view() |
|
134 | ||
135 | ||
97 | 136 |
class MapCellDeleteLayer(DeleteView): |
98 | 137 |
template_name = 'combo/generic_confirm_delete.html' |
99 | 138 |
combo/apps/maps/migrations/0010_map_layer_opacity.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
import django.core.validators |
|
5 |
from django.db import migrations, models |
|
6 | ||
7 | ||
8 |
class Migration(migrations.Migration): |
|
9 | ||
10 |
dependencies = [ |
|
11 |
('maps', '0009_map_layer_kind'), |
|
12 |
] |
|
13 | ||
14 |
operations = [ |
|
15 |
migrations.AddField( |
|
16 |
model_name='maplayeroptions', |
|
17 |
name='opacity', |
|
18 |
field=models.FloatField(null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), |
|
19 |
), |
|
20 |
] |
combo/apps/maps/models.py | ||
---|---|---|
17 | 17 |
import json |
18 | 18 | |
19 | 19 |
from django.core import serializers |
20 |
from django.core import validators |
|
20 | 21 |
from django.db import models |
21 | 22 |
from django.utils import six |
22 | 23 |
from django.utils.encoding import python_2_unicode_compatible |
... | ... | |
333 | 334 |
def is_enabled(cls): |
334 | 335 |
return MapLayer.objects.exists() |
335 | 336 | |
337 |
def get_tiles_layers(self): |
|
338 |
tiles_layers = [] |
|
339 |
options_qs = ( |
|
340 |
self.maplayeroptions_set |
|
341 |
.filter(map_layer__kind='tiles') |
|
342 |
.select_related('map_layer') |
|
343 |
.order_by('-opacity')) |
|
344 |
for options in options_qs: |
|
345 |
tiles_layers.append({ |
|
346 |
'tile_urltemplate': options.map_layer.tiles_template_url, |
|
347 |
'map_attribution': options.map_layer.tiles_attribution, |
|
348 |
'opacity': options.opacity or 0, |
|
349 |
}) |
|
350 |
# check if at least one layer with opacity set to 1 exists |
|
351 |
if any([l['opacity'] == 1 for l in tiles_layers]): |
|
352 |
return tiles_layers |
|
353 |
# add the default tiles layer |
|
354 |
default_tiles_layer = MapLayer.get_default_tiles_layer() |
|
355 |
if default_tiles_layer is not None: |
|
356 |
tiles_layers.insert(0, { |
|
357 |
'tile_urltemplate': default_tiles_layer.tiles_template_url, |
|
358 |
'map_attribution': default_tiles_layer.tiles_attribution, |
|
359 |
'opacity': 1, |
|
360 |
}) |
|
361 |
else: |
|
362 |
tiles_layers.insert(0, { |
|
363 |
'tile_urltemplate': settings.COMBO_MAP_TILE_URLTEMPLATE, |
|
364 |
'map_attribution': settings.COMBO_MAP_ATTRIBUTION, |
|
365 |
'opacity': 1, |
|
366 |
}) |
|
367 |
return tiles_layers |
|
368 | ||
336 | 369 |
def get_cell_extra_context(self, context): |
337 | 370 |
ctx = super(Map, self).get_cell_extra_context(context) |
338 | 371 |
ctx['title'] = self.title |
... | ... | |
344 | 377 |
ctx['min_zoom'] = self.min_zoom |
345 | 378 |
ctx['max_zoom'] = self.max_zoom |
346 | 379 |
ctx['geojson_url'] = reverse_lazy('mapcell-geojson', kwargs={'cell_id': self.pk}) |
347 |
default_tiles_layer = MapLayer.get_default_tiles_layer() |
|
348 |
if default_tiles_layer is not None: |
|
349 |
ctx['tile_urltemplate'] = default_tiles_layer.tiles_template_url |
|
350 |
ctx['map_attribution'] = default_tiles_layer.tiles_attribution |
|
351 |
else: |
|
352 |
ctx['tile_urltemplate'] = settings.COMBO_MAP_TILE_URLTEMPLATE |
|
353 |
ctx['map_attribution'] = settings.COMBO_MAP_ATTRIBUTION |
|
380 |
ctx['tiles_layers'] = self.get_tiles_layers() |
|
354 | 381 |
ctx['max_bounds'] = settings.COMBO_MAP_MAX_BOUNDS |
355 | 382 |
ctx['group_markers'] = self.group_markers |
356 | 383 |
ctx['marker_behaviour_onclick'] = self.marker_behaviour_onclick |
... | ... | |
400 | 427 |
class MapLayerOptions(models.Model): |
401 | 428 |
map_cell = models.ForeignKey(Map, on_delete=models.CASCADE, db_column='map_id') |
402 | 429 |
map_layer = models.ForeignKey(MapLayer, verbose_name=_('Layer'), on_delete=models.CASCADE, db_column='maplayer_id') |
430 |
opacity = models.FloatField( |
|
431 |
validators=[ |
|
432 |
validators.MinValueValidator(0), |
|
433 |
validators.MaxValueValidator(1)], |
|
434 |
null=True |
|
435 |
) |
|
403 | 436 | |
404 | 437 |
class Meta: |
405 | 438 |
db_table = 'maps_map_layers' |
combo/apps/maps/static/js/combo.map.js | ||
---|---|---|
124 | 124 |
map_options.zoomControl = false; |
125 | 125 |
var latlng = [$map_widget.data('init-lat'), $map_widget.data('init-lng')]; |
126 | 126 |
var geojson_url = $map_widget.data('geojson-url'); |
127 |
var map_tile_url = $map_widget.data('tile-urltemplate'); |
|
128 |
var map_attribution = $map_widget.data('map-attribution'); |
|
129 | 127 |
if ($map_widget.data('max-bounds-lat1')) { |
130 | 128 |
map_options.maxBounds = L.latLngBounds( |
131 | 129 |
L.latLng($map_widget.data('max-bounds-lat1'), $map_widget.data('max-bounds-lng1')), |
... | ... | |
175 | 173 |
}); |
176 | 174 |
} |
177 | 175 | |
178 |
L.tileLayer(map_tile_url, |
|
179 |
{ |
|
180 |
attribution: map_attribution, |
|
181 |
maxZoom: map_options.maxZoom |
|
182 |
}).addTo(map); |
|
176 |
var map_id = $map_widget.data('cell-id'); |
|
177 |
var tiles_layers = window['tiles_'+map_id]; |
|
178 |
$.each(tiles_layers, function(idx, layer) { |
|
179 |
L.tileLayer( |
|
180 |
layer.tile_urltemplate, |
|
181 |
{ |
|
182 |
attribution: layer.map_attribution, |
|
183 |
opacity: layer.opacity, |
|
184 |
maxZoom: map_options.maxZoom |
|
185 |
} |
|
186 |
).addTo(map); |
|
187 |
}); |
|
183 | 188 |
if (geojson_url) { |
184 | 189 |
map.add_geojson_layer(function(geo_json) { |
185 | 190 |
var bounds = geo_json.getBounds(); |
combo/apps/maps/templates/maps/map_cell.html | ||
---|---|---|
6 | 6 |
data-init-zoom="{{ initial_zoom }}" data-min-zoom="{{ min_zoom }}" |
7 | 7 |
data-max-zoom="{{ max_zoom }}" data-init-lat="{{ init_lat }}" |
8 | 8 |
data-init-lng="{{ init_lng }}" data-geojson-url="{{ geojson_url }}" |
9 |
data-tile-urltemplate="{{ tile_urltemplate}}" data-map-attribution="{{ map_attribution}}" |
|
10 | 9 |
data-include-geoloc-button="true" |
11 | 10 |
{% if group_markers %}data-group-markers="1"{% endif %} |
12 | 11 |
data-marker-behaviour-onclick="{{ cell.marker_behaviour_onclick }}" |
... | ... | |
16 | 15 |
data-max-bounds-lat2="{{ max_bounds.corner2.lat }}" |
17 | 16 |
data-max-bounds-lng2="{{ max_bounds.corner2.lng }}" |
18 | 17 |
{% endif %} |
18 |
data-cell-id="{{ cell.pk }}" |
|
19 | 19 |
> |
20 | 20 |
{% endlocalize %} |
21 |
<script> |
|
22 |
var tiles_{{ cell.pk }} = []; |
|
23 |
{% for layer in tiles_layers %} |
|
24 |
tiles_{{ cell.pk }}.push({ |
|
25 |
tile_urltemplate: {{ layer.tile_urltemplate|as_json|safe }}, |
|
26 |
map_attribution: {{ layer.map_attribution|as_json|safe }}, |
|
27 |
opacity: {{ layer.opacity|as_json|safe }} |
|
28 |
}); |
|
29 |
{% endfor %} |
|
30 |
</script> |
|
21 | 31 |
</div> |
22 | 32 |
{% endblock %} |
combo/apps/maps/templates/maps/map_cell_form.html | ||
---|---|---|
11 | 11 |
{% for option in options %} |
12 | 12 |
<li> |
13 | 13 |
<span>{{ option.map_layer.label }} {% if option.map_layer.kind == 'tiles' %}({{ option.map_layer.get_kind_display }}){% endif %}</span> |
14 |
{% if option.map_layer.kind == 'tiles' %} |
|
15 |
<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> |
|
16 |
{% endif %} |
|
14 | 17 |
<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> |
15 | 18 |
</li> |
16 | 19 |
{% endfor %} |
combo/apps/maps/urls.py | ||
---|---|---|
32 | 32 |
url(r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/add-layer/(?P<kind>geojson|tiles)/$', |
33 | 33 |
manager_views.map_cell_add_layer, |
34 | 34 |
name='maps-manager-cell-add-layer'), |
35 |
url(r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/layer/(?P<layeroptions_pk>\d+)/edit/$', |
|
36 |
manager_views.map_cell_edit_layer, |
|
37 |
name='maps-manager-cell-edit-layer'), |
|
35 | 38 |
url(r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_-]+)/layer/(?P<layeroptions_pk>\d+)/delete/$', |
36 | 39 |
manager_views.map_cell_delete_layer, |
37 | 40 |
name='maps-manager-cell-delete-layer'), |
tests/test_maps_cells.py | ||
---|---|---|
137 | 137 |
page.save() |
138 | 138 |
cell = Map(page=page, placeholder='content', order=0, title='Map with points') |
139 | 139 |
cell.save() |
140 |
MapLayerOptions.objects.create(map_cell=cell, map_layer=layer) |
|
140 |
options = MapLayerOptions.objects.create(map_cell=cell, map_layer=layer)
|
|
141 | 141 |
context = {'request': RequestFactory().get('/')} |
142 | 142 |
rendered = cell.render(context) |
143 | 143 |
assert 'data-init-zoom="13"' in rendered |
... | ... | |
147 | 147 |
assert 'data-init-lng="2.3233688436448574"' in rendered |
148 | 148 |
assert 'data-geojson-url="/ajax/mapcell/geojson/1/"' in rendered |
149 | 149 |
assert 'data-group-markers="1"' not in rendered |
150 |
assert 'data-tile-urltemplate="%s"' % tiles_layer.tiles_template_url in rendered |
|
151 |
assert 'data-map-attribution="%s"' % tiles_layer.tiles_attribution in rendered |
|
152 | 150 |
resp = app.get('/test_map_cell/') |
153 | 151 |
assert 'xstatic/leaflet.js' in resp.text |
154 | 152 |
assert 'js/combo.map.js' in resp.text |
... | ... | |
160 | 158 |
rendered = cell.render(context) |
161 | 159 |
assert 'data-group-markers="1"' in rendered |
162 | 160 | |
161 | ||
162 |
def test_cell_tiles_layers(tiles_layer): |
|
163 |
page = Page.objects.create(title='xxx', slug='test_map_cell', template_name='standard') |
|
164 |
cell = Map.objects.create(page=page, placeholder='content', order=0, title='Map with points') |
|
165 | ||
166 |
# no tiles layer for this map, take default tiles layers, tiles_layer |
|
167 |
assert cell.get_tiles_layers() == [{ |
|
168 |
'tile_urltemplate': tiles_layer.tiles_template_url, |
|
169 |
'map_attribution': tiles_layer.tiles_attribution, |
|
170 |
'opacity': 1, |
|
171 |
}] |
|
172 | ||
173 |
# tiles_layer is not set as default, fallback on settings |
|
163 | 174 |
tiles_layer.tiles_default = False |
164 | 175 |
tiles_layer.save() |
165 |
rendered = cell.render(context) |
|
166 |
assert 'data-tile-urltemplate="%s"' % settings.COMBO_MAP_TILE_URLTEMPLATE in rendered |
|
167 |
assert 'data-map-attribution="%s"' % escape(settings.COMBO_MAP_ATTRIBUTION) in rendered |
|
168 | ||
169 |
tiles_layer.delete() |
|
170 |
rendered = cell.render(context) |
|
171 |
assert 'data-tile-urltemplate="%s"' % settings.COMBO_MAP_TILE_URLTEMPLATE in rendered |
|
172 |
assert 'data-map-attribution="%s"' % escape(settings.COMBO_MAP_ATTRIBUTION) in rendered |
|
176 |
assert cell.get_tiles_layers() == [{ |
|
177 |
'tile_urltemplate': settings.COMBO_MAP_TILE_URLTEMPLATE, |
|
178 |
'map_attribution': settings.COMBO_MAP_ATTRIBUTION, |
|
179 |
'opacity': 1, |
|
180 |
}] |
|
181 | ||
182 |
# add a tile layer to the map, with opacity 1 |
|
183 |
options = MapLayerOptions.objects.create(map_cell=cell, map_layer=tiles_layer, opacity=1) |
|
184 |
assert cell.get_tiles_layers() == [{ |
|
185 |
'tile_urltemplate': tiles_layer.tiles_template_url, |
|
186 |
'map_attribution': tiles_layer.tiles_attribution, |
|
187 |
'opacity': 1, |
|
188 |
}] |
|
189 | ||
190 |
# opacity is less than 1 => add default tiles layer, defined in settings |
|
191 |
options.opacity = 0.5 |
|
192 |
options.save() |
|
193 |
assert cell.get_tiles_layers() == [{ |
|
194 |
'tile_urltemplate': settings.COMBO_MAP_TILE_URLTEMPLATE, |
|
195 |
'map_attribution': settings.COMBO_MAP_ATTRIBUTION, |
|
196 |
'opacity': 1, |
|
197 |
}, { |
|
198 |
'tile_urltemplate': tiles_layer.tiles_template_url, |
|
199 |
'map_attribution': tiles_layer.tiles_attribution, |
|
200 |
'opacity': 0.5, |
|
201 |
}] |
|
202 | ||
203 |
# set tiles_layer as default => add tiles_layer |
|
204 |
tiles_layer.tiles_default = True |
|
205 |
tiles_layer.save() |
|
206 |
assert cell.get_tiles_layers() == [{ |
|
207 |
'tile_urltemplate': tiles_layer.tiles_template_url, |
|
208 |
'map_attribution': tiles_layer.tiles_attribution, |
|
209 |
'opacity': 1, |
|
210 |
}, { |
|
211 |
'tile_urltemplate': tiles_layer.tiles_template_url, |
|
212 |
'map_attribution': tiles_layer.tiles_attribution, |
|
213 |
'opacity': 0.5, |
|
214 |
}] |
|
173 | 215 | |
174 | 216 | |
175 | 217 |
def test_get_geojson_on_non_public_page(app, layer): |
tests/test_maps_manager.py | ||
---|---|---|
219 | 219 |
assert list(cell.get_free_tiles_layers()) == [tiles_layer] |
220 | 220 |
resp = resp.click(href='.*/add-layer/geojson/$') |
221 | 221 |
assert list(resp.context['form'].fields['map_layer'].queryset) == [layer] |
222 |
assert 'opacity' not in resp.context['form'].fields |
|
222 | 223 |
resp.forms[0]['map_layer'] = layer.pk |
223 | 224 |
resp = resp.forms[0].submit() |
224 | 225 |
assert resp.status_int == 302 |
... | ... | |
229 | 230 |
assert options.map_layer == layer |
230 | 231 | |
231 | 232 |
resp = resp.follow() |
233 |
assert '/layer/%s/edit/' % options.pk not in resp.text |
|
232 | 234 |
assert list(cell.get_free_geojson_layers()) == [] |
233 | 235 |
assert list(cell.get_free_tiles_layers()) == [tiles_layer] |
234 | 236 |
assert '/add-layer/geojson/' not in resp.text |
... | ... | |
244 | 246 |
resp = resp.click(href='.*/add-layer/tiles/$') |
245 | 247 |
assert list(resp.context['form'].fields['map_layer'].queryset) == [tiles_layer] |
246 | 248 |
resp.forms[0]['map_layer'] = tiles_layer.pk |
249 |
resp.forms[0]['opacity'] = 1 |
|
247 | 250 |
resp = resp.forms[0].submit() |
248 | 251 |
assert resp.status_int == 302 |
249 | 252 |
assert resp.location.endswith('/manage/pages/%s/#cell-%s' % (page.pk, cell.get_reference())) |
... | ... | |
251 | 254 |
options = MapLayerOptions.objects.get() |
252 | 255 |
assert options.map_cell == cell |
253 | 256 |
assert options.map_layer == tiles_layer |
257 |
assert options.opacity == 1 |
|
258 | ||
259 |
resp = resp.follow() |
|
260 |
resp = resp.click(href='.*/layer/%s/edit/$' % options.pk) |
|
261 |
assert 'map_layer' not in resp.context['form'].fields |
|
262 |
resp.forms[0]['opacity'] = 0.5 |
|
263 |
resp = resp.forms[0].submit() |
|
264 |
assert resp.status_int == 302 |
|
265 |
assert resp.location.endswith('/manage/pages/%s/#cell-%s' % (page.pk, cell.get_reference())) |
|
266 |
options.refresh_from_db() |
|
267 |
assert options.opacity == 0.5 |
|
254 | 268 | |
255 | 269 |
resp = resp.follow() |
256 | 270 |
assert list(cell.get_free_geojson_layers()) == [layer] |
257 |
- |