Skip to content

Commit 85265bb

Browse files
committed
Extract style properties to a Theme class
1 parent e13f8e9 commit 85265bb

11 files changed

Lines changed: 235 additions & 168 deletions

File tree

src/components.d.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ export namespace Components {
3030
* @default 0
3131
*/
3232
"padding": number;
33-
/**
34-
* @default 100
35-
*/
36-
"previewOpacity": number;
3733
"record": OgmRecord;
3834
"theme": 'light' | 'dark';
3935
}
@@ -232,10 +228,6 @@ declare namespace LocalJSX {
232228
* @default 0
233229
*/
234230
"padding"?: number;
235-
/**
236-
* @default 100
237-
*/
238-
"previewOpacity"?: number;
239231
"record"?: OgmRecord;
240232
"theme"?: 'light' | 'dark';
241233
}
@@ -279,7 +271,6 @@ declare namespace LocalJSX {
279271
}
280272
interface OgmMapAttributes {
281273
"theme": 'light' | 'dark';
282-
"previewOpacity": number;
283274
"padding": number;
284275
}
285276
interface OgmMenubarAttributes {

src/components/ogm-map/ogm-map.tsx

Lines changed: 35 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,12 @@ import { getElement } from '../../lib/elements';
77
import { getSources } from '../../lib/sources';
88
import { getMapPreviewers } from '../../lib/previewers';
99
import MapLibrePreviewer from '../../lib/previewers/maplibre';
10+
import MapLibreTheme from '../../lib/themes/maplibre';
1011

1112
// Register PMTiles protocol
1213
const protocol = new PMTilesProtocol();
1314
maplibregl.addProtocol('pmtiles', protocol.tile);
1415

15-
// Web Awesome's palette tokens are documented inline (e.g. "#0a3a1d /* oklch(...) */").
16-
// Browsers are supposed to strip CSS comments before exposing a custom property's computed
17-
// value, but Safari has a bug where the comment survives, which MapLibre then rejects as an
18-
// invalid color. Strip it defensively here so we don't depend on browser-specific behavior.
19-
const readColorProperty = (el: Element, property: string): string => {
20-
return window
21-
.getComputedStyle(el)
22-
.getPropertyValue(property)
23-
.replace(/\/\*.*?\*\//g, '')
24-
.trim();
25-
};
26-
2716
@Component({
2817
tag: 'ogm-map',
2918
styleUrl: 'ogm-map.css',
@@ -33,15 +22,16 @@ export class OgmMap {
3322
@Element() el: HTMLElement;
3423
@Prop() record: OgmRecord;
3524
@Prop() theme: 'light' | 'dark';
36-
@Prop() previewOpacity: number = 100;
3725
@Prop() padding: number = 0;
3826
@Event() mapIdle: EventEmitter<void>;
3927
@Event() mapLoading: EventEmitter<void>;
4028

4129
// MapLibre map instance and popup instance for feature info display
4230
protected map: maplibregl.Map;
31+
protected mapTheme: MapLibreTheme;
4332
protected popup: maplibregl.Popup | undefined = undefined;
4433
protected attributesEl: HTMLOgmAttributesElement;
34+
protected hoveredFeature: maplibregl.MapGeoJSONFeature | undefined = undefined;
4535

4636
// Container element reference for fullscreen
4737
protected containerEl: HTMLElement;
@@ -51,11 +41,12 @@ export class OgmMap {
5141

5242
// Set up the mapLibre map and event bindings on load
5343
componentDidLoad() {
44+
this.mapTheme = new MapLibreTheme(this.el);
5445
this.map = new maplibregl.Map({
5546
container: getElement(this.el, '#map'),
5647
attributionControl: false,
5748
cooperativeGestures: true,
58-
style: this.baseMapStyle,
49+
style: this.mapTheme.getBaseMapStyle(),
5950
center: [0, 0],
6051
zoom: 2,
6152
minZoom: 2,
@@ -69,18 +60,8 @@ export class OgmMap {
6960

7061
// View as a globe with atmosphere effects
7162
this.map.on('style.load', () => {
72-
this.map.setProjection({
73-
type: 'globe',
74-
});
75-
this.map.setSky({
76-
'sky-color': '#199EF3',
77-
'sky-horizon-blend': 0.5,
78-
'horizon-color': '#ffffff',
79-
'horizon-fog-blend': 0.5,
80-
'fog-color': '#0000ff',
81-
'fog-ground-blend': 0.5,
82-
'atmosphere-blend': ['interpolate', ['linear'], ['zoom'], 0, 1, 10, 1, 12, 0],
83-
});
63+
this.map.setProjection({ type: 'globe' });
64+
this.map.setSky(this.mapTheme.getSkyStyle());
8465
});
8566

8667
// Keep attributes outside Stencil render pipeline so that MapLibre can
@@ -135,31 +116,20 @@ export class OgmMap {
135116
this.clearFeatureSelection();
136117
this.destroyPopup();
137118

138-
// Collect previewers to remove after the new record is previewed
139-
const oldPreviewers = [];
140-
while (this.previewers.length) oldPreviewers.push(this.previewers.pop() as MapLibrePreviewer);
119+
// Clear existing previews
120+
while (this.previewers.length) await this.previewers.pop()?.clearPreview();
141121

142122
// Populate the new previewers and render them
143123
const sources = getSources(record);
144-
const previewOptions = {
145-
fillColor: this.fillColor,
146-
fillHighlightColor: this.fillHighlightColor,
147-
fillInvalidColor: this.fillInvalidColor,
148-
lineColor: this.lineColor,
149-
lineHighlightColor: this.lineHighlightColor,
150-
opacity: this.previewOpacity / 100,
151-
};
152-
const newPreviewers = await getMapPreviewers(sources, this.map, previewOptions);
153-
while (newPreviewers.length) this.previewers.push(newPreviewers.pop() as MapLibrePreviewer);
154-
for (const previewer of this.previewers) await previewer.preview();
124+
for (const previewer of await getMapPreviewers(sources, this.map, this.mapTheme.getStyle())) {
125+
this.previewers.push(previewer);
126+
await previewer.preview();
127+
}
155128

156129
// Fit to bounds from the record
157130
const bounds = record.getBounds();
158131
if (bounds) await this.fitMapBounds(bounds);
159132

160-
// Remove old previewers after the new ones are rendered
161-
for (const previewer of oldPreviewers) await previewer.clearPreview();
162-
163133
// Emit map idle event to signal that the map is ready
164134
this.mapIdle.emit();
165135
}
@@ -186,8 +156,13 @@ export class OgmMap {
186156
// Use the crosshair cursor if there's something to inspect
187157
protected handleHover(event: maplibregl.MapMouseEvent) {
188158
const features = this.map.queryRenderedFeatures(event.point, { layers: this.previewLayers });
189-
if (features.length > 0) this.map.getCanvas().style.cursor = 'crosshair';
190-
else this.map.getCanvas().style.cursor = '';
159+
if (features.length > 0) {
160+
this.map.getCanvas().style.cursor = 'crosshair';
161+
this.hoverFeature(features[0]);
162+
} else {
163+
this.map.getCanvas().style.cursor = '';
164+
this.clearHoveredFeature();
165+
}
191166
}
192167

193168
// Show the attributes popup on click
@@ -219,13 +194,28 @@ export class OgmMap {
219194
this.attributesEl.features.forEach(feature => {
220195
this.map.setFeatureState({ source: feature.source, id: feature.id, sourceLayer: feature.sourceLayer }, { selected: false });
221196
});
197+
this.attributesEl.features = [];
222198
}
223199

224200
// Set styling of a single feature to selected state
225201
protected selectFeature(feature: maplibregl.MapGeoJSONFeature) {
226202
this.map.setFeatureState({ source: feature.source, id: feature.id, sourceLayer: feature.sourceLayer }, { selected: true });
227203
}
228204

205+
// Set styling of a single feature to hovered state
206+
protected hoverFeature(feature: maplibregl.MapGeoJSONFeature) {
207+
this.clearHoveredFeature();
208+
this.hoveredFeature = feature;
209+
this.map.setFeatureState({ source: feature.source, id: feature.id, sourceLayer: feature.sourceLayer }, { hover: true });
210+
}
211+
212+
protected clearHoveredFeature() {
213+
if (this.hoveredFeature) {
214+
this.map.setFeatureState({ source: this.hoveredFeature.source, id: this.hoveredFeature.id, sourceLayer: this.hoveredFeature.sourceLayer }, { hover: false });
215+
this.hoveredFeature = undefined;
216+
}
217+
}
218+
229219
// Create a new popup and set its content and location
230220
protected createPopup(location: maplibregl.LngLatLike) {
231221
this.popup = new maplibregl.Popup({ maxWidth: 'none', closeButton: false }).setDOMContent(this.attributesEl).setLngLat(location).addTo(this.map);
@@ -244,41 +234,6 @@ export class OgmMap {
244234
return this.previewers.flatMap(previewer => previewer.layerIds);
245235
}
246236

247-
// Base map style based on the theme
248-
protected get baseMapStyle() {
249-
return this.theme === 'dark' ? 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json' : 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json';
250-
}
251-
252-
// Fill colors for vector data, based on the theme
253-
protected get fillColor() {
254-
return readColorProperty(this.el, '--wa-color-brand-50');
255-
}
256-
257-
// Fill color for vector data when selected, based on the theme
258-
protected get fillHighlightColor() {
259-
return readColorProperty(this.el, '--wa-color-success-50');
260-
}
261-
262-
// Fill color for vector data when missing/invalid, based on the theme
263-
protected get fillInvalidColor() {
264-
return readColorProperty(this.el, '--wa-color-warning-50');
265-
}
266-
267-
// Line/stroke color for vector data, based on the theme
268-
protected get lineColor() {
269-
return readColorProperty(this.el, '--wa-color-brand-80');
270-
}
271-
272-
// Line/stroke color for vector data when selected, based on the theme
273-
protected get lineHighlightColor() {
274-
return readColorProperty(this.el, '--wa-color-success-80');
275-
}
276-
277-
// Line/stroke color for vector data when missing/invalid, based on the theme
278-
protected get lineInvalidColor() {
279-
return readColorProperty(this.el, '--wa-color-warning-80');
280-
}
281-
282237
render() {
283238
return <div id="map" class={`wa-${this.theme}`}></div>;
284239
}

src/index.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,15 @@ <h1>OpenGeoMetadata Viewer Preview</h1>
7676
<p>Click a button to load a record.</p>
7777
<div class="inputs">
7878
<div class="buttons">
79+
<button
80+
data-record-url="https://raw.githubusercontent.com/OpenGeoMetadata/edu.berkeley/02b742b5d7ea01aa77bad37a4429951164b34eab/metadata-aardvark/s7sq63/geoblacklight.json"
81+
>
82+
Calaveras Contours (Line, WMS)
83+
</button>
7984
<button
8085
data-record-url="https://raw.githubusercontent.com/OpenGeoMetadata/edu.stanford.purl/b74664cbb36fb985fe2ebee8d0d6d9657f06ada1/metadata-aardvark/cg/357/zz/0321/geoblacklight.json"
8186
>
82-
Contours (Line, WMS, Restricted)
87+
Russian River Contours (Line, WMS, Restricted)
8388
</button>
8489
<button data-record-url="https://raw.githubusercontent.com/geoblacklight/geoblacklight/refs/heads/main/spec/fixtures/solr_documents/public_cog_princeton.json">
8590
Tibet (Scanned Map, COG)

src/lib/previewers/cog-deck.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,27 @@ import { DecoderPool } from '@developmentseed/geotiff';
44

55
import Previewer from './previewer';
66
import type Source from '../sources/source';
7+
import { type MapLibreStyle } from '../themes/maplibre';
78

89
// Deck.gl-based previewer for Cloud Optimized GeoTIFF (COG) sources
910
// Can warp COGs on the fly; supports projections other than Web Mercator
1011
// NOTE: can't be built currently due to a bug; see:
1112
// https://github.com/OpenGeoMetadata/ogm-viewer/issues/100
1213
export default class DeckCogPreviewer extends Previewer {
14+
// Store reference to the map and styles
15+
protected style: MapLibreStyle;
1316
protected map: maplibregl.Map;
17+
18+
// Store reference to the deck.gl overlay and the layer ID for cleanup
1419
protected deckOverlay: DeckOverlay;
1520
protected layerId: string | undefined = undefined;
1621

1722
private bounds: maplibregl.LngLatBoundsLike | undefined = undefined;
1823

19-
constructor(source: Source, map: maplibregl.Map) {
24+
constructor(source: Source, map: maplibregl.Map, style: MapLibreStyle) {
2025
super(source);
2126
this.map = map;
27+
this.style = style;
2228
this.deckOverlay = this.getDeckOverlay();
2329
}
2430

src/lib/previewers/cog.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { cogProtocol } from '@geomatico/maplibre-cog-protocol';
33

44
import RasterPreviewer from './raster';
55
import CogSource from '../sources/cog';
6-
import { MapLibreOptions } from './maplibre';
6+
import { type MapLibreStyle } from '../themes/maplibre';
77

88
// COG previewer using MapLibre COG protocol plugin
99
// Only works for COGs in Web Mercator projection; can't warp in-browser
@@ -12,8 +12,8 @@ export default class CogPreviewer extends RasterPreviewer {
1212
declare protected source: CogSource;
1313

1414
// Register the 'cog://' protocol handler with MapLibre when the previewer is created
15-
constructor(source: CogSource, map: maplibregl.Map, options?: Partial<MapLibreOptions>) {
16-
super(source, map, options);
15+
constructor(source: CogSource, map: maplibregl.Map, style: MapLibreStyle) {
16+
super(source, map, style);
1717
maplibregl.addProtocol('cog', cogProtocol);
1818
}
1919

src/lib/previewers/index.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,21 @@ import PMTilesVectorPreviewer from './pmtiles-vector';
1515
import RasterPreviewer from './raster';
1616
import WmsPreviewer from './wms';
1717

18+
import { type MapLibreStyle } from '../themes/maplibre';
19+
1820
// Given a list of sources, return a list of previewers that can be used to preview them on a map
19-
export const getMapPreviewers = async (sources: Source[], map: maplibregl.Map, options: any) => {
21+
export const getMapPreviewers = async (sources: Source[], map: maplibregl.Map, style: MapLibreStyle) => {
2022
const previewers = [] as MapLibrePreviewer[];
2123

2224
for (const source of sources) {
23-
if (source instanceof OpenIndexMapSource) previewers.push(new OpenIndexMapPreviewer(source, map, options));
24-
else if (source instanceof GeoJSONSource) previewers.push(new GeoJSONPreviewer(source, map, options));
25+
if (source instanceof OpenIndexMapSource) previewers.push(new OpenIndexMapPreviewer(source, map, style));
26+
else if (source instanceof GeoJSONSource) previewers.push(new GeoJSONPreviewer(source, map, style));
2527
else if (source instanceof PMTilesSource) {
26-
if (await source.isVector()) previewers.push(new PMTilesVectorPreviewer(source, map, options));
27-
else previewers.push(new PMTilesRasterPreviewer(source, map, options));
28-
} else if (source instanceof WmsSource) previewers.push(new WmsPreviewer(source, map, options));
29-
else if (source instanceof CogSource) previewers.push(new CogPreviewer(source, map, options));
30-
else if (source instanceof RasterSource) previewers.push(new RasterPreviewer(source, map, options));
28+
if (await source.isVector()) previewers.push(new PMTilesVectorPreviewer(source, map, style));
29+
else previewers.push(new PMTilesRasterPreviewer(source, map, style));
30+
} else if (source instanceof WmsSource) previewers.push(new WmsPreviewer(source, map, style));
31+
else if (source instanceof CogSource) previewers.push(new CogPreviewer(source, map, style));
32+
else if (source instanceof RasterSource) previewers.push(new RasterPreviewer(source, map, style));
3133
}
3234

3335
return previewers;

src/lib/previewers/maplibre.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,26 @@ import type { SourceSpecification, AddLayerObject } from 'maplibre-gl';
22

33
import Previewer from './previewer';
44
import Source from '../sources/source';
5-
6-
export type MapLibreOptions = {
7-
padding: number; // pixels
8-
opacity: number; // decimal between 0 and 1
9-
};
10-
11-
const defaultOptions: MapLibreOptions = {
12-
padding: 40,
13-
opacity: 1,
14-
} as const;
5+
import { type MapLibreStyle } from '../themes/maplibre';
156

167
export default abstract class MapLibrePreviewer extends Previewer {
8+
// Store reference to the map and styles
9+
protected style: MapLibreStyle;
10+
protected map: maplibregl.Map;
11+
1712
// Stored state for added source and layers to allow for cleanup
1813
sourceId: string | null = null;
1914
layerIds: string[] = [];
2015

21-
protected map: maplibregl.Map;
22-
protected options: MapLibreOptions;
16+
// Current opacity state
2317
protected opacity: number;
2418

25-
constructor(source: Source, map: maplibregl.Map, options?: Partial<MapLibreOptions>) {
19+
// Initialize with opacity at the theme's opacity value
20+
constructor(source: Source, map: maplibregl.Map, style: MapLibreStyle) {
2621
super(source);
2722
this.map = map;
28-
this.options = { ...defaultOptions, ...options } as MapLibreOptions;
29-
this.opacity = this.options.opacity;
23+
this.style = style;
24+
this.opacity = this.style.opacity;
3025
}
3126

3227
// Add source and preview layers if they don't already exist

src/lib/previewers/pmtiles-vector.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ export default class PMTilesVectorPreviewer extends VectorPreviewer {
1919
};
2020
}
2121

22+
protected createPolygonOutlineLayer(layerId: string): LineLayerSpecification {
23+
return {
24+
...super.createPolygonOutlineLayer(layerId),
25+
'source-layer': layerId,
26+
};
27+
}
28+
2229
protected createLineLayer(layerId: string): LineLayerSpecification {
2330
return {
2431
...super.createLineLayer(layerId),

0 commit comments

Comments
 (0)