diff --git a/geotrek/common/templates/common/leaflet_test.html b/geotrek/common/templates/common/leaflet_test.html
new file mode 100644
index 0000000000..7f3bdec502
--- /dev/null
+++ b/geotrek/common/templates/common/leaflet_test.html
@@ -0,0 +1,89 @@
+{% load static %}
+
+
+
+
+ Leaflet tiling test
+
+
+
+
+
+
+
+
+
+
+
diff --git a/geotrek/common/urls.py b/geotrek/common/urls.py
index 3b3bb2bd48..e6bef90dc6 100644
--- a/geotrek/common/urls.py
+++ b/geotrek/common/urls.py
@@ -2,7 +2,7 @@
from mapentity.registry import MapEntityOptions
from .views import (JSSettings, admin_check_extents, DocumentPublic, DocumentBookletPublic, import_view, import_update_json,
- ThemeViewSet, MarkupPublic, sync_view, sync_update_json, SyncRandoRedirect)
+ ThemeViewSet, MarkupPublic, sync_view, sync_update_json, SyncRandoRedirect, LeafletTestView)
class LangConverter(converters.StringConverter):
@@ -21,6 +21,7 @@ class LangConverter(converters.StringConverter):
path('commands/syncview', sync_view, name='sync_randos_view'),
path('commands/statesync/', sync_update_json, name='sync_randos_state'),
path('api//themes.json', ThemeViewSet.as_view({'get': 'list'}), name="themes_json"),
+ path('leaflet-tiles/', LeafletTestView.as_view(), name="leaflet-tiles"),
]
diff --git a/geotrek/common/views.py b/geotrek/common/views.py
index f1c593c197..f409fb496d 100644
--- a/geotrek/common/views.py
+++ b/geotrek/common/views.py
@@ -12,8 +12,7 @@
from django.utils import timezone
from django.views import static
from django.utils.translation import gettext as _
-from django.views.generic import TemplateView
-from django.views.generic import RedirectView, View
+from django.views.generic import TemplateView, RedirectView, View
from mapentity.helpers import api_bbox
from mapentity.registry import registry
@@ -479,3 +478,7 @@ def post(self, request, *args, **kwargs):
home = last_list
+
+
+class LeafletTestView(TemplateView):
+ template_name = "common/leaflet_test.html"
diff --git a/geotrek/core/static/core/geotrek.forms.snap.js b/geotrek/core/static/core/geotrek.forms.snap.js
index 37a8207923..d4bd868098 100644
--- a/geotrek/core/static/core/geotrek.forms.snap.js
+++ b/geotrek/core/static/core/geotrek.forms.snap.js
@@ -40,11 +40,10 @@ MapEntity.GeometryField.GeometryFieldPathMixin = {
* (At least for the fix to propagate events)
*/
buildPathsLayer: function (objectsLayer) {
- var url_path = window.SETTINGS.urls.path_layer
- var pathsLayer = MapEntity.pathsLayer({style: {clickable: true}, no_draft: objectsLayer.modelname != 'path'});
+ var url_path = window.SETTINGS.urls.tile.replace(new RegExp('modelname', 'g'), 'path');
if (objectsLayer.modelname != 'path')
url_path += '?no_draft=true';
- pathsLayer.load(url_path, true);
+ var pathsLayer = MapEntity.pathsLayer(url_path, {style: {clickable: true}, no_draft: objectsLayer.modelname != 'path'});
this._map.addLayer(pathsLayer);
@@ -94,8 +93,8 @@ MapEntity.GeometryField.GeometryFieldSnap = MapEntity.GeometryField.extend({
this._objectsLayer = null;
},
- buildObjectsLayer: function () {
- this._objectsLayer = MapEntity.GeometryField.prototype.buildObjectsLayer(arguments);
+ buildObjectsLayer: function (url) {
+ this._objectsLayer = MapEntity.GeometryField.prototype.buildObjectsLayer(url);
this._guidesLayers.push(this._objectsLayer);
if (this.getModelName() != 'path') {
@@ -147,19 +146,20 @@ MapEntity.GeometryField.GeometryFieldSnap = MapEntity.GeometryField.extend({
}, this);
// On edition, show start and end markers as snapped
- this._map.on('draw:editstart', function (e) {
- setTimeout(function () {
- if (!layer.editing) {
- console.warn('Layer has no snap editing');
- return; // should never happen ;)
- }
- var markers = layer.editing._markers;
- var first = markers[0],
- last = markers[markers.length - 1];
- first.fire('move');
- last.fire('move');
- }, 0);
- });
+ // FIXME: disabled temporarily
+ // this._map.on('draw:editstart', function (e) {
+ // setTimeout(function () {
+ // if (!layer.editing) {
+ // console.warn('Layer has no snap editing');
+ // return; // should never happen ;)
+ // }
+ // var markers = layer.editing._markers;
+ // var first = markers[0],
+ // last = markers[markers.length - 1];
+ // first.fire('move');
+ // last.fire('move');
+ // }, 0);
+ // });
},
diff --git a/geotrek/core/static/core/geotrek.forms.topology.js b/geotrek/core/static/core/geotrek.forms.topology.js
index 2224101b4f..83ebcc659e 100644
--- a/geotrek/core/static/core/geotrek.forms.topology.js
+++ b/geotrek/core/static/core/geotrek.forms.topology.js
@@ -97,8 +97,8 @@ MapEntity.GeometryField.TopologyField = MapEntity.GeometryField.extend({
}
},
- buildObjectsLayer: function () {
- this._objectsLayer = MapEntity.GeometryField.prototype.buildObjectsLayer.call(this);
+ buildObjectsLayer: function (url) {
+ this._objectsLayer = MapEntity.GeometryField.prototype.buildObjectsLayer.call(this, url);
this._pathsLayer = this.buildPathsLayer(this._objectsLayer);
this._pathsLayer.on('loaded', this._loadTopologyGraph, this);
return this._objectsLayer;
diff --git a/geotrek/core/static/core/images/marker-source-2x.png b/geotrek/core/static/core/images/marker-source-2x.png
new file mode 100644
index 0000000000..f838bd937c
Binary files /dev/null and b/geotrek/core/static/core/images/marker-source-2x.png differ
diff --git a/geotrek/core/static/core/images/marker-target-2x.png b/geotrek/core/static/core/images/marker-target-2x.png
new file mode 100644
index 0000000000..ddce49ed20
Binary files /dev/null and b/geotrek/core/static/core/images/marker-target-2x.png differ
diff --git a/geotrek/core/static/core/leaflet.textpath.js b/geotrek/core/static/core/leaflet.textpath.js
index f0b783334f..cf027b3382 100644
--- a/geotrek/core/static/core/leaflet.textpath.js
+++ b/geotrek/core/static/core/leaflet.textpath.js
@@ -1,34 +1,38 @@
/*
+ * Leaflet.TextPath - Shows text along a polyline
* Inspired by Tom Mac Wright article :
* http://mapbox.com/osmdev/2012/11/20/getting-serious-about-svg/
*/
-var PolylineTextPath = {
+(function () {
+
+var __onAdd = L.Polyline.prototype.onAdd,
+ __onRemove = L.Polyline.prototype.onRemove,
+ __updatePath = L.Polyline.prototype._updatePath,
+ __bringToFront = L.Polyline.prototype.bringToFront;
- __updatePath: L.Polyline.prototype._updatePath,
- __bringToFront: L.Polyline.prototype.bringToFront,
- __onAdd: L.Polyline.prototype.onAdd,
- __onRemove: L.Polyline.prototype.onRemove,
+
+var PolylineTextPath = {
onAdd: function (map) {
- this.__onAdd.call(this, map);
+ __onAdd.call(this, map);
this._textRedraw();
},
onRemove: function (map) {
map = map || this._map;
- if (map && this._textNode)
- map._pathRoot.removeChild(this._textNode);
- this.__onRemove.call(this, map);
+ if (map && this._textNode && map._renderer._container)
+ map._renderer._container.removeChild(this._textNode);
+ __onRemove.call(this, map);
},
bringToFront: function () {
- this.__bringToFront.call(this);
+ __bringToFront.call(this);
this._textRedraw();
},
_updatePath: function () {
- this.__updatePath.call(this);
+ __updatePath.call(this);
this._textRedraw();
},
@@ -44,24 +48,39 @@ var PolylineTextPath = {
this._text = text;
this._textOptions = options;
- var defaults = {repeat: false, fillColor: 'black', attributes: {}};
+ /* If not in SVG mode or Polyline not added to map yet return */
+ /* setText will be called by onAdd, using value stored in this._text */
+ if (!L.Browser.svg || typeof this._map === 'undefined') {
+ return this;
+ }
+
+ var defaults = {
+ repeat: false,
+ fillColor: 'black',
+ attributes: {},
+ below: false,
+ };
options = L.Util.extend(defaults, options);
/* If empty text, hide */
if (!text) {
- if (this._textNode)
- this._map._pathRoot.removeChild(this._textNode);
+ if (this._textNode && this._textNode.parentNode) {
+ this._map._renderer._container.removeChild(this._textNode);
+
+ /* delete the node, so it will not be removed a 2nd time if the layer is later removed from the map */
+ delete this._textNode;
+ }
return this;
}
text = text.replace(/ /g, '\u00A0'); // Non breakable spaces
var id = 'pathdef-' + L.Util.stamp(this);
- var svg = this._map._pathRoot;
+ var svg = this._map._renderer._container;
this._path.setAttribute('id', id);
if (options.repeat) {
/* Compute single pattern length */
- var pattern = L.Path.prototype._createElement('text');
+ var pattern = L.SVG.create('text');
for (var attr in options.attributes)
pattern.setAttribute(attr, options.attributes[attr]);
pattern.appendChild(document.createTextNode(text));
@@ -70,12 +89,12 @@ var PolylineTextPath = {
svg.removeChild(pattern);
/* Create string as long as path */
- text = new Array(Math.ceil(this._path.getTotalLength() / alength)).join(text);
+ text = new Array(Math.ceil(isNaN(this._path.getTotalLength() / alength) ? 0 : this._path.getTotalLength() / alength)).join(text);
}
/* Put it along the path using textPath */
- var textNode = L.Path.prototype._createElement('text'),
- textPath = L.Path.prototype._createElement('textPath');
+ var textNode = L.SVG.create('text'),
+ textPath = L.SVG.create('textPath');
var dy = options.offset || this._path.getAttribute('stroke-width');
@@ -85,8 +104,55 @@ var PolylineTextPath = {
textNode.setAttribute(attr, options.attributes[attr]);
textPath.appendChild(document.createTextNode(text));
textNode.appendChild(textPath);
- svg.appendChild(textNode);
this._textNode = textNode;
+
+ if (options.below) {
+ svg.insertBefore(textNode, svg.firstChild);
+ }
+ else {
+ svg.appendChild(textNode);
+ }
+
+ /* Center text according to the path's bounding box */
+ if (options.center) {
+ var textLength = textNode.getComputedTextLength();
+ var pathLength = this._path.getTotalLength();
+ /* Set the position for the left side of the textNode */
+ textNode.setAttribute('dx', ((pathLength / 2) - (textLength / 2)));
+ }
+
+ /* Change label rotation (if required) */
+ if (options.orientation) {
+ var rotateAngle = 0;
+ switch (options.orientation) {
+ case 'flip':
+ rotateAngle = 180;
+ break;
+ case 'perpendicular':
+ rotateAngle = 90;
+ break;
+ default:
+ rotateAngle = options.orientation;
+ }
+
+ var rotatecenterX = (textNode.getBBox().x + textNode.getBBox().width / 2);
+ var rotatecenterY = (textNode.getBBox().y + textNode.getBBox().height / 2);
+ textNode.setAttribute('transform','rotate(' + rotateAngle + ' ' + rotatecenterX + ' ' + rotatecenterY + ')');
+ }
+
+ /* Initialize mouse events for the additional nodes */
+ if (this.options.interactive) {
+ if (L.Browser.svg || !L.Browser.vml) {
+ textPath.setAttribute('class', 'leaflet-interactive');
+ }
+
+ var events = ['click', 'dblclick', 'mousedown', 'mouseover',
+ 'mouseout', 'mousemove', 'contextmenu'];
+ for (var i = 0; i < events.length; i++) {
+ L.DomEvent.on(textNode, events[i], this.fire, this);
+ }
+ }
+
return this;
}
};
@@ -103,3 +169,7 @@ L.LayerGroup.include({
return this;
}
});
+
+
+
+})();
diff --git a/geotrek/core/static/core/main.js b/geotrek/core/static/core/main.js
index ae9e675739..3338458ec0 100644
--- a/geotrek/core/static/core/main.js
+++ b/geotrek/core/static/core/main.js
@@ -1,8 +1,8 @@
-MapEntity.pathsLayer = function buildPathLayer(options) {
+MapEntity.pathsLayer = function buildPathLayer(url, options) {
var options = options || {};
options.style = L.Util.extend(options.style || {}, window.SETTINGS.map.styles.path);
- var pathsLayer = new L.ObjectsLayer(null, options);
+ var pathsLayer = new L.ObjectsLayer(url, options);
// Show paths extremities
if (window.SETTINGS.showExtremities) {
@@ -21,15 +21,14 @@ $(window).on('entity:map', function (e, data) {
var is_form_view = /add|update/.test(data.viewname);
if (!is_form_view && (data.viewname == 'detail' || data.modelname != 'path')) {
-
- var pathsLayer = MapEntity.pathsLayer({
+ var pathsLayer = MapEntity.pathsLayer(window.SETTINGS.urls.tile.replace(new RegExp('modelname', 'g'), 'path'), {
indexing: false,
style: { clickable: false },
modelname: 'path',
no_draft: data.modelname != 'path',
});
+
if (data.viewname == 'detail'){
- pathsLayer.load(window.SETTINGS.urls.path_layer);
pathsLayer.addTo(map);
};
pathsLayer.on('loaded', function () {
@@ -37,53 +36,6 @@ $(window).on('entity:map', function (e, data) {
pathsLayer.bringToBack();
});
- map.on('layeradd', function (e) {
- // Start ajax loading at last
- url = window.SETTINGS.urls.path_layer
-
- var options = e.layer.options || {'modelname': 'None'};
- if (! loaded_path){
- if (options.modelname == 'path' && data.viewname != 'detail'){
- e.layer.load(url + '?no_draft=true', true);
- loaded_path = true;
- };
-
- }
-
- if (e.layer === pathsLayer) {
-
- if (!e.layer._map) {
- return;
- }
- if (e.layer.loading) {
- e.layer.on('loaded', function () {
- if (!e.layer._map) {
- return;
- }
- e.layer.bringToBack();
- });
- }
- else {
- e.layer.bringToBack();
- }
- }
- else {
- if (e.layer instanceof L.ObjectsLayer) {
- if (e.layer.loading) {
- e.layer.on('loaded', function () {
- if (!e.layer._map) {
- return;
- }
- e.layer.bringToFront();
- });
- }
- else {
- e.layer.bringToFront();
- }
- }
- }
- });
-
var style = pathsLayer.options.style;
var nameHTML = '━ ' + tr('Paths');
map.layerscontrol.addOverlay(pathsLayer, nameHTML, tr('Objects'));
@@ -112,22 +64,36 @@ $(window).on('detailmap:ready', function (e, data) {
// Show start and end
layer.eachLayer(function (layer) {
- if (layer instanceof L.MultiPolyline)
- return;
if (layer instanceof L.Polygon)
return;
if (typeof layer.getLatLngs != 'function') // points
return;
// Show start and end markers (similar to edition)
- var _iconUrl = window.SETTINGS.urls.static + "core/images/marker-";
+ var imagePath = window.SETTINGS.urls.static + "core/images/";
L.marker(layer.getLatLngs()[0], {
clickable: false,
- icon: new L.Icon.Default({iconUrl: _iconUrl + "source.png"})
+ icon: new L.Icon({
+ iconSize: [25, 41],
+ iconAnchor: [12, 41],
+ popupAnchor: [1, -34],
+ tooltipAnchor: [16, -28],
+ iconUrl: "/static/core/images/marker-source.png",
+ iconRetinaUrl: "/static/core/images/marker-source-2x.png",
+ shadowUrl: null
+ })
}).addTo(map);
L.marker(layer.getLatLngs().slice(-1)[0], {
clickable: false,
- icon: new L.Icon.Default({iconUrl: _iconUrl + "target.png"})
+ icon: new L.Icon({
+ iconSize: [25, 41],
+ iconAnchor: [12, 41],
+ popupAnchor: [1, -34],
+ tooltipAnchor: [16, -28],
+ iconUrl: "/static/core/images/marker-target.png",
+ iconRetinaUrl: "/static/core/images/marker-target-2x.png",
+ shadowUrl: null
+ })
}).addTo(map);
// Also add line orientation
diff --git a/geotrek/core/static/core/multipath.js b/geotrek/core/static/core/multipath.js
index ad9990655f..5a09c17b97 100644
--- a/geotrek/core/static/core/multipath.js
+++ b/geotrek/core/static/core/multipath.js
@@ -234,7 +234,7 @@ L.ActivableMarker = L.Marker.extend({
L.Handler.MultiPath = L.Handler.extend({
- includes: L.Mixin.Events,
+ includes: L.Evented.prototype,
initialize: function (map, guidesLayer, options) {
this.map = map;
@@ -272,7 +272,7 @@ L.Handler.MultiPath = L.Handler.extend({
var self = this;
(function() {
function dragstart(e) {
- var next_step_idx = self.draggable_marker.group_layer.step_idx + 1;
+ var next_step_idx = self.draggable_marker.layer.step_idx + 1;
self.addViaStep(self.draggable_marker, next_step_idx);
}
function dragend(e) {
@@ -797,17 +797,15 @@ L.Handler.MultiPath = L.Handler.extend({
var layerPoint = a.layerPoint
, min_dist = Number.MAX_VALUE
, closest_point = null
- , matching_group_layer = null;
-
- topology.layer && topology.layer.eachLayer(function(group_layer) {
- group_layer.eachLayer(function(layer) {
- var p = layer.closestLayerPoint(layerPoint);
- if (p && p.distance < min_dist && p.distance < MIN_DIST) {
- min_dist = p.distance;
- closest_point = p;
- matching_group_layer = group_layer;
- }
- });
+ , matching_layer = null;
+
+ topology.layer && topology.layer.eachLayer(function(layer) {
+ var p = layer.closestLayerPoint(layerPoint);
+ if (p && p.distance < min_dist && p.distance < MIN_DIST) {
+ min_dist = p.distance;
+ closest_point = p;
+ matching_layer = layer;
+ }
});
if (closest_point) {
@@ -815,7 +813,7 @@ L.Handler.MultiPath = L.Handler.extend({
self.draggable_marker.addTo(self.map);
L.DomUtil.addClass(self.draggable_marker._icon, self.draggable_marker.classname);
self.draggable_marker._removeShadow();
- self.draggable_marker.group_layer = matching_group_layer;
+ self.draggable_marker.layer = matching_layer;
} else {
self.draggable_marker && self.map.removeLayer(self.draggable_marker);
}
@@ -844,7 +842,7 @@ Geotrek.PointOnPolyline = function (marker) {
this.percent_distance = null;
this._activated = false;
- this.events = L.Util.extend({}, L.Mixin.Events);
+ this.events = L.Util.extend({}, L.Evented.prototype);
this.markerEvents = {
'move': function onMove (e) {
diff --git a/geotrek/core/static/core/topology_helper.js b/geotrek/core/static/core/topology_helper.js
index 41b935cb49..1627a12347 100644
--- a/geotrek/core/static/core/topology_helper.js
+++ b/geotrek/core/static/core/topology_helper.js
@@ -237,7 +237,7 @@ Geotrek.TopologyHelper = (function() {
/**
* @param topology {Object} with ``offset``, ``positions`` and ``paths`` as returned by buildSubTopology()
* @param idToLayer {function} callback to obtain layer from id
- * @returns L.multiPolyline
+ * @returns L.polyline
*/
function buildGeometryFromTopology(topology, idToLayer) {
var latlngs = [];
@@ -252,7 +252,7 @@ Geotrek.TopologyHelper = (function() {
console.warn('Topology problem: ' + i + ' not in ' + JSON.stringify(topology.positions));
}
}
- return L.multiPolyline(latlngs);
+ return L.polyline(latlngs);
}
/**
diff --git a/geotrek/core/templates/core/core_extrabody_fragment.html b/geotrek/core/templates/core/core_extrabody_fragment.html
index 164661d713..4746d15a9b 100644
--- a/geotrek/core/templates/core/core_extrabody_fragment.html
+++ b/geotrek/core/templates/core/core_extrabody_fragment.html
@@ -2,7 +2,6 @@
diff --git a/geotrek/core/templates/core/path_list.html b/geotrek/core/templates/core/path_list.html
index 45d4b55831..f4c4354267 100644
--- a/geotrek/core/templates/core/path_list.html
+++ b/geotrek/core/templates/core/path_list.html
@@ -23,8 +23,10 @@
$(function(){
$('span#nbresults').replaceWith(' ?')
$('#objects-list').on( 'xhr', function(e,o) {
- obj = JSON.parse(o.jqXHR.responseText);
- $( "#sumPath" ).html(' ('+ obj.sumPath + ' km)')
+ if (o.jqXHR.responseText !== undefined) {
+ obj = JSON.parse(o.jqXHR.responseText);
+ $( "#sumPath" ).html(' ('+ obj.sumPath + ' km)')
+ }
});
$('#btn-merge').click(function() {
diff --git a/geotrek/core/tests/test_views.py b/geotrek/core/tests/test_views.py
index d69f1c3718..652ea232aa 100644
--- a/geotrek/core/tests/test_views.py
+++ b/geotrek/core/tests/test_views.py
@@ -644,7 +644,7 @@ def test_denormalized_path_trails(self):
PathFactory.create_batch(size=50)
TrailFactory.create_batch(size=50)
self.login()
- with self.assertNumQueries(7):
+ with self.assertNumQueries(9):
self.client.get(reverse('core:path_json_list'))
diff --git a/geotrek/core/views.py b/geotrek/core/views.py
index 5f138eb103..9daee2fbd7 100644
--- a/geotrek/core/views.py
+++ b/geotrek/core/views.py
@@ -65,6 +65,7 @@ def get_initial(self):
class PathLayer(MapEntityLayer):
properties = ['name', 'draft']
queryset = Path.objects.all()
+ geometry_field_db = 'geom'
def get_queryset(self):
qs = super().get_queryset()
@@ -303,6 +304,7 @@ def get_graph_json(request):
class TrailLayer(MapEntityLayer):
queryset = Trail.objects.existing()
properties = ['name']
+ geometry_field_db = 'geom'
class TrailList(MapEntityList):
@@ -312,7 +314,8 @@ class TrailList(MapEntityList):
class TrailJsonList(MapEntityJsonList, TrailList):
- pass
+ def get_context_data(self, **kwargs):
+ return super(TrailJsonList, self).get_context_data(queryset=self.filterform(self.request.GET).qs, **kwargs)
class TrailFormatList(MapEntityFormat, TrailList):
diff --git a/geotrek/diving/tests/test_views.py b/geotrek/diving/tests/test_views.py
index 06f546d6ae..64e2a23a9f 100644
--- a/geotrek/diving/tests/test_views.py
+++ b/geotrek/diving/tests/test_views.py
@@ -113,6 +113,13 @@ def test_pois_on_treks_not_public(self):
response = self.client.get(reverse('diving:dive_poi_geojson', kwargs={'lang': translation.get_language(), 'pk': dive.pk}))
self.assertEqual(response.status_code, 404)
+ def test_bbox_filter(self):
+ class DiveGoodGeomFactory(DiveFactory):
+ geom = 'Point(700000 6600000)'
+
+ self.modelfactory = DiveGoodGeomFactory
+ super(DiveViewsTests, self).test_bbox_filter()
+
class DiveViewsLiveTests(CommonLiveTest):
model = Dive
diff --git a/geotrek/diving/views.py b/geotrek/diving/views.py
index 90aa82d6a6..7e5a5ca843 100644
--- a/geotrek/diving/views.py
+++ b/geotrek/diving/views.py
@@ -28,12 +28,14 @@
class DiveLayer(MapEntityLayer):
properties = ['name', 'published']
queryset = Dive.objects.existing()
+ geometry_field_db = 'geom'
class DiveList(FlattenPicturesMixin, MapEntityList):
filterform = DiveFilterSet
columns = ['id', 'name', 'levels', 'thumbnail']
queryset = Dive.objects.existing()
+ template_name = 'diving/dive_list.html'
class DiveJsonList(MapEntityJsonList, DiveList):
diff --git a/geotrek/feedback/views.py b/geotrek/feedback/views.py
index 9f01615c52..8d43d8fc72 100644
--- a/geotrek/feedback/views.py
+++ b/geotrek/feedback/views.py
@@ -21,6 +21,7 @@ class ReportLayer(mapentity_views.MapEntityLayer):
model = feedback_models.Report
filterform = ReportFilterSet
properties = ['email']
+ geometry_field_db = 'geom'
class ReportList(mapentity_views.MapEntityList):
diff --git a/geotrek/infrastructure/views.py b/geotrek/infrastructure/views.py
index 3d34487efa..437889b706 100755
--- a/geotrek/infrastructure/views.py
+++ b/geotrek/infrastructure/views.py
@@ -20,6 +20,7 @@
class InfrastructureLayer(MapEntityLayer):
queryset = Infrastructure.objects.existing()
properties = ['name', 'published']
+ geometry_field_db = 'geom'
class InfrastructureList(MapEntityList):
diff --git a/geotrek/land/views.py b/geotrek/land/views.py
index 8ebe618059..b1f46d6187 100644
--- a/geotrek/land/views.py
+++ b/geotrek/land/views.py
@@ -12,6 +12,7 @@
class PhysicalEdgeLayer(MapEntityLayer):
queryset = PhysicalEdge.objects.existing()
properties = ['color_index', 'name']
+ geometry_field_db = 'geom'
class PhysicalEdgeList(MapEntityList):
@@ -57,6 +58,7 @@ class PhysicalEdgeDelete(MapEntityDelete):
class LandEdgeLayer(MapEntityLayer):
queryset = LandEdge.objects.existing()
properties = ['color_index', 'name']
+ geometry_field_db = 'geom'
class LandEdgeList(MapEntityList):
@@ -102,6 +104,7 @@ class LandEdgeDelete(MapEntityDelete):
class CompetenceEdgeLayer(MapEntityLayer):
queryset = CompetenceEdge.objects.existing()
properties = ['color_index', 'name']
+ geometry_field_db = 'geom'
class CompetenceEdgeList(MapEntityList):
@@ -147,6 +150,7 @@ class CompetenceEdgeDelete(MapEntityDelete):
class WorkManagementEdgeLayer(MapEntityLayer):
queryset = WorkManagementEdge.objects.existing()
properties = ['color_index', 'name']
+ geometry_field_db = 'geom'
class WorkManagementEdgeList(MapEntityList):
@@ -192,6 +196,7 @@ class WorkManagementEdgeDelete(MapEntityDelete):
class SignageManagementEdgeLayer(MapEntityLayer):
queryset = SignageManagementEdge.objects.existing()
properties = ['color_index', 'name']
+ geometry_field_db = 'geom'
class SignageManagementEdgeList(MapEntityList):
diff --git a/geotrek/maintenance/filters.py b/geotrek/maintenance/filters.py
index d10aba6788..307038f596 100644
--- a/geotrek/maintenance/filters.py
+++ b/geotrek/maintenance/filters.py
@@ -4,7 +4,7 @@
from django.utils.translation import gettext_lazy as _
from django_filters import ChoiceFilter, MultipleChoiceFilter
-from mapentity.filters import PolygonFilter, PythonPolygonFilter
+from mapentity.filters import PolygonFilter
from geotrek.core.models import Topology
from geotrek.authent.filters import StructureRelatedFilterSet
@@ -116,7 +116,6 @@ class Meta(StructureRelatedFilterSet.Meta):
class ProjectFilterSet(StructureRelatedFilterSet):
- bbox = PythonPolygonFilter(field_name='geom')
year = MultipleChoiceFilter(
label=_("Year of activity"), method='filter_year',
choices=lambda: Project.objects.year_choices() # Could change over time
diff --git a/geotrek/maintenance/templates/maintenance/project_detail.html b/geotrek/maintenance/templates/maintenance/project_detail.html
index f2735b3d7e..b90b0b1e8b 100644
--- a/geotrek/maintenance/templates/maintenance/project_detail.html
+++ b/geotrek/maintenance/templates/maintenance/project_detail.html
@@ -118,12 +118,12 @@
function showInterventionLabel (geojson, layer) {
- if (geojson.properties.name) layer.bindLabel(geojson.properties.name);
+ if (geojson.properties.name) layer.bindTooltip(geojson.properties.name);
}
- function interventionUrl(properties, layer) {
+ function interventionUrl(id) {
return window.SETTINGS.urls.detail.replace(new RegExp('modelname', 'g'), 'intervention')
- .replace('0', properties.pk);
+ .replace('0', id);
};
});
diff --git a/geotrek/maintenance/tests/test_views.py b/geotrek/maintenance/tests/test_views.py
index dfb2fa198e..d44ab20fc0 100644
--- a/geotrek/maintenance/tests/test_views.py
+++ b/geotrek/maintenance/tests/test_views.py
@@ -505,10 +505,10 @@ def jsonlist(bbox):
# Check that projects without interventions are always present
self.assertEqual(len(Project.objects.all()), 3)
self.assertEqual(len(jsonlist('')), 3)
- self.assertEqual(len(jsonlist('?bbox=POLYGON((1%202%200%2C1%202%200%2C1%202%200%2C1%202%200%2C1%202%200))')), 2)
+ self.assertEqual(len(jsonlist('?tiles=5,16,11')), 2)
# Give a bbox that match intervention, and check that all 3 projects are back
- bbox = '?bbox=POLYGON((2.9%2046.4%2C%203.1%2046.4%2C%203.1%2046.6%2C%202.9%2046.6%2C%202.9%2046.4))'
+ bbox = '?tiles=5,16,11'
self.assertEqual(len(jsonlist(bbox)), 3)
def test_deleted_interventions(self):
diff --git a/geotrek/maintenance/views.py b/geotrek/maintenance/views.py
index 38b100dd83..d7104042f9 100755
--- a/geotrek/maintenance/views.py
+++ b/geotrek/maintenance/views.py
@@ -110,6 +110,7 @@ def get_queryset(self):
class ProjectLayer(MapEntityLayer):
queryset = Project.objects.existing()
properties = ['name']
+ geometry_field_db = 'geom'
def get_queryset(self):
nonemptyqs = Intervention.objects.existing().filter(project__isnull=False).values('project')
diff --git a/geotrek/sensitivity/views.py b/geotrek/sensitivity/views.py
index e3e1efa2ab..be6e5db5f8 100644
--- a/geotrek/sensitivity/views.py
+++ b/geotrek/sensitivity/views.py
@@ -30,6 +30,7 @@
class SensitiveAreaLayer(MapEntityLayer):
queryset = SensitiveArea.objects.existing()
properties = ['species', 'radius', 'published']
+ geometry_field_db = 'geom'
class SensitiveAreaList(MapEntityList):
diff --git a/geotrek/settings/base.py b/geotrek/settings/base.py
index 6eb742d1bf..a2a32c9f09 100644
--- a/geotrek/settings/base.py
+++ b/geotrek/settings/base.py
@@ -433,6 +433,7 @@ def api_bbox(bbox, buffer):
# Let this be defined at instance-level
LEAFLET_CONFIG = {
+ # 'ANIMATE': False,
'SRID': 3857,
'TILES': [
('OpenTopoMap', 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', 'Données: © Contributeurs OpenStreetMap, SRTM | Affichage: © OpenTopoMap (CC-BY-SA)'),
diff --git a/geotrek/signage/static/signage/main.js b/geotrek/signage/static/signage/main.js
index 65ef1fc921..c83b607b01 100644
--- a/geotrek/signage/static/signage/main.js
+++ b/geotrek/signage/static/signage/main.js
@@ -10,7 +10,7 @@ $(window).on('entity:map', function (e, data) {
modelname: 'signage',
style: L.Util.extend(window.SETTINGS.map.styles['signage'] || {}, { clickable:false }),
pointToLayer: function (feature, latlng) {
- return L.marker(latlng).bindLabel(feature.properties.name, { noHide: true });
+ return L.marker(latlng).bindTooltip(feature.properties.name, { permanent: true });
}
});
var url = window.SETTINGS.urls['signage_layer'];
diff --git a/geotrek/signage/views.py b/geotrek/signage/views.py
index 0f076b1829..e2c0ea6275 100755
--- a/geotrek/signage/views.py
+++ b/geotrek/signage/views.py
@@ -31,6 +31,7 @@ class LineMixin(FormsetMixin):
class SignageLayer(MapEntityLayer):
queryset = Signage.objects.existing()
properties = ['name', 'published']
+ geometry_field_db = 'geom'
class SignageList(MapEntityList):
@@ -184,6 +185,7 @@ class BladeJsonList(MapEntityJsonList, BladeList):
class BladeLayer(MapEntityLayer):
queryset = Blade.objects.all()
properties = ['number']
+ geometry_field_db = 'signage__geom'
class BladeFormatList(MapEntityFormat, BladeList):
diff --git a/geotrek/static/style.css b/geotrek/static/style.css
index 5ea9495e40..c8aad631f8 100644
--- a/geotrek/static/style.css
+++ b/geotrek/static/style.css
@@ -159,6 +159,7 @@ span.aggregation {
border: 1px solid #d4d4d4;
padding: 5px;
width: 30%;
+ z-index: 1000;
}
#altitudegraph h4 {
@@ -237,7 +238,7 @@ body.screamshot #altitudegraph {
.poi-marker-icon img {
- max-width: 100% !important;
+ width: 100%;
}
.leaflet-marker-icon.point-reference {
diff --git a/geotrek/tourism/static/tourism/main.js b/geotrek/tourism/static/tourism/main.js
index 03e3150191..6c28467054 100644
--- a/geotrek/tourism/static/tourism/main.js
+++ b/geotrek/tourism/static/tourism/main.js
@@ -5,32 +5,15 @@
$(window).on('entity:map', function (e, data) {
var map = data.map;
- var loaded_event = false;
- var loaded_touristic = false;
// Show tourism layer in application maps
$.each(['touristiccontent', 'touristicevent'], function (i, modelname) {
- var layer = new L.ObjectsLayer(null, {
- modelname: modelname,
- style: L.Util.extend(window.SETTINGS.map.styles[modelname] || {}, {clickable:false}),
- });
if (data.modelname != modelname){
+ var layer = new L.ObjectsLayer(window.SETTINGS.urls.tile.replace('modelname', modelname), {
+ modelname: modelname,
+ style: L.Util.extend(window.SETTINGS.map.styles[modelname] || {}, {clickable:false}),
+ });
map.layerscontrol.addOverlay(layer, tr(modelname), tr('Tourism'));
};
- map.on('layeradd', function(e){
- var options = e.layer.options || {'modelname': 'None'};
- if (! loaded_event){
- if (options.modelname == 'touristicevent' && options.modelname != data.modelname){
- e.layer.load(window.SETTINGS.urls.touristicevent_layer);
- loaded_event = true;
- }
- }
- if (! loaded_touristic){
- if (options.modelname == 'touristiccontent' && options.modelname != data.modelname){
- e.layer.load(window.SETTINGS.urls.touristiccontent_layer);
- loaded_touristic = true;
- }
- }
- });
});
});
diff --git a/geotrek/tourism/tests/test_functional.py b/geotrek/tourism/tests/test_functional.py
index d623f22559..f56c35020f 100644
--- a/geotrek/tourism/tests/test_functional.py
+++ b/geotrek/tourism/tests/test_functional.py
@@ -125,6 +125,12 @@ def test_intersection_zoning(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()["map_obj_pk"]), 0)
+ def test_bbox_filter(self):
+ class TouristicContentGoodGeomFactory(TouristicContentFactory):
+ geom = 'Point(700000 6600000)'
+ self.modelfactory = TouristicContentGoodGeomFactory
+ super(TouristicContentViewsTests, self).test_bbox_filter()
+
class TouristicEventViewsTests(CommonTest):
model = TouristicEvent
@@ -234,3 +240,9 @@ def test_document_export_with_attachment(self, mock_requests):
first_path = os.path.join(settings.MEDIA_ROOT, 'maps', element_in_dir[0])
second_path = os.path.join(settings.MEDIA_ROOT, attachment.attachment_file.name)
self.assertTrue(filecmp.cmp(first_path, second_path))
+
+ def test_bbox_filter(self):
+ class TouristicEventGoodGeomFactory(TouristicEventFactory):
+ geom = 'Point(700000 6600000)'
+ self.modelfactory = TouristicEventGoodGeomFactory
+ super(TouristicEventViewsTests, self).test_bbox_filter()
diff --git a/geotrek/tourism/views.py b/geotrek/tourism/views.py
index 83b3c41700..a7ea1386d9 100644
--- a/geotrek/tourism/views.py
+++ b/geotrek/tourism/views.py
@@ -42,6 +42,7 @@
class TouristicContentLayer(MapEntityLayer):
queryset = TouristicContent.objects.existing()
properties = ['name']
+ geometry_field_db = 'geom'
class TouristicContentList(MapEntityList):
@@ -158,6 +159,7 @@ class TouristicContentMeta(MetaMixin, DetailView):
class TouristicEventLayer(MapEntityLayer):
queryset = TouristicEvent.objects.existing()
properties = ['name']
+ geometry_field_db = 'geom'
class TouristicEventList(MapEntityList):
diff --git a/geotrek/trekking/static/trekking/signagelayer.js b/geotrek/trekking/static/trekking/signagelayer.js
index d4b1a9be57..c5eca3d1c7 100644
--- a/geotrek/trekking/static/trekking/signagelayer.js
+++ b/geotrek/trekking/static/trekking/signagelayer.js
@@ -24,6 +24,6 @@ var SignagesLayer = L.GeoJSON.extend({
iconSize: [this.options.iconSize, this.options.iconSize],
html: img});
- return L.marker(latlng, {icon: serviceicon}).bindLabel(featureData.properties.name, {noHide: true});
+ return L.marker(latlng, {icon: serviceicon}).bindTooltip(featureData.properties.name, {permanent: true});
}
});
diff --git a/geotrek/trekking/tests/test_views.py b/geotrek/trekking/tests/test_views.py
index 0b1c3b0137..3746e83aa0 100755
--- a/geotrek/trekking/tests/test_views.py
+++ b/geotrek/trekking/tests/test_views.py
@@ -140,10 +140,10 @@ def test_listing_number_queries(self):
self.modelfactory.build_batch(1000)
DistrictFactory.build_batch(10)
- with self.assertNumQueries(6):
+ with self.assertNumQueries(7):
self.client.get(self.model.get_jsonlist_url())
- with self.assertNumQueries(9):
+ with self.assertNumQueries(10):
self.client.get(self.model.get_format_list_url())
def test_pois_on_treks_do_not_exist(self):
@@ -1496,8 +1496,8 @@ def test_listing_number_queries(self):
self.modelfactory.build_batch(1000)
DistrictFactory.build_batch(10)
- # 1) session, 2) user, 3) user perms, 4) group perms, 5) last modified, 6) list
- with self.assertNumQueries(6):
+ # 1) session, 2) user, 3) user perms, 4) group perms, 5) last modified, 6) list json init x3
+ with self.assertNumQueries(8):
self.client.get(self.model.get_jsonlist_url())
# 1) session, 2) user, 3) user perms, 4) group perms, 5) list
diff --git a/geotrek/trekking/views.py b/geotrek/trekking/views.py
index 45b42632f2..46f2c5e19f 100755
--- a/geotrek/trekking/views.py
+++ b/geotrek/trekking/views.py
@@ -11,7 +11,7 @@
from django.views.generic import CreateView, ListView, DetailView
from django.views.generic.detail import BaseDetailView
from mapentity.helpers import alphabet_enumeration
-from mapentity.views import (MapEntityLayer, MapEntityList, MapEntityJsonList,
+from mapentity.views import (MapEntityLayer, MapEntityTileLayer, MapEntityList, MapEntityJsonList,
MapEntityFormat, MapEntityDetail, MapEntityMapImage,
MapEntityDocument, MapEntityCreate, MapEntityUpdate,
MapEntityDelete, LastModifiedMixin, MapEntityViewSet)
@@ -57,16 +57,23 @@ def get_queryset(self):
class TrekLayer(MapEntityLayer):
properties = ['name', 'published']
queryset = Trek.objects.existing()
+ geometry_field_db = 'geom'
+
+
+class TrekTileLayer(MapEntityTileLayer):
+ queryset = Trek.objects.existing()
class TrekList(FlattenPicturesMixin, MapEntityList):
filterform = TrekFilterSet
columns = ['id', 'name', 'duration', 'difficulty', 'departure', 'thumbnail']
queryset = Trek.objects.existing()
+ template_name = 'trekking/trek_list.html'
class TrekJsonList(MapEntityJsonList, TrekList):
- pass
+ def get_context_data(self, **kwargs):
+ return super(TrekJsonList, self).get_context_data(queryset=self.filterform(self.request.GET).qs, **kwargs)
class TrekFormatList(MapEntityFormat, TrekList):
@@ -242,13 +249,15 @@ class TrekMeta(MetaMixin, DetailView):
class POILayer(MapEntityLayer):
queryset = POI.objects.existing()
properties = ['name', 'published']
+ geometry_field_db = 'geom'
class POIList(FlattenPicturesMixin, MapEntityList):
model = POI
filterform = POIFilterSet
columns = ['id', 'name', 'type', 'thumbnail']
- queryset = model.objects.existing()
+ queryset = POI.objects.existing()
+ template_name = 'trekking/poi_list.html'
class POIJsonList(MapEntityJsonList, POIList):
@@ -428,6 +437,7 @@ def get_queryset(self):
class ServiceLayer(MapEntityLayer):
properties = ['label', 'published']
queryset = Service.objects.existing()
+ geometry_field_db = 'geom'
class ServiceList(MapEntityList):
diff --git a/geotrek/zoning/views.py b/geotrek/zoning/views.py
index e43bb9c5be..85e8ea7aad 100644
--- a/geotrek/zoning/views.py
+++ b/geotrek/zoning/views.py
@@ -4,6 +4,7 @@
from django.utils.decorators import method_decorator
from djgeojson.views import GeoJSONLayerView
+from mapentity.filters import MapEntityFilterSet
from .models import City, RestrictedArea, RestrictedAreaType, District
@@ -17,6 +18,12 @@ class LandLayerMixin:
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
+ def get_queryset(self):
+ # ensure mapentity filters are working for non Mapentity view
+ qs = super().get_queryset()
+ filtered = MapEntityFilterSet(self.request.GET, queryset=qs)
+ return filtered.qs
+
class CityGeoJSONLayer(LandLayerMixin, GeoJSONLayerView):
model = City
diff --git a/mapentity/decorators.py b/mapentity/decorators.py
index 661ffe3aa9..a198b5e096 100644
--- a/mapentity/decorators.py
+++ b/mapentity/decorators.py
@@ -68,7 +68,7 @@ def decorator(view_func):
def _wrapped_view(self, request, *args, **kwargs):
view_model = self.get_model()
- cache_latest = cache_last_modified(lambda x: view_model.latest_updated())
+ cache_latest = cache_last_modified(lambda request, *args, **kwargs: view_model.latest_updated())
cbv_cache_latest = method_decorator(cache_latest)
@method_decorator(cache_control(max_age=0, must_revalidate=True))
diff --git a/mapentity/models.py b/mapentity/models.py
index 88504dd994..fa6342028a 100755
--- a/mapentity/models.py
+++ b/mapentity/models.py
@@ -24,6 +24,7 @@
# Used to create the matching url name
ENTITY_LAYER = "layer"
+ENTITY_TILE_LAYER = "tile_layer"
ENTITY_LIST = "list"
ENTITY_JSON_LIST = "json_list"
ENTITY_FORMAT_LIST = "format_list"
@@ -37,7 +38,7 @@
ENTITY_UPDATE_GEOM = "update_geom"
ENTITY_KINDS = (
- ENTITY_LAYER, ENTITY_LIST, ENTITY_JSON_LIST,
+ ENTITY_LAYER, ENTITY_TILE_LAYER, ENTITY_LIST, ENTITY_JSON_LIST,
ENTITY_FORMAT_LIST, ENTITY_DETAIL, ENTITY_MAPIMAGE, ENTITY_DOCUMENT, ENTITY_MARKUP, ENTITY_CREATE,
ENTITY_UPDATE, ENTITY_DELETE, ENTITY_UPDATE_GEOM
)
@@ -95,6 +96,7 @@ def get_entity_kind_permission(cls, entity_kind):
ENTITY_DELETE: ENTITY_PERMISSION_DELETE,
ENTITY_DETAIL: ENTITY_PERMISSION_READ,
ENTITY_LAYER: ENTITY_PERMISSION_READ,
+ ENTITY_TILE_LAYER: ENTITY_PERMISSION_READ,
ENTITY_LIST: ENTITY_PERMISSION_READ,
ENTITY_JSON_LIST: ENTITY_PERMISSION_READ,
ENTITY_MARKUP: ENTITY_PERMISSION_READ,
@@ -148,6 +150,10 @@ def delete(self, *args, **kwargs):
def get_layer_url(cls):
return reverse(cls._entity.url_name(ENTITY_LAYER))
+ @classmethod
+ def get_tile_layer_url(cls):
+ return reverse(cls._entity.url_name(ENTITY_TILE_LAYER))
+
@classmethod
def get_list_url(cls):
return reverse(cls._entity.url_name(ENTITY_LIST))
diff --git a/mapentity/registry.py b/mapentity/registry.py
index 1245bc0dcc..0c2dff24f1 100644
--- a/mapentity/registry.py
+++ b/mapentity/registry.py
@@ -65,12 +65,13 @@ def scan_views(self):
# Filter to views inherited from MapEntity base views
picked = []
rest_viewset = None
+ tile_view = None
list_view = None
for name, view in inspect.getmembers(views_module):
if inspect.isclass(view) and issubclass(view, View):
# Pick-up views
- if hasattr(view, 'get_entity_kind') or issubclass(view, mapentity_views.MapEntityViewSet):
+ if hasattr(view, 'get_entity_kind') or issubclass(view, mapentity_views.MapEntityViewSet) or issubclass(view, mapentity_views.MapEntityTileLayer):
try:
view_model = view.model or view.queryset.model
except AttributeError:
@@ -79,6 +80,8 @@ def scan_views(self):
if view_model is self.model:
if issubclass(view, mapentity_views.MapEntityViewSet):
rest_viewset = view
+ elif issubclass(view, mapentity_views.MapEntityTileLayer):
+ tile_view = view
elif issubclass(view, mapentity_views.MapEntityList):
picked.append(view)
list_view = view
@@ -123,6 +126,19 @@ class dynamic_viewset(mapentity_views.MapEntityViewSet):
self.rest_router.register(self.modelname + 's', rest_viewset, basename=self.modelname)
+ # Dynamically define tile view
+ if tile_view is None:
+ class dynamic_tile_view(mapentity_views.MapEntityTileLayer):
+ pass
+ tile_view = dynamic_tile_view
+
+ if tile_view.queryset is None:
+ tile_view.queryset = self.get_queryset()
+ if tile_view.serializer_class is None:
+ tile_view.serializer_class = self.get_tile_serializer()
+
+ picked.append(tile_view)
+
# Returns Django URL patterns
return self.__view_classes_to_url(*picked)
@@ -151,12 +167,27 @@ class Meta:
return Serializer
+ def get_tile_serializer(self):
+ _model = self.model
+
+ class Serializer(GeoFeatureModelSerializer):
+ api_geom = GeometryField(read_only=True, precision=7)
+
+ class Meta:
+ model = _model
+ geo_field = "api_geom"
+ id_field = 'id'
+ fields = ('id', )
+
+ return Serializer
+
def get_queryset(self):
return self.model.objects.all()
def _url_path(self, view_kind):
kind_to_urlpath = {
mapentity_models.ENTITY_LAYER: r'^api/{modelname}/{modelname}.geojson$',
+ mapentity_models.ENTITY_TILE_LAYER: r'^api/{modelname}/(?P\d+)/(?P\d+)/(?P\d+).geojson$',
mapentity_models.ENTITY_LIST: r'^{modelname}/list/$',
mapentity_models.ENTITY_JSON_LIST: r'^api/{modelname}/{modelname}s.json$',
mapentity_models.ENTITY_FORMAT_LIST: r'^{modelname}/list/export/$',
diff --git a/mapentity/serializers/datatables.py b/mapentity/serializers/datatables.py
index a17d5cd2f5..fb2078c89b 100644
--- a/mapentity/serializers/datatables.py
+++ b/mapentity/serializers/datatables.py
@@ -6,12 +6,12 @@
class DatatablesSerializer(Serializer):
- def serialize(self, queryset, **options):
- model = options.pop('model', None) or queryset.model
- columns = options.pop('fields')
+ def serialize(self, queryset, fields, model=None, total_records=0, total_display_records=0, echo=None, **options):
+ if model is None:
+ model = queryset.model
getters = {}
- for field in columns:
+ for field in fields:
if hasattr(model, field + '_display'):
getters[field] = lambda obj, field: getattr(obj, field + '_display')
else:
@@ -31,11 +31,14 @@ def fixfloat(obj, field):
map_obj_pk = []
data_table_rows = []
for obj in queryset:
- row = [getters[field](obj, field) for field in columns]
+ row = [getters[field](obj, field) for field in fields]
data_table_rows.append(row)
map_obj_pk.append(obj.pk)
return {
+ 'sEcho': echo,
+ 'iTotalRecords': total_records,
+ 'iTotalDisplayRecords': total_display_records,
# aaData is the key looked up by dataTables
'aaData': data_table_rows,
'map_obj_pk': map_obj_pk,
diff --git a/mapentity/settings.py b/mapentity/settings.py
index 594cf77471..e534cbdcf2 100644
--- a/mapentity/settings.py
+++ b/mapentity/settings.py
@@ -100,10 +100,6 @@
('leaflet.overintent', {
'js': 'mapentity/Leaflet.OverIntent/leaflet.overintent.js',
}),
- ('leaflet.label', {
- 'css': 'mapentity/Leaflet.label/dist/leaflet.label.css',
- 'js': 'mapentity/Leaflet.label/dist/leaflet.label.js'
- }),
('leaflet.spin', {
'js': ['paperclip/spin.min.js',
'mapentity/Leaflet.Spin/leaflet.spin.js']
@@ -135,6 +131,9 @@
'css': 'mapentity/Leaflet.groupedlayercontrol/src/leaflet.groupedlayercontrol.css',
'js': 'mapentity/Leaflet.groupedlayercontrol/src/leaflet.groupedlayercontrol.js'
}),
+ ('leaflet.geojson.grid', {
+ 'js': 'mapentity/leaflet.geojson.grid.js',
+ }),
('mapentity', {
'js': ['mapentity/mapentity.js',
'mapentity/mapentity.forms.js'],
diff --git a/mapentity/static/mapentity/Leaflet.FileLayer/README.md b/mapentity/static/mapentity/Leaflet.FileLayer/README.md
index ca77e3cc66..1cd285da10 100644
--- a/mapentity/static/mapentity/Leaflet.FileLayer/README.md
+++ b/mapentity/static/mapentity/Leaflet.FileLayer/README.md
@@ -1,7 +1,7 @@
Leaflet.FileLayer
=================
-Loads local files (GeoJSON, GPX, KML) into the map using the [HTML5 FileReader API](http://caniuse.com/filereader), **without server call** !
+Loads local files (GeoJSON, JSON, GPX, KML) into the map using the [HTML5 FileReader API](http://caniuse.com/filereader), **without server call** !
* A simple map control
* The user can browse a file locally
@@ -12,29 +12,124 @@ Check out the [demo](http://makinacorpus.github.com/Leaflet.FileLayer/) !
For GPX and KML files, it currently depends on [Tom MacWright's togeojson.js](https://github.com/tmcw/togeojson).
+[](https://travis-ci.org/makinacorpus/Leaflet.FileLayer)
+
+Install
+-----
+In order to use this plugin in your app you can either:
+* install it via your favorite package manager:
+ * `npm i leaflet-filelayer`
+ * `bower install git://github.com:makinacorpus/Leaflet.FileLayer.git`
+* download the repository and import the `leaflet.filelayer.js` file in your app.
+
+Dependencies and compatibilities
+-----
+In order to use this plugin, you need to have both `leaflet` and `togeojson` installed.
+If you're using Leaflet < 1, you need to use the version `0.6.0` of this plugin. After that, Leaflet > 1 is required.
+
Usage
-----
-```
+```javascript
var map = L.map('map').fitWorld();
...
L.Control.fileLayerLoad({
- layerOptions: {style: {color:'red'}}
+ // Allows you to use a customized version of L.geoJson.
+ // For example if you are using the Proj4Leaflet leaflet plugin,
+ // you can pass L.Proj.geoJson and load the files into the
+ // L.Proj.GeoJson instead of the L.geoJson.
+ layer: L.geoJson,
+ // See http://leafletjs.com/reference.html#geojson-options
+ layerOptions: {style: {color:'red'}},
+ // Add to map after loading (default: true) ?
+ addToMap: true,
+ // File size limit in kb (default: 1024) ?
+ fileSizeLimit: 1024,
+ // Restrict accepted file formats (default: .geojson, .json, .kml, and .gpx) ?
+ formats: [
+ '.geojson',
+ '.kml'
+ ]
}).addTo(map);
```
Events:
-```
+* **data:loaded** (event)
+
+```javascript
var control = L.Control.fileLayerLoad();
- control.loader.on('data:loaded', function (e) {
+ control.loader.on('data:loaded', function (event) {
+ // event.layer gives you access to the layers you just uploaded!
+
// Add to map layer switcher
- layerswitcher.addOverlay(e.layer, e.filename);
+ layerswitcher.addOverlay(event.layer, event.filename);
});
```
+* **data:error** (error)
+```javascript
+ var control = L.Control.fileLayerLoad();
+ control.loader.on('data:error', function (error) {
+ // Do something usefull with the error!
+ console,error(error);
+ });
+```
+
+Changelog
+---------
+
+### 1.2.0 ###
+
+* Leaflet 1.2.0 compatibility
+* Accept `json` file as input (thanks kkdd)
+
+### 1.1.0 ###
+
+* Leaflet 1.1.0 compatibility (thanks @thorinii)
+
+### 0.6.0 ###
+
+* Better plugin packaging and dependencies
+* Adding bower support (thanks @george-silva)
+* Adding support for custom geoJson layers (thanks @MuellerMatthew)
+* Treating json files as geoJson (thanks @Jmuccigr)
+
+### 0.5.0 ###
+
+* Load multiple files (thanks @jens-duttke)
+
+### 0.4.0 ###
+
+* Support whitelist for file formats (thanks CJ Cenizal)
+
+### 0.3.0 ###
+
+* Add `data:error` event (thanks @joeybaker)
+* Fix multiple uploads (thanks @joeybaker)
+* Add `addToMap` option (thanks @joeybaker)
+
+(* Did not release version 0.2 to prevent conflicts with Joey's fork. *)
+
+### 0.1.0 ###
+
+* Initial working version
Authors
-------
[](http://makinacorpus.com)
+
+Contributions
+
+* Mathieu Leplatre
+* Joey Baker http://byjoeybaker.com
+* CJ Cenizal
+* Jens-duttke
+* Jmuccigr
+* Matthew Mueller
+* George Silva
+* Simon Bats
+* Opoto
+* Lachlan Phillips
+* kkdd
diff --git a/mapentity/static/mapentity/Leaflet.FileLayer/leaflet.filelayer.js b/mapentity/static/mapentity/Leaflet.FileLayer/leaflet.filelayer.js
index 1b3df3d50e..19b6ae7d6b 100644
--- a/mapentity/static/mapentity/Leaflet.FileLayer/leaflet.filelayer.js
+++ b/mapentity/static/mapentity/Leaflet.FileLayer/leaflet.filelayer.js
@@ -1,168 +1,342 @@
/*
* Load files *locally* (GeoJSON, KML, GPX) into the map
* using the HTML5 File API.
- *
- * Requires Pavel Shramov's GPX.js
- * https://github.com/shramov/leaflet-plugins/blob/d74d67/layer/vector/GPX.js
+ *
+ * Requires Mapbox's togeojson.js to be in global scope
+ * https://github.com/mapbox/togeojson
*/
-var FileLoader = L.Class.extend({
- includes: L.Mixin.Events,
- options: {
- layerOptions: {}
- },
-
- initialize: function (map, options) {
- this._map = map;
- L.Util.setOptions(this, options);
-
- this._parsers = {
- 'geojson': this._loadGeoJSON,
- 'gpx': this._convertToGeoJSON,
- 'kml': this._convertToGeoJSON
+
+(function (factory, window) {
+ // define an AMD module that relies on 'leaflet'
+ if (typeof define === 'function' && define.amd && window.toGeoJSON) {
+ define(['leaflet'], function (L) {
+ factory(L, window.toGeoJSON);
+ });
+ } else if (typeof module === 'object' && module.exports) {
+ // require('LIBRARY') returns a factory that requires window to
+ // build a LIBRARY instance, we normalize how we use modules
+ // that require this pattern but the window provided is a noop
+ // if it's defined
+ module.exports = function (root, L, toGeoJSON) {
+ if (L === undefined) {
+ if (typeof window !== 'undefined') {
+ L = require('leaflet');
+ } else {
+ L = require('leaflet')(root);
+ }
+ }
+ if (toGeoJSON === undefined) {
+ if (typeof window !== 'undefined') {
+ toGeoJSON = require('togeojson');
+ } else {
+ toGeoJSON = require('togeojson')(root);
+ }
+ }
+ factory(L, toGeoJSON);
+ return L;
};
- },
+ } else if (typeof window !== 'undefined' && window.L && window.toGeoJSON) {
+ factory(window.L, window.toGeoJSON);
+ }
+}(function fileLoaderFactory(L, toGeoJSON) {
+ var FileLoader = L.Layer.extend({
+ options: {
+ layer: L.geoJson,
+ layerOptions: {},
+ fileSizeLimit: 1024
+ },
+
+ initialize: function (map, options) {
+ this._map = map;
+ L.Util.setOptions(this, options);
+
+ this._parsers = {
+ geojson: this._loadGeoJSON,
+ json: this._loadGeoJSON,
+ gpx: this._convertToGeoJSON,
+ kml: this._convertToGeoJSON
+ };
+ },
+
+ load: function (file, ext) {
+ var parser,
+ reader;
+
+ // Check file is defined
+ if (this._isParameterMissing(file, 'file')) {
+ return false;
+ }
+
+ // Check file size
+ if (!this._isFileSizeOk(file.size)) {
+ return false;
+ }
+
+ // Get parser for this data type
+ parser = this._getParser(file.name, ext);
+ if (!parser) {
+ return false;
+ }
+
+ // Read selected file using HTML5 File API
+ reader = new FileReader();
+ reader.onload = L.Util.bind(function (e) {
+ var layer;
+ try {
+ this.fire('data:loading', { filename: file.name, format: parser.ext });
+ layer = parser.processor.call(this, e.target.result, parser.ext);
+ this.fire('data:loaded', {
+ layer: layer,
+ filename: file.name,
+ format: parser.ext
+ });
+ } catch (err) {
+ this.fire('data:error', { error: err });
+ }
+ }, this);
+ // Testing trick: tests don't pass a real file,
+ // but an object with file.testing set to true.
+ // This object cannot be read by reader, just skip it.
+ if (!file.testing) {
+ reader.readAsText(file);
+ }
+ // We return this to ease testing
+ return reader;
+ },
- load: function (file /* File */) {
- // Check file extension
- var ext = file.name.split('.').pop(),
+ loadMultiple: function (files, ext) {
+ var readers = [];
+ if (files[0]) {
+ files = Array.prototype.slice.apply(files);
+ while (files.length > 0) {
+ readers.push(this.load(files.shift(), ext));
+ }
+ }
+ // return first reader (or false if no file),
+ // which is also used for subsequent loadings
+ return readers;
+ },
+
+ loadData: function (data, name, ext) {
+ var parser;
+ var layer;
+
+ // Check required parameters
+ if ((this._isParameterMissing(data, 'data'))
+ || (this._isParameterMissing(name, 'name'))) {
+ return;
+ }
+
+ // Check file size
+ if (!this._isFileSizeOk(data.length)) {
+ return;
+ }
+
+ // Get parser for this data type
+ parser = this._getParser(name, ext);
+ if (!parser) {
+ return;
+ }
+
+ // Process data
+ try {
+ this.fire('data:loading', { filename: name, format: parser.ext });
+ layer = parser.processor.call(this, data, parser.ext);
+ this.fire('data:loaded', {
+ layer: layer,
+ filename: name,
+ format: parser.ext
+ });
+ } catch (err) {
+ this.fire('data:error', { error: err });
+ }
+ },
+
+ _isParameterMissing: function (v, vname) {
+ if (typeof v === 'undefined') {
+ this.fire('data:error', {
+ error: new Error('Missing parameter: ' + vname)
+ });
+ return true;
+ }
+ return false;
+ },
+
+ _getParser: function (name, ext) {
+ var parser;
+ ext = ext || name.split('.').pop();
parser = this._parsers[ext];
- if (!parser) {
- window.alert("Unsupported file type " + file.type + '(' + ext + ')');
- return;
- }
- // Read selected file using HTML5 File API
- var reader = new FileReader();
- reader.onload = L.Util.bind(function (e) {
- this.fire('data:loading', {filename: file.name, format: ext});
- var layer = parser.call(this, e.target.result, ext);
- this.fire('data:loaded', {layer: layer, filename: file.name, format: ext});
- }, this);
- reader.readAsText(file);
- },
-
- _loadGeoJSON: function (content) {
- if (typeof content == 'string') {
- content = JSON.parse(content);
- }
- return L.geoJson(content, this.options.layerOptions).addTo(this._map);
- },
+ if (!parser) {
+ this.fire('data:error', {
+ error: new Error('Unsupported file type (' + ext + ')')
+ });
+ return undefined;
+ }
+ return {
+ processor: parser,
+ ext: ext
+ };
+ },
+
+ _isFileSizeOk: function (size) {
+ var fileSize = (size / 1024).toFixed(4);
+ if (fileSize > this.options.fileSizeLimit) {
+ this.fire('data:error', {
+ error: new Error(
+ 'File size exceeds limit (' +
+ fileSize + ' > ' +
+ this.options.fileSizeLimit + 'kb)'
+ )
+ });
+ return false;
+ }
+ return true;
+ },
+
+ _loadGeoJSON: function _loadGeoJSON(content) {
+ var layer;
+ if (typeof content === 'string') {
+ content = JSON.parse(content);
+ }
+ layer = this.options.layer(content, this.options.layerOptions);
+
+ if (layer.getLayers().length === 0) {
+ throw new Error('GeoJSON has no valid layers.');
+ }
- _convertToGeoJSON: function (content, format) {
- // Format is either 'gpx' or 'kml'
- if (typeof content == 'string') {
- content = ( new window.DOMParser() ).parseFromString(content, "text/xml");
+ if (this.options.addToMap) {
+ layer.addTo(this._map);
+ }
+ return layer;
+ },
+
+ _convertToGeoJSON: function _convertToGeoJSON(content, format) {
+ var geojson;
+ // Format is either 'gpx' or 'kml'
+ if (typeof content === 'string') {
+ content = (new window.DOMParser()).parseFromString(content, 'text/xml');
+ }
+ geojson = toGeoJSON[format](content);
+ return this._loadGeoJSON(geojson);
}
- var geojson = toGeoJSON[format](content);
- return this._loadGeoJSON(geojson);
- }
-});
-
-
-L.Control.FileLayerLoad = L.Control.extend({
- statics: {
- TITLE: 'Load local file (GPX, KML, GeoJSON)',
- LABEL: '⌅'
- },
- options: {
- position: 'topleft',
- fitBounds: true,
- layerOptions: {}
- },
-
- initialize: function (options) {
- L.Util.setOptions(this, options);
- this.loader = null;
- },
-
- onAdd: function (map) {
- this.loader = new FileLoader(map, {layerOptions: this.options.layerOptions});
-
- this.loader.on('data:loaded', function (e) {
- // Fit bounds after loading
- if (this.options.fitBounds) {
- window.setTimeout(function () {
- map.fitBounds(e.layer.getBounds()).zoomOut();
- }, 500);
- }
- }, this);
-
- // Initialize Drag-and-drop
- this._initDragAndDrop(map);
-
- // Initialize map control
- return this._initContainer();
- },
-
- _initDragAndDrop: function (map) {
- var fileLoader = this.loader,
- dropbox = map._container;
-
- var callbacks = {
- dragenter: function () {
- map.scrollWheelZoom.disable();
- },
- dragleave: function () {
- map.scrollWheelZoom.enable();
- },
- dragover: function (e) {
- e.stopPropagation();
- e.preventDefault();
- },
- drop: function (e) {
- e.stopPropagation();
- e.preventDefault();
+ });
+
+ var FileLayerLoad = L.Control.extend({
+ statics: {
+ TITLE: 'Load local file (GPX, KML, GeoJSON)',
+ LABEL: '⌅'
+ },
+ options: {
+ position: 'topleft',
+ fitBounds: true,
+ layerOptions: {},
+ addToMap: true,
+ fileSizeLimit: 1024
+ },
+
+ initialize: function (options) {
+ L.Util.setOptions(this, options);
+ this.loader = null;
+ },
- var files = Array.prototype.slice.apply(e.dataTransfer.files),
- i = files.length;
- setTimeout(function(){
- fileLoader.load(files.shift());
- if (files.length > 0) {
- setTimeout(arguments.callee, 25);
- }
- }, 25);
- map.scrollWheelZoom.enable();
+ onAdd: function (map) {
+ this.loader = L.FileLayer.fileLoader(map, this.options);
+
+ this.loader.on('data:loaded', function (e) {
+ // Fit bounds after loading
+ if (this.options.fitBounds) {
+ window.setTimeout(function () {
+ map.fitBounds(e.layer.getBounds());
+ }, 500);
+ }
+ }, this);
+
+ // Initialize Drag-and-drop
+ this._initDragAndDrop(map);
+
+ // Initialize map control
+ return this._initContainer();
+ },
+
+ _initDragAndDrop: function (map) {
+ var callbackName;
+ var thisLoader = this.loader;
+ var dropbox = map._container;
+
+ var callbacks = {
+ dragenter: function () {
+ map.scrollWheelZoom.disable();
+ },
+ dragleave: function () {
+ map.scrollWheelZoom.enable();
+ },
+ dragover: function (e) {
+ e.stopPropagation();
+ e.preventDefault();
+ },
+ drop: function (e) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ thisLoader.loadMultiple(e.dataTransfer.files);
+ map.scrollWheelZoom.enable();
+ }
+ };
+ for (callbackName in callbacks) {
+ if (callbacks.hasOwnProperty(callbackName)) {
+ dropbox.addEventListener(callbackName, callbacks[callbackName], false);
+ }
}
- };
- for (var name in callbacks)
- dropbox.addEventListener(name, callbacks[name], false);
- },
-
- _initContainer: function () {
- // Create an invisible file input
- var fileInput = L.DomUtil.create('input', 'hidden', container);
- fileInput.type = 'file';
- fileInput.accept = '.gpx,.kml,.geojson';
- fileInput.style.display = 'none';
- // Load on file change
- var fileLoader = this.loader;
- fileInput.addEventListener("change", function (e) {
- fileLoader.load(this.files[0]);
- }, false);
-
- // Create a button, and bind click on hidden file input
- var zoomName = 'leaflet-control-filelayer leaflet-control-zoom',
- barName = 'leaflet-bar',
- partName = barName + '-part',
- container = L.DomUtil.create('div', zoomName + ' ' + barName);
- var link = L.DomUtil.create('a', zoomName + '-in ' + partName, container);
- link.innerHTML = L.Control.FileLayerLoad.LABEL;
- link.href = '#';
- link.title = L.Control.FileLayerLoad.TITLE;
-
- var stop = L.DomEvent.stopPropagation;
- L.DomEvent
- .on(link, 'click', stop)
- .on(link, 'mousedown', stop)
- .on(link, 'dblclick', stop)
- .on(link, 'click', L.DomEvent.preventDefault)
- .on(link, 'click', function (e) {
+ },
+
+ _initContainer: function () {
+ var thisLoader = this.loader;
+
+ // Create a button, and bind click on hidden file input
+ var fileInput;
+ var zoomName = 'leaflet-control-filelayer leaflet-control-zoom';
+ var barName = 'leaflet-bar';
+ var partName = barName + '-part';
+ var container = L.DomUtil.create('div', zoomName + ' ' + barName);
+ var link = L.DomUtil.create('a', zoomName + '-in ' + partName, container);
+ link.innerHTML = L.Control.FileLayerLoad.LABEL;
+ link.href = '#';
+ link.title = L.Control.FileLayerLoad.TITLE;
+
+ // Create an invisible file input
+ fileInput = L.DomUtil.create('input', 'hidden', container);
+ fileInput.type = 'file';
+ fileInput.multiple = 'multiple';
+ if (!this.options.formats) {
+ fileInput.accept = '.gpx,.kml,.json,.geojson';
+ } else {
+ fileInput.accept = this.options.formats.join(',');
+ }
+ fileInput.style.display = 'none';
+ // Load on file change
+ fileInput.addEventListener('change', function () {
+ thisLoader.loadMultiple(this.files);
+ // reset so that the user can upload the same file again if they want to
+ this.value = '';
+ }, false);
+
+ L.DomEvent.disableClickPropagation(link);
+ L.DomEvent.on(link, 'click', function (e) {
fileInput.click();
e.preventDefault();
});
- return container;
- }
-});
+ return container;
+ }
+ });
+
+ L.FileLayer = {};
+ L.FileLayer.FileLoader = FileLoader;
+ L.FileLayer.fileLoader = function (map, options) {
+ return new L.FileLayer.FileLoader(map, options);
+ };
-L.Control.fileLayerLoad = function (options) {
- return new L.Control.FileLayerLoad(options);
-};
+ L.Control.FileLayerLoad = FileLayerLoad;
+ L.Control.fileLayerLoad = function (options) {
+ return new L.Control.FileLayerLoad(options);
+ };
+}, window));
diff --git a/mapentity/static/mapentity/Leaflet.FileLayer/package.json b/mapentity/static/mapentity/Leaflet.FileLayer/package.json
index 34e2950068..3ca8216cf0 100644
--- a/mapentity/static/mapentity/Leaflet.FileLayer/package.json
+++ b/mapentity/static/mapentity/Leaflet.FileLayer/package.json
@@ -1,21 +1,40 @@
{
- "name": "leaflet-filelayer"
- , "version": "0.1.0"
- , "description": "Loads local files (GeoJSON, GPX, KML) into the map using the HTML5 FileReader API"
- , "keywords": ["Leaflet", "GIS", "HTML5"]
- , "main": "leaflet.filelayer.js"
- , "bugs": {
+ "name": "leaflet-filelayer",
+ "version": "1.2.0",
+ "description": "Loads local files (GeoJSON, GPX, KML) into the map using the HTML5 FileReader API",
+ "keywords": [
+ "Leaflet",
+ "GIS",
+ "HTML5"
+ ],
+ "main": "src/leaflet.filelayer.js",
+ "bugs": {
"url": "https://github.com/makinacorpus/Leaflet.FileLayer/issues"
- }
- , "repository": {
+ },
+ "repository": {
"type": "git",
"url": "git://github.com/makinacorpus/Leaflet.FileLayer.git"
- }
- , "license": "MIT"
- , "scripts": {
- }
- , "dependencies": {
- "leaflet": "*",
- "togeojson": "*"
+ },
+ "license": "MIT",
+ "scripts": {
+ "test": "mocha-phantomjs -p ./node_modules/.bin/phantomjs test/index.html",
+ "start": "live-server --open=./dev"
+ },
+ "peerDependencies": {
+ "leaflet": ">= 0.7.7 < 2",
+ "togeojson": "^0.16.0"
+ },
+ "devDependencies": {
+ "chai": "^3.5.0",
+ "eslint": "^3.2.2",
+ "eslint-config-airbnb-base": "^5.0.1",
+ "eslint-plugin-import": "^1.12.0",
+ "happen": "^0.3.1",
+ "live-server": "^1.1.0",
+ "mocha": "^4.0.1",
+ "mocha-phantomjs": "^4.1.0",
+ "phantomjs-prebuilt": "^2.1.16",
+ "requirejs": "^2.2.0",
+ "sinon": "^1.17.5"
}
}
diff --git a/mapentity/static/mapentity/Leaflet.GeometryUtil/dist/leaflet.geometryutil.js b/mapentity/static/mapentity/Leaflet.GeometryUtil/dist/leaflet.geometryutil.js
index 56ee763a38..145a8428ce 100644
--- a/mapentity/static/mapentity/Leaflet.GeometryUtil/dist/leaflet.geometryutil.js
+++ b/mapentity/static/mapentity/Leaflet.GeometryUtil/dist/leaflet.geometryutil.js
@@ -17,6 +17,11 @@
}(function (L) {
"use strict";
+L.Polyline._flat = L.LineUtil.isFlat || L.Polyline._flat || function (latlngs) {
+ // true if it's a flat array of latlngs; false if nested
+ return !L.Util.isArray(latlngs[0]) || (typeof latlngs[0][0] !== 'object' && typeof latlngs[0][0] !== 'undefined');
+};
+
/**
* @fileOverview Leaflet Geometry utilities for distances and linear referencing.
* @name L.GeometryUtil
@@ -26,10 +31,13 @@ L.GeometryUtil = L.extend(L.GeometryUtil || {}, {
/**
Shortcut function for planar distance between two {L.LatLng} at current zoom.
- @param {L.Map} map
- @param {L.LatLng} latlngA
- @param {L.LatLng} latlngB
- @returns {Number} in pixels
+
+ @tutorial distance-length
+
+ @param {L.Map} map Leaflet map to be used for this method
+ @param {L.LatLng} latlngA geographical point A
+ @param {L.LatLng} latlngB geographical point B
+ @returns {Number} planar distance
*/
distance: function (map, latlngA, latlngB) {
return map.latLngToLayerPoint(latlngA).distanceTo(map.latLngToLayerPoint(latlngB));
@@ -37,11 +45,11 @@ L.GeometryUtil = L.extend(L.GeometryUtil || {}, {
/**
Shortcut function for planar distance between a {L.LatLng} and a segment (A-B).
- @param {L.Map} map
- @param {L.LatLng} latlng
- @param {L.LatLng} latlngA
- @param {L.LatLng} latlngB
- @returns {Number} in pixels
+ @param {L.Map} map Leaflet map to be used for this method
+ @param {L.LatLng} latlng - The position to search
+ @param {L.LatLng} latlngA geographical point A of the segment
+ @param {L.LatLng} latlngB geographical point B of the segment
+ @returns {Number} planar distance
*/
distanceSegment: function (map, latlng, latlngA, latlngB) {
var p = map.latLngToLayerPoint(latlng),
@@ -52,9 +60,9 @@ L.GeometryUtil = L.extend(L.GeometryUtil || {}, {
/**
Shortcut function for converting distance to readable distance.
- @param {Number} distance
- @param {String} unit ('metric' or 'imperial')
- @returns {Number} in yard or miles
+ @param {Number} distance distance to be converted
+ @param {String} unit 'metric' or 'imperial'
+ @returns {String} in yard or miles
*/
readableDistance: function (distance, unit) {
var isMetric = (unit !== 'imperial'),
@@ -81,11 +89,11 @@ L.GeometryUtil = L.extend(L.GeometryUtil || {}, {
},
/**
- Returns true if the latlng belongs to segment.
- param {L.LatLng} latlng
- @param {L.LatLng} latlngA
- @param {L.LatLng} latlngB
- @param {?Number} [tolerance=0.2]
+ Returns true if the latlng belongs to segment A-B
+ @param {L.LatLng} latlng - The position to search
+ @param {L.LatLng} latlngA geographical point A of the segment
+ @param {L.LatLng} latlngB geographical point B of the segment
+ @param {?Number} [tolerance=0.2] tolerance to accept if latlng belongs really
@returns {boolean}
*/
belongsSegment: function(latlng, latlngA, latlngB, tolerance) {
@@ -97,8 +105,10 @@ L.GeometryUtil = L.extend(L.GeometryUtil || {}, {
/**
* Returns total length of line
- * @param {L.Polyline|Array|Array}
- * @returns {Number} in meters
+ * @tutorial distance-length
+ *
+ * @param {L.Polyline|Array|Array} coords Set of coordinates
+ * @returns {Number} Total length (pixels for Point, meters for LatLng)
*/
length: function (coords) {
var accumulated = L.GeometryUtil.accumulatedLengths(coords);
@@ -107,8 +117,8 @@ L.GeometryUtil = L.extend(L.GeometryUtil || {}, {
/**
* Returns a list of accumulated length along a line.
- * @param {L.Polyline|Array|Array}
- * @returns {Number} in meters
+ * @param {L.Polyline|Array|Array} coords Set of coordinates
+ * @returns {Array} Array of accumulated lengths (pixels for Point, meters for LatLng)
*/
accumulatedLengths: function (coords) {
if (typeof coords.getLatLngs == 'function') {
@@ -127,11 +137,14 @@ L.GeometryUtil = L.extend(L.GeometryUtil || {}, {
/**
Returns the closest point of a {L.LatLng} on the segment (A-B)
- @param {L.Map} map
- @param {L.LatLng} latlng
- @param {L.LatLng} latlngA
- @param {L.LatLng} latlngB
- @returns {L.LatLng}
+
+ @tutorial closest
+
+ @param {L.Map} map Leaflet map to be used for this method
+ @param {L.LatLng} latlng - The position to search
+ @param {L.LatLng} latlngA geographical point A of the segment
+ @param {L.LatLng} latlngB geographical point B of the segment
+ @returns {L.LatLng} Closest geographical point
*/
closestOnSegment: function (map, latlng, latlngA, latlngB) {
var maxzoom = map.getMaxZoom();
@@ -146,59 +159,123 @@ L.GeometryUtil = L.extend(L.GeometryUtil || {}, {
/**
Returns the closest latlng on layer.
- @param {L.Map} map
- @param {Array|L.PolyLine} layer - Layer that contains the result.
- @param {L.LatLng} latlng
+
+ Accept nested arrays
+
+ @tutorial closest
+
+ @param {L.Map} map Leaflet map to be used for this method
+ @param {Array|Array>|L.PolyLine|L.Polygon} layer - Layer that contains the result
+ @param {L.LatLng} latlng - The position to search
@param {?boolean} [vertices=false] - Whether to restrict to path vertices.
- @returns {L.LatLng}
+ @returns {L.LatLng} Closest geographical point or null if layer param is incorrect
*/
closest: function (map, layer, latlng, vertices) {
- if (typeof layer.getLatLngs != 'function')
- layer = L.polyline(layer);
- var latlngs = layer.getLatLngs().slice(0),
+ var latlngs,
mindist = Infinity,
result = null,
- i, n, distance;
-
- // Lookup vertices
- if (vertices) {
- for(i = 0, n = latlngs.length; i < n; i++) {
- var ll = latlngs[i];
- distance = L.GeometryUtil.distance(map, latlng, ll);
- if (distance < mindist) {
- mindist = distance;
- result = ll;
- result.distance = distance;
+ i, n, distance, subResult;
+
+ if (layer instanceof Array) {
+ // if layer is Array>
+ if (layer[0] instanceof Array && typeof layer[0][0] !== 'number') {
+ // if we have nested arrays, we calc the closest for each array
+ // recursive
+ for (i = 0; i < layer.length; i++) {
+ subResult = L.GeometryUtil.closest(map, layer[i], latlng, vertices);
+ if (subResult && subResult.distance < mindist) {
+ mindist = subResult.distance;
+ result = subResult;
+ }
}
+ return result;
+ } else if (layer[0] instanceof L.LatLng
+ || typeof layer[0][0] === 'number'
+ || typeof layer[0].lat === 'number') { // we could have a latlng as [x,y] with x & y numbers or {lat, lng}
+ layer = L.polyline(layer);
+ } else {
+ return result;
}
- return result;
}
+ // if we don't have here a Polyline, that means layer is incorrect
+ // see https://github.com/makinacorpus/Leaflet.GeometryUtil/issues/23
+ if (! ( layer instanceof L.Polyline ) )
+ return result;
+
+ // deep copy of latlngs
+ latlngs = JSON.parse(JSON.stringify(layer.getLatLngs().slice(0)));
+
+ // add the last segment for L.Polygon
if (layer instanceof L.Polygon) {
- latlngs.push(latlngs[0]);
+ // add the last segment for each child that is a nested array
+ var addLastSegment = function(latlngs) {
+ if (L.Polyline._flat(latlngs)) {
+ latlngs.push(latlngs[0]);
+ } else {
+ for (var i = 0; i < latlngs.length; i++) {
+ addLastSegment(latlngs[i]);
+ }
+ }
+ };
+ addLastSegment(latlngs);
}
- // Keep the closest point of all segments
- for (i = 0, n = latlngs.length; i < n-1; i++) {
- var latlngA = latlngs[i],
- latlngB = latlngs[i+1];
- distance = L.GeometryUtil.distanceSegment(map, latlng, latlngA, latlngB);
- if (distance <= mindist) {
- mindist = distance;
- result = L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB);
- result.distance = distance;
+ // we have a multi polygon / multi polyline / polygon with holes
+ // use recursive to explore and return the good result
+ if ( ! L.Polyline._flat(latlngs) ) {
+ for (i = 0; i < latlngs.length; i++) {
+ // if we are at the lower level, and if we have a L.Polygon, we add the last segment
+ subResult = L.GeometryUtil.closest(map, latlngs[i], latlng, vertices);
+ if (subResult.distance < mindist) {
+ mindist = subResult.distance;
+ result = subResult;
+ }
}
+ return result;
+
+ } else {
+
+ // Lookup vertices
+ if (vertices) {
+ for(i = 0, n = latlngs.length; i < n; i++) {
+ var ll = latlngs[i];
+ distance = L.GeometryUtil.distance(map, latlng, ll);
+ if (distance < mindist) {
+ mindist = distance;
+ result = ll;
+ result.distance = distance;
+ }
+ }
+ return result;
+ }
+
+ // Keep the closest point of all segments
+ for (i = 0, n = latlngs.length; i < n-1; i++) {
+ var latlngA = latlngs[i],
+ latlngB = latlngs[i+1];
+ distance = L.GeometryUtil.distanceSegment(map, latlng, latlngA, latlngB);
+ if (distance <= mindist) {
+ mindist = distance;
+ result = L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB);
+ result.distance = distance;
+ }
+ }
+ return result;
}
- return result;
+
},
/**
Returns the closest layer to latlng among a list of layers.
- @param {L.Map} map
- @param {Array} layers
- @param {L.LatLng} latlng
- @returns {object} with layer, latlng and distance or {null} if list is empty;
+
+ @tutorial closest
+
+ @param {L.Map} map Leaflet map to be used for this method
+ @param {Array} layers Set of layers
+ @param {L.LatLng} latlng - The position to search
+ @returns {object} ``{layer, latlng, distance}`` or ``null`` if list is empty;
*/
closestLayer: function (map, layers, latlng) {
var mindist = Infinity,
@@ -208,31 +285,132 @@ L.GeometryUtil = L.extend(L.GeometryUtil || {}, {
for (var i = 0, n = layers.length; i < n; i++) {
var layer = layers[i];
- // Single dimension, snap on points, else snap on closest
- if (typeof layer.getLatLng == 'function') {
- ll = layer.getLatLng();
- distance = L.GeometryUtil.distance(map, latlng, ll);
- }
- else {
- ll = L.GeometryUtil.closest(map, layer, latlng);
- if (ll) distance = ll.distance; // Can return null if layer has no points.
- }
- if (distance < mindist) {
- mindist = distance;
- result = {layer: layer, latlng: ll, distance: distance};
+ if (layer instanceof L.LayerGroup) {
+ // recursive
+ var subResult = L.GeometryUtil.closestLayer(map, layer.getLayers(), latlng);
+ if (subResult.distance < mindist) {
+ mindist = subResult.distance;
+ result = subResult;
+ }
+ } else {
+ // Single dimension, snap on points, else snap on closest
+ if (typeof layer.getLatLng == 'function') {
+ ll = layer.getLatLng();
+ distance = L.GeometryUtil.distance(map, latlng, ll);
+ }
+ else {
+ ll = L.GeometryUtil.closest(map, layer, latlng);
+ if (ll) distance = ll.distance; // Can return null if layer has no points.
+ }
+ if (distance < mindist) {
+ mindist = distance;
+ result = {layer: layer, latlng: ll, distance: distance};
+ }
}
}
return result;
},
+ /**
+ Returns the n closest layers to latlng among a list of input layers.
+
+ @param {L.Map} map - Leaflet map to be used for this method
+ @param {Array} layers - Set of layers
+ @param {L.LatLng} latlng - The position to search
+ @param {?Number} [n=layers.length] - the expected number of output layers.
+ @returns {Array