Skip to content

Commit 9a7241c

Browse files
committed
Implement inspection for vector layers
Ref #37
1 parent c3d9b91 commit 9a7241c

13 files changed

Lines changed: 402 additions & 65 deletions

File tree

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"@mapbox/geojson-extent": "^1.0.1",
6767
"@shoelace-style/shoelace": "^2.20.1",
6868
"@terraformer/wkt": "^2.2.1",
69+
"autolinker": "^4.1.5",
6970
"d3-scale": "^4.0.2",
7071
"lerc": "^4.1.1",
7172
"maplibre-gl": "^5.24.0",

src/components.d.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@
55
* It contains typing information for all components that exist in this project.
66
*/
77
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
8+
import { MapGeoJSONFeature } from "maplibre-gl";
89
import { OgmRecord } from "./lib/record";
9-
import { EaseToOptions } from "maplibre-gl";
10+
export { MapGeoJSONFeature } from "maplibre-gl";
1011
export { OgmRecord } from "./lib/record";
11-
export { EaseToOptions } from "maplibre-gl";
1212
export namespace Components {
13+
interface OgmAttributes {
14+
/**
15+
* @default []
16+
*/
17+
"features": MapGeoJSONFeature[];
18+
}
1319
interface OgmImage {
1420
/**
1521
* @default 0
@@ -19,7 +25,7 @@ export namespace Components {
1925
"theme": 'light' | 'dark';
2026
}
2127
interface OgmMap {
22-
"easeMapTo": (options: EaseToOptions) => Promise<maplibregl.Map>;
28+
"easeMapTo": (options: maplibregl.EaseToOptions) => Promise<maplibregl.Map>;
2329
/**
2430
* @default 0
2531
*/
@@ -64,6 +70,10 @@ export namespace Components {
6470
"theme": 'light' | 'dark';
6571
}
6672
}
73+
export interface OgmAttributesCustomEvent<T> extends CustomEvent<T> {
74+
detail: T;
75+
target: HTMLOgmAttributesElement;
76+
}
6777
export interface OgmImageCustomEvent<T> extends CustomEvent<T> {
6878
detail: T;
6979
target: HTMLOgmImageElement;
@@ -81,6 +91,23 @@ export interface OgmSettingsCustomEvent<T> extends CustomEvent<T> {
8191
target: HTMLOgmSettingsElement;
8292
}
8393
declare global {
94+
interface HTMLOgmAttributesElementEventMap {
95+
"featureSelected": MapGeoJSONFeature;
96+
}
97+
interface HTMLOgmAttributesElement extends Components.OgmAttributes, HTMLStencilElement {
98+
addEventListener<K extends keyof HTMLOgmAttributesElementEventMap>(type: K, listener: (this: HTMLOgmAttributesElement, ev: OgmAttributesCustomEvent<HTMLOgmAttributesElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
99+
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
100+
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
101+
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
102+
removeEventListener<K extends keyof HTMLOgmAttributesElementEventMap>(type: K, listener: (this: HTMLOgmAttributesElement, ev: OgmAttributesCustomEvent<HTMLOgmAttributesElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
103+
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
104+
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
105+
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
106+
}
107+
var HTMLOgmAttributesElement: {
108+
prototype: HTMLOgmAttributesElement;
109+
new (): HTMLOgmAttributesElement;
110+
};
84111
interface HTMLOgmImageElementEventMap {
85112
"imageLoaded": void;
86113
"imageLoading": void;
@@ -170,6 +197,7 @@ declare global {
170197
new (): HTMLOgmViewerElement;
171198
};
172199
interface HTMLElementTagNameMap {
200+
"ogm-attributes": HTMLOgmAttributesElement;
173201
"ogm-image": HTMLOgmImageElement;
174202
"ogm-map": HTMLOgmMapElement;
175203
"ogm-menubar": HTMLOgmMenubarElement;
@@ -180,6 +208,13 @@ declare global {
180208
}
181209
}
182210
declare namespace LocalJSX {
211+
interface OgmAttributes {
212+
/**
213+
* @default []
214+
*/
215+
"features"?: MapGeoJSONFeature[];
216+
"onFeatureSelected"?: (event: OgmAttributesCustomEvent<MapGeoJSONFeature>) => void;
217+
}
183218
interface OgmImage {
184219
"onImageLoaded"?: (event: OgmImageCustomEvent<void>) => void;
185220
"onImageLoading"?: (event: OgmImageCustomEvent<void>) => void;
@@ -264,6 +299,7 @@ declare namespace LocalJSX {
264299
}
265300

266301
interface IntrinsicElements {
302+
"ogm-attributes": OgmAttributes;
267303
"ogm-image": Omit<OgmImage, keyof OgmImageAttributes> & { [K in keyof OgmImage & keyof OgmImageAttributes]?: OgmImage[K] } & { [K in keyof OgmImage & keyof OgmImageAttributes as `attr:${K}`]?: OgmImageAttributes[K] } & { [K in keyof OgmImage & keyof OgmImageAttributes as `prop:${K}`]?: OgmImage[K] };
268304
"ogm-map": Omit<OgmMap, keyof OgmMapAttributes> & { [K in keyof OgmMap & keyof OgmMapAttributes]?: OgmMap[K] } & { [K in keyof OgmMap & keyof OgmMapAttributes as `attr:${K}`]?: OgmMapAttributes[K] } & { [K in keyof OgmMap & keyof OgmMapAttributes as `prop:${K}`]?: OgmMap[K] };
269305
"ogm-menubar": Omit<OgmMenubar, keyof OgmMenubarAttributes> & { [K in keyof OgmMenubar & keyof OgmMenubarAttributes]?: OgmMenubar[K] } & { [K in keyof OgmMenubar & keyof OgmMenubarAttributes as `attr:${K}`]?: OgmMenubarAttributes[K] } & { [K in keyof OgmMenubar & keyof OgmMenubarAttributes as `prop:${K}`]?: OgmMenubar[K] };
@@ -277,6 +313,7 @@ export { LocalJSX as JSX };
277313
declare module "@stencil/core" {
278314
export namespace JSX {
279315
interface IntrinsicElements {
316+
"ogm-attributes": LocalJSX.IntrinsicElements["ogm-attributes"] & JSXBase.HTMLAttributes<HTMLOgmAttributesElement>;
280317
"ogm-image": LocalJSX.IntrinsicElements["ogm-image"] & JSXBase.HTMLAttributes<HTMLOgmImageElement>;
281318
"ogm-map": LocalJSX.IntrinsicElements["ogm-map"] & JSXBase.HTMLAttributes<HTMLOgmMapElement>;
282319
"ogm-menubar": LocalJSX.IntrinsicElements["ogm-menubar"] & JSXBase.HTMLAttributes<HTMLOgmMenubarElement>;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
.attribute-table {
2+
font-family: var(--sl-font-mono);
3+
font-size: var(--sl-font-size-x-small);
4+
line-height: var(--sl-line-height-dense);
5+
word-wrap: break-word;
6+
border-collapse: collapse;
7+
border-style: hidden;
8+
overflow: hidden;
9+
10+
th,
11+
td {
12+
border: var(--sl-panel-border-width) solid var(--sl-panel-border-color);
13+
padding: 0.2em 0.5em;
14+
}
15+
16+
.key {
17+
max-width: 50%;
18+
vertical-align: top;
19+
}
20+
21+
.header,
22+
.value {
23+
word-wrap: anywhere;
24+
}
25+
}
26+
27+
.pagination {
28+
display: flex;
29+
align-items: center;
30+
justify-content: space-between;
31+
gap: 1em;
32+
33+
.count {
34+
flex-shrink: 0;
35+
}
36+
}
37+
38+
.label {
39+
font-size: var(--sl-font-size-small);
40+
font-weight: var(--sl-font-weight-bold);
41+
overflow: hidden;
42+
min-width: 0;
43+
max-width: 40ch;
44+
width: 100%;
45+
padding: 0.2em 0.5em;
46+
display: flex;
47+
justify-content: center;
48+
49+
div {
50+
overflow: hidden;
51+
text-overflow: ellipsis;
52+
white-space: nowrap;
53+
}
54+
}
55+
56+
sl-icon-button + .label {
57+
max-width: 30ch;
58+
padding: 0;
59+
}
60+
61+
.count {
62+
flex-shrink: 0;
63+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Component, Prop, State, Watch, Event, EventEmitter, h } from '@stencil/core';
2+
import { MapGeoJSONFeature } from 'maplibre-gl';
3+
import Autolinker from 'autolinker';
4+
5+
const LABEL_KEYS = ['name', 'title', 'label', 'id'] as const;
6+
7+
const getFeatureLabel = (feature: MapGeoJSONFeature): string | undefined => {
8+
const originalKeys = Object.keys(feature.properties || {});
9+
if (originalKeys.length === 0) return;
10+
const key = LABEL_KEYS.map(k => originalKeys.find(ok => ok.toLowerCase() === k)).find(Boolean);
11+
if (key) return feature.properties?.[key];
12+
};
13+
14+
@Component({
15+
tag: 'ogm-attributes',
16+
styleUrl: 'ogm-attributes.css',
17+
shadow: true,
18+
})
19+
export class OgmAttributes {
20+
@Prop() features: MapGeoJSONFeature[] = [];
21+
@Event() featureSelected: EventEmitter<MapGeoJSONFeature>;
22+
@State() private currentIndex = 0;
23+
24+
@Watch('features')
25+
onFeaturesChange() {
26+
this.currentIndex = 0;
27+
}
28+
29+
@Watch('currentIndex')
30+
onCurrentIndexChange() {
31+
const feature = this.features[this.currentIndex];
32+
if (feature) this.featureSelected.emit(feature);
33+
}
34+
35+
render() {
36+
if (this.features.length === 0) return null;
37+
38+
const feature = this.features[this.currentIndex];
39+
const multiple = this.features.length > 1;
40+
const label = getFeatureLabel(feature);
41+
42+
return (
43+
<table class="attribute-table">
44+
{multiple && label ? (
45+
<thead>
46+
<tr class="header">
47+
<td colSpan={2}>
48+
<div class="pagination">
49+
<sl-icon-button name="arrow-left" label="Previous feature" disabled={this.currentIndex === 0} onClick={() => this.currentIndex--} />
50+
{label && (
51+
<div class="label">
52+
<div>{label}</div>
53+
</div>
54+
)}
55+
<div class="count">
56+
{' '}
57+
({this.currentIndex + 1}/{this.features.length})
58+
</div>
59+
<sl-icon-button name="arrow-right" label="Next feature" disabled={this.currentIndex === this.features.length - 1} onClick={() => this.currentIndex++} />
60+
</div>
61+
</td>
62+
</tr>
63+
</thead>
64+
) : (
65+
label && (
66+
<thead>
67+
<tr class="header">
68+
<td colSpan={2}>
69+
<div class="label">
70+
<div>{label}</div>
71+
</div>
72+
</td>
73+
</tr>
74+
</thead>
75+
)
76+
)}
77+
<tbody>
78+
{Object.entries(feature.properties || {}).map(([key, value]) => (
79+
<tr key={key}>
80+
<td class="key">{key}</td>
81+
<td class="value" innerHTML={Autolinker.link(value.toString(), { hashtag: false, mention: false, phone: false })}></td>
82+
</tr>
83+
))}
84+
</tbody>
85+
</table>
86+
);
87+
}
88+
}

src/components/ogm-map/ogm-map.css

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66
position: relative;
77
}
88

9-
/* inspection cursor
10-
canvas {
11-
cursor: crosshair;
9+
/* Inspection popups */
10+
.maplibregl-popup-content {
11+
max-height: 200px;
12+
max-width: 400px;
13+
overflow: auto;
14+
padding: 0;
15+
box-shadow: var(--sl-shadow-large);
16+
border: var(--sl-panel-border-width) solid var(--sl-panel-border-color);
1217
}
13-
*/
1418

1519
/* Dark mode map adjustments */
1620
.sl-theme-dark {
@@ -47,6 +51,7 @@ canvas {
4751

4852
div.maplibregl-popup-content {
4953
background-color: black;
54+
color: #ccc;
5055
}
5156

5257
div.maplibregl-popup-anchor-right div.maplibregl-popup-tip {

0 commit comments

Comments
 (0)