diff --git a/cadasta/core/static/css/single.scss b/cadasta/core/static/css/single.scss index b5904210b..ac4261d1c 100644 --- a/cadasta/core/static/css/single.scss +++ b/cadasta/core/static/css/single.scss @@ -541,7 +541,6 @@ #project-map { width: 100%; } - } diff --git a/cadasta/core/static/js/map_utils.js b/cadasta/core/static/js/map_utils.js index 7b920099b..ff3a4c69b 100644 --- a/cadasta/core/static/js/map_utils.js +++ b/cadasta/core/static/js/map_utils.js @@ -63,10 +63,9 @@ function renderFeatures(map, featuresUrl, options) { } else { $('#messages #loading').addClass('hidden'); if (options.fitBounds === 'locations') { - // var bounds = markers.getBounds(); - var bounds = geoJson.getBounds(); + var bounds = markers.getBounds(); if (bounds.isValid()) { - map.fitBounds(bounds); + map.fitBounds(bounds); } } } @@ -97,7 +96,7 @@ function renderFeatures(map, featuresUrl, options) { } else { map.fitBounds([[-45.0, -180.0], [45.0, 180.0]]); } - + var geoJson = L.geoJson(null, { style: { weight: 2 }, onEachFeature: function(feature, layer) { @@ -106,17 +105,18 @@ function renderFeatures(map, featuresUrl, options) { "

Location" + feature.properties.type + "

" + "
" + options.trans['open'] + "" + - "
"); + ""); } } }); - // var markers = L.Deflate({minSize: 20, layerGroup: geoJson}); - // markers.addTo(map); + + var markers = L.Deflate({minSize: 20, layerGroup: geoJson}); + markers.addTo(map); geoJson.addTo(map); if (options.location) { options.location.addTo(map); - map.fitBounds(options.location.getBounds()); + map.fitBounds(options.location.getBounds()); } else if (projectBounds) { map.fitBounds(projectBounds); } @@ -142,7 +142,7 @@ function switch_layer_controls(map, options){ var groupedOptions = { groupCheckboxes: false }; - // map.removeControl(map.layerscontrol); + map.removeControl(map.layerscontrol); map.layerscontrol = L.control.groupedLayers( baseLayers, groupedOptions).addTo(map); } @@ -186,7 +186,3 @@ function saveOnMapEditMode() { saveButton.dispatchEvent(clickEvent); } } - - - - diff --git a/cadasta/core/static/js/smap/L.Map.Deflate4.js b/cadasta/core/static/js/smap/L.Map.Deflate4.js new file mode 100644 index 000000000..69d8c1326 --- /dev/null +++ b/cadasta/core/static/js/smap/L.Map.Deflate4.js @@ -0,0 +1,141 @@ +L.Deflate4 = L.LayerGroup.extend({ + options: { + minSize: 10, + markerCluster: false + }, + + initialize: function (options) { + L.Util.setOptions(this, options); + this._allLayers = []; + this._featureGroup = this.options.markerCluster ? L.markerClusterGroup() : L.featureGroup(); + }, + + _isCollapsed: function(path, zoom) { + var bounds = path.getBounds(); + + var ne_px = this._map.project(bounds.getNorthEast(), zoom); + var sw_px = this._map.project(bounds.getSouthWest(), zoom); + + var width = ne_px.x - sw_px.x; + var height = sw_px.y - ne_px.y; + return (height < this.options.minSize || width < this.options.minSize); + }, + + _getZoomThreshold: function(path) { + var zoomThreshold = null; + var zoom = this._map.getZoom(); + if (this._isCollapsed(path, this._map.getZoom())) { + while (!zoomThreshold) { + zoom += 1; + if (!this._isCollapsed(path, zoom)) { + zoomThreshold = zoom - 1; + } + } + } else { + while (!zoomThreshold) { + zoom -= 1; + if (this._isCollapsed(path, zoom)) { + zoomThreshold = zoom; + } + } + } + return zoomThreshold; + }, + + addLayer: function (layer) { + if (layer instanceof L.FeatureGroup) { + for (var i in layer._layers) { + this.addLayer(layer._layers[i]); + } + } else { + var layerToAdd = layer; + if (layer.getBounds && !layer.zoomThreshold && !layer.marker) { + var zoomThreshold = this._getZoomThreshold(layer); + var marker = L.marker(layer.getBounds().getCenter()); + + if (layer._popupHandlersAdded) { + marker.bindPopup(layer._popup._content) + } + + var events = layer._events; + for (var event in events) { + if (events.hasOwnProperty(event)) { + var listeners = events[event]; + for (var i = 0, len = listeners.length; i < len; i++) { + marker.on(event, listeners[i].fn) + } + } + } + + layer.zoomThreshold = zoomThreshold; + layer.marker = marker; + layer.zoomState = this._map.getZoom(); + + if (this._map.getZoom() <= zoomThreshold) { + layerToAdd = layer.marker; + } + this._allLayers.push(layer); + } + + this._featureGroup.addLayer(layerToAdd); + } + }, + + removeLayer(layer) { + if (layer instanceof L.FeatureGroup) { + for (var i in layer._layers) { + this.removeLayer(layer._layers[i]); + } + } else { + this._featureGroup.removeLayer(layer.marker); + this._featureGroup.removeLayer(layer); + + const index = this._allLayers.indexOf(layer); + if (index !== -1) { this._allLayers.splice(index, 1); } + } + }, + + _switchDisplay: function(layer, showMarker) { + if (showMarker) { + this._featureGroup.addLayer(layer.marker); + this._featureGroup.removeLayer(layer); + } else { + this._featureGroup.addLayer(layer); + this._featureGroup.removeLayer(layer.marker); + } + }, + + _deflate: function() { + const bounds = this._map.getBounds(); + const endZoom = this._map.getZoom(); + var markersToAdd = []; + var markersToRemove = []; + + for (var i = 0, len = this._allLayers.length; i < len; i++) { + if (this._allLayers[i].zoomState !== endZoom && this._allLayers[i].getBounds().intersects(bounds)) { + this._switchDisplay(this._allLayers[i], endZoom <= this._allLayers[i].zoomThreshold); + this._allLayers[i].zoomState = endZoom; + } + } + }, + + getBounds: function() { + return this._featureGroup.getBounds(); + }, + + onAdd: function(map) { + this._featureGroup.addTo(map); + this._map.on('zoomend', this._deflate, this); + this._map.on('dragend', this._deflate, this); + }, + + onRemove: function(map) { + map.removeLayer(this._featureGroup); + this._map.off('zoomend', this._deflate, this); + this._map.off('dragend', this._deflate, this); + } +}); + +L.deflate4 = function (options) { + return new L.Deflate4(options); +}; \ No newline at end of file diff --git a/cadasta/core/static/js/smap/L.TileLayer.GeoJSON.js b/cadasta/core/static/js/smap/L.TileLayer.GeoJSON.js new file mode 100644 index 000000000..5e19b8cf7 --- /dev/null +++ b/cadasta/core/static/js/smap/L.TileLayer.GeoJSON.js @@ -0,0 +1,291 @@ +// Load data tiles from an AJAX data source +L.TileLayer.Ajax = L.TileLayer.extend({ + _requests: [], + _loadedfeatures: {}, + _tiles: {}, + _addTile: function (tilePoint) { + var tile = { datum: null, processed: false }; + this._tiles[tilePoint.x + ':' + tilePoint.y] = { + el: tile, + coords: { + x: tilePoint.x, + y: tilePoint.y, + z: tilePoint.z + }, + current: true + }; + this._loadTile(tile, tilePoint); + }, + // XMLHttpRequest handler; closure over the XHR object, the layer, and the tile + _xhrHandler: function (req, layer, tile, tilePoint) { + return function () { + if (req.readyState !== 4) { + return; + } + var s = req.status; + if ((s >= 200 && s < 300 && s != 204) || s === 304) { + tile.datum = JSON.parse(req.responseText); + layer._tileLoaded(tile, tilePoint); + } else { + layer._tileLoaded(tile, tilePoint); + } + }; + }, + // Load the requested tile via AJAX + _loadTile: function (tile, tilePoint) { + // **** + // _update has been refactored is no longer required? + // **** + // this._update(tilePoint); + var layer = this; + var req = new XMLHttpRequest(); + this._requests.push(req); + req.onreadystatechange = this._xhrHandler(req, layer, tile, tilePoint); + req.open('GET', this.getTileUrl(tilePoint), true); + req.send(); + }, + _reset: function () { + L.TileLayer.prototype._reset.apply(this, arguments); + for (var i = 0; i < this._requests.length; i++) { + this._requests[i].abort(); + } + this._requests = []; + }, + _update: function () { + if (this._map && this._map._panTransition && this._map._panTransition._inProgress) { return; } + if (this._tilesToLoad < 0) { this._tilesToLoad = 0; } + L.TileLayer.prototype._update.apply(this, arguments); + } +}); + + +L.TileLayer.GeoJSON = L.TileLayer.Ajax.extend({ + // Store each GeometryCollection's layer by key, if options.unique function is present + _keyLayers: {}, + + // Used to calculate svg path string for clip path elements + _clipPathRectangles: {}, + + initialize: function (url, options, geojsonOptions) { + L.TileLayer.Ajax.prototype.initialize.call(this, url, options); + this.geojsonLayer = new L.GeoJSON(null, geojsonOptions); + }, + onAdd: function (map) { + this._lazyTiles = new Tile(0, 0, 0, map.maxZoom); + this._map = map; + L.TileLayer.Ajax.prototype.onAdd.call(this, map); + map.addLayer(this.geojsonLayer); + }, + onRemove: function (map) { + map.removeLayer(this.geojsonLayer); + L.TileLayer.Ajax.prototype.onRemove.call(this, map); + }, + _reset: function () { + this.geojsonLayer.clearLayers(); + this._keyLayers = {}; + this._removeOldClipPaths(); + L.TileLayer.Ajax.prototype._reset.apply(this, arguments); + }, + + _getUniqueId: function() { + return String(this._leaflet_id || ''); // jshint ignore:line + }, + + // Remove clip path elements from other earlier zoom levels + _removeOldClipPaths: function () { + for (var clipPathId in this._clipPathRectangles) { + var prefix = clipPathId.split('tileClipPath')[0]; + if (this._getUniqueId() === prefix) { + var clipPathZXY = clipPathId.split('_').slice(1); + var zoom = parseInt(clipPathZXY[0], 10); + if (zoom !== this._map.getZoom()) { + var rectangle = this._clipPathRectangles[clipPathId]; + this._map.removeLayer(rectangle); + var clipPath = document.getElementById(clipPathId); + if (clipPath !== null) { + clipPath.parentNode.removeChild(clipPath); + } + delete this._clipPathRectangles[clipPathId]; + } + } + } + }, + + // Recurse LayerGroups and call func() on L.Path layer instances + _recurseLayerUntilPath: function (func, layer) { + if (layer instanceof L.Path) { + func(layer); + } + else if (layer instanceof L.LayerGroup) { + // Recurse each child layer + layer.getLayers().forEach(this._recurseLayerUntilPath.bind(this, func), this); + } + }, + + _clipLayerToTileBoundary: function (layer, tilePoint) { + // Only perform SVG clipping if the browser is using SVG + if (!L.Path.SVG) { return; } + if (!this._map) { return; } + + if (!this._map._pathRoot) { + this._map._pathRoot = L.Path.prototype._createElement('svg'); + this._map._panes.overlayPane.appendChild(this._map._pathRoot); + } + var svg = this._map._pathRoot; + + // create the defs container if it doesn't exist + var defs = null; + if (svg.getElementsByTagName('defs').length === 0) { + defs = document.createElementNS(L.Path.SVG_NS, 'defs'); + svg.insertBefore(defs, svg.firstChild); + } + else { + defs = svg.getElementsByTagName('defs')[0]; + } + + // Create the clipPath for the tile if it doesn't exist + var clipPathId = this._getUniqueId() + 'tileClipPath_' + tilePoint.z + '_' + tilePoint.x + '_' + tilePoint.y; + var clipPath = document.getElementById(clipPathId); + if (clipPath === null) { + clipPath = document.createElementNS(L.Path.SVG_NS, 'clipPath'); + clipPath.id = clipPathId; + + // Create a hidden L.Rectangle to represent the tile's area + var tileSize = this.options.tileSize, + nwPoint = tilePoint.multiplyBy(tileSize), + sePoint = nwPoint.add([tileSize, tileSize]), + nw = this._map.unproject(nwPoint), + se = this._map.unproject(sePoint); + this._clipPathRectangles[clipPathId] = new L.Rectangle(new L.LatLngBounds([nw, se]), { + opacity: 0, + fillOpacity: 0, + clickable: false, + noClip: true + }); + this._map.addLayer(this._clipPathRectangles[clipPathId]); + + // Add a clip path element to the SVG defs element + // With a path element that has the hidden rectangle's SVG path string + var path = document.createElementNS(L.Path.SVG_NS, 'path'); + var pathString = this._clipPathRectangles[clipPathId].getPathString(); + path.setAttribute('d', pathString); + clipPath.appendChild(path); + defs.appendChild(clipPath); + } + + // Add the clip-path attribute to reference the id of the tile clipPath + this._recurseLayerUntilPath(function (pathLayer) { + pathLayer._container.setAttribute('clip-path', 'url(' + window.location.href + '#' + clipPathId + ')'); + }, layer); + }, + + // Add a geojson object from a tile to the GeoJSON layer + // * If the options.unique function is specified, merge geometries into GeometryCollections + // grouped by the key returned by options.unique(feature) for each GeoJSON feature + // * If options.clipTiles is set, and the browser is using SVG, perform SVG clipping on each + // tile's GeometryCollection + addTileData: function (geojson, tilePoint) { + var features = L.Util.isArray(geojson) ? geojson : geojson.features, + i, len, feature; + if (features) { + $('#messages #loading').removeClass('hidden'); + for (i = 0, len = features.length; i < len; i++) { + // Only add this if geometry or geometries are set and not null + feature = features[i]; + if (feature.geometries || feature.geometry || feature.features || feature.coordinates) { + // **** + // Added to prevent reloading of already loaded SUs. + // **** + if (!this._loadedfeatures[features[i]['id']]){ + this.addTileData(features[i], tilePoint); + this._loadedfeatures[features[i]['id']] = true; + } + } + } + $('#messages #loading').addClass('hidden'); + return this; + } + + var options = this.geojsonLayer.options; + + if (options.filter && !options.filter(geojson)) { return; } + + var parentLayer = this.geojsonLayer; + var incomingLayer = null; + if (this.options.unique && typeof(this.options.unique) === 'function') { + var key = this.options.unique(geojson); + + // When creating the layer for a unique key, + // Force the geojson to be a geometry collection + if (!(key in this._keyLayers && geojson.geometry.type !== 'GeometryCollection')) { + geojson.geometry = { + type: 'GeometryCollection', + geometries: [geojson.geometry] + }; + } + + // Transform the geojson into a new Layer + try { + incomingLayer = L.GeoJSON.geometryToLayer(geojson, options.pointToLayer, options.coordsToLatLng); + } + // Ignore GeoJSON objects that could not be parsed + catch (e) { + return this; + } + + incomingLayer.feature = L.GeoJSON.asFeature(geojson); + // Add the incoming Layer to existing key's GeometryCollection + if (key in this._keyLayers) { + parentLayer = this._keyLayers[key]; + parentLayer.feature.geometry.geometries.push(geojson.geometry); + } + // Convert the incoming GeoJSON feature into a new GeometryCollection layer + else { + this._keyLayers[key] = incomingLayer; + } + } + // Add the incoming geojson feature to the L.GeoJSON Layer + else { + // Transform the geojson into a new layer + try { + incomingLayer = L.GeoJSON.geometryToLayer(geojson, options.pointToLayer, options.coordsToLatLng); + } + // Ignore GeoJSON objects that could not be parsed + catch (e) { + return this; + } + incomingLayer.feature = L.GeoJSON.asFeature(geojson); + } + incomingLayer.defaultOptions = incomingLayer.options; + + this.geojsonLayer.resetStyle(incomingLayer); + + if (options.onEachFeature) { + options.onEachFeature(geojson, incomingLayer); + } + parentLayer.addLayer(incomingLayer); + + // If options.clipTiles is set and the browser is using SVG + // then clip the layer using SVG clipping + if (this.options.clipTiles) { + this._clipLayerToTileBoundary(incomingLayer, tilePoint); + } + return this; + }, + + _tileLoaded: function (tile, tilePoint) { + // **** + // _tileOnLoad has been refactored and requires a callback function + // **** + // L.TileLayer.Ajax.prototype._tileOnLoad.apply(this, tile); + if (tile.datum === null) { return null; } + this.addTileData(tile.datum, tilePoint); + this._lazyTiles.load(tilePoint.x, tilePoint.y, tilePoint.z); + }, + + _loadTile: function (tile, tilePoint) { + if (!this._lazyTiles.isLoaded(tilePoint.x, tilePoint.y, tilePoint.z)) { + L.TileLayer.Ajax.prototype._loadTile.call(this, tile, tilePoint); + } + }, +}); diff --git a/cadasta/core/static/js/smap/index.js b/cadasta/core/static/js/smap/index.js index f3c533caa..4847de9eb 100644 --- a/cadasta/core/static/js/smap/index.js +++ b/cadasta/core/static/js/smap/index.js @@ -1,5 +1,10 @@ $(window).load(function () { - var js_files = ['map.js'] + var js_files = [ + 'lazytiles.js', + 'L.Map.Deflate4.js', + 'L.TileLayer.GeoJSON.js', + 'map.js' + ]; var body = $('body') for (i in js_files) { body.append($('')); diff --git a/cadasta/core/static/js/smap/lazytiles.js b/cadasta/core/static/js/smap/lazytiles.js new file mode 100644 index 000000000..91ee73a8b --- /dev/null +++ b/cadasta/core/static/js/smap/lazytiles.js @@ -0,0 +1,86 @@ +;(function(window, Math) { + function Tile(x, y, z, maxLevels, parent) { + this.x = x; + this.y = y; + this.z = z; + this.loaded = false; + this.maxLevels = maxLevels || 18; + this.children = []; + this.parent = parent || null; + } + + Tile.prototype.findPathToChild = function (x, y, z) { + const nleafs = Math.pow(2, z - this.z); + const l0x = nleafs * this.x; + const l0y = nleafs * this.y; + const xleft = (x < l0x + (nleafs / 2)); + const yleft = (y < l0y + (nleafs / 2)); + xt = xleft ? this.x * 2 : this.x * 2 + 1; + yt = yleft ? this.y * 2 : this.y * 2 + 1; + + if (!this.children.length && this.z < this.maxLevels) { + this.children = [ + new window.Tile(this.x * 2, this.y * 2, this.z + 1, this.maxLevels, this), + new window.Tile(this.x * 2, this.y * 2 + 1, this.z + 1, this.maxLevels, this), + new window.Tile(this.x * 2 + 1, this.y * 2, this.z + 1, this.maxLevels, this), + new window.Tile(this.x * 2 + 1, this.y * 2 + 1, this.z + 1, this.maxLevels, this) + ]; + } + + for (var i in this.children) { + const child = this.children[i]; + if (child.x === x && child.y === y && child.z === z) { + return child; + } + } + + for (var i in this.children) { + const child = this.children[i]; + if (child.x === xt && child.y === yt) { + return child; + } + } + } + + Tile.prototype.load = function(x, y, z) { + if (this.x === x && this.y === y && this.z === z && !this.loaded) { + this.loaded = true; + if (this.parent) { this.parent.loadFromChild(); } + } else { + const child = this.findPathToChild(x, y, z); + if (child) {child.load(x, y, z);} + } + } + + Tile.prototype.loadFromChild = function() { + var loaded = true; + for (var i in this.children) { + if (!this.children[i].loaded) { + loaded = false; + } + } + if (loaded) { + this.loaded = true; + if (this.parent) { this.parent.loadFromChild(); } + } + } + + Tile.prototype.isLoaded = function(x, y, z) { + if (this.loaded) { + return true; + } + + if (this.z === z) { + return false; + } + + const child = this.findPathToChild(x, y, z); + if (child) { + return child.isLoaded(x, y, z); + } else { + return false; + } + } + + window.Tile = Tile; +})(window, Math); diff --git a/cadasta/core/static/js/smap/map.js b/cadasta/core/static/js/smap/map.js index 6fa9d6747..8b06d0c9c 100644 --- a/cadasta/core/static/js/smap/map.js +++ b/cadasta/core/static/js/smap/map.js @@ -1,87 +1,83 @@ var SMap = (function() { - var map = L.map('mapid'); - var layerscontrol = L.control.layers().addTo(map); + var map = L.map('mapid'); + var layerscontrol = L.control.layers().addTo(map); + var prev_urls = []; + var loaded_features = {}; - function add_tile_layers() { - for (var i = 0, n = layers.length; i < n; i++) { - var options = L.Util.extend(layers[i]['attrs']); - var layer = {name: layers[i]['label'], url: layers[i]['url'], options: options}; + var geojsonTileLayer = new L.TileLayer.GeoJSON( + url, + { + clipTiles: true, + unique: function (feature) {return feature.id;} + }, + { + style: { weight: 2 }, + onEachFeature: function(feature, layer) { + if (options.trans) { + layer.bindPopup("
" + + "

Location" + + feature.properties.type + "

" + + "
" + options.trans['open'] + "" + + "
"); + } + } + }); - var l = L.tileLayer(layer.url, layer.options); - layerscontrol.addBaseLayer(l, layer.name); + function add_tile_layers() { + for (var i = 0, n = layers.length; i < n; i++) { + var attrs = L.Util.extend(layers[i]['attrs']); + var layer = {name: layers[i]['label'], url: layers[i]['url'], options: attrs}; + var l = L.tileLayer(layer.url, layer.options); + layerscontrol.addBaseLayer(l, layer.name); - if (i === 0) { - l.addTo(map); - } + if (i === 0) { + l.addTo(map); } } + } - add_tile_layers(); + add_tile_layers(); + // var features = L.deflate4({minSize: 20}).addTo(map); + // features.addLayer(geojsonTileLayer); + map.addLayer(geojsonTileLayer); - function load_project_extent() { - if (options.projectExtent) { - var boundary = L.geoJson( - options.projectExtent, { - style: { - stroke: true, - color: "#0e305e", - weight: 2, - dashArray: "5, 5", - opacity: 1, - fill: false, - clickable: false, - } + function load_project_extent() { + if (options.projectExtent) { + var boundary = L.geoJson( + options.projectExtent, { + style: { + stroke: true, + color: "#0e305e", + weight: 2, + dashArray: "5, 5", + opacity: 1, + fill: false, + clickable: false, } - ); - boundary.addTo(map); - projectBounds = boundary.getBounds(); - } + } + ); + boundary.addTo(map); + projectBounds = boundary.getBounds(); if (options.fitBounds === 'project') { map.fitBounds(projectBounds); - } else if (options.fitBounds !== 'locations') { - map.fitBounds([[-45.0, -180.0], [45.0, 180.0]]); } + } else { + map.fitBounds([[-45.0, -180.0], [45.0, 180.0]]); } + } - load_project_extent() - - function render_features(){ - var geoJson = L.geoJson(null, { - style: { weight: 2 }, - onEachFeature: function(feature, layer) { - if (options.trans) { - layer.bindPopup("
" + - "

Location" + - feature.properties.type + "

" + - "
" + options.trans['open'] + "" + - "
"); - } - } - }); + load_project_extent() - function load_features(request_url) { - $('#messages #loading').removeClass('hidden'); - $.get(request_url, function(response) { - geoJson.addData(response); - if (response.next) { - load_features(response.next); - } else { - $('#messages #loading').addClass('hidden'); - if (options.fitBounds === 'locations') { - var bounds = geoJson.getBounds(); - if (bounds.isValid()) { - map.fitBounds(bounds); - } - } - } - }); + function load_features() {; + if (options.fitBounds === 'locations') { + var bounds = geojsonTileLayer.geojsonLayer.getBounds(); + if (bounds.isValid()) { + map.fitBounds(bounds); } - - geoJson.addTo(map); - load_features(url); + } } - render_features() + load_features(); function render_spatial_resource(){ $.ajax(fetch_spatial_resources).done(function(data){ diff --git a/cadasta/spatial/urls/async.py b/cadasta/spatial/urls/async.py index e41096b7d..ca67449e8 100644 --- a/cadasta/spatial/urls/async.py +++ b/cadasta/spatial/urls/async.py @@ -8,6 +8,10 @@ r'^$', async.SpatialUnitList.as_view(), name='list'), + url( + r'^tiled/(?P[0-9]+)/(?P[0-9]+)/(?P[0-9]+)/$', + async.SpatialUnitTiles.as_view(), + name='tiled'), ] @@ -16,4 +20,8 @@ r'^organizations/(?P[-\w]+)/projects/' '(?P[-\w]+)/spatial/', include(urls)), + # url(urls.tilepath(r'^organizations/(?P[-\w]+)/projects/' + # '(?P[-\w]+)/spatial/'), + # async.SpatialUnitList.as_view(), + # name='location-tiles'), ] diff --git a/cadasta/spatial/views/async.py b/cadasta/spatial/views/async.py index a2c48cac8..2b83c500f 100644 --- a/cadasta/spatial/views/async.py +++ b/cadasta/spatial/views/async.py @@ -1,3 +1,5 @@ +import math +from django.contrib.gis.geos import Polygon from tutelary.mixins import APIPermissionRequiredMixin from rest_framework import generics from rest_framework_gis.pagination import GeoJsonPagination @@ -6,6 +8,24 @@ from .. import serializers +def deg2num(lat_deg, lon_deg, zoom): + lat_rad = math.radians(lat_deg) + n = 2.0 ** zoom + xtile = int((lon_deg + 180.0) / 360.0 * n) + ytile = int( + (1.0 - math.log(math.tan(lat_rad) + + (1 / math.cos(lat_rad))) / math.pi) / 2.0 * n) + return [xtile, ytile] + + +def num2deg(xtile, ytile, zoom): + n = 2.0 ** zoom + lon_deg = xtile / n * 360.0 - 180.0 + lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) + lat_deg = math.degrees(lat_rad) + return [lon_deg, lat_deg] + + class Paginator(GeoJsonPagination): page_size = 1000 @@ -34,3 +54,40 @@ def get_queryset(self, *args, **kwargs): def get_perms_objects(self): return [self.get_project()] + + +class SpatialUnitTiles(APIPermissionRequiredMixin, + mixins.SpatialQuerySetMixin, + generics.ListAPIView): + + serializer_class = serializers.SpatialUnitGeoJsonSerializer + + def get_actions(self, request): + if self.get_project().archived: + return ['project.view_archived', 'spatial.list'] + if self.get_project().public(): + return ['project.view', 'spatial.list'] + else: + return ['project.view_private', 'spatial.list'] + + permission_required = { + 'GET': get_actions + } + + def get_queryset(self, *args, **kwargs): + queryset = super().get_queryset(*args, **kwargs) + x = int(self.kwargs['x']) + y = int(self.kwargs['y']) + zoom = int(self.kwargs['z']) + + bbox = num2deg(xtile=x, ytile=y, zoom=zoom) + bbox.extend(num2deg(xtile=x+1, ytile=y+1, zoom=zoom)) + bbox = Polygon.from_bbox(bbox) + final_queryset = queryset.filter( + geometry__intersects=bbox).exclude( + id=self.request.GET.get('exclude')) + + return final_queryset + + def get_perms_objects(self): + return [self.get_project()] diff --git a/cadasta/templates/organization/project_dashboard.html b/cadasta/templates/organization/project_dashboard.html index 6d1066234..50c7c90b5 100644 --- a/cadasta/templates/organization/project_dashboard.html +++ b/cadasta/templates/organization/project_dashboard.html @@ -24,18 +24,21 @@ open: "{% trans 'Open location' %}" }; - add_map_controls(map); - - switch_layer_controls(map, options); - {% if project.extent %} var projectExtent = {{ project.extent.geojson|safe }}; {% else %} var projectExtent = null; {% endif %} + options.project_slug = '{{ project.slug }}' + options.org_slug = '{{ project.organization.slug }}' + options.trans = trans + + switch_layer_controls(map, options); + renderFeatures(map, '{% url "async:spatial:list" project.organization.slug project.slug %}', + '{% url "async:spatial:tiled" project.organization.slug project.slug 0 0 0 %}', {projectExtent: projectExtent, trans: trans, fitBounds: 'project'}); var orgSlug = '{{ project.organization.slug }}'; diff --git a/cadasta/templates/organization/project_map.html b/cadasta/templates/organization/project_map.html index 16255af13..8799e293e 100644 --- a/cadasta/templates/organization/project_map.html +++ b/cadasta/templates/organization/project_map.html @@ -13,13 +13,16 @@ {% block extra_script %} + + - +