Enable moving own features on MapLibre and switch to raster tiles.
CI / test (push) Successful in 4s

Add feature geometry PATCH API support and update MapLibre demo to use OSM raster tiles, load all public/owned features, and let logged-in users drag their own feature markers to persist new positions.

Made-with: Cursor
This commit is contained in:
2026-03-02 22:28:44 +00:00
parent 59c9a719e0
commit 5716d4adf6
5 changed files with 136 additions and 2 deletions
+73 -1
View File
@@ -36,6 +36,8 @@ let threeScene;
let threeRenderer;
let threeCamera;
const featureMeshes = new Map();
const ownFeatureMarkers = new Map();
const ownFeatureCoords = new Map();
function setStatus(message) {
statusEl.textContent = message;
@@ -71,6 +73,27 @@ function setClientBase(baseUrl) {
setStatus(`API base updated: ${normalized}`);
}
function buildRasterStyle() {
return {
version: 8,
sources: {
"osm-raster": {
type: "raster",
tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
tileSize: 256,
attribution: "© OpenStreetMap contributors",
},
},
layers: [
{
id: "osm-raster-layer",
type: "raster",
source: "osm-raster",
},
],
};
}
function buildMapShareLink(feature, asset) {
const coords = feature?.geometry?.coordinates;
if (!Array.isArray(coords) || coords.length < 2) {
@@ -156,6 +179,14 @@ function clearFeatureMeshes() {
featureMeshes.clear();
}
function clearOwnFeatureMarkers() {
for (const marker of ownFeatureMarkers.values()) {
marker.remove();
}
ownFeatureMarkers.clear();
ownFeatureCoords.clear();
}
function addObjectMesh(featureId, lng, lat, isPublic, kind) {
const merc = maplibregl.MercatorCoordinate.fromLngLat({ lng, lat }, 0);
const meters = merc.meterInMercatorCoordinateUnits();
@@ -173,6 +204,40 @@ function addObjectMesh(featureId, lng, lat, isPublic, kind) {
featureMeshes.set(featureId, mesh);
}
async function moveOwnFeature(featureID, lng, lat) {
const existing = ownFeatureCoords.get(featureID);
const alt = Array.isArray(existing) && existing.length >= 3 ? existing[2] : 0;
const res = await fetch(`${currentApiBase()}/v1/features/${featureID}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
geometry: { type: "Point", coordinates: [lng, lat, alt] },
}),
});
if (!res.ok) {
const maybeJson = await res.json().catch(() => ({}));
throw new Error(maybeJson.error || `Failed to move feature (${res.status})`);
}
}
function addOwnFeatureMarker(featureID, lng, lat) {
const marker = new maplibregl.Marker({ color: "#2563eb", draggable: true }).setLngLat({ lng, lat }).addTo(map);
marker.on("dragend", async () => {
const p = marker.getLngLat();
try {
await moveOwnFeature(featureID, p.lng, p.lat);
await refreshFeatures();
setStatus(`Feature ${featureID} moved.`);
} catch (error) {
setStatus(error.message);
}
});
ownFeatureMarkers.set(featureID, marker);
}
async function ensureKeys() {
keys = await client.ensureKeysInStorage();
publicKeyPreviewEl.textContent = `Public key: ${keys.publicKey.slice(0, 24)}...`;
@@ -259,6 +324,7 @@ async function refreshFeatures() {
const publicData = await publicResp.json();
const byID = new Map((publicData.features || []).map((feature) => [feature.id, feature]));
const ownFeatureIDs = new Set();
if (accessToken) {
const { collections } = await client.listCollections();
if (!collectionId && collections.length > 0) {
@@ -273,21 +339,27 @@ async function refreshFeatures() {
);
for (const feature of ownFeatureSets.flat()) {
byID.set(feature.id, feature);
ownFeatureIDs.add(feature.id);
}
}
const features = Array.from(byID.values());
clearFeatureMeshes();
clearOwnFeatureMarkers();
for (const feature of features) {
const coords = feature.geometry?.coordinates;
if (!coords || coords.length < 2) continue;
const lng = coords[0];
const lat = coords[1];
ownFeatureCoords.set(feature.id, coords);
const assets = Array.isArray(feature.properties?.assets) ? feature.properties.assets : [];
const first = assets[0];
if (first) {
addObjectMesh(feature.id, lng, lat, first.isPublic, first.kind);
}
if (ownFeatureIDs.has(feature.id) && accessToken) {
addOwnFeatureMarker(feature.id, lng, lat);
}
}
renderAssets(features);
}
@@ -385,7 +457,7 @@ document.getElementById("uploadAsset").onclick = async () => {
map = new maplibregl.Map({
container: "map",
style: "./osm-liberty-gl-style/style.json",
style: buildRasterStyle(),
center: [-16.2518, 28.4636],
zoom: 12,
pitch: 55,