From ce7386ffd6e6a25011612c050d1525e0db58c1b5 Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Mon, 12 Feb 2018 19:20:18 +0100 Subject: [PATCH] maps: add option to clusterize markers (#21048) --- .../migrations/0005_auto_20180212_1742.py | 19 +++++++ combo/apps/maps/models.py | 6 ++- combo/apps/maps/static/css/combo.map.scss | 51 ++++++++++++++++++- combo/apps/maps/static/js/combo.map.js | 25 ++++++++- combo/apps/maps/templates/maps/map_cell.html | 1 + combo/settings.py | 1 + debian/control | 1 + setup.py | 1 + tests/test_maps_cells.py | 6 +++ 9 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 combo/apps/maps/migrations/0005_auto_20180212_1742.py diff --git a/combo/apps/maps/migrations/0005_auto_20180212_1742.py b/combo/apps/maps/migrations/0005_auto_20180212_1742.py new file mode 100644 index 00000000..8970ebdc --- /dev/null +++ b/combo/apps/maps/migrations/0005_auto_20180212_1742.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('maps', '0004_map_initial_state'), + ] + + operations = [ + migrations.AddField( + model_name='map', + name='group_markers', + field=models.BooleanField(default=False, verbose_name='Group markers in clusters'), + ), + ] diff --git a/combo/apps/maps/models.py b/combo/apps/maps/models.py index eb5bcf63..37023416 100644 --- a/combo/apps/maps/models.py +++ b/combo/apps/maps/models.py @@ -200,6 +200,7 @@ class Map(CellBase): choices=ZOOM_LEVELS, default='0') max_zoom = models.CharField(_('Maximal zoom level'), max_length=2, choices=ZOOM_LEVELS, default=19) + group_markers = models.BooleanField(_('Group markers in clusters'), default=False) layers = models.ManyToManyField(MapLayer, verbose_name=_('Layers'), blank=True) template_name = 'maps/map_cell.html' @@ -208,7 +209,7 @@ class Map(CellBase): verbose_name = _('Map') class Media: - js = ('xstatic/leaflet.js', 'js/combo.map.js') + js = ('xstatic/leaflet.js', 'js/combo.map.js', 'xstatic/leaflet.markercluster.js') css = {'all': ('xstatic/leaflet.css', 'css/combo.map.css')} def get_default_position(self): @@ -216,7 +217,7 @@ class Map(CellBase): def get_default_form_class(self): fields = ('title', 'initial_state', 'initial_zoom', 'min_zoom', - 'max_zoom', 'layers') + 'max_zoom', 'group_markers', 'layers') widgets = {'layers': forms.widgets.CheckboxSelectMultiple} return forms.models.modelform_factory(self.__class__, fields=fields, widgets=widgets) @@ -245,4 +246,5 @@ class Map(CellBase): ctx['tile_urltemplate'] = settings.COMBO_MAP_TILE_URLTEMPLATE ctx['map_attribution'] = settings.COMBO_MAP_ATTRIBUTION ctx['max_bounds'] = settings.COMBO_MAP_MAX_BOUNDS + ctx['group_markers'] = self.group_markers return ctx diff --git a/combo/apps/maps/static/css/combo.map.scss b/combo/apps/maps/static/css/combo.map.scss index 9c9ad860..8e841862 100644 --- a/combo/apps/maps/static/css/combo.map.scss +++ b/combo/apps/maps/static/css/combo.map.scss @@ -44,7 +44,7 @@ div.combo-cell-map.leaflet-container { /* leaflet styles */ -div.leaflet-marker-icon { +div.leaflet-marker-icon.leaflet-div-icon { border: none; background: transparent; } @@ -135,3 +135,52 @@ ul#id_icon { } } } + + +/* leaflet markercluster styles */ + +.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { + transition: transform 0.3s ease-out, opacity 0.3s ease-in; +} + +.leaflet-cluster-spider-leg { + /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */ + transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in; +} + +.marker-cluster { + background-clip: padding-box; + border-radius: 30px; + div { + display: block; + background: #fff; + border-radius: 30px; + font-weight: bold; + height: 50px; + width: 50px; + text-align: center; + margin-left: 5px; + margin-top: 5px; + } + span { + line-height: 50px; + } + &-small { + background: #b5e28c; + div { + font-size: 200%; + } + } + &-medium { + background: #f1d357; + div { + font-size: 200%; + } + } + &-large { + background: #fd9c73; + div { + font-size: 150%; + } + } +} diff --git a/combo/apps/maps/static/js/combo.map.js b/combo/apps/maps/static/js/combo.map.js index 915b8134..31744c56 100644 --- a/combo/apps/maps/static/js/combo.map.js +++ b/combo/apps/maps/static/js/combo.map.js @@ -29,7 +29,30 @@ $(function() { }); if (map.geo_json) map.geo_json.remove(); map.geo_json = geo_json; - geo_json.addTo(map); + if ($map_widget.data('group-markers')) { + var markers = L.markerClusterGroup({showCoverageOnHover: false, + zoomToBoundsOnClick: true, + removeOutsideVisibleBounds: true, + iconCreateFunction: function (cluster) { + var icon_size = 60; + var childCount = cluster.getChildCount(); + var icon_html = '
' + childCount + '
'; + var c = ' marker-cluster-'; + if (childCount < 10) { + c += 'small'; + } else if (childCount < 100) { + c += 'medium'; + } else { + c += 'large'; + } + return new L.DivIcon({html: icon_html, className: 'marker-cluster' + c, iconSize: new L.Point(icon_size, icon_size)}); + }}); + markers.addLayer(geo_json); + map.addLayer(markers); + map.clustered_markers = markers; + } else { + geo_json.addTo(map); + } if (callback) { callback(geo_json); } diff --git a/combo/apps/maps/templates/maps/map_cell.html b/combo/apps/maps/templates/maps/map_cell.html index b4e51aed..a37a2e94 100644 --- a/combo/apps/maps/templates/maps/map_cell.html +++ b/combo/apps/maps/templates/maps/map_cell.html @@ -6,6 +6,7 @@ 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}}" + {% if group_markers %}data-group-markers="1"{% endif %} {% if max_bounds.corner1.lat %} data-max-bounds-lat1="{{ max_bounds.corner1.lat }}" data-max-bounds-lng1="{{ max_bounds.corner1.lng }}" diff --git a/combo/settings.py b/combo/settings.py index c1172b07..7ea8af4b 100644 --- a/combo/settings.py +++ b/combo/settings.py @@ -82,6 +82,7 @@ INSTALLED_APPS = ( 'xstatic.pkg.leaflet', 'xstatic.pkg.opensans', 'xstatic.pkg.roboto_fontface', + 'xstatic.pkg.leaflet_markercluster', ) INSTALLED_APPS = plugins.register_plugins_apps(INSTALLED_APPS) diff --git a/debian/control b/debian/control index 96a14654..32bbc9f5 100644 --- a/debian/control +++ b/debian/control @@ -17,6 +17,7 @@ Depends: ${misc:Depends}, ${python:Depends}, python-xstatic-chartnew-js, python-xstatic-josefinsans, python-xstatic-leaflet, + python-xstatic-leaflet-markercluster, python-xstatic-opensans, python-xstatic-roboto-fontface, python-eopayment (>= 1.9), diff --git a/setup.py b/setup.py index ac9c7560..d66c289d 100644 --- a/setup.py +++ b/setup.py @@ -142,6 +142,7 @@ setup( 'requests', 'XStatic-ChartNew.js', 'XStatic-Leaflet', + 'XStatic-Leaflet-MarkerCluster', 'XStatic_JosefinSans', 'XStatic_OpenSans', 'XStatic_roboto-fontface', diff --git a/tests/test_maps_cells.py b/tests/test_maps_cells.py index 15c29084..c50fc81a 100644 --- a/tests/test_maps_cells.py +++ b/tests/test_maps_cells.py @@ -94,12 +94,18 @@ def test_cell_rendering(layer): assert 'data-init-lat="48.83369263315934"' in rendered 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 resp = client.get('/test_map_cell/') assert 'xstatic/leaflet.js' in resp.content assert 'js/combo.map.js' in resp.content assert 'xstatic/leaflet.css' in resp.content assert 'css/combo.map.css' in resp.content + cell.group_markers = True + cell.save() + rendered = cell.render(context) + assert 'data-group-markers="1"' in rendered + def test_get_geojson_on_non_public_page(layer): page = Page(title='xxx', slug='new', template_name='standard',