diff --git a/combo/apps/maps/migrations/0002_map.py b/combo/apps/maps/migrations/0002_map.py new file mode 100644 index 00000000..de5606a4 --- /dev/null +++ b/combo/apps/maps/migrations/0002_map.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('data', '0027_page_picture'), + ('auth', '0006_require_contenttypes_0002'), + ('maps', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Map', + 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)), + ('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)), + ('layers', models.ManyToManyField(to='maps.MapLayer', verbose_name='Layers', blank=True)), + ('page', models.ForeignKey(to='data.Page')), + ], + options={ + 'verbose_name': 'Map', + }, + ), + ] diff --git a/combo/apps/maps/models.py b/combo/apps/maps/models.py index 371755f6..94b9fb86 100644 --- a/combo/apps/maps/models.py +++ b/combo/apps/maps/models.py @@ -18,7 +18,14 @@ from django.db import models from django.utils.text import slugify from django.utils.translation import ugettext_lazy as _ +from django.core.urlresolvers import reverse_lazy +from django import forms +from django import template +from django.conf import settings +from django.core.exceptions import PermissionDenied +from combo.data.models import CellBase +from combo.data.library import register_cell_class from combo.utils import requests @@ -38,6 +45,13 @@ ICONS = [ ('truck', _('Truck')), ] +ZOOM_LEVELS = [ ('0', _('Whole world')), + ('9', _('Wide area')), + ('11', _('Area')), + ('13', _('Town')), + ('16', _('Small road')), + ('19', _('Ant')),] + class MapLayer(models.Model): label = models.CharField(_('Label'), max_length=128) @@ -84,3 +98,58 @@ class MapLayer(models.Model): feature['properties']['label'] = self.label feature['properties']['icon'] = self.icon return features + + +@register_cell_class +class Map(CellBase): + title = models.CharField(_('Title'), max_length=150, blank=True) + 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) + layers = models.ManyToManyField(MapLayer, verbose_name=_('Layers'), blank=True) + + template_name = 'maps/map_cell.html' + + class Meta: + verbose_name = _('Map') + + class Media: + js = ('xstatic/leaflet.js', 'js/combo.map.js') + css = {'all': ('xstatic/leaflet.css', 'css/combo.map.css')} + + def get_default_position(self): + return settings.COMBO_MAP_DEFAULT_POSITION + + def get_default_form_class(self): + fields = ('title', 'initial_zoom', 'min_zoom', + 'max_zoom', 'layers') + widgets = {'layers': forms.widgets.CheckboxSelectMultiple} + return forms.models.modelform_factory(self.__class__, fields=fields, + widgets=widgets) + + def get_geojson(self, request): + geojson = {'type': 'FeatureCollection', 'features': []} + for layer in self.layers.all(): + geojson['features'] += layer.get_geojson(request) + return geojson + + @classmethod + def is_enabled(cls): + return MapLayer.objects.count() > 0 + + def get_cell_extra_context(self, context): + ctx = super(Map, self).get_cell_extra_context(context) + ctx['title'] = self.title + default_position = self.get_default_position() + ctx['init_lat'] = default_position['lat'] + ctx['init_lng'] = default_position['lng'] + ctx['initial_zoom'] = self.initial_zoom + ctx['min_zoom'] = self.min_zoom + ctx['max_zoom'] = self.max_zoom + ctx['geojson_url'] = reverse_lazy('mapcell-geojson', kwargs={'cell_id': self.pk}) + ctx['tile_urltemplate'] = settings.COMBO_MAP_TILE_URLTEMPLATE + ctx['map_attribution'] = settings.COMBO_MAP_ATTRIBUTION + return ctx diff --git a/combo/apps/maps/static/css/combo.map.css b/combo/apps/maps/static/css/combo.map.css new file mode 100644 index 00000000..0cfcc9f9 --- /dev/null +++ b/combo/apps/maps/static/css/combo.map.css @@ -0,0 +1,90 @@ +div.combo-cell-map { + 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-value { + font-weight: bold; +} + +div.leaflet-div-icon span i:before { + display: inline-block; + margin: 9px; + transform: scale(1.1) rotate(-45deg); +} + +/* leaflet markers icons */ + +i.leaflet-marker-icon { + font: normal normal normal 1rem/1 FontAwesome; +} + +i.leaflet-marker-icon.home::before { + content: "\f015"; /* home */ +} + +i.leaflet-marker-icon.building::before { + content: "\f0f7"; /* building */ +} + +i.leaflet-marker-icon.hospital::before { + content: "\f0f8"; /* hospital */ +} + +i.leaflet-marker-icon.ambulance::before { + content: "\f0f9"; /* ambulance */ +} + +i.leaflet-marker-icon.taxi::before { + content: "\f1ba"; /* taxi */ +} + +i.leaflet-marker-icon.subway::before { + content: "\f239"; /* subway */ +} + +i.leaflet-marker-icon.wheelchair::before { + content: "\f193"; /* wheelchair */ +} + +i.leaflet-marker-icon.bicycle::before { + content: "\f206"; /* bicycle */ +} + +i.leaflet-marker-icon.car::before { + content: "\f1b9"; /* car */ +} + +i.leaflet-marker-icon.train::before { + content: "\f238"; /* train */ +} + +i.leaflet-marker-icon.bus::before { + content: "\f207"; /* bus */ +} + +i.leaflet-marker-icon.motorcycle::before { + content: "\f21c"; /* motorcycle */ +} + +i.leaflet-marker-icon.truck::before { + content: "\f0d1"; /* truck */ +} + diff --git a/combo/apps/maps/static/js/combo.map.js b/combo/apps/maps/static/js/combo.map.js new file mode 100644 index 00000000..5059252d --- /dev/null +++ b/combo/apps/maps/static/js/combo.map.js @@ -0,0 +1,53 @@ +$(function() { + function render_map(cell) { + var $map_widget = $(cell).find('div.combo-cell-map'); + var map_options = Object(); + var initial_zoom = parseInt($map_widget.data('init-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_tile_url = $map_widget.data('tile-urltemplate'); + var map_attribution = $map_widget.data('map-attribution'); + var map = L.map($map_widget[0], map_options); + var store_position_selector = $map_widget.data('store-position'); + map.setView(latlng, map_options.zoom); + + L.tileLayer(map_tile_url, + { + attribution: map_attribution + }).addTo(map); + if (geojson_url) { + $.getJSON(geojson_url, function(data) { + var geo_json = L.geoJson(data, { + onEachFeature: function(feature, layer) { + $(cell).trigger('combo:map-feature-click', {'feature': feature, 'layer': layer}); + }, + 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}); + } + }); + var bounds = geo_json.getBounds(); + if (bounds.isValid()) { + map.fitBounds(bounds); + geo_json.addTo(map); + } + }); + } + }; + $('div.cell.map').each(function() { + render_map(this); + }); +}); diff --git a/combo/apps/maps/templates/maps/map_cell.html b/combo/apps/maps/templates/maps/map_cell.html new file mode 100644 index 00000000..1e7c24d8 --- /dev/null +++ b/combo/apps/maps/templates/maps/map_cell.html @@ -0,0 +1,3 @@ +{% if title %}