Order callback calls by distance to click position

All callback calls for hits with a tolerance > 0 are queued and
called ordered by distance after all hits are detected.
This commit is contained in:
Maximilian Krög
2020-11-29 02:32:04 +01:00
parent 4546eff66e
commit 23dc768c2e
10 changed files with 239 additions and 119 deletions

View File

@@ -106,10 +106,19 @@ class LayerRenderer extends Observable {
* @param {import("../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {import("./vector.js").FeatureCallback<T>} callback Feature callback.
* @return {T|void} Callback result.
* @param {Array<import("./Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T
*/
forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) {}
forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
return undefined;
}
/**
* @abstract

View File

@@ -10,6 +10,16 @@ import {shared as iconImageCache} from '../style/IconImageCache.js';
import {inView} from '../layer/Layer.js';
import {wrapX} from '../coordinate.js';
/**
* @typedef HitMatch
* @property {import("../Feature.js").FeatureLike} feature
* @property {import("../layer/Layer.js").default} layer
* @property {import("../geom/SimpleGeometry.js").default} geometry
* @property {number} distanceSq
* @property {import("./vector.js").FeatureCallback<T>} callback
* @template T
*/
/**
* @abstract
*/
@@ -92,7 +102,7 @@ class MapRenderer extends Disposable {
* @param {import("../Feature.js").FeatureLike} feature Feature.
* @param {import("../layer/Layer.js").default} layer Layer.
* @param {import("../geom/Geometry.js").default} geometry Geometry.
* @return {?} Callback result.
* @return {T|undefined} Callback result.
*/
function forEachFeatureAtCoordinate(managed, feature, layer, geometry) {
return callback.call(thisArg, feature, managed ? layer : null, geometry);
@@ -111,11 +121,12 @@ class MapRenderer extends Disposable {
const layerStates = frameState.layerStatesArray;
const numLayers = layerStates.length;
const matches = /** @type {Array<HitMatch<T>>} */ ([]);
const tmpCoord = [];
for (let i = 0; i < offsets.length; i++) {
for (let j = numLayers - 1; j >= 0; --j) {
const layerState = layerStates[j];
const layer = /** @type {import("../layer/Layer.js").default} */ (layerState.layer);
const layer = layerState.layer;
if (
layer.hasRenderer() &&
inView(layerState, viewState) &&
@@ -137,7 +148,8 @@ class MapRenderer extends Disposable {
tmpCoord,
frameState,
hitTolerance,
callback
callback,
matches
);
}
if (result) {
@@ -146,7 +158,16 @@ class MapRenderer extends Disposable {
}
}
}
return undefined;
if (matches.length === 0) {
return undefined;
}
const order = 1 / matches.length;
matches.forEach((m, i) => (m.distanceSq += i * order));
matches.sort((a, b) => a.distanceSq - b.distanceSq);
matches.some((m) => {
return (result = m.callback(m.feature, m.layer, m.geometry));
});
return result;
}
/**

View File

@@ -195,23 +195,32 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer {
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {import("../vector.js").FeatureCallback<T>} callback Feature callback.
* @return {T|void} Callback result.
* @param {Array<import("../Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T
*/
forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) {
forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
if (this.vectorRenderer_) {
return this.vectorRenderer_.forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback
callback,
matches
);
} else {
return super.forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback
callback,
matches
);
}
}

View File

@@ -407,55 +407,81 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {import("../vector.js").FeatureCallback<T>} callback Feature callback.
* @return {T|void} Callback result.
* @param {Array<import("../Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T
*/
forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) {
forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
if (!this.replayGroup_) {
return undefined;
} else {
const resolution = frameState.viewState.resolution;
const rotation = frameState.viewState.rotation;
const layer = this.getLayer();
}
const resolution = frameState.viewState.resolution;
const rotation = frameState.viewState.rotation;
const layer = this.getLayer();
/** @type {!Object<string, boolean>} */
const features = {};
/** @type {!Object<string, import("../Map.js").HitMatch<T>|true>} */
const features = {};
/**
* @param {import("../../Feature.js").FeatureLike} feature Feature.
* @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry.
* @return {?} Callback result.
*/
const featureCallback = function (feature, geometry) {
const key = getUid(feature);
if (!(key in features)) {
/**
* @param {import("../../Feature.js").FeatureLike} feature Feature.
* @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry.
* @param {number} distanceSq The squared distance to the click position
* @return {T|undefined} Callback result.
*/
const featureCallback = function (feature, geometry, distanceSq) {
const key = getUid(feature);
const match = features[key];
if (!match) {
if (distanceSq === 0) {
features[key] = true;
return callback(feature, layer, geometry);
}
};
let result;
const executorGroups = [this.replayGroup_];
if (this.declutterExecutorGroup) {
executorGroups.push(this.declutterExecutorGroup);
matches.push(
(features[key] = {
feature: feature,
layer: layer,
geometry: geometry,
distanceSq: distanceSq,
callback: callback,
})
);
} else if (match !== true && distanceSq < match.distanceSq) {
if (distanceSq === 0) {
features[key] = true;
matches.splice(matches.lastIndexOf(match), 1);
return callback(feature, layer, geometry);
}
match.geometry = geometry;
match.distanceSq = distanceSq;
}
executorGroups.forEach((executorGroup) => {
result =
result ||
executorGroup.forEachFeatureAtCoordinate(
coordinate,
resolution,
rotation,
hitTolerance,
featureCallback,
executorGroup === this.declutterExecutorGroup
? frameState.declutterTree.all().map((item) => item.value)
: null
);
});
return undefined;
};
return result;
let result;
const executorGroups = [this.replayGroup_];
if (this.declutterExecutorGroup) {
executorGroups.push(this.declutterExecutorGroup);
}
executorGroups.some((executorGroup) => {
return (result = executorGroup.forEachFeatureAtCoordinate(
coordinate,
resolution,
rotation,
hitTolerance,
featureCallback,
executorGroup === this.declutterExecutorGroup
? frameState.declutterTree.all().map((item) => item.value)
: null
));
});
return result;
}
/**

View File

@@ -381,10 +381,17 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {import("../vector.js").FeatureCallback<T>} callback Feature callback.
* @return {T|void} Callback result.
* @param {Array<import("../Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T
*/
forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) {
forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
const resolution = frameState.viewState.resolution;
const rotation = frameState.viewState.rotation;
hitTolerance = hitTolerance == undefined ? 0 : hitTolerance;
@@ -397,55 +404,82 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
const hitExtent = boundingExtent([coordinate]);
buffer(hitExtent, resolution * hitTolerance, hitExtent);
/** @type {!Object<string, boolean>} */
/** @type {!Object<string, import("../Map.js").HitMatch<T>|true>} */
const features = {};
/**
* @param {import("../../Feature.js").FeatureLike} feature Feature.
* @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry.
* @param {number} distanceSq The squared distance to the click position.
* @return {T|undefined} Callback result.
*/
const featureCallback = function (feature, geometry, distanceSq) {
let key = feature.getId();
if (key === undefined) {
key = getUid(feature);
}
const match = features[key];
if (!match) {
if (distanceSq === 0) {
features[key] = true;
return callback(feature, layer, geometry);
}
matches.push(
(features[key] = {
feature: feature,
layer: layer,
geometry: geometry,
distanceSq: distanceSq,
callback: callback,
})
);
} else if (match !== true && distanceSq < match.distanceSq) {
if (distanceSq === 0) {
features[key] = true;
matches.splice(matches.lastIndexOf(match), 1);
return callback(feature, layer, geometry);
}
match.geometry = geometry;
match.distanceSq = distanceSq;
}
return undefined;
};
const renderedTiles = /** @type {Array<import("../../VectorRenderTile.js").default>} */ (this
.renderedTiles);
let found;
let i, ii;
for (i = 0, ii = renderedTiles.length; i < ii; ++i) {
for (let i = 0, ii = renderedTiles.length; !found && i < ii; ++i) {
const tile = renderedTiles[i];
const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord);
if (!intersects(tileExtent, hitExtent)) {
continue;
}
const layerUid = getUid(layer);
const executorGroups = [tile.executorGroups[layerUid]];
const declutterExecutorGroups = tile.declutterExecutorGroups[layerUid];
if (declutterExecutorGroups) {
executorGroups.push(declutterExecutorGroups);
}
executorGroups.forEach((executorGroups) => {
executorGroups.some((executorGroups) => {
const declutteredFeatures =
executorGroups === declutterExecutorGroups
? frameState.declutterTree.all().map((item) => item.value)
: null;
for (let t = 0, tt = executorGroups.length; t < tt; ++t) {
const executorGroup = executorGroups[t];
found =
found ||
executorGroup.forEachFeatureAtCoordinate(
coordinate,
resolution,
rotation,
hitTolerance,
/**
* @param {import("../../Feature.js").FeatureLike} feature Feature.
* @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry.
* @return {?} Callback result.
*/
function (feature, geometry) {
let key = feature.getId();
if (key === undefined) {
key = getUid(feature);
}
if (!(key in features)) {
features[key] = true;
return callback(feature, layer, geometry);
}
},
executorGroups === declutterExecutorGroups
? frameState.declutterTree.all().map((item) => item.value)
: null
);
found = executorGroup.forEachFeatureAtCoordinate(
coordinate,
resolution,
rotation,
hitTolerance,
featureCallback,
declutteredFeatures
);
if (found) {
return true;
}
}
});
}

View File

@@ -598,13 +598,20 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {import("../vector.js").FeatureCallback<T>} callback Feature callback.
* @return {T|void} Callback result.
* @param {Array<import("../Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T
*/
forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) {
forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
assert(this.hitDetectionEnabled_, 66);
if (!this.hitRenderInstructions_) {
return;
return undefined;
}
const pixel = applyTransform(
@@ -623,6 +630,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
if (feature) {
return callback(feature, this.getLayer(), null);
}
return undefined;
}
/**