Skip to content

Commit

Permalink
feat(landing): show labels on landing page
Browse files Browse the repository at this point in the history
  • Loading branch information
blacha committed Aug 25, 2024
1 parent 1dfb33a commit 9dda799
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 95 deletions.
66 changes: 44 additions & 22 deletions packages/lambda-tiler/src/routes/tile.style.json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ export function convertStyleJson(
const sources = JSON.parse(JSON.stringify(style.sources)) as Sources;
for (const [key, value] of Object.entries(sources)) {
if (value.type === 'vector') {
if (tileMatrix !== GoogleTms) {
throw new LambdaHttpResponse(400, `TileMatrix is not supported for the vector source ${value.url}.`);
}
value.url = convertRelativeUrl(value.url, tileMatrix, apiKey, config);
} else if ((value.type === 'raster' || value.type === 'raster-dem') && Array.isArray(value.tiles)) {
for (let i = 0; i < value.tiles.length; i++) {
Expand Down Expand Up @@ -82,6 +79,13 @@ export interface StyleGet {
};
}

export interface StyleConfig {
/** Name of the terrain layer */
terrain?: string | null;
/** Combine layer with the labels layer */
labels: boolean;
}

function setStyleTerrain(style: StyleJson, terrain: string, tileMatrix: TileMatrixSet): void {
const source = Object.keys(style.sources).find((s) => s === terrain);
if (source == null) throw new LambdaHttpResponse(400, `Terrain: ${terrain} is not exists in the style source.`);
Expand All @@ -91,32 +95,47 @@ function setStyleTerrain(style: StyleJson, terrain: string, tileMatrix: TileMatr
};
}

async function setStyleLabels(req: LambdaHttpRequest<StyleGet>, style: StyleJson): Promise<void> {
const config = await ConfigLoader.load(req);
const labels = await config.Style.get('labels');
if (labels == null) {
req.log.warn('LabelsStyle:Missing');
return;
}

if (style.glyphs == null) style.glyphs = labels.style.glyphs;
if (style.sprite == null) style.sprite = labels.style.sprite;
if (style.sky == null) style.sky = labels.style.sky;

Object.assign(style.sources, labels.style.sources);
style.layers = style.layers.concat(labels.style.layers);
}

async function ensureTerrain(
req: LambdaHttpRequest<StyleGet>,
tileMatrix: TileMatrixSet,
apiKey: string,
style: StyleJson,
): Promise<void> {
const config = await ConfigLoader.load(req);
const terrain = await config.TileSet.get('ts_elevation');
if (terrain) {
const configLocation = ConfigLoader.extract(req);
const elevationQuery = toQueryString({ config: configLocation, api: apiKey, pipeline: 'terrain-rgb' });
style.sources['LINZ-Terrain'] = {
type: 'raster-dem',
tileSize: 256,
maxzoom: 18,
tiles: [convertRelativeUrl(`/v1/tiles/elevation/${tileMatrix.identifier}/{z}/{x}/{y}.png${elevationQuery}`)],
};
}
const terrain = await config.TileSet.get('elevation');
if (terrain == null) return;
const configLocation = ConfigLoader.extract(req);
const elevationQuery = toQueryString({ config: configLocation, api: apiKey, pipeline: 'terrain-rgb' });
style.sources['LINZ-Terrain'] = {
type: 'raster-dem',
tileSize: 256,
maxzoom: 18,
tiles: [convertRelativeUrl(`/v1/tiles/elevation/${tileMatrix.identifier}/{z}/{x}/{y}.png${elevationQuery}`)],
};
}

export async function tileSetToStyle(
req: LambdaHttpRequest<StyleGet>,
tileSet: ConfigTileSetRaster,
tileMatrix: TileMatrixSet,
apiKey: string,
terrain?: string,
cfg: StyleConfig,
): Promise<LambdaHttpResponse> {
const [tileFormat] = Validate.getRequestedFormats(req) ?? ['webp'];
if (tileFormat == null) return new LambdaHttpResponse(400, 'Invalid image format');
Expand Down Expand Up @@ -144,9 +163,10 @@ export async function tileSetToStyle(
await ensureTerrain(req, tileMatrix, apiKey, style);

// Add terrain in style
if (terrain) setStyleTerrain(style, terrain, tileMatrix);
if (cfg.terrain) setStyleTerrain(style, cfg.terrain, tileMatrix);
if (cfg.labels) await setStyleLabels(req, style);

const data = Buffer.from(JSON.stringify(style));
const data = Buffer.from(JSON.stringify(convertStyleJson(style, tileMatrix, apiKey, configLocation)));

const cacheKey = Etag.key(data);
if (Etag.isNotModified(req, cacheKey)) return NotModified();
Expand All @@ -164,7 +184,7 @@ export async function tileSetOutputToStyle(
tileSet: ConfigTileSetRaster,
tileMatrix: TileMatrixSet,
apiKey: string,
terrain?: string,
cfg: StyleConfig,
): Promise<LambdaHttpResponse> {
const configLocation = ConfigLoader.extract(req);
const query = toQueryString({ config: configLocation, api: apiKey });
Expand Down Expand Up @@ -227,9 +247,10 @@ export async function tileSetOutputToStyle(
await ensureTerrain(req, tileMatrix, apiKey, style);

// Add terrain in style
if (terrain) setStyleTerrain(style, terrain, tileMatrix);
if (cfg.terrain) setStyleTerrain(style, cfg.terrain, tileMatrix);
if (cfg.labels) await setStyleLabels(req, style);

const data = Buffer.from(JSON.stringify(style));
const data = Buffer.from(JSON.stringify(convertStyleJson(style, tileMatrix, apiKey, configLocation)));

const cacheKey = Etag.key(data);
if (Etag.isNotModified(req, cacheKey)) return Promise.resolve(NotModified());
Expand All @@ -250,6 +271,7 @@ export async function styleJsonGet(req: LambdaHttpRequest<StyleGet>): Promise<La
const tileMatrix = TileMatrixSets.find(req.query.get('tileMatrix') ?? GoogleTms.identifier);
if (tileMatrix == null) return new LambdaHttpResponse(400, 'Invalid tile matrix');
const terrain = req.query.get('terrain') ?? undefined;
const labels = Boolean(req.query.get('labels') ?? false);

// Get style Config from db
const config = await ConfigLoader.load(req);
Expand All @@ -260,8 +282,8 @@ export async function styleJsonGet(req: LambdaHttpRequest<StyleGet>): Promise<La
const tileSet = await config.TileSet.get(config.TileSet.id(styleName));
if (tileSet == null) return NotFound();
if (tileSet.type !== TileSetType.Raster) return NotFound();
if (tileSet.outputs) return await tileSetOutputToStyle(req, tileSet, tileMatrix, apiKey, terrain);
else return await tileSetToStyle(req, tileSet, tileMatrix, apiKey, terrain);
if (tileSet.outputs) return await tileSetOutputToStyle(req, tileSet, tileMatrix, apiKey, { terrain, labels });
else return await tileSetToStyle(req, tileSet, tileMatrix, apiKey, { terrain, labels });
}

// Prepare sources and add linz source
Expand Down
69 changes: 69 additions & 0 deletions packages/landing/src/components/map.label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { IControl } from 'maplibre-gl';

import { Config } from '../config.js';

const LabelsDisabledLayers = new Set(['topographic']);

export class MapLabelControl implements IControl {
map?: maplibregl.Map;
container?: HTMLDivElement;
button?: HTMLButtonElement;
buttonIcon?: HTMLElement;
events: (() => boolean)[] = [];

onAdd(map: maplibregl.Map): HTMLDivElement {
this.map = map;
this.container = document.createElement('div');
this.container.className = 'maplibregl-ctrl maplibregl-ctrl-group';

this.button = document.createElement('button');
this.button.className = 'maplibregl-ctrl-labels';
this.button.type = 'button';
this.button.addEventListener('click', this.toggleLabels);

this.buttonIcon = document.createElement('i');
this.buttonIcon.className = 'material-icons-round';
this.buttonIcon.innerText = 'more';
this.button.appendChild(this.buttonIcon);
this.container.appendChild(this.button);

// this.button.innerHTML = `<i class="material-icons material-icons-outlined">more</span>`;

this.events.push(Config.map.on('labels', this.updateLabelIcon));
this.events.push(Config.map.on('layer', this.updateLabelIcon));

this.updateLabelIcon();
return this.container;
}

onRemove(): void {
this.container?.parentNode?.removeChild(this.container);
for (const evt of this.events) evt();
this.events = [];
this.map = undefined;
}

toggleLabels = (): void => {
Config.map.setLabels(!Config.map.labels);
};

updateLabelIcon = (): void => {
if (this.button == null) return;
this.button.classList.remove('maplibregl-ctrl-labels-enabled');

// Topographic style disables the button
if (Config.map.style && LabelsDisabledLayers.has(Config.map.style)) {
this.button.classList.add('display-none');
this.button.title = 'Topographic style does not support layers';
return;
}
this.button.classList.remove('display-none');

if (Config.map.labels) {
this.button.classList.add('maplibregl-ctrl-labels-enabled');
this.button.title = 'Hide Labels';
} else {
this.button.title = 'Show Labels';
}
};
}
4 changes: 2 additions & 2 deletions packages/landing/src/components/map.switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class MapSwitcher extends Component {
const target = this.getStyleType();
this.currentStyle = `${target.layerId}::${target.style}`;

const style = tileGrid.getStyle(target.layerId, target.style);
const style = tileGrid.getStyle({ layerId: target.layerId, style: target.style });
const location = cfg.transformedLocation;

this.map = new maplibre.Map({
Expand Down Expand Up @@ -71,7 +71,7 @@ export class MapSwitcher extends Component {
const styleId = `${target.layerId}::${target.style}`;
if (this.currentStyle !== styleId) {
const tileGrid = getTileGrid(Config.map.tileMatrix.identifier);
const style = tileGrid.getStyle(target.layerId, target.style);
const style = tileGrid.getStyle({ layerId: target.layerId, style: target.style });
this.currentStyle = styleId;
this.map.setStyle(style);
}
Expand Down
64 changes: 6 additions & 58 deletions packages/landing/src/components/map.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { DefaultExaggeration } from '@basemaps/config/build/config/vector.style.js';
import { GoogleTms, LocationUrl } from '@basemaps/geo';
import maplibre, { RasterLayerSpecification } from 'maplibre-gl';
import maplibre from 'maplibre-gl';
import { Component, ReactNode } from 'react';

import { MapAttribution } from '../attribution.js';
import { Config } from '../config.js';
import { getTileGrid, locationTransform } from '../tile.matrix.js';
import { MapOptionType, WindowUrl } from '../url.js';
import { Debug } from './debug.js';
import { MapLabelControl } from './map.label.js';
import { MapSwitcher } from './map.switcher.js';

const LayerFadeTime = 750;

/**
* Map loading in maplibre is weird, the on('load') event is different to 'loaded'
* this function waits until the map.loaded() function is true before being run.
Expand Down Expand Up @@ -138,7 +136,7 @@ export class Basemaps extends Component<unknown, { isLayerSwitcherEnabled: boole
this.ensureScaleControl();
this.ensureElevationControl();
const tileGrid = getTileGrid(Config.map.tileMatrix.identifier);
const style = tileGrid.getStyle(Config.map.layerId, Config.map.style, undefined, Config.map.filter.date);
const style = tileGrid.getStyle(Config.map);
this.map.setStyle(style);
if (Config.map.tileMatrix !== GoogleTms) {
this.map.setMaxBounds([-179.9, -85, 179.9, 85]);
Expand All @@ -149,55 +147,6 @@ export class Basemaps extends Component<unknown, { isLayerSwitcherEnabled: boole
this.forceUpdate();
};

updateVisibleLayers = (newLayers: string): void => {
if (Config.map.visibleLayers == null) Config.map.visibleLayers = newLayers;
if (newLayers !== Config.map.visibleLayers) {
Config.map.visibleLayers = newLayers;
const newStyleId = `${Config.map.styleId}` + `before=${Config.map.filter.date.before?.slice(0, 4)}`;
if (this.map.getSource(newStyleId) == null) {
this.map.addSource(newStyleId, {
type: 'raster',
tiles: [
WindowUrl.toTileUrl({
urlType: MapOptionType.TileRaster,
tileMatrix: Config.map.tileMatrix,
layerId: Config.map.layerId,
config: Config.map.config,
date: Config.map.filter.date,
}),
],
tileSize: 256,
});
this.map.addLayer({
id: newStyleId,
type: 'raster',
source: newStyleId,
paint: { 'raster-opacity': 0 },
});
this.map.moveLayer(newStyleId); // Move to front
this.map.setPaintProperty(newStyleId, 'raster-opacity-transition', { duration: LayerFadeTime });
this.map.setPaintProperty(newStyleId, 'raster-opacity', 1);
}
}
};

removeOldLayers = (): void => {
const filteredLayers = this.map
?.getStyle()
.layers.filter((layer) => layer.id.startsWith(Config.map.styleId)) as RasterLayerSpecification[];
if (filteredLayers == null) return;
// The last item in the array is the top layer, we pop that to ensure it isn't removed
filteredLayers.pop();
for (const layer of filteredLayers) {
this.map.setPaintProperty(layer.id, 'raster-opacity-transition', { duration: LayerFadeTime });
this.map.setPaintProperty(layer.id, 'raster-opacity', 0);
setTimeout(() => {
this.map.removeLayer(layer.id);
this.map.removeSource(layer.source);
}, LayerFadeTime);
}
};

override componentDidMount(): void {
// Force the URL to be read before loading the map
Config.map.updateFromUrl();
Expand All @@ -206,7 +155,7 @@ export class Basemaps extends Component<unknown, { isLayerSwitcherEnabled: boole
if (this.el == null) throw new Error('Unable to find #map element');
const cfg = Config.map;
const tileGrid = getTileGrid(cfg.tileMatrix.identifier);
const style = tileGrid.getStyle(cfg.layerId, cfg.style, cfg.config);
const style = tileGrid.getStyle(Config.map);
const location = locationTransform(cfg.location, cfg.tileMatrix, GoogleTms);

this.map = new maplibre.Map({
Expand All @@ -227,22 +176,21 @@ export class Basemaps extends Component<unknown, { isLayerSwitcherEnabled: boole
const nav = new maplibre.NavigationControl({ visualizePitch: true });
this.map.addControl(nav, 'top-left');
if (!Config.map.isDebug) this.map.addControl(new maplibre.FullscreenControl({ container: this.el }));
this.map.addControl(new MapLabelControl(), 'top-left');

this.controlScale = new maplibre.ScaleControl({});
this.map.addControl(this.controlScale, 'bottom-right');
}

this.map.on('render', this.onRender);
this.map.on('idle', this.removeOldLayers);

onMapLoaded(this.map, () => {
this._events.push(
Config.map.on('location', this.updateLocation),
Config.map.on('tileMatrix', this.updateStyle),
Config.map.on('layer', this.updateStyle),
Config.map.on('labels', this.updateStyle),
Config.map.on('bounds', this.updateBounds),
// TODO: Disable updateVisibleLayers for now before we need implement date range slider
// Config.map.on('visibleLayers', this.updateVisibleLayers),
);
this.map.on('terrain', this.updateTerrainFromEvent);

Expand Down
Loading

0 comments on commit 9dda799

Please sign in to comment.