From 7e05b99ba421ce7024f1f421d212623ce5a4b045 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 9 Apr 2018 16:06:07 -0400 Subject: [PATCH 1/2] Allow changing the feature.mouseover order. Add an event that allows changing the selection order for feature.mouseover. Also, add two common handlers for such ordering. Specifically, the geo.event.feature.mouseover_order event is triggered if the mouse is located on multiple elements of a feature. The `feature.mouseOverOrderHighestIndex` function will always sort these so that the highest index element is on top. The `polygonFeature.mouseOverOrderClosestBorder` function will sort polygons so that they are sorted based on how far the mouse is from their borders, with the closest on top. Note that, as always, the mouse must be within the polygon for it to be considered. --- src/event.js | 7 ++ src/feature.js | 34 +++++++++ src/polygonFeature.js | 55 ++++++++++++++ tests/cases/feature.js | 6 ++ tests/cases/polygonFeature.js | 134 +++++++++++++++++++++++++--------- 5 files changed, 200 insertions(+), 36 deletions(-) diff --git a/src/event.js b/src/event.js index 97cbd2c97d..9315e74c67 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 `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 diff --git a/src/feature.js b/src/feature.js index 44ec938f06..4f24123fe5 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.data. 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]; @@ -733,6 +748,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..022f465ac3 100644 --- a/tests/cases/feature.js +++ b/tests/cases/feature.js @@ -254,6 +254,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 () { From 12706fc9d429a1f8ad91ff6be1bee83fce04ab30 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Wed, 11 Apr 2018 12:34:23 -0400 Subject: [PATCH 2/2] Add a geo.event.feature.mouseclick_order event. One event is generate per feature that is under the mouse when a mouse click is generated on features. By reordering them, this affects the order those events are generated and tags a different event as the top element. This also fixes some documentation. --- src/event.js | 14 ++++++++++---- src/feature.js | 12 +++++++++++- tests/cases/feature.js | 5 +++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/event.js b/src/event.js index 9315e74c67..84350ccafe 100644 --- a/src/event.js +++ b/src/event.js @@ -324,10 +324,10 @@ geo_event.feature = { */ mouseover: 'geo_feature_mouseover', /** - * The event contains the `feature`, 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 + * 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', /** @@ -350,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 4f24123fe5..1982418c5f 100644 --- a/src/feature.js +++ b/src/feature.js @@ -211,7 +211,7 @@ var feature = function (arg) { // 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.data. Handlers should not modify evt.over.extra or + // 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, { @@ -326,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) { diff --git a/tests/cases/feature.js b/tests/cases/feature.js index 022f465ac3..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();