diff --git a/src/c3nav/site/static/site/css/c3nav.scss b/src/c3nav/site/static/site/css/c3nav.scss index abcab5f4..e30dae3e 100644 --- a/src/c3nav/site/static/site/css/c3nav.scss +++ b/src/c3nav/site/static/site/css/c3nav.scss @@ -575,6 +575,14 @@ main.show-options #resultswrapper #route-options { cursor: pointer; } +.location-label { + white-space: nowrap; +} +.location-label-text { + white-space: nowrap; + transform: translateX(-50%) translateY(-50%); +} + .locationinput { position: relative; padding: 0; diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index e5355000..29a3fa4b 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -33,6 +33,18 @@ Math.log2 = Math.log2 || function(x) { return Math.log(x) * Math.LOG2E; }; + + var originalGetIconBox = L.LayerGroup.Collision.prototype._getIconBox; + L.LayerGroup.Collision.prototype._getIconBox = function(el) { + var result = originalGetIconBox(el); + var offsetX = (result[2]-result[0]/2), + offsetY = (result[3]-result[1]/2); + result[0] -= offsetX; + result[1] -= offsetY; + result[2] -= offsetX; + result[3] -= offsetY; + return result + }; }()); c3nav = { @@ -83,6 +95,7 @@ c3nav = { _last_time_searchable_locations_loaded: null, _searchable_locations_interval: 120000, _searchable_locations_loaded: function(data) { + // todo, do nothing on 304 not modified c3nav._last_time_searchable_locations_loaded = Date.now(); var locations = [], locations_by_id = {}; @@ -161,6 +174,25 @@ c3nav = { window.onpopstate = c3nav._onpopstate; + var layer, count= 0; + for (var location of c3nav.locations) { + if (location.point) { + layer = c3nav._labelLayers[location.point[0]]; + if (layer) { + L.marker(L.GeoJSON.coordsToLatLng(location.point.slice(1)), { + icon: L.divIcon({ + html: $('
').append($('
').text(location.title)).html(), + iconSize: null, + className: 'location-label' + }), + interactive: false, // Post-0.7.3 + clickable: false // 0.7.3 + }).addTo(c3nav._labelLayers[location.point[0]]); + } + //if (count++ > 1) break; + } + } + if (window.mobileclient) { c3nav.startWifiScanning(); } @@ -1094,6 +1126,7 @@ c3nav = { // setup level control c3nav._levelControl = new LevelControl().addTo(c3nav.map); c3nav._locationLayers = {}; + c3nav._labelLayers = {}; c3nav._locationLayerBounds = {}; c3nav._detailLayers = {}; c3nav._routeLayers = {}; @@ -1105,6 +1138,7 @@ c3nav = { var layerGroup = c3nav._levelControl.addLevel(level[0], level[1]); c3nav._detailLayers[level[0]] = L.layerGroup().addTo(layerGroup); c3nav._locationLayers[level[0]] = L.layerGroup().addTo(layerGroup); + c3nav._labelLayers[level[0]] = L.LayerGroup.collision({margin: 10}).addTo(layerGroup); c3nav._routeLayers[level[0]] = L.layerGroup().addTo(layerGroup); c3nav._userLocationLayers[level[0]] = L.layerGroup().addTo(layerGroup); } diff --git a/src/c3nav/site/templates/site/map.html b/src/c3nav/site/templates/site/map.html index c13852d8..53364edf 100644 --- a/src/c3nav/site/templates/site/map.html +++ b/src/c3nav/site/templates/site/map.html @@ -170,6 +170,8 @@ {% compress js %} + + {% endcompress %} {% endblock %} diff --git a/src/c3nav/static/leaflet-layergroup-collision/Leaflet.LayerGroup.Collision.js b/src/c3nav/static/leaflet-layergroup-collision/Leaflet.LayerGroup.Collision.js new file mode 100644 index 00000000..5dbb7d84 --- /dev/null +++ b/src/c3nav/static/leaflet-layergroup-collision/Leaflet.LayerGroup.Collision.js @@ -0,0 +1,244 @@ + + +var isMSIE8 = !('getComputedStyle' in window && typeof window.getComputedStyle === 'function') + +function extensions(parentClass) { return { + + initialize: function (arg1, arg2) { + var options; + if (parentClass === L.GeoJSON) { + parentClass.prototype.initialize.call(this, arg1, arg2); + options = arg2; + } else { + parentClass.prototype.initialize.call(this, arg1); + options = arg1; + } + this._originalLayers = []; + this._visibleLayers = []; + this._staticLayers = []; + this._rbush = []; + this._cachedRelativeBoxes = []; + this._margin = options.margin || 0; + this._rbush = null; + }, + + addLayer: function(layer) { + if ( !('options' in layer) || !('icon' in layer.options)) { + this._staticLayers.push(layer); + parentClass.prototype.addLayer.call(this, layer); + return; + } + + this._originalLayers.push(layer); + if (this._map) { + this._maybeAddLayerToRBush( layer ); + } + }, + + removeLayer: function(layer) { + this._rbush.remove(this._cachedRelativeBoxes[layer._leaflet_id]); + delete this._cachedRelativeBoxes[layer._leaflet_id]; + parentClass.prototype.removeLayer.call(this,layer); + var i; + + i = this._originalLayers.indexOf(layer); + if (i !== -1) { this._originalLayers.splice(i,1); } + + i = this._visibleLayers.indexOf(layer); + if (i !== -1) { this._visibleLayers.splice(i,1); } + + i = this._staticLayers.indexOf(layer); + if (i !== -1) { this._staticLayers.splice(i,1); } + }, + + clearLayers: function() { + this._rbush = rbush(); + this._originalLayers = []; + this._visibleLayers = []; + this._staticLayers = []; + this._cachedRelativeBoxes = []; + parentClass.prototype.clearLayers.call(this); + }, + + onAdd: function (map) { + this._map = map; + + for (var i in this._staticLayers) { + map.addLayer(this._staticLayers[i]); + } + + this._onZoomEnd(); + map.on('zoomend', this._onZoomEnd, this); + }, + + onRemove: function(map) { + for (var i in this._staticLayers) { + map.removeLayer(this._staticLayers[i]); + } + map.off('zoomend', this._onZoomEnd, this); + parentClass.prototype.onRemove.call(this, map); + }, + + _maybeAddLayerToRBush: function(layer) { + + var z = this._map.getZoom(); + var bush = this._rbush; + + var boxes = this._cachedRelativeBoxes[layer._leaflet_id]; + var visible = false; + if (!boxes) { + // Add the layer to the map so it's instantiated on the DOM, + // in order to fetch its position and size. + parentClass.prototype.addLayer.call(this, layer); + var visible = true; +// var htmlElement = layer._icon; + var box = this._getIconBox(layer._icon); + boxes = this._getRelativeBoxes(layer._icon.children, box); + boxes.push(box); + this._cachedRelativeBoxes[layer._leaflet_id] = boxes; + } + + boxes = this._positionBoxes(this._map.latLngToLayerPoint(layer.getLatLng()),boxes); + + var collision = false; + for (var i=0; i 0; + } + + if (!collision) { + if (!visible) { + parentClass.prototype.addLayer.call(this, layer); + } + this._visibleLayers.push(layer); + bush.load(boxes); + } else { + parentClass.prototype.removeLayer.call(this, layer); + } + }, + + + // Returns a plain array with the relative dimensions of a L.Icon, based + // on the computed values from iconSize and iconAnchor. + _getIconBox: function (el) { + + if (isMSIE8) { + // Fallback for MSIE8, will most probably fail on edge cases + return [ 0, 0, el.offsetWidth, el.offsetHeight]; + } + + var styles = window.getComputedStyle(el); + + // getComputedStyle() should return values already in pixels, so using parseInt() + // is not as much as a hack as it seems to be. + + return [ + parseInt(styles.marginLeft), + parseInt(styles.marginTop), + parseInt(styles.marginLeft) + parseInt(styles.width), + parseInt(styles.marginTop) + parseInt(styles.height) + ]; + }, + + + // Much like _getIconBox, but works for positioned HTML elements, based on offsetWidth/offsetHeight. + _getRelativeBoxes: function(els,baseBox) { + var boxes = []; + for (var i=0; i= 0) { + if (insertPath[level].children.length > this._maxEntries) { + this._split(insertPath, level); + level--; + } else break; + } + + // adjust bboxes along the insertion path + this._adjustParentBBoxes(bbox, insertPath, level); + }, + + // split overflowed node into two + _split: function (insertPath, level) { + + var node = insertPath[level], + M = node.children.length, + m = this._minEntries; + + this._chooseSplitAxis(node, m, M); + + var newNode = { + children: node.children.splice(this._chooseSplitIndex(node, m, M)), + height: node.height + }; + + if (node.leaf) newNode.leaf = true; + + calcBBox(node, this.toBBox); + calcBBox(newNode, this.toBBox); + + if (level) insertPath[level - 1].children.push(newNode); + else this._splitRoot(node, newNode); + }, + + _splitRoot: function (node, newNode) { + // split root node + this.data = { + children: [node, newNode], + height: node.height + 1 + }; + calcBBox(this.data, this.toBBox); + }, + + _chooseSplitIndex: function (node, m, M) { + + var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index; + + minOverlap = minArea = Infinity; + + for (i = m; i <= M - m; i++) { + bbox1 = distBBox(node, 0, i, this.toBBox); + bbox2 = distBBox(node, i, M, this.toBBox); + + overlap = intersectionArea(bbox1, bbox2); + area = bboxArea(bbox1) + bboxArea(bbox2); + + // choose distribution with minimum overlap + if (overlap < minOverlap) { + minOverlap = overlap; + index = i; + + minArea = area < minArea ? area : minArea; + + } else if (overlap === minOverlap) { + // otherwise choose distribution with minimum area + if (area < minArea) { + minArea = area; + index = i; + } + } + } + + return index; + }, + + // sorts node children by the best axis for split + _chooseSplitAxis: function (node, m, M) { + + var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX, + compareMinY = node.leaf ? this.compareMinY : compareNodeMinY, + xMargin = this._allDistMargin(node, m, M, compareMinX), + yMargin = this._allDistMargin(node, m, M, compareMinY); + + // if total distributions margin value is minimal for x, sort by minX, + // otherwise it's already sorted by minY + if (xMargin < yMargin) node.children.sort(compareMinX); + }, + + // total margin of all possible split distributions where each node is at least m full + _allDistMargin: function (node, m, M, compare) { + + node.children.sort(compare); + + var toBBox = this.toBBox, + leftBBox = distBBox(node, 0, m, toBBox), + rightBBox = distBBox(node, M - m, M, toBBox), + margin = bboxMargin(leftBBox) + bboxMargin(rightBBox), + i, child; + + for (i = m; i < M - m; i++) { + child = node.children[i]; + extend(leftBBox, node.leaf ? toBBox(child) : child.bbox); + margin += bboxMargin(leftBBox); + } + + for (i = M - m - 1; i >= m; i--) { + child = node.children[i]; + extend(rightBBox, node.leaf ? toBBox(child) : child.bbox); + margin += bboxMargin(rightBBox); + } + + return margin; + }, + + _adjustParentBBoxes: function (bbox, path, level) { + // adjust bboxes along the given tree path + for (var i = level; i >= 0; i--) { + extend(path[i].bbox, bbox); + } + }, + + _condense: function (path) { + // go through the path, removing empty nodes and updating bboxes + for (var i = path.length - 1, siblings; i >= 0; i--) { + if (path[i].children.length === 0) { + if (i > 0) { + siblings = path[i - 1].children; + siblings.splice(siblings.indexOf(path[i]), 1); + + } else this.clear(); + + } else calcBBox(path[i], this.toBBox); + } + }, + + _initFormat: function (format) { + // data format (minX, minY, maxX, maxY accessors) + + // uses eval-type function compilation instead of just accepting a toBBox function + // because the algorithms are very sensitive to sorting functions performance, + // so they should be dead simple and without inner calls + + // jshint evil: true + + var compareArr = ['return a', ' - b', ';']; + + this.compareMinX = new Function('a', 'b', compareArr.join(format[0])); + this.compareMinY = new Function('a', 'b', compareArr.join(format[1])); + + this.toBBox = new Function('a', 'return [a' + format.join(', a') + '];'); + } +}; + + +// calculate node's bbox from bboxes of its children +function calcBBox(node, toBBox) { + node.bbox = distBBox(node, 0, node.children.length, toBBox); +} + +// min bounding rectangle of node children from k to p-1 +function distBBox(node, k, p, toBBox) { + var bbox = empty(); + + for (var i = k, child; i < p; i++) { + child = node.children[i]; + extend(bbox, node.leaf ? toBBox(child) : child.bbox); + } + + return bbox; +} + +function empty() { return [Infinity, Infinity, -Infinity, -Infinity]; } + +function extend(a, b) { + a[0] = Math.min(a[0], b[0]); + a[1] = Math.min(a[1], b[1]); + a[2] = Math.max(a[2], b[2]); + a[3] = Math.max(a[3], b[3]); + return a; +} + +function compareNodeMinX(a, b) { return a.bbox[0] - b.bbox[0]; } +function compareNodeMinY(a, b) { return a.bbox[1] - b.bbox[1]; } + +function bboxArea(a) { return (a[2] - a[0]) * (a[3] - a[1]); } +function bboxMargin(a) { return (a[2] - a[0]) + (a[3] - a[1]); } + +function enlargedArea(a, b) { + return (Math.max(b[2], a[2]) - Math.min(b[0], a[0])) * + (Math.max(b[3], a[3]) - Math.min(b[1], a[1])); +} + +function intersectionArea(a, b) { + var minX = Math.max(a[0], b[0]), + minY = Math.max(a[1], b[1]), + maxX = Math.min(a[2], b[2]), + maxY = Math.min(a[3], b[3]); + + return Math.max(0, maxX - minX) * + Math.max(0, maxY - minY); +} + +function contains(a, b) { + return a[0] <= b[0] && + a[1] <= b[1] && + b[2] <= a[2] && + b[3] <= a[3]; +} + +function intersects(a, b) { + return b[0] <= a[2] && + b[1] <= a[3] && + b[2] >= a[0] && + b[3] >= a[1]; +} + +// sort an array so that items come in groups of n unsorted items, with groups sorted between each other; +// combines selection algorithm with binary divide & conquer approach + +function multiSelect(arr, left, right, n, compare) { + var stack = [left, right], + mid; + + while (stack.length) { + right = stack.pop(); + left = stack.pop(); + + if (right - left <= n) continue; + + mid = left + Math.ceil((right - left) / n / 2) * n; + select(arr, left, right, mid, compare); + + stack.push(left, mid, mid, right); + } +} + +// sort array between left and right (inclusive) so that the smallest k elements come first (unordered) +function select(arr, left, right, k, compare) { + var n, i, z, s, sd, newLeft, newRight, t, j; + + while (right > left) { + if (right - left > 600) { + n = right - left + 1; + i = k - left + 1; + z = Math.log(n); + s = 0.5 * Math.exp(2 * z / 3); + sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (i - n / 2 < 0 ? -1 : 1); + newLeft = Math.max(left, Math.floor(k - i * s / n + sd)); + newRight = Math.min(right, Math.floor(k + (n - i) * s / n + sd)); + select(arr, newLeft, newRight, k, compare); + } + + t = arr[k]; + i = left; + j = right; + + swap(arr, left, k); + if (compare(arr[right], t) > 0) swap(arr, left, right); + + while (i < j) { + swap(arr, i, j); + i++; + j--; + while (compare(arr[i], t) < 0) i++; + while (compare(arr[j], t) > 0) j--; + } + + if (compare(arr[left], t) === 0) swap(arr, left, j); + else { + j++; + swap(arr, j, right); + } + + if (j <= k) left = j + 1; + if (k <= j) right = j - 1; + } +} + +function swap(arr, i, j) { + var tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; +} + + +// export as AMD/CommonJS module or global variable +if (typeof define === 'function' && define.amd) define('rbush', function() { return rbush; }); +else if (typeof module !== 'undefined') module.exports = rbush; +else if (typeof self !== 'undefined') self.rbush = rbush; +else window.rbush = rbush; + +})();