From 97bfa9e3005f982f7deb80bbaa1ee61037e0f80f Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Mon, 15 May 2017 19:01:38 +0200 Subject: [PATCH 2/2] maps: map cell (#8454) --- combo/apps/maps/migrations/0002_mapcell.py | 40 ++++++++++++++ combo/apps/maps/models.py | 73 +++++++++++++++++++++++++ combo/apps/maps/static/css/map.css | 32 +++++++++++ combo/apps/maps/static/js/map.form.js | 18 ++++++ combo/apps/maps/static/js/map.js | 64 ++++++++++++++++++++++ combo/apps/maps/templates/maps/map_widget.html | 4 ++ combo/apps/maps/templates/maps/mapcell.html | 4 ++ combo/apps/maps/urls.py | 4 ++ combo/apps/maps/views.py | 21 ++++++- combo/manager/static/js/combo.manager.js | 2 + combo/manager/templates/combo/manager_base.html | 4 ++ combo/settings.py | 2 + debian/control | 1 + setup.py | 1 + 14 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 combo/apps/maps/migrations/0002_mapcell.py create mode 100644 combo/apps/maps/static/css/map.css create mode 100644 combo/apps/maps/static/js/map.form.js create mode 100644 combo/apps/maps/static/js/map.js create mode 100644 combo/apps/maps/templates/maps/map_widget.html create mode 100644 combo/apps/maps/templates/maps/mapcell.html diff --git a/combo/apps/maps/migrations/0002_mapcell.py b/combo/apps/maps/migrations/0002_mapcell.py new file mode 100644 index 0000000..7fa9d77 --- /dev/null +++ b/combo/apps/maps/migrations/0002_mapcell.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0006_require_contenttypes_0002'), + ('data', '0026_jsoncell_force_async'), + ('maps', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='MapCell', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('placeholder', models.CharField(max_length=20)), + ('order', models.PositiveIntegerField()), + ('slug', models.SlugField(verbose_name='Slug', blank=True)), + ('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)), + ('public', models.BooleanField(default=True, verbose_name='Public')), + ('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')), + ('last_update_timestamp', models.DateTimeField(auto_now=True)), + ('title', models.CharField(max_length=150, verbose_name='Title', blank=True)), + ('default_position', models.CharField(default=b'48.83369263315934;2.3233688436448574', max_length=128, null=True, verbose_name='Default position', blank=True)), + ('initial_zoom', models.CharField(default=b'13', max_length=2, verbose_name='Initial zoom level', choices=[(b'0', 'Whole world'), (b'9', 'Wide area'), (b'11', 'Area'), (b'13', 'Town'), (b'16', 'Small road'), (b'19', 'Ant')])), + ('min_zoom', models.CharField(default=b'0', max_length=2, verbose_name='Minimal zoom level', choices=[(b'0', 'Whole world'), (b'9', 'Wide area'), (b'11', 'Area'), (b'13', 'Town'), (b'16', 'Small road'), (b'19', 'Ant')])), + ('max_zoom', models.CharField(default=19, max_length=2, verbose_name='Maximal zoom level', choices=[(b'0', 'Whole world'), (b'9', 'Wide area'), (b'11', 'Area'), (b'13', 'Town'), (b'16', 'Small road'), (b'19', 'Ant')])), + ('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)), + ('layer', models.ManyToManyField(to='maps.MapLayer', verbose_name='Layers', blank=True)), + ('page', models.ForeignKey(to='data.Page')), + ], + options={ + 'verbose_name': 'Map Cell', + }, + ), + ] diff --git a/combo/apps/maps/models.py b/combo/apps/maps/models.py index 1faea6e..d9c4900 100644 --- a/combo/apps/maps/models.py +++ b/combo/apps/maps/models.py @@ -17,10 +17,33 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ +from django.core.urlresolvers import reverse_lazy +from django import forms +from django import template + +from combo.data.models import CellBase +from combo.data.library import register_cell_class + +zoom_levels = [ ('0', _('Whole world')), + ('9', _('Wide area')), + ('11', _('Area')), + ('13', _('Town')), + ('16', _('Small road')), + ('19', _('Ant')),] from combo.utils import requests +class MapWidget(forms.TextInput): + template_name = 'maps/map_widget.html' + + def render(self, name, value, attrs): + final_attrs = self.build_attrs(attrs, name=name, value=value, + type='hidden') + cell_form_template = template.loader.get_template(self.template_name) + return cell_form_template.render(final_attrs) + + class MapLayer(models.Model): label = models.CharField(_('Label'), max_length=128) geojson_url = models.URLField(_('Geojson URL')) @@ -46,3 +69,53 @@ class MapLayer(models.Model): feature['properties']['label'] = self.label feature['properties']['icon'] = self.icon return features + + +@register_cell_class +class MapCell(CellBase): + title = models.CharField(_('Title'), max_length=150, blank=True) + default_position = models.CharField(_('Default position'), null=True, blank=True, + default='48.83369263315934;2.3233688436448574', + max_length=128) + initial_zoom = models.CharField(_('Initial zoom level'), max_length=2, + choices=zoom_levels, default='13') + min_zoom = models.CharField(_('Minimal zoom level'), max_length=2, + choices=zoom_levels, default='0') + max_zoom = models.CharField(_('Maximal zoom level'), max_length=2, + choices=zoom_levels, default=19) + layer = models.ManyToManyField(MapLayer, verbose_name=_('Layers'), blank=True) + + template_name = 'maps/mapcell.html' + + class Meta: + verbose_name = _('Map Cell') + + class Media: + js = ('xstatic/leaflet.js', 'js/map.js') + css = {'all': ('xstatic/leaflet.css', 'xstatic/css/font-awesome.min.css', + 'css/map.css')} + + def get_default_form_class(self): + fields = ('title', 'default_position', 'initial_zoom', 'min_zoom', + 'max_zoom', 'layer') + lat, lng = self.default_position.split(';') + widgets = {'layer': forms.widgets.CheckboxSelectMultiple, + 'default_position': MapWidget(attrs={'init_lat': lat, + 'init_lng': lng})} + return forms.models.modelform_factory(self.__class__, fields=fields, + widgets=widgets) + + + @classmethod + def is_enabled(cls): + return MapLayer.objects.count() > 0 + + def get_cell_extra_context(self, context): + ctx = super(MapCell, self).get_cell_extra_context(context) + ctx['title'] = self.title + ctx['initial_lat'], ctx['initial_lng'] = self.default_position.split(';'); + ctx['initial_zoom'] = self.initial_zoom + ctx['min_zoom'] = self.min_zoom + ctx['max_zoom'] = self.max_zoom + ctx['geojson_url'] = reverse_lazy('cell-geojson', kwargs={'cell_id': self.pk}) + return ctx diff --git a/combo/apps/maps/static/css/map.css b/combo/apps/maps/static/css/map.css new file mode 100644 index 0000000..5c1013d --- /dev/null +++ b/combo/apps/maps/static/css/map.css @@ -0,0 +1,32 @@ +div#mapcell { + height: 60vh; +} + +/* leaflet styles */ + +div.leaflet-div-icon span { + width: 2.3rem; + height: 2.3rem; + display: block; + left: -1rem; + top: -1rem; + position: relative; + border-radius: 11rem 6rem 0.8rem; + transform: scale(1, 1.3) rotate(45deg); + border: 1px solid #aaa; +} + +div.leaflet-popup-content span { + display: block; +} + +div.leaflet-popup-content span.field-label { + font-weight: bold; + text-transform: capitalize; +} + +div.leaflet-div-icon span i:before { + display: inline-block; + margin: 9px; + transform: scale(1.1) rotate(-45deg); +} \ No newline at end of file diff --git a/combo/apps/maps/static/js/map.form.js b/combo/apps/maps/static/js/map.form.js new file mode 100644 index 0000000..b82fd43 --- /dev/null +++ b/combo/apps/maps/static/js/map.form.js @@ -0,0 +1,18 @@ +$(window).on('load', function() { + $('div#mapcell_widget').each(function() { + var $map_widget = $(this); + var map_options = Object(); + map_options.zoom = 13; + var map = L.map($(this).attr('id'), map_options); + map.marker = null; + latlng = [$map_widget.data('init-lat'), $map_widget.data('init-lng')]; + map.marker = L.marker(latlng); + map.marker.addTo(map); + map.setView(latlng, map_options.zoom); + L.tileLayer( + 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + { + attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA' + }).addTo(map); + }) +}); diff --git a/combo/apps/maps/static/js/map.js b/combo/apps/maps/static/js/map.js new file mode 100644 index 0000000..26ea132 --- /dev/null +++ b/combo/apps/maps/static/js/map.js @@ -0,0 +1,64 @@ +$(window).on('load combo:cellform-reload', function() { + $('div#mapcell').each(function() { + var $map_widget = $(this); + var map_options = Object(); + var initial_zoom = parseInt($map_widget.data('initial-zoom')); + if (! isNaN(initial_zoom)) { + map_options.zoom = initial_zoom; + } else { + map_options.zoom = 13; + } + var max_zoom = parseInt($map_widget.data('max_zoom')); + if (!isNaN(max_zoom)) map_options.maxZoom = max_zoom; + var min_zoom = parseInt($map_widget.data('min-zoom')); + if (!isNaN(min_zoom)) map_options.minZoom = min_zoom; + var latlng = [$map_widget.data('init-lat'), $map_widget.data('init-lng')]; + var geojson_url = $map_widget.data('geojson-url'); + var map = L.map($(this).attr('id'), map_options); + var store_position_selector = $map_widget.data('store-position'); + map.setView(latlng, map_options.zoom); + + L.tileLayer( + 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + { + attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA' + }).addTo(map); + if (store_position_selector) { + map.marker = L.marker(latlng); + map.marker.addTo(map); + var hidden = $('input#' + store_position_selector); + map.on('click', function(e) { + map.marker.setLatLng(e.latlng); + hidden.val(e.latlng.lat + ';' + e.latlng.lng); + }); + } + if (geojson_url) { + $.getJSON(geojson_url, function(data) { + var geo_json = L.geoJson(data, { + onEachFeature: function(feature, layer) { + if (feature.display_fields) { + var popup = ''; + $.each(feature.display_fields, function(key, value) { + popup += ''; + }); + } else { + var popup = ''; + } + layer.bindPopup(popup); + }, + pointToLayer: function (feature, latlng) { + var markerStyles = "background-color: "+feature.properties.colour+";"; + marker = L.divIcon({iconAnchor: [0, 30], + popupAnchor: [5, -45], + html: '' + }); + return L.marker(latlng, {icon: marker}); + } + }); + map.fitBounds(geo_json.getBounds()); + geo_json.addTo(map); + }); + } + }); +}); diff --git a/combo/apps/maps/templates/maps/map_widget.html b/combo/apps/maps/templates/maps/map_widget.html new file mode 100644 index 0000000..e116ecd --- /dev/null +++ b/combo/apps/maps/templates/maps/map_widget.html @@ -0,0 +1,4 @@ +
+
+ + diff --git a/combo/apps/maps/templates/maps/mapcell.html b/combo/apps/maps/templates/maps/mapcell.html new file mode 100644 index 0000000..f899849 --- /dev/null +++ b/combo/apps/maps/templates/maps/mapcell.html @@ -0,0 +1,4 @@ +{% load i18n %} +

{{ title }}

+
+
diff --git a/combo/apps/maps/urls.py b/combo/apps/maps/urls.py index 8b970b0..b0864e5 100644 --- a/combo/apps/maps/urls.py +++ b/combo/apps/maps/urls.py @@ -21,6 +21,8 @@ from combo.urls_utils import decorated_includes, manager_required from .manager_views import (ManagerHomeView, LayersManagerView, LayerAddView, LayerEditView, LayerDeleteView) +from .views import GeojsonView + maps_manager_urls = [ url('^$', ManagerHomeView.as_view(), name='maps-manager-homepage'), url('^layers/$', LayersManagerView.as_view(), name='maps-manager-layers-list'), @@ -36,4 +38,6 @@ maps_manager_urls = [ urlpatterns = [ url(r'^manage/maps/', decorated_includes(manager_required, include(maps_manager_urls))), + url(r'^maps/geojson/(?P\w+)/$', GeojsonView.as_view(), + name='cell-geojson'), ] diff --git a/combo/apps/maps/views.py b/combo/apps/maps/views.py index 5c657aa..d49e479 100644 --- a/combo/apps/maps/views.py +++ b/combo/apps/maps/views.py @@ -14,4 +14,23 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.shortcuts import render +import json + +from django.views.generic.base import View +from django.http import HttpResponse + +from combo.utils import requests + +from .models import MapCell + + +class GeojsonView(View): + + def get(self, request, *args, **kwargs): + cell = MapCell.objects.get(pk=kwargs['cell_id']) + geojson = {'type': 'FeatureCollection', 'features': []} + for layer in cell.layer.all(): + geojson['features'] += layer.get_geojson() + + content_type = 'application/json' + return HttpResponse(json.dumps(geojson), content_type=content_type) diff --git a/combo/manager/static/js/combo.manager.js b/combo/manager/static/js/combo.manager.js index 9f5acb3..373b99a 100644 --- a/combo/manager/static/js/combo.manager.js +++ b/combo/manager/static/js/combo.manager.js @@ -175,6 +175,8 @@ $(function() { /* update form with new content, unless it has a ckeditor, as * this causes an unpleasant flickering */ $button.parents('form').find('div.cell-form').html(data); + var e = $.Event('combo:cellform-reload'); + $(document).trigger(e); } if (data.indexOf('class="errorlist"') == -1) { $.getJSON($form.data('label-url'), diff --git a/combo/manager/templates/combo/manager_base.html b/combo/manager/templates/combo/manager_base.html index 5fc29ec..3992307 100644 --- a/combo/manager/templates/combo/manager_base.html +++ b/combo/manager/templates/combo/manager_base.html @@ -3,6 +3,8 @@ {% block css %} + + {% endblock %} {% block page-title %}{% firstof site_title "Combo" %}{% endblock %} {% block site-title %}{% firstof site_title "Combo" %}{% endblock %} @@ -31,4 +33,6 @@ + + {% endblock %} diff --git a/combo/settings.py b/combo/settings.py index 877cf28..9154c1d 100644 --- a/combo/settings.py +++ b/combo/settings.py @@ -78,6 +78,8 @@ INSTALLED_APPS = ( 'combo.apps.maps', 'haystack', 'xstatic.pkg.chartnew_js', + 'xstatic.pkg.leaflet', + 'xstatic.pkg.font_awesome', ) INSTALLED_APPS = plugins.register_plugins_apps(INSTALLED_APPS) diff --git a/debian/control b/debian/control index 1ef3ef8..6f4f06f 100644 --- a/debian/control +++ b/debian/control @@ -16,6 +16,7 @@ Depends: ${misc:Depends}, ${python:Depends}, python-feedparser, python-django-cmsplugin-blurp, python-xstatic-chartnew-js, + python-xstatic-leaflet, python-eopayment (>= 1.9), python-django-haystack (>= 2.4.0) Recommends: python-django-mellon, python-whoosh diff --git a/setup.py b/setup.py index 6a7351e..e2ea4a4 100644 --- a/setup.py +++ b/setup.py @@ -111,6 +111,7 @@ setup( 'django-jsonfield', 'requests', 'XStatic-ChartNew.js', + 'XStatic-Leaflet', 'eopayment>=1.13', 'python-dateutil', 'djangorestframework>=3.3, <3.4', -- 2.11.0