diff --git a/src/event.js b/src/event.js index 97cbd2c97d..84350ccafe 100644 --- a/src/event.js +++ b/src/event.js @@ -323,6 +323,13 @@ geo_event.feature = { * @event geo.event.feature.mouseover */ mouseover: 'geo_feature_mouseover', + /** + * The event contains the `feature`, the `mouse` record, the `previous` + * record of data elements that were under the mouse, and `over`, the new + * record of data elements that are unrder the mouse. + * @event geo.event.feature.mouseover_order + */ + mouseover_order: 'geo_feature_mouseover_order', /** * The event is the feature version of {@link geo.event.mouseout}. * @event geo.event.feature.mouseout @@ -343,6 +350,12 @@ geo_event.feature = { * @event geo.event.feature.mouseclick */ mouseclick: 'geo_feature_mouseclick', + /** + * The event contains the `feature`, the `mouse` record, and `over`, the + * record of data elements that are unrder the mouse. + * @event geo.event.feature.mouseclick_order + */ + mouseclick_order: 'geo_feature_mouseclick_order', /** * The event is the feature version of {@link geo.event.brushend}. * @event geo.event.feature.brushend diff --git a/src/feature.js b/src/feature.js index 44ec938f06..1982418c5f 100644 --- a/src/feature.js +++ b/src/feature.js @@ -206,7 +206,22 @@ var feature = function (arg) { if (!m_selectedFeatures.length && !over.index.length) { return; } + extra = over.extra || {}; + + // if we are over more than one item, trigger an event that is allowed to + // reorder the values in evt.over.index. Event handlers don't have to + // maintain evt.over.found. Handlers should not modify evt.over.extra or + // evt.previous. + if (over.index.length > 1) { + m_this.geoTrigger(geo_event.feature.mouseover_order, { + feature: this, + mouse: mouse, + previous: m_selectedFeatures, + over: over + }); + } + // Get the index of the element that was previously on top if (m_selectedFeatures.length) { lastTop = m_selectedFeatures[m_selectedFeatures.length - 1]; @@ -311,6 +326,16 @@ var feature = function (arg) { over = m_this.pointSearch(mouse.geo), extra = over.extra || {}; + // if we are over more than one item, trigger an event that is allowed to + // reorder the values in evt.over.index. Event handlers don't have to + // maintain evt.over.found. Handlers should not modify evt.over.extra. + if (over.index.length > 1) { + m_this.geoTrigger(geo_event.feature.mouseclick_order, { + feature: this, + mouse: mouse, + over: over + }); + } mouse.buttonsDown = evt.buttonsDown; feature.eventID += 1; over.index.forEach(function (i, idx) { @@ -733,6 +758,25 @@ var feature = function (arg) { return this; }; + /** + * If the selectionAPI is on, then setting + * `this.geoOn(geo.event.feature.mouseover_order, this.mouseOverOrderHighestIndex)` + * will make it so that the mouseon events prefer the highest index feature. + * + * @param {geo.event} evt The event; this should be triggered from + * `geo.event.feature.mouseover_order`. + */ + this.mouseOverOrderHighestIndex = function (evt) { + // sort the found indices. The last one is the one "on top". + evt.over.index.sort(); + // this isn't necessary, but ensures that other event handlers have + // consistent information + var data = evt.feature.data(); + evt.over.index.forEach(function (di, idx) { + evt.over.found[idx] = data[di]; + }); + }; + /** * Initialize the class instance. Derived classes should implement this. * diff --git a/src/polygonFeature.js b/src/polygonFeature.js index 4b2874b92a..4e6bd40977 100644 --- a/src/polygonFeature.js +++ b/src/polygonFeature.js @@ -153,6 +153,16 @@ var polygonFeature = function (arg) { return coordinates; } + /** + * Get the set of normalized polygon coordinates. + * + * @returns {object[]} An array of polygon positions. Each has `outer` and + * `inner` if it has any coordinates, or is undefined. + */ + this.polygonCoordinates = function () { + return m_coordinates; + }; + /** * Get the style for the stroke of the polygon. Since polygons can have * holes, the number of stroke lines may not be the same as the number of @@ -484,6 +494,51 @@ var polygonFeature = function (arg) { return m_this; }; + /** + * If the selectionAPI is on, then setting + * `this.geoOn(geo.event.feature.mouseover_order, this.mouseOverOrderClosestBorder)` + * will make it so that the mouseon events prefer the polygon with the + * closet border, including hole edges. + * + * @param {geo.event} evt The event; this should be triggered from + * `geo.event.feature.mouseover_order`. + */ + this.mouseOverOrderClosestBorder = function (evt) { + var data = evt.feature.data(), + map = evt.feature.layer().map(), + pt = transform.transformCoordinates(map.ingcs(), evt.feature.gcs(), evt.mouse.geo), + coor = evt.feature.polygonCoordinates(), + dist = {}; + evt.over.index.forEach(function (di, idx) { + var poly = coor[di], mindist; + poly.outer.forEach(function (line1, pidx) { + var line2 = poly.outer[(pidx + 1) % poly.outer.length]; + var dist = util.distance2dToLineSquared(pt, line1, line2); + if (mindist === undefined || dist < mindist) { + mindist = dist; + } + }); + poly.inner.forEach(function (inner) { + inner.forEach(function (line1, pidx) { + var line2 = inner[(pidx + 1) % inner.length]; + var dist = util.distance2dToLineSquared(pt, line1, line2); + if (mindist === undefined || dist < mindist) { + mindist = dist; + } + }); + }); + dist[di] = mindist; + }); + evt.over.index.sort(function (i1, i2) { + return dist[i1] - dist[i2]; + }).reverse(); + // this isn't necessary, but ensures that other event handlers have + // consistent information + evt.over.index.forEach(function (di, idx) { + evt.over.found[idx] = data[di]; + }); + }; + /** * Destroy. */ diff --git a/tests/cases/feature.js b/tests/cases/feature.js index 29b03ae63e..826ce25a39 100644 --- a/tests/cases/feature.js +++ b/tests/cases/feature.js @@ -90,6 +90,11 @@ describe('geo.feature', function () { feat.geoOn(geo.event.feature.brushend, function (evt) { events.brushend = evt; }); map.interactor().simulateEvent('mousemove', {map: {x: 20, y: 20}}); expect(events.mouseover.index).toBe(1); + points.index = [1, 2]; + map.interactor().simulateEvent('mousedown', {map: {x: 20, y: 20}, button: 'left'}); + map.interactor().simulateEvent('mouseup', {map: {x: 20, y: 20}, button: 'left'}); + expect(events.mouseclick.index).toBe(2); + expect(events.mouseclick.top).toBe(true); }); it('_unbindMouseHandlers', function () { feat._unbindMouseHandlers(); @@ -254,6 +259,12 @@ describe('geo.feature', function () { feat.updateStyleFromArray('radius', [11, 12, 13, 14], true); expect(count).toBe(1); }); + it('mouseOverOrderHighestIndex', function () { + var evt = {over: {index: [3, 4, 2], found: []}, feature: feat}; + expect(feat.mouseOverOrderHighestIndex(evt)).toBe(undefined); + expect(evt.over.index).toEqual([2, 3, 4]); + expect(evt.over.found.length).toBe(3); + }); }); describe('Check class accessors', function () { var map, layer, feat; diff --git a/tests/cases/polygonFeature.js b/tests/cases/polygonFeature.js index c9c7d2bd86..fa60cac527 100644 --- a/tests/cases/polygonFeature.js +++ b/tests/cases/polygonFeature.js @@ -147,44 +147,106 @@ describe('geo.polygonFeature', function () { }); describe('Public utility methods', function () { - describe('pointSearch', function () { - it('basic usage', function () { - mockVGLRenderer(); - var map, layer, polygon, data, pt; - map = createMap(); - layer = map.createLayer('feature', {renderer: 'vgl'}); - polygon = geo.polygonFeature({layer: layer}); - polygon._init(); - data = testPolygons; - polygon.data(data); - pt = polygon.pointSearch({x: 5, y: 5}); - expect(pt.index).toEqual([0]); - expect(pt.found.length).toBe(1); - expect(pt.found[0][0]).toEqual(data[0][0]); - pt = polygon.pointSearch({x: 21, y: 10}); - expect(pt.index).toEqual([1]); - expect(pt.found.length).toBe(1); - pt = polygon.pointSearch({x: 30, y: 10}); - expect(pt.index).toEqual([]); - expect(pt.found.length).toBe(0); - pt = polygon.pointSearch({x: 51, y: 10}); - expect(pt.index).toEqual([2, 3]); - expect(pt.found.length).toBe(2); - pt = polygon.pointSearch({x: 57, y: 10}); - expect(pt.index).toEqual([3]); - expect(pt.found.length).toBe(1); - /* If the inner hole extends past the outside, it doesn't make that - * point in the polygon */ - pt = polygon.pointSearch({x: 60, y: 13}); - expect(pt.index).toEqual([]); - expect(pt.found.length).toBe(0); + it('pointSearch', function () { + mockVGLRenderer(); + var map, layer, polygon, data, pt; + map = createMap(); + layer = map.createLayer('feature', {renderer: 'vgl'}); + polygon = geo.polygonFeature({layer: layer}); + polygon._init(); + data = testPolygons; + polygon.data(data); + pt = polygon.pointSearch({x: 5, y: 5}); + expect(pt.index).toEqual([0]); + expect(pt.found.length).toBe(1); + expect(pt.found[0][0]).toEqual(data[0][0]); + pt = polygon.pointSearch({x: 21, y: 10}); + expect(pt.index).toEqual([1]); + expect(pt.found.length).toBe(1); + pt = polygon.pointSearch({x: 30, y: 10}); + expect(pt.index).toEqual([]); + expect(pt.found.length).toBe(0); + pt = polygon.pointSearch({x: 51, y: 10}); + expect(pt.index).toEqual([2, 3]); + expect(pt.found.length).toBe(2); + pt = polygon.pointSearch({x: 57, y: 10}); + expect(pt.index).toEqual([3]); + expect(pt.found.length).toBe(1); + /* If the inner hole extends past the outside, it doesn't make that + * point in the polygon */ + pt = polygon.pointSearch({x: 60, y: 13}); + expect(pt.index).toEqual([]); + expect(pt.found.length).toBe(0); - // enable stroke and test very close, but outside, of an edge - polygon.style({stroke: true, strokeWidth: 20}); - pt = polygon.pointSearch({x: 5, y: 2.499}); - expect(pt.index).toEqual([0]); - restoreVGLRenderer(); + // enable stroke and test very close, but outside, of an edge + polygon.style({stroke: true, strokeWidth: 20}); + pt = polygon.pointSearch({x: 5, y: 2.499}); + expect(pt.index).toEqual([0]); + restoreVGLRenderer(); + }); + + it('polygonCoordinates', function () { + mockVGLRenderer(); + var map, layer, polygon; + map = createMap(); + layer = map.createLayer('feature', {renderer: 'vgl'}); + polygon = geo.polygonFeature({layer: layer}); + polygon._init(); + polygon.data(testPolygons); + var result = polygon.polygonCoordinates(); + expect(result.length).toEqual(testPolygons.length); + expect(result[0].outer.length).toBe(3); + expect(result[0].inner.length).toBe(0); + expect(result[1].outer.length).toBe(4); + expect(result[1].inner.length).toBe(1); + expect(result[1].inner[0].length).toBe(4); + restoreVGLRenderer(); + }); + + it('mouseOverOrderClosestBorder', function () { + mockVGLRenderer(); + var map, layer, polygon, data; + map = createMap(); + layer = map.createLayer('feature', {renderer: 'vgl'}); + polygon = geo.polygonFeature({layer: layer}); + polygon._init(); + // define some overlapping polygons for testing + data = [{ + outer: [[29, 20], [35, 20], [32, 25]] + }, { + outer: [[29, 22], [35, 22], [32, 27]], + inner: [[[30, 22.6], [34, 22.6], [32, 26]]] + }, { + outer: [[22, 30], [27, 32], [24, 35], [22, 35]] + }, { + outer: [[20, 30], [25, 32], [22, 35], [20, 35]] + }]; + polygon.data(data).position(function (vertex) { + return {x: vertex[0], y: vertex[1]}; }); + + var evt = { + over: {index: [2, 3], found: []}, + feature: polygon, + mouse: {geo: {x: 22.7, y: 34}} + }; + expect(polygon.mouseOverOrderClosestBorder(evt)).toBe(undefined); + expect(evt.over.index).toEqual([2, 3]); + evt.mouse.geo = {x: 22.2, y: 34}; + polygon.mouseOverOrderClosestBorder(evt); + expect(evt.over.index).toEqual([3, 2]); + evt.over.index = [0, 1]; + evt.mouse.geo = {x: 32, y: 22.2}; + polygon.mouseOverOrderClosestBorder(evt); + expect(evt.over.index).toEqual([0, 1]); + evt.mouse.geo = {x: 30.5, y: 22.2}; + polygon.mouseOverOrderClosestBorder(evt); + expect(evt.over.index).toEqual([1, 0]); + evt.mouse.geo = {x: 30.7, y: 22.5}; + polygon.mouseOverOrderClosestBorder(evt); + expect(evt.over.index).toEqual([0, 1]); + + restoreVGLRenderer(); }); describe('rdpSimplifyData', function () {