From cc1b70c74b1574e66ba2af9cfc3865986b5e1d72 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Mon, 29 Apr 2013 16:26:29 +0200 Subject: [PATCH 1/4] Giving the map a getFeatureInfoForPixel method This method is an entry point for getting feature information. Renderers can use a hit canvas or defer to a layer (source) to get matching features for a pixel. For now this is only implemented for vector layers, and it uses a bbox query because we cannot refine the result because of missing geometry intersection functions yet. --- examples/vector-layer.js | 9 ++++++ src/ol/map.exports | 1 + src/ol/map.js | 28 +++++++++++++++++++ src/ol/mapbrowserevent.js | 26 ++++++++++++++++- .../canvas/canvasvectorlayerrenderer.js | 20 +++++++++++++ 5 files changed, 83 insertions(+), 1 deletion(-) diff --git a/examples/vector-layer.js b/examples/vector-layer.js index 1f23f67028..c15023685a 100644 --- a/examples/vector-layer.js +++ b/examples/vector-layer.js @@ -42,6 +42,15 @@ var map = new ol.Map({ }) }); +map.on('mousemove', function(evt) { + var features = map.getFeatureInfoForPixel(evt.getPixel(), [vector]); + var info = []; + for (var i = 0, ii = features.length; i < ii; ++i) { + info.push(features[i].get('name')); + } + document.getElementById('map').title = info.join(', '); +}); + var geojson = new ol.parser.GeoJSON(); var url = 'data/countries.json'; diff --git a/src/ol/map.exports b/src/ol/map.exports index 6fa9bba48a..048d03366d 100644 --- a/src/ol/map.exports +++ b/src/ol/map.exports @@ -2,6 +2,7 @@ @exportProperty ol.Map.prototype.addLayer @exportProperty ol.Map.prototype.addPreRenderFunction @exportProperty ol.Map.prototype.addPreRenderFunctions +@exportProperty ol.Map.prototype.getFeatureInfoForPixel @exportProperty ol.Map.prototype.getInteractions @exportProperty ol.Map.prototype.getRenderer @exportProperty ol.Map.prototype.removeLayer diff --git a/src/ol/map.js b/src/ol/map.js index 616ed73c0c..7da8568ae2 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -427,6 +427,34 @@ ol.Map.prototype.getCoordinateFromPixel = function(pixel) { }; +/** + * Get feature information for a pixel on the map. + * + * @param {ol.Pixel} pixel Pixel coordinate relative to the map viewport. + * @param {Array.|undefined} opt_layers Layers to restrict the + * query to. All layers will be queried if not provided. + * @return {Array.} Feature information. Layers that are + * able to return attribute data will return ol.Feature instances, other + * layers will return a string which can either be plain text or markup. + */ +ol.Map.prototype.getFeatureInfoForPixel = function(pixel, opt_layers) { + var renderer = this.getRenderer(); + var layers = goog.isDefAndNotNull(opt_layers) ? + opt_layers : this.getLayers().getArray(); + var layer, layerRenderer; + var featureInfo = []; + for (var i = 0, ii = layers.length; i < ii; ++i) { + layer = layers[i]; + layerRenderer = renderer.getLayerRenderer(layer); + if (goog.isFunction(layerRenderer.getFeatureInfoForPixel)) { + featureInfo.push.apply(featureInfo, + layerRenderer.getFeatureInfoForPixel(pixel)); + } + } + return featureInfo; +}; + + /** * @return {ol.Collection} Interactions. */ diff --git a/src/ol/mapbrowserevent.js b/src/ol/mapbrowserevent.js index 108458b198..4e7c982f26 100644 --- a/src/ol/mapbrowserevent.js +++ b/src/ol/mapbrowserevent.js @@ -147,6 +147,12 @@ ol.MapBrowserEventHandler = function(map) { */ this.downListenerKey_ = null; + /** + * @type {?number} + * @private + */ + this.moveListenerKey_ = null; + /** * @type {Array.} * @private @@ -172,6 +178,9 @@ ol.MapBrowserEventHandler = function(map) { this.downListenerKey_ = goog.events.listen(element, goog.events.EventType.MOUSEDOWN, this.handleMouseDown_, false, this); + this.moveListenerKey_ = goog.events.listen(element, + goog.events.EventType.MOUSEMOVE, + this.relayMouseMove_, false, this); // touch events this.touchListenerKeys_ = [ goog.events.listen(element, [ @@ -281,6 +290,19 @@ ol.MapBrowserEventHandler.prototype.handleMouseMove_ = function(browserEvent) { }; +/** + * @param {goog.events.BrowserEvent} browserEvent Browser event. + * @private + */ +ol.MapBrowserEventHandler.prototype.relayMouseMove_ = function(browserEvent) { + if (goog.events.hasListener( + this.map_, ol.MapBrowserEvent.EventType.MOUSEMOVE)) { + this.dispatchEvent(new ol.MapBrowserEvent( + ol.MapBrowserEvent.EventType.MOUSEMOVE, this.map_, browserEvent)); + } +}; + + /** * @param {goog.events.BrowserEvent} browserEvent Browser event. * @private @@ -335,6 +357,7 @@ ol.MapBrowserEventHandler.prototype.handleTouchEnd_ = function(browserEvent) { ol.MapBrowserEventHandler.prototype.disposeInternal = function() { goog.events.unlistenByKey(this.clickListenerKey_); goog.events.unlistenByKey(this.downListenerKey_); + goog.events.unlistenByKey(this.moveListenerKey_); if (!goog.isNull(this.dragListenerKeys_)) { goog.array.forEach(this.dragListenerKeys_, goog.events.unlistenByKey); this.dragListenerKeys_ = null; @@ -360,5 +383,6 @@ ol.MapBrowserEvent.EventType = { DRAGEND: 'dragend', TOUCHSTART: goog.events.EventType.TOUCHSTART, TOUCHMOVE: goog.events.EventType.TOUCHMOVE, - TOUCHEND: goog.events.EventType.TOUCHEND + TOUCHEND: goog.events.EventType.TOUCHEND, + MOUSEMOVE: goog.events.EventType.MOUSEMOVE }; diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index b343880a36..266815fb22 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -187,6 +187,26 @@ ol.renderer.canvas.VectorLayer.prototype.getTransform = function() { }; +/** + * @param {ol.Pixel} pixel Pixel coordinate relative to the map viewport. + * @return {Array.} Features at the pixel location. + */ +ol.renderer.canvas.VectorLayer.prototype.getFeatureInfoForPixel = + function(pixel) { + // TODO adjust pixel tolerance for point features + var minPixel = new ol.Pixel(pixel.x - 1, pixel.y - 1); + var maxPixel = new ol.Pixel(pixel.x + 1, pixel.y + 1); + var map = this.getMap(); + var minCoordinate = map.getCoordinateFromPixel(minPixel); + var maxCoordinate = map.getCoordinateFromPixel(maxPixel); + var bbox = ol.extent.boundingExtent([minCoordinate, maxCoordinate]); + var filter = new ol.filter.Extent(bbox); + + // TODO do a real intersect against the filtered result for exact matches + return this.getLayer().getFeatures(filter); +}; + + /** * @param {goog.events.Event} event Layer change event. * @private From 58c8b07ab561846bc8e6d32b8ac64fdc62a1492d Mon Sep 17 00:00:00 2001 From: ahocevar Date: Tue, 30 Apr 2013 13:34:12 +0200 Subject: [PATCH 2/4] Get hit candidates from RTree, then refine result Now we get exact hits also for lines and polygons. --- src/ol/coordinate.js | 15 ++++- src/ol/geom/base.js | 58 +++++++++++++++++++ src/ol/geom/linestring.js | 17 ++++++ src/ol/geom/multilinestring.js | 19 ++++++ src/ol/geom/multipolygon.js | 18 ++++++ src/ol/geom/polygon.js | 23 ++++++++ src/ol/map.js | 17 +----- .../canvas/canvasvectorlayerrenderer.js | 39 +++++++++++-- src/ol/renderer/maprenderer.js | 23 ++++++++ 9 files changed, 208 insertions(+), 21 deletions(-) diff --git a/src/ol/coordinate.js b/src/ol/coordinate.js index bf1d7c0ee9..ad60916902 100644 --- a/src/ol/coordinate.js +++ b/src/ol/coordinate.js @@ -12,7 +12,8 @@ ol.CoordinateFormatType; /** - * @typedef {Array.} + * An array representing a coordinate. + * @typedef {Array.} ol.Coordinate */ ol.Coordinate; @@ -84,6 +85,18 @@ ol.coordinate.scale = function(coordinate, s) { }; +/** + * @param {ol.Coordinate} coord1 First coordinate. + * @param {ol.Coordinate} coord2 Second coordinate. + * @return {number} Squared distance between coord1 and coord2. + */ +ol.coordinate.squaredDistance = function(coord1, coord2) { + var dx = coord1[0] - coord2[0]; + var dy = coord1[1] - coord2[1]; + return dx * dx + dy * dy; +}; + + /** * @param {ol.Coordinate|undefined} coordinate Coordinate. * @return {string} Hemisphere, degrees, minutes and seconds. diff --git a/src/ol/geom/base.js b/src/ol/geom/base.js index 31ebb15f95..1b8d3e4a04 100644 --- a/src/ol/geom/base.js +++ b/src/ol/geom/base.js @@ -1,6 +1,8 @@ goog.provide('ol.geom.Vertex'); goog.provide('ol.geom.VertexArray'); +goog.require('ol.coordinate'); + /** * @typedef {Array.} @@ -12,3 +14,59 @@ ol.geom.Vertex; * @typedef {Array.} */ ol.geom.VertexArray; + + +/** + * Calculate the squared distance from a point to a line segment. + * + * @param {ol.Coordinate} coordinate Coordinate of the point. + * @param {Array.} segment Line segment (2 coordinates). + * @return {number} Squared distance from the point to the line segment. + */ +ol.geom.squaredDistanceToSegment = function(coordinate, segment) { + // http://de.softuses.com/103478, Kommentar #1 + var v = segment[0]; + var w = segment[1]; + var l2 = ol.coordinate.squaredDistance(v, w); + if (l2 == 0) { + return ol.coordinate.squaredDistance(coordinate, v); + } + var t = ((coordinate[0] - v[0]) * (w[0] - v[0]) + + (coordinate[1] - v[1]) * (w[1] - v[1])) / l2; + if (t < 0) { + return ol.coordinate.squaredDistance(coordinate, v); + } + if (t > 1) { + return ol.coordinate.squaredDistance(coordinate, w); + } + return ol.coordinate.squaredDistance(coordinate, + [v[0] + t * (w[0] - v[0]), v[1] + t * (w[1] - v[1])]); +}; + + +/** + * Calculate whether a point falls inside a polygon. + * + * @param {ol.Coordinate} coordinate Coordinate of the point. + * @param {Array.} vertices Vertices of the polygon. + * @return {boolean} Whether the point falls inside the polygon. + */ +ol.geom.pointInPolygon = function(coordinate, vertices) { + // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html + var x = coordinate[0], y = coordinate[1]; + var inside = false; + var xi, yi, xj, yj, intersect; + var numVertices = vertices.length; + for (var i = 0, j = numVertices - 1; i < numVertices; j = i++) { + xi = vertices[i][0]; + yi = vertices[i][1]; + xj = vertices[j][0]; + yj = vertices[j][1]; + intersect = ((yi > y) != (yj > y)) && + (x < (xj - xi) * (y - yi) / (yj - yi) + xi); + if (intersect) { + inside = !inside; + } + } + return inside; +}; diff --git a/src/ol/geom/linestring.js b/src/ol/geom/linestring.js index 7f92785690..7a7f2321fd 100644 --- a/src/ol/geom/linestring.js +++ b/src/ol/geom/linestring.js @@ -146,3 +146,20 @@ ol.geom.LineString.prototype.getType = function() { ol.geom.LineString.prototype.getSharedId = function() { return this.sharedId_; }; + + +/** + * Calculate the distance from a coordinate to this linestring. + * + * @param {ol.Coordinate} coordinate Coordinate. + * @return {number} Distance from the coordinate to this linestring. + */ +ol.geom.LineString.prototype.distanceFromCoordinate = function(coordinate) { + var coordinates = this.getCoordinates(); + var dist2 = Infinity; + for (var i = 0, j = 1, len = coordinates.length; j < len; i = j++) { + dist2 = Math.min(dist2, ol.geom.squaredDistanceToSegment(coordinate, + [coordinates[i], coordinates[j]])); + } + return Math.sqrt(dist2); +}; diff --git a/src/ol/geom/multilinestring.js b/src/ol/geom/multilinestring.js index 71f99afbe1..b656648992 100644 --- a/src/ol/geom/multilinestring.js +++ b/src/ol/geom/multilinestring.js @@ -55,6 +55,25 @@ ol.geom.MultiLineString.prototype.getType = function() { }; +/** + * Calculate the distance from a coordinate to this multilinestring. This is + * the closest distance of the coordinate to one of this multilinestring's + * components.< + * + * @param {ol.Coordinate} coordinate Coordinate. + * @return {number} Distance from the coordinate to this multilinestring. + */ +ol.geom.MultiLineString.prototype.distanceFromCoordinate = + function(coordinate) { + var distance = Infinity; + for (var i = 0, ii = this.components.length; i < ii; ++i) { + distance = Math.min(distance, + this.components[i].distanceFromCoordinate(coordinate)); + } + return distance; +}; + + /** * Create a multi-linestring geometry from an array of linestring geometries. * diff --git a/src/ol/geom/multipolygon.js b/src/ol/geom/multipolygon.js index b3c87c6175..452f487386 100644 --- a/src/ol/geom/multipolygon.js +++ b/src/ol/geom/multipolygon.js @@ -56,6 +56,24 @@ ol.geom.MultiPolygon.prototype.getType = function() { }; +/** + * Check whether a given coordinate is inside this multipolygon. + * + * @param {ol.Coordinate} coordinate Coordinate. + * @return {boolean} Whether the coordinate is inside the multipolygon. + */ +ol.geom.MultiPolygon.prototype.containsCoordinate = function(coordinate) { + var containsCoordinate = false; + for (var i = 0, ii = this.components.length; i < ii; ++i) { + if (this.components[i].containsCoordinate(coordinate)) { + containsCoordinate = true; + break; + } + } + return containsCoordinate; +}; + + /** * Create a multi-polygon geometry from an array of polygon geometries. * diff --git a/src/ol/geom/polygon.js b/src/ol/geom/polygon.js index c38b4aa57a..383cdebd98 100644 --- a/src/ol/geom/polygon.js +++ b/src/ol/geom/polygon.js @@ -88,3 +88,26 @@ ol.geom.Polygon.prototype.getCoordinates = function() { ol.geom.Polygon.prototype.getType = function() { return ol.geom.GeometryType.POLYGON; }; + + +/** + * Check whether a given coordinate is inside this polygon. + * + * @param {ol.Coordinate} coordinate Coordinate. + * @return {boolean} Whether the coordinate is inside the polygon. + */ +ol.geom.Polygon.prototype.containsCoordinate = function(coordinate) { + var rings = this.rings; + var containsCoordinate = ol.geom.pointInPolygon(coordinate, + rings[0].getCoordinates()); + if (containsCoordinate) { + // if inner ring contains point, polygon does not contain it + for (var i = 1, ii = rings.length; i < ii; ++i) { + if (ol.geom.pointInPolygon(coordinate, rings[i].getCoordinates())) { + containsCoordinate = false; + break; + } + } + } + return containsCoordinate; +}; diff --git a/src/ol/map.js b/src/ol/map.js index 7da8568ae2..e144fe7e68 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -431,27 +431,16 @@ ol.Map.prototype.getCoordinateFromPixel = function(pixel) { * Get feature information for a pixel on the map. * * @param {ol.Pixel} pixel Pixel coordinate relative to the map viewport. - * @param {Array.|undefined} opt_layers Layers to restrict the - * query to. All layers will be queried if not provided. + * @param {Array.=} opt_layers Layers to restrict the query to. + * All layers will be queried if not provided. * @return {Array.} Feature information. Layers that are * able to return attribute data will return ol.Feature instances, other * layers will return a string which can either be plain text or markup. */ ol.Map.prototype.getFeatureInfoForPixel = function(pixel, opt_layers) { - var renderer = this.getRenderer(); var layers = goog.isDefAndNotNull(opt_layers) ? opt_layers : this.getLayers().getArray(); - var layer, layerRenderer; - var featureInfo = []; - for (var i = 0, ii = layers.length; i < ii; ++i) { - layer = layers[i]; - layerRenderer = renderer.getLayerRenderer(layer); - if (goog.isFunction(layerRenderer.getFeatureInfoForPixel)) { - featureInfo.push.apply(featureInfo, - layerRenderer.getFeatureInfoForPixel(pixel)); - } - } - return featureInfo; + return this.getRenderer().getFeatureInfoForPixel(pixel, layers); }; diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 266815fb22..83dcbdff06 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -6,6 +6,7 @@ goog.require('goog.events'); goog.require('goog.events.EventType'); goog.require('goog.object'); goog.require('goog.vec.Mat4'); +goog.require('ol.Pixel'); goog.require('ol.Size'); goog.require('ol.TileCache'); goog.require('ol.TileCoord'); @@ -193,17 +194,43 @@ ol.renderer.canvas.VectorLayer.prototype.getTransform = function() { */ ol.renderer.canvas.VectorLayer.prototype.getFeatureInfoForPixel = function(pixel) { - // TODO adjust pixel tolerance for point features + // TODO adjust pixel tolerance for applied styles var minPixel = new ol.Pixel(pixel.x - 1, pixel.y - 1); var maxPixel = new ol.Pixel(pixel.x + 1, pixel.y + 1); var map = this.getMap(); - var minCoordinate = map.getCoordinateFromPixel(minPixel); - var maxCoordinate = map.getCoordinateFromPixel(maxPixel); - var bbox = ol.extent.boundingExtent([minCoordinate, maxCoordinate]); - var filter = new ol.filter.Extent(bbox); + var locationMin = map.getCoordinateFromPixel(minPixel); + var locationMax = map.getCoordinateFromPixel(maxPixel); + var locationBbox = ol.extent.boundingExtent([locationMin, locationMax]); + var filter = new ol.filter.Extent(locationBbox); // TODO do a real intersect against the filtered result for exact matches - return this.getLayer().getFeatures(filter); + var candidates = this.getLayer().getFeatures(filter); + + var location = map.getCoordinateFromPixel(pixel); + // TODO adjust tolerance for stroke width or use configurable tolerance + var tolerance = map.getView().getView2D().getResolution() * 3; + var result = []; + var candidate, geom; + for (var i = 0, ii = candidates.length; i < ii; ++i) { + candidate = candidates[i]; + geom = candidate.getGeometry(); + if (goog.isFunction(geom.containsCoordinate)) { + // For polygons, check if the pixel location is inside the polygon + if (geom.containsCoordinate(location)) { + result.push(candidate); + } + } else if (goog.isFunction(geom.distanceFromCoordinate)) { + // For lines, check if the ditance to the pixel location is within the + // tolerance threshold + if (geom.distanceFromCoordinate(location) < tolerance) { + result.push(candidate); + } + } else { + // For points, the bbox filter is all we need + result.push(candidate); + } + } + return result; }; diff --git a/src/ol/renderer/maprenderer.js b/src/ol/renderer/maprenderer.js index 607bcebc6e..3197bdbe54 100644 --- a/src/ol/renderer/maprenderer.js +++ b/src/ol/renderer/maprenderer.js @@ -103,6 +103,29 @@ ol.renderer.Map.prototype.disposeInternal = function() { ol.renderer.Map.prototype.getCanvas = goog.functions.NULL; +/** + * @param {ol.Pixel} pixel Pixel coordinate relative to the map viewport. + * @param {Array.} layers Layers to query. + * @return {Array.} Feature information. Layers that are + * able to return attribute data will return ol.Feature instances, other + * layers will return a string which can either be plain text or markup. + */ +ol.renderer.Map.prototype.getFeatureInfoForPixel = + function(pixel, layers) { + var layer, layerRenderer; + var featureInfo = []; + for (var i = 0, ii = layers.length; i < ii; ++i) { + layer = layers[i]; + layerRenderer = this.getLayerRenderer(layer); + if (goog.isFunction(layerRenderer.getFeatureInfoForPixel)) { + featureInfo.push.apply(featureInfo, + layerRenderer.getFeatureInfoForPixel(pixel)); + } + } + return featureInfo; +}; + + /** * @param {ol.layer.Layer} layer Layer. * @protected From d2c81db06a41a987aea0e27d9ae972fa6572a587 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Tue, 30 Apr 2013 14:15:39 +0200 Subject: [PATCH 3/4] Info box instead of tooltip as suggested by @fredj --- examples/vector-layer.html | 5 +++++ examples/vector-layer.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/vector-layer.html b/examples/vector-layer.html index d286840586..c34ff9cde5 100644 --- a/examples/vector-layer.html +++ b/examples/vector-layer.html @@ -43,6 +43,11 @@
vector, geojson, style
+
+
+   +
+
diff --git a/examples/vector-layer.js b/examples/vector-layer.js index c15023685a..d830d654ee 100644 --- a/examples/vector-layer.js +++ b/examples/vector-layer.js @@ -48,7 +48,7 @@ map.on('mousemove', function(evt) { for (var i = 0, ii = features.length; i < ii; ++i) { info.push(features[i].get('name')); } - document.getElementById('map').title = info.join(', '); + document.getElementById('info').innerHTML = info.join(', ') || ' '; }); From 00777de581c4ea527de6de4799e8386dff7e81cf Mon Sep 17 00:00:00 2001 From: ahocevar Date: Tue, 30 Apr 2013 14:45:47 +0200 Subject: [PATCH 4/4] Do not check for listeners, as suggested by @fredj --- src/ol/mapbrowserevent.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ol/mapbrowserevent.js b/src/ol/mapbrowserevent.js index 4e7c982f26..035178f538 100644 --- a/src/ol/mapbrowserevent.js +++ b/src/ol/mapbrowserevent.js @@ -295,11 +295,8 @@ ol.MapBrowserEventHandler.prototype.handleMouseMove_ = function(browserEvent) { * @private */ ol.MapBrowserEventHandler.prototype.relayMouseMove_ = function(browserEvent) { - if (goog.events.hasListener( - this.map_, ol.MapBrowserEvent.EventType.MOUSEMOVE)) { - this.dispatchEvent(new ol.MapBrowserEvent( - ol.MapBrowserEvent.EventType.MOUSEMOVE, this.map_, browserEvent)); - } + this.dispatchEvent(new ol.MapBrowserEvent( + ol.MapBrowserEvent.EventType.MOUSEMOVE, this.map_, browserEvent)); };