refactor: replace UI module CSS files and add map icon assets for enhanced styling
Build and Release / release (push) Successful in 1m3s
|
After Width: | Height: | Size: 448 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 430 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
@@ -5,7 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import Map, { type MapHoverPayload } from "@/uhm/components/Map";
|
import Map, { type MapHoverPayload } from "@/uhm/components/Map";
|
||||||
import PublicWikiSidebar from "@/uhm/components/wiki/PublicWikiSidebar";
|
import PublicWikiSidebar from "@/uhm/components/wiki/PublicWikiSidebar";
|
||||||
import TimelineBar from "@/uhm/components/ui/TimelineBar";
|
import TimelineBar from "@/uhm/components/ui/TimelineBar";
|
||||||
import mapLayersStyles from "./MapLayers.module.css";
|
import mapLayersStyles from "@/styles/MapLayers.module.css";
|
||||||
import { fetchEntities, type Entity } from "@/uhm/api/entities";
|
import { fetchEntities, type Entity } from "@/uhm/api/entities";
|
||||||
import { fetchGeometriesByBBox } from "@/uhm/api/geometries";
|
import { fetchGeometriesByBBox } from "@/uhm/api/geometries";
|
||||||
import { ApiError } from "@/uhm/api/http";
|
import { ApiError } from "@/uhm/api/http";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FIXED_TIMELINE_END_YEAR, FIXED_TIMELINE_START_YEAR, clampYearValue } from "@/uhm/lib/utils/timeline";
|
import { FIXED_TIMELINE_END_YEAR, FIXED_TIMELINE_START_YEAR, clampYearValue } from "@/uhm/lib/utils/timeline";
|
||||||
import styles from "./TimelineBar.module.css";
|
import styles from "@/styles/TimelineBar.module.css";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
year: number;
|
year: number;
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ export const POINT_GEOTYPE_IDS = [
|
|||||||
|
|
||||||
export type PointGeotypeId = (typeof POINT_GEOTYPE_IDS)[number];
|
export type PointGeotypeId = (typeof POINT_GEOTYPE_IDS)[number];
|
||||||
|
|
||||||
|
export const POINT_GEOTYPE_ICON_PATHS: Partial<Record<PointGeotypeId, string>> = {
|
||||||
|
person_birthplace: "/images/mapIcon/point/house.png",
|
||||||
|
person_deathplace: "/images/mapIcon/point/tombstone.png",
|
||||||
|
person_activity: "/images/mapIcon/point/flag.png",
|
||||||
|
temple: "/images/mapIcon/point/temple.png",
|
||||||
|
capital: "/images/mapIcon/point/capital.png",
|
||||||
|
city: "/images/mapIcon/point/city.png",
|
||||||
|
fortress: "/images/mapIcon/point/fortress.png",
|
||||||
|
castle: "/images/mapIcon/point/castle.png",
|
||||||
|
ruin: "/images/mapIcon/point/ruin.png",
|
||||||
|
};
|
||||||
|
|
||||||
type PointIconVariant = "default" | "draft";
|
type PointIconVariant = "default" | "draft";
|
||||||
|
|
||||||
type PointLayerOptions = {
|
type PointLayerOptions = {
|
||||||
@@ -36,7 +48,7 @@ const TYPE_MATCH_EXPR: maplibregl.ExpressionSpecification = ["coalesce", ["get",
|
|||||||
const DRAFT_ENTITY_EXPR: maplibregl.ExpressionSpecification = ["==", ["coalesce", ["get", "entity_id"], ""], ""];
|
const DRAFT_ENTITY_EXPR: maplibregl.ExpressionSpecification = ["==", ["coalesce", ["get", "entity_id"], ""], ""];
|
||||||
const SELECTED_EXPR: maplibregl.ExpressionSpecification = ["boolean", ["feature-state", "selected"], false];
|
const SELECTED_EXPR: maplibregl.ExpressionSpecification = ["boolean", ["feature-state", "selected"], false];
|
||||||
|
|
||||||
const ICON_CANVAS_SIZE = 64;
|
const ICON_CANVAS_SIZE = 48;
|
||||||
const DRAFT_FILL = "#ef4444";
|
const DRAFT_FILL = "#ef4444";
|
||||||
const DRAFT_RIM = "#7f1d1d";
|
const DRAFT_RIM = "#7f1d1d";
|
||||||
const POINT_GEOMETRY_FILTER: maplibregl.ExpressionSpecification = [
|
const POINT_GEOMETRY_FILTER: maplibregl.ExpressionSpecification = [
|
||||||
@@ -141,11 +153,11 @@ export function buildPointGeotypeLayers(
|
|||||||
source: pointSourceId,
|
source: pointSourceId,
|
||||||
filter: pointFilter(typeId),
|
filter: pointFilter(typeId),
|
||||||
paint: {
|
paint: {
|
||||||
"circle-color": "#22c55e",
|
"circle-color": config.fill,
|
||||||
"circle-radius": ["case", SELECTED_EXPR, haloRadius, 0],
|
"circle-radius": ["case", SELECTED_EXPR, haloRadius, 0],
|
||||||
"circle-opacity": ["case", SELECTED_EXPR, 0.24, 0],
|
"circle-opacity": ["case", SELECTED_EXPR, 0.24, 0],
|
||||||
"circle-blur": ["case", SELECTED_EXPR, 0.8, 0],
|
"circle-blur": ["case", SELECTED_EXPR, 0.8, 0],
|
||||||
"circle-stroke-color": "#14532d",
|
"circle-stroke-color": config.rim,
|
||||||
"circle-stroke-width": ["case", SELECTED_EXPR, 1.6, 0],
|
"circle-stroke-width": ["case", SELECTED_EXPR, 1.6, 0],
|
||||||
"circle-stroke-opacity": ["case", SELECTED_EXPR, 0.48, 0],
|
"circle-stroke-opacity": ["case", SELECTED_EXPR, 0.48, 0],
|
||||||
},
|
},
|
||||||
@@ -197,9 +209,58 @@ export function buildPointGeotypeLayers(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const preloadedImages: Record<string, HTMLImageElement> = {};
|
||||||
|
const loadedImageKeys = new Set<string>();
|
||||||
|
const mapsToUpdate = new Set<maplibregl.Map>();
|
||||||
|
|
||||||
|
function preloadPointIcons() {
|
||||||
|
if (typeof window === "undefined" || typeof document === "undefined") return;
|
||||||
|
for (const [typeId, path] of Object.entries(POINT_GEOTYPE_ICON_PATHS)) {
|
||||||
|
if (!preloadedImages[typeId]) {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = path;
|
||||||
|
img.onload = () => {
|
||||||
|
loadedImageKeys.add(typeId);
|
||||||
|
for (const map of mapsToUpdate) {
|
||||||
|
updateIconsOnMap(map, typeId as PointGeotypeId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
preloadedImages[typeId] = img;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateIconsOnMap(map: maplibregl.Map, typeId: PointGeotypeId) {
|
||||||
|
if (!map || !map.getStyle()) return;
|
||||||
|
try {
|
||||||
|
for (const variant of ["default", "draft"] as const) {
|
||||||
|
const iconId = getPointIconId(typeId, variant);
|
||||||
|
const imageData = createPointIconImageData(typeId, variant);
|
||||||
|
if (imageData) {
|
||||||
|
if (map.hasImage(iconId)) {
|
||||||
|
map.updateImage(iconId, imageData);
|
||||||
|
} else {
|
||||||
|
map.addImage(iconId, imageData, { pixelRatio: 2 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Failed to update icon ${typeId} on map:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function ensurePointGeotypeIcons(map: maplibregl.Map): boolean {
|
export function ensurePointGeotypeIcons(map: maplibregl.Map): boolean {
|
||||||
if (typeof document === "undefined") return false;
|
if (typeof document === "undefined") return false;
|
||||||
|
|
||||||
|
preloadPointIcons();
|
||||||
|
|
||||||
|
const missingAny = Object.keys(POINT_GEOTYPE_ICON_PATHS).some(
|
||||||
|
(key) => !loadedImageKeys.has(key)
|
||||||
|
);
|
||||||
|
if (missingAny) {
|
||||||
|
mapsToUpdate.add(map);
|
||||||
|
}
|
||||||
|
|
||||||
for (const typeId of POINT_GEOTYPE_IDS) {
|
for (const typeId of POINT_GEOTYPE_IDS) {
|
||||||
for (const variant of ["default", "draft"] as const) {
|
for (const variant of ["default", "draft"] as const) {
|
||||||
const iconId = getPointIconId(typeId, variant);
|
const iconId = getPointIconId(typeId, variant);
|
||||||
@@ -271,6 +332,10 @@ function drawGlyphWithOutline(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function drawHouseGlyph(ctx: CanvasRenderingContext2D) {
|
function drawHouseGlyph(ctx: CanvasRenderingContext2D) {
|
||||||
|
const img = preloadedImages["person_birthplace"];
|
||||||
|
if (img && loadedImageKeys.has("person_birthplace")) {
|
||||||
|
ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
|
||||||
|
} else {
|
||||||
ctx.lineWidth = 3.5;
|
ctx.lineWidth = 3.5;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(22, 34);
|
ctx.moveTo(22, 34);
|
||||||
@@ -286,9 +351,14 @@ function drawHouseGlyph(ctx: CanvasRenderingContext2D) {
|
|||||||
ctx.moveTo(32, 43);
|
ctx.moveTo(32, 43);
|
||||||
ctx.lineTo(32, 36.5);
|
ctx.lineTo(32, 36.5);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawMemorialGlyph(ctx: CanvasRenderingContext2D) {
|
function drawMemorialGlyph(ctx: CanvasRenderingContext2D) {
|
||||||
|
const img = preloadedImages["person_deathplace"];
|
||||||
|
if (img && loadedImageKeys.has("person_deathplace")) {
|
||||||
|
ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
|
||||||
|
} else {
|
||||||
ctx.lineWidth = 3.6;
|
ctx.lineWidth = 3.6;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(32, 22);
|
ctx.moveTo(32, 22);
|
||||||
@@ -302,9 +372,14 @@ function drawMemorialGlyph(ctx: CanvasRenderingContext2D) {
|
|||||||
ctx.moveTo(24, 45);
|
ctx.moveTo(24, 45);
|
||||||
ctx.lineTo(40, 45);
|
ctx.lineTo(40, 45);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawFlagGlyph(ctx: CanvasRenderingContext2D) {
|
function drawFlagGlyph(ctx: CanvasRenderingContext2D) {
|
||||||
|
const img = preloadedImages["person_activity"];
|
||||||
|
if (img && loadedImageKeys.has("person_activity")) {
|
||||||
|
ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
|
||||||
|
} else {
|
||||||
ctx.lineWidth = 3.2;
|
ctx.lineWidth = 3.2;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(26, 22);
|
ctx.moveTo(26, 22);
|
||||||
@@ -323,9 +398,14 @@ function drawFlagGlyph(ctx: CanvasRenderingContext2D) {
|
|||||||
ctx.moveTo(22.5, 44.5);
|
ctx.moveTo(22.5, 44.5);
|
||||||
ctx.lineTo(31, 44.5);
|
ctx.lineTo(31, 44.5);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawTempleGlyph(ctx: CanvasRenderingContext2D) {
|
function drawTempleGlyph(ctx: CanvasRenderingContext2D) {
|
||||||
|
const img = preloadedImages["temple"];
|
||||||
|
if (img && loadedImageKeys.has("temple")) {
|
||||||
|
ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
|
||||||
|
} else {
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(22, 30);
|
ctx.moveTo(22, 30);
|
||||||
@@ -347,9 +427,14 @@ function drawTempleGlyph(ctx: CanvasRenderingContext2D) {
|
|||||||
ctx.lineTo(x, 42);
|
ctx.lineTo(x, 42);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawCrownGlyph(ctx: CanvasRenderingContext2D) {
|
function drawCrownGlyph(ctx: CanvasRenderingContext2D) {
|
||||||
|
const img = preloadedImages["capital"];
|
||||||
|
if (img && loadedImageKeys.has("capital")) {
|
||||||
|
ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
|
||||||
|
} else {
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(22, 41);
|
ctx.moveTo(22, 41);
|
||||||
@@ -367,9 +452,14 @@ function drawCrownGlyph(ctx: CanvasRenderingContext2D) {
|
|||||||
ctx.moveTo(23.5, 41.5);
|
ctx.moveTo(23.5, 41.5);
|
||||||
ctx.lineTo(40.5, 41.5);
|
ctx.lineTo(40.5, 41.5);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawCityGlyph(ctx: CanvasRenderingContext2D) {
|
function drawCityGlyph(ctx: CanvasRenderingContext2D) {
|
||||||
|
const img = preloadedImages["city"];
|
||||||
|
if (img && loadedImageKeys.has("city")) {
|
||||||
|
ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
|
||||||
|
} else {
|
||||||
ctx.fillRect(23, 33, 7, 10);
|
ctx.fillRect(23, 33, 7, 10);
|
||||||
ctx.fillRect(30, 27, 6, 16);
|
ctx.fillRect(30, 27, 6, 16);
|
||||||
ctx.fillRect(36, 30, 6, 13);
|
ctx.fillRect(36, 30, 6, 13);
|
||||||
@@ -380,9 +470,14 @@ function drawCityGlyph(ctx: CanvasRenderingContext2D) {
|
|||||||
ctx.clearRect(32, 35, 1.5, 1.5);
|
ctx.clearRect(32, 35, 1.5, 1.5);
|
||||||
ctx.clearRect(38, 33, 1.5, 1.5);
|
ctx.clearRect(38, 33, 1.5, 1.5);
|
||||||
ctx.clearRect(38, 37, 1.5, 1.5);
|
ctx.clearRect(38, 37, 1.5, 1.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawShieldGlyph(ctx: CanvasRenderingContext2D) {
|
function drawShieldGlyph(ctx: CanvasRenderingContext2D) {
|
||||||
|
const img = preloadedImages["fortress"];
|
||||||
|
if (img && loadedImageKeys.has("fortress")) {
|
||||||
|
ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
|
||||||
|
} else {
|
||||||
ctx.lineWidth = 3.2;
|
ctx.lineWidth = 3.2;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(32, 22.5);
|
ctx.moveTo(32, 22.5);
|
||||||
@@ -398,9 +493,14 @@ function drawShieldGlyph(ctx: CanvasRenderingContext2D) {
|
|||||||
ctx.moveTo(32, 25);
|
ctx.moveTo(32, 25);
|
||||||
ctx.lineTo(32, 39);
|
ctx.lineTo(32, 39);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawCastleGlyph(ctx: CanvasRenderingContext2D) {
|
function drawCastleGlyph(ctx: CanvasRenderingContext2D) {
|
||||||
|
const img = preloadedImages["castle"];
|
||||||
|
if (img && loadedImageKeys.has("castle")) {
|
||||||
|
ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
|
||||||
|
} else {
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.rect(24, 31, 16, 11);
|
ctx.rect(24, 31, 16, 11);
|
||||||
@@ -425,9 +525,14 @@ function drawCastleGlyph(ctx: CanvasRenderingContext2D) {
|
|||||||
ctx.moveTo(32, 42);
|
ctx.moveTo(32, 42);
|
||||||
ctx.lineTo(32, 34);
|
ctx.lineTo(32, 34);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawRuinGlyph(ctx: CanvasRenderingContext2D) {
|
function drawRuinGlyph(ctx: CanvasRenderingContext2D) {
|
||||||
|
const img = preloadedImages["ruin"];
|
||||||
|
if (img && loadedImageKeys.has("ruin")) {
|
||||||
|
ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
|
||||||
|
} else {
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.rect(26, 24, 12, 18);
|
ctx.rect(26, 24, 12, 18);
|
||||||
@@ -446,6 +551,7 @@ function drawRuinGlyph(ctx: CanvasRenderingContext2D) {
|
|||||||
ctx.lineTo(35, 33);
|
ctx.lineTo(35, 33);
|
||||||
ctx.lineTo(30, 39);
|
ctx.lineTo(30, 39);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawAnchorGlyph(ctx: CanvasRenderingContext2D) {
|
function drawAnchorGlyph(ctx: CanvasRenderingContext2D) {
|
||||||
@@ -494,3 +600,5 @@ function drawBridgeGlyph(ctx: CanvasRenderingContext2D) {
|
|||||||
ctx.lineTo(38, 31);
|
ctx.lineTo(38, 31);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||