Skip to content

Commit

Permalink
App improvements
Browse files Browse the repository at this point in the history
* Display OSM IDs on right click/long press.
* Make the zooming to tile extents explicit.
* Fix changing of theme/lang when drag-and-drop files are loaded.
  • Loading branch information
bdon committed Jan 8, 2025
1 parent 8b523a8 commit dd05c67
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 52 deletions.
162 changes: 120 additions & 42 deletions app/src/MapView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import {
removeProtocol,
setRTLTextPlugin,
} from "maplibre-gl";
import type { MapGeoJSONFeature, StyleSpecification } from "maplibre-gl";
import type {
MapGeoJSONFeature,
MapTouchEvent,
StyleSpecification,
} from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import type { LayerSpecification } from "@maplibre/maplibre-gl-style-spec";
import { FileSource, PMTiles, Protocol } from "pmtiles";
Expand Down Expand Up @@ -48,6 +52,38 @@ function getSourceLayer(l: LayerSpecification): string {
return "";
}

const featureIdToOsmId = (raw: string | number) => {
return Number(BigInt(raw) & ((BigInt(1) << BigInt(44)) - BigInt(1)));
};

const featureIdToOsmType = (i: string | number) => {
const t = (BigInt(i) >> BigInt(44)) & BigInt(3);
if (t === BigInt(1)) return "node";
if (t === BigInt(2)) return "way";
if (t === BigInt(3)) return "relation";
return "not_osm";
};

const displayId = (featureId?: string | number) => {
if (featureId) {
const osmType = featureIdToOsmType(featureId);
if (osmType !== "not_osm") {
const osmId = featureIdToOsmId(featureId);
return (
<a
class="underline text-purple"
target="_blank"
rel="noreferrer"
href={`https://openstreetmap.org/${osmType}/${osmId}`}
>
{osmType} {osmId}
</a>
);
}
}
return featureId;
};

const FeaturesProperties = (props: { features: MapGeoJSONFeature[] }) => {
return (
<div class="features-properties">
Expand All @@ -62,7 +98,7 @@ const FeaturesProperties = (props: { features: MapGeoJSONFeature[] }) => {
<tbody>
<tr>
<td>id</td>
<td>{f.id}</td>
<td>{displayId(f.id)}</td>
</tr>
<For each={Object.entries(f.properties)}>
{([key, value]) => (
Expand Down Expand Up @@ -174,6 +210,8 @@ function StyleJsonPane(props: { theme: string; lang: string }) {
);
}

type MapLibreViewRef = { fit: () => void };

function MapLibreView(props: {
theme: string;
lang: string;
Expand All @@ -182,16 +220,20 @@ function MapLibreView(props: {
tiles?: string;
npmLayers: LayerSpecification[];
droppedArchive?: PMTiles;
ref?: (ref: MapLibreViewRef) => void;
}) {
let mapContainer: HTMLDivElement | undefined;
let mapRef: MaplibreMap | undefined;
let protocolRef: Protocol | undefined;
let hiddenRef: HTMLDivElement | undefined;
let longPressTimeout: ReturnType<typeof setTimeout>;

const [error, setError] = createSignal<string | undefined>();
const [timelinessInfo, setTimelinessInfo] = createSignal<string>();

onMount(() => {
props.ref?.({ fit });

if (getRTLTextPluginStatus() === "unavailable") {
setRTLTextPlugin(
"https://unpkg.com/@mapbox/[email protected]/mapbox-gl-rtl-text.min.js",
Expand Down Expand Up @@ -272,18 +314,43 @@ function MapLibreView(props: {
}
});

map.on("contextmenu", (e) => {
const showContextMenu = (e: MapTouchEvent) => {
const features = map.queryRenderedFeatures(e.point);
if (hiddenRef && features.length) {
hiddenRef.innerHTML = "";
render(() => <FeaturesProperties features={features} />, hiddenRef);
popup.setHTML(hiddenRef.innerHTML);
popup.setLngLat(e.lngLat);
popup.addTo(map);
} else {
popup.remove();
}
};

map.on("contextmenu", (e: MapTouchEvent) => {
showContextMenu(e);
});

map.on("touchstart", (e: MapTouchEvent) => {
longPressTimeout = setTimeout(() => {
showContextMenu(e);
}, 500);
});

const clearLongPress = () => {
clearTimeout(longPressTimeout);
};

map.on("touchend", clearLongPress);
map.on("touchcancel", clearLongPress);
map.on("touchmove", clearLongPress);
map.on("pointerdrag", clearLongPress);
map.on("pointermove", clearLongPress);
map.on("moveend", clearLongPress);
map.on("gesturestart", clearLongPress);
map.on("gesturechange", clearLongPress);
map.on("gestureend", clearLongPress);

mapRef = map;

return () => {
Expand All @@ -293,24 +360,25 @@ function MapLibreView(props: {
};
});

createEffect(() => {
const fit = async () => {
if (protocolRef) {
const archive = props.droppedArchive;
if (archive) {
let archive = props.droppedArchive;
if (!archive && props.tiles) {
archive = new PMTiles(props.tiles);
protocolRef.add(archive);
(async () => {
const header = await archive.getHeader();
mapRef?.fitBounds(
[
[header.minLon, header.minLat],
[header.maxLon, header.maxLat],
],
{ animate: false },
);
})();
}
if (archive) {
const header = await archive.getHeader();
mapRef?.fitBounds(
[
[header.minLon, header.minLat],
[header.maxLon, header.maxLat],
],
{ animate: false },
);
}
}
});
};

createEffect(() => {
if (mapRef) {
Expand All @@ -319,30 +387,31 @@ function MapLibreView(props: {
}
});

createEffect(() => {
(async () => {
if (mapRef) {
let minZoom: number | undefined;
let maxZoom: number | undefined;
if (props.droppedArchive) {
const header = await props.droppedArchive.getHeader();
minZoom = header.minZoom;
maxZoom = header.maxZoom;
}
mapRef.setStyle(
getMaplibreStyle(
props.theme,
props.lang,
props.localSprites,
props.tiles,
props.npmLayers,
props.droppedArchive,
minZoom,
maxZoom,
),
);
createEffect(async () => {
// HACK: do this to ensure a tracking scope is created
// because async effects are not correct
[props.theme, props.lang, props.localSprites, props.tiles, props.npmLayers];
if (mapRef) {
let minZoom: number | undefined;
let maxZoom: number | undefined;
if (props.droppedArchive) {
const header = await props.droppedArchive.getHeader();
minZoom = header.minZoom;
maxZoom = header.maxZoom;
}
})();
mapRef.setStyle(
getMaplibreStyle(
props.theme,
props.lang,
props.localSprites,
props.tiles,
props.npmLayers,
props.droppedArchive,
minZoom,
maxZoom,
),
);
}
});

return (
Expand Down Expand Up @@ -381,6 +450,7 @@ function MapView() {
const [knownNpmVersions, setKnownNpmVersions] = createSignal<string[]>([]);
const [npmLayers, setNpmLayers] = createSignal<LayerSpecification[]>([]);
const [droppedArchive, setDroppedArchive] = createSignal<PMTiles>();
const [maplibreView, setMaplibreView] = createSignal<MapLibreViewRef>();

createEffect(() => {
const record = {
Expand Down Expand Up @@ -454,13 +524,17 @@ function MapView() {

language_script_pairs.sort((a, b) => a.full_name.localeCompare(b.full_name));

const fit = () => {
maplibreView()?.fit();
};

return (
<div class="flex flex-col h-dvh w-full">
<Nav page={0} />
<div class="max-w-[1500px] mx-auto">
<form onSubmit={loadTiles} class="flex">
<form onSubmit={loadTiles} class="flex space-x-2">
<input
class="border-2 border-gray p-1 flex-1 mr-2 text-xs lg:text-base"
class="border-2 border-gray p-1 flex-1 text-xs lg:text-base"
type="text"
name="tiles"
value={tiles()}
Expand All @@ -470,6 +544,9 @@ function MapView() {
<button class="btn-primary" type="submit">
load
</button>
<button class="btn-primary" type="submit" onClick={fit}>
fit bounds
</button>
</form>
<div class="flex my-2 space-y-2 lg:space-y-0 space-x-2 flex-col lg:flex-row items-center">
<div class="flex items-center">
Expand Down Expand Up @@ -568,6 +645,7 @@ function MapView() {
ondrop={drop}
>
<MapLibreView
ref={setMaplibreView}
tiles={tiles()}
localSprites={localSprites()}
showBoxes={showBoxes()}
Expand Down
10 changes: 0 additions & 10 deletions app/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,6 @@
@tailwind components;
@tailwind utilities;

.maplibregl-popup,
.maplibregl-popup-close-button {
color: black;
}

.features-properties {
max-height: 45vh;
overflow-y: scroll;
}

@layer components {
.btn-primary {
@apply rounded bg-purple px-2 py-1 text-xs font-semibold text-white shadow-sm hover:bg-black focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-purple;
Expand Down

0 comments on commit dd05c67

Please sign in to comment.