Refactor UI

This commit is contained in:
Andras Schmelczer 2026-02-04 22:27:56 +00:00
parent ce4c0cc08c
commit 34a4d0ba86
32 changed files with 1726 additions and 845 deletions

View file

@ -1,29 +1,47 @@
import type { ViewState, Bounds } from '../types';
import type { StyleSpecification } from 'maplibre-gl';
import { layers, namedFlavor } from '@protomaps/basemaps';
import {
GLYPHS_URL,
SPRITE_URL_BASE,
TILE_MAX_ZOOM,
OSM_ATTRIBUTION,
FEATURE_GRADIENT,
DENSITY_GRADIENT,
ZOOM_TO_RESOLUTION_THRESHOLDS,
TWEMOJI_BASE,
} from './consts';
// Self-hosted tile styles from server
export const MAP_STYLE_LIGHT = '/api/tiles/style.json?theme=light';
export const MAP_STYLE_DARK = '/api/tiles/style.json?theme=dark';
// Re-export constants for backwards compatibility
export { FEATURE_GRADIENT as GRADIENT, DENSITY_GRADIENT, POSTCODE_ZOOM_THRESHOLD } from './consts';
export const GRADIENT: { t: number; color: [number, number, number] }[] = [
{ t: 0, color: [46, 204, 113] },
{ t: 0.33, color: [241, 196, 15] },
{ t: 0.66, color: [231, 76, 60] },
{ t: 1, color: [142, 68, 173] },
];
export const DENSITY_GRADIENT: { t: number; color: [number, number, number] }[] = [
{ t: 0, color: [130, 234, 220] },
{ t: 0.5, color: [20, 140, 180] },
{ t: 1, color: [88, 28, 140] },
];
export function getMapStyle(theme: 'light' | 'dark'): StyleSpecification {
const flavor = namedFlavor(theme);
// Use absolute URL for tiles - required by MapLibre
const tileUrl = `${window.location.origin}/api/tiles/{z}/{x}/{y}`;
return {
version: 8,
glyphs: GLYPHS_URL,
sprite: `${SPRITE_URL_BASE}/${theme}`,
sources: {
protomaps: {
type: 'vector',
tiles: [tileUrl],
maxzoom: TILE_MAX_ZOOM,
attribution: OSM_ATTRIBUTION,
},
},
layers: layers('protomaps', flavor, { lang: 'en' }),
} as StyleSpecification;
}
export function normalizedToColor(t: number): [number, number, number] {
if (t <= 0) return GRADIENT[0].color;
if (t >= 1) return GRADIENT[GRADIENT.length - 1].color;
if (t <= 0) return FEATURE_GRADIENT[0].color;
if (t >= 1) return FEATURE_GRADIENT[FEATURE_GRADIENT.length - 1].color;
for (let i = 0; i < GRADIENT.length - 1; i++) {
const lo = GRADIENT[i];
const hi = GRADIENT[i + 1];
for (let i = 0; i < FEATURE_GRADIENT.length - 1; i++) {
const lo = FEATURE_GRADIENT[i];
const hi = FEATURE_GRADIENT[i + 1];
if (t >= lo.t && t <= hi.t) {
const frac = (t - lo.t) / (hi.t - lo.t);
return [
@ -33,7 +51,7 @@ export function normalizedToColor(t: number): [number, number, number] {
];
}
}
return GRADIENT[GRADIENT.length - 1].color;
return FEATURE_GRADIENT[FEATURE_GRADIENT.length - 1].color;
}
export function countToColor(t: number): [number, number, number] {
@ -55,17 +73,11 @@ export function countToColor(t: number): [number, number, number] {
return DENSITY_GRADIENT[DENSITY_GRADIENT.length - 1].color;
}
/** Zoom threshold at which we switch from hexagons to postcode polygons */
export const POSTCODE_ZOOM_THRESHOLD = 15;
export function zoomToResolution(zoom: number): number {
if (zoom < 6) return 5;
if (zoom < 7) return 6;
if (zoom < 9.5) return 8;
if (zoom < 11) return 9;
if (zoom < 13) return 10;
if (zoom < 15) return 11;
return 12;
for (const { maxZoom, resolution } of ZOOM_TO_RESOLUTION_THRESHOLDS) {
if (zoom < maxZoom) return resolution;
}
return ZOOM_TO_RESOLUTION_THRESHOLDS[ZOOM_TO_RESOLUTION_THRESHOLDS.length - 1].resolution;
}
export function getBoundsFromViewState(
@ -103,8 +115,6 @@ export function getBoundsFromViewState(
return { south, west, north, east };
}
const TWEMOJI_BASE = 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/';
export function emojiToTwemojiUrl(emoji: string): string {
const codePoint = emoji.codePointAt(0);
if (!codePoint) return `${TWEMOJI_BASE}1f4cd.png`;