Skip to content

Commit

Permalink
Add queryTileFeaturesDebug method (#144)
Browse files Browse the repository at this point in the history
* Add queryTileFeaturesDebug method

* Query features in tile at a given geographic position [#143]

* add comments to recommend queryTileFeaturesDebug for only basic use cases [#143]

* js 3.1.0
  • Loading branch information
bdon committed Mar 11, 2024
1 parent a8a5849 commit 7f4a3c1
Show file tree
Hide file tree
Showing 18 changed files with 338 additions and 15 deletions.
2 changes: 1 addition & 1 deletion benchmark/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<script src="pixelmatch.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.0.1/dist/protomaps-leaflet.min.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.1.0/dist/protomaps-leaflet.min.js"></script>
<!-- <script src="../dist/protomaps-leaflet.js"></script> -->
<style>
html {
Expand Down
2 changes: 1 addition & 1 deletion examples/comparison.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.css" crossorigin="anonymous">
<script src="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@maplibre/maplibre-gl-leaflet@0.0.20/leaflet-maplibre-gl.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.0.1/dist/protomaps-leaflet.min.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.1.0/dist/protomaps-leaflet.min.js"></script>
<style>
#parent {
display:flex;
Expand Down
2 changes: 1 addition & 1 deletion examples/fonts.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-hash@0.2.1/leaflet-hash.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.0.1/dist/protomaps-leaflet.min.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.1.0/dist/protomaps-leaflet.min.js"></script>
<!-- <script src="../dist/protomaps-leaflet.js"></script> -->

<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=Work+Sans:wght@100..900&family=Petrona:wght@100..900&family=Raleway:wght@100..900" rel="stylesheet">
Expand Down
2 changes: 1 addition & 1 deletion examples/inset.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/protomaps-leaflet@3.0.1/dist/protomaps-leaflet.min.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.1.0/dist/protomaps-leaflet.min.js"></script>
<!-- <script src="../dist/protomaps-leaflet.js"></script> -->
<style>
#map {
Expand Down
2 changes: 1 addition & 1 deletion examples/labels.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-hash@0.2.1/leaflet-hash.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.0.1/dist/protomaps-leaflet.min.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.1.0/dist/protomaps-leaflet.min.js"></script>
<!-- <script src="../dist/protomaps-leaflet.js"></script> -->
<style>
body, #map {
Expand Down
9 changes: 8 additions & 1 deletion examples/leaflet.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-hash@0.2.1/leaflet-hash.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.0.1/dist/protomaps-leaflet.min.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.1.0/dist/protomaps-leaflet.min.js"></script>
<!-- <script src="../dist/protomaps-leaflet.js"></script> -->
<style>
body, #map {
Expand All @@ -22,6 +22,13 @@
if (!window.location.hash) map.setView(new L.LatLng(0,0),0)
var layer = protomapsL.leafletLayer({url:'https://api.protomaps.com/tiles/v3/{z}/{x}/{y}.mvt?key=1003762824b9687f',theme:'light'})
layer.addTo(map)

map.on("click", (ev) => {
const wrapped = map.wrapLatLng(ev.latlng);
// note: this method supports only basic use,
// see comments in source code
console.log(layer.queryTileFeaturesDebug(wrapped.lng, wrapped.lat));
})
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion examples/multi_source.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-hash@0.2.1/leaflet-hash.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.0.1/dist/protomaps-leaflet.min.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.1.0/dist/protomaps-leaflet.min.js"></script>
<!-- <script src="../dist/protomaps-leaflet.js"></script> -->
<style>
body, #map {
Expand Down
2 changes: 1 addition & 1 deletion examples/pmtiles.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.0.1/dist/protomaps-leaflet.min.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.1.0/dist/protomaps-leaflet.min.js"></script>
<!-- <script src="../dist/protomaps-leaflet.js"></script> -->
<style>
body, #map {
Expand Down
2 changes: 1 addition & 1 deletion examples/pmtiles_headers.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<script src="https://unpkg.com/pmtiles@3.0.3/dist/pmtiles.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.0.1/dist/protomaps-leaflet.min.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.1.0/dist/protomaps-leaflet.min.js"></script>
<!-- <script src="../dist/protomaps-leaflet.js"></script> -->
<style>
body, #map {
Expand Down
2 changes: 1 addition & 1 deletion examples/sandwich.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-hash@0.2.1/leaflet-hash.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.0.1/dist/protomaps-leaflet.min.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.1.0/dist/protomaps-leaflet.min.js"></script>
<!-- <script src="../dist/protomaps-leaflet.js"></script> -->
<style>
body, #map {
Expand Down
2 changes: 1 addition & 1 deletion examples/sprites.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-hash@0.2.1/leaflet-hash.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.0.1/dist/protomaps-leaflet.min.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.1.0/dist/protomaps-leaflet.min.js"></script>
<!-- <script src="../dist/protomaps-leaflet.js"></script> -->
<style>
body, #map {
Expand Down
2 changes: 1 addition & 1 deletion examples/static.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/protomaps-leaflet@3.0.1/dist/protomaps-leaflet.min.js"></script>
<script src="https://unpkg.com/protomaps-leaflet@3.1.0/dist/protomaps-leaflet.min.js"></script>
<!-- <script src="../dist/protomaps-leaflet.js"></script> -->
<style>
#map {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "protomaps-leaflet",
"version": "3.0.1",
"version": "3.1.0",
"files": [
"dist",
"src"
Expand Down
21 changes: 21 additions & 0 deletions src/frontends/leaflet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import themes from "../default_style/themes";
import { LabelRule, Labelers } from "../labeler";
import { PaintRule, paint } from "../painter";
import { PreparedTile, SourceOptions, sourcesToViews } from "../view";
import { PickedFeature } from "../tilecache";

const timer = (duration: number) => {
return new Promise<void>((resolve) => {
Expand Down Expand Up @@ -270,6 +271,26 @@ const leafletLayer = (options: LeafletLayerOptions = {}): unknown => {
}
}

// a primitive way to check the features at a certain point.
// it does not support hover states, cursor changes, or changing the style of the selected feature,
// so is only appropriate for debuggging or very basic use cases.
// those features are outside of the scope of this library:
// for fully pickable, interactive features, use MapLibre GL JS instead.
public queryTileFeaturesDebug(
lng: number,
lat: number,
brushSize = 16,
): Map<string, PickedFeature[]> {
const featuresBySourceName = new Map<string, PickedFeature[]>();
for (const [sourceName, view] of this.views) {
featuresBySourceName.set(
sourceName,
view.queryFeatures(lng, lat, this._map.getZoom(), brushSize),
);
}
return featuresBySourceName;
}

public clearLayout() {
this.labelers = new Labelers(
this.scratch,
Expand Down
143 changes: 143 additions & 0 deletions src/tilecache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,104 @@ interface PromiseOptions {
reject: (e: Error) => void;
}

export interface PickedFeature {
feature: Feature;
layerName: string;
}

const R = 6378137;
const MAX_LATITUDE = 85.0511287798;
const MAXCOORD = R * Math.PI;

const project = (latlng: number[]) => {
const d = Math.PI / 180;
const constrainedLat = Math.max(
Math.min(MAX_LATITUDE, latlng[0]),
-MAX_LATITUDE,
);
const sin = Math.sin(constrainedLat * d);
return new Point(
R * latlng[1] * d,
(R * Math.log((1 + sin) / (1 - sin))) / 2,
);
};

function sqr(x: number) {
return x * x;
}

function dist2(v: Point, w: Point) {
return sqr(v.x - w.x) + sqr(v.y - w.y);
}

function distToSegmentSquared(p: Point, v: Point, w: Point) {
const l2 = dist2(v, w);
if (l2 === 0) return dist2(p, v);
let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
t = Math.max(0, Math.min(1, t));
return dist2(p, new Point(v.x + t * (w.x - v.x), v.y + t * (w.y - v.y)));
}

export function isInRing(point: Point, ring: Point[]): boolean {
let inside = false;
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
const xi = ring[i].x;
const yi = ring[i].y;
const xj = ring[j].x;
const yj = ring[j].y;
const intersect =
yi > point.y !== yj > point.y &&
point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}

export function isCcw(ring: Point[]): boolean {
let area = 0;
for (let i = 0; i < ring.length; i++) {
const j = (i + 1) % ring.length;
area += ring[i].x * ring[j].y;
area -= ring[j].x * ring[i].y;
}
return area < 0;
}

export function pointInPolygon(point: Point, geom: Point[][]): boolean {
let isInCurrentExterior = false;
for (const ring of geom) {
if (isCcw(ring)) {
// it is an interior ring
if (isInRing(point, ring)) isInCurrentExterior = false;
} else {
// it is an exterior ring
if (isInCurrentExterior) return true;
if (isInRing(point, ring)) isInCurrentExterior = true;
}
}
return isInCurrentExterior;
}

export function pointMinDistToPoints(point: Point, geom: Point[][]): number {
let min = Infinity;
for (const l of geom) {
const dist = Math.sqrt(dist2(point, l[0]));
if (dist < min) min = dist;
}
return min;
}

export function pointMinDistToLines(point: Point, geom: Point[][]): number {
let min = Infinity;
for (const l of geom) {
for (let i = 0; i < l.length - 1; i++) {
const dist = Math.sqrt(distToSegmentSquared(point, l[i], l[i + 1]));
if (dist < min) min = dist;
}
}
return min;
}

export class TileCache {
source: TileSource;
cache: Map<string, CacheEntry>;
Expand Down Expand Up @@ -288,4 +386,49 @@ export class TileCache {
}
});
}

public queryFeatures(
lng: number,
lat: number,
zoom: number,
brushSize: number,
): PickedFeature[] {
const projected = project([lat, lng]);
const normalized = new Point(
(projected.x + MAXCOORD) / (MAXCOORD * 2),
1 - (projected.y + MAXCOORD) / (MAXCOORD * 2),
);
if (normalized.x > 1)
normalized.x = normalized.x - Math.floor(normalized.x);
const onZoom = normalized.mult(1 << zoom);
const tileX = Math.floor(onZoom.x);
const tileY = Math.floor(onZoom.y);
const idx = toIndex({ z: zoom, x: tileX, y: tileY });
const retval: PickedFeature[] = [];
const entry = this.cache.get(idx);
if (entry) {
const center = new Point(
(onZoom.x - tileX) * this.tileSize,
(onZoom.y - tileY) * this.tileSize,
);
for (const [layerName, layerArr] of entry.data.entries()) {
for (const feature of layerArr) {
if (feature.geomType === GeomType.Point) {
if (pointMinDistToPoints(center, feature.geom) < brushSize) {
retval.push({ feature, layerName: layerName });
}
} else if (feature.geomType === GeomType.Line) {
if (pointMinDistToLines(center, feature.geom) < brushSize) {
retval.push({ feature, layerName: layerName });
}
} else {
if (pointInPolygon(center, feature.geom)) {
retval.push({ feature, layerName: layerName });
}
}
}
}
}
return retval;
}
}
12 changes: 12 additions & 0 deletions src/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,18 @@ export class View {
dim: tt.dim,
};
}

public queryFeatures(
lng: number,
lat: number,
displayZoom: number,
brushSize: number,
) {
const roundedZoom = Math.round(displayZoom);
const dataZoom = Math.min(roundedZoom - this.levelDiff, this.maxDataLevel);
const brushSizeAtZoom = brushSize / (1 << (roundedZoom - dataZoom));
return this.tileCache.queryFeatures(lng, lat, dataZoom, brushSizeAtZoom);
}
}

export interface SourceOptions {
Expand Down
Loading

0 comments on commit 7f4a3c1

Please sign in to comment.