Compare commits

...

6 commits

Author SHA1 Message Date
d91e0bd8ad Fix attribution, add a detector for sharp angles
The sharp angle detector prevents common errors such as accidentally
taking a wrong turn or switching to the wrong track, especially in
tram networks
2025-05-01 21:07:45 +02:00
198175fde2 Improve point handling, fix path saving 2025-03-25 22:35:31 +01:00
4c9b1b555f Build points from repo 2025-03-25 22:22:03 +01:00
634df5ebf4 Alert on invalid filename (fixes #1) 2025-03-18 20:02:01 +01:00
13900c8092 Bugfix: refresh the path correctly 2025-03-13 23:29:33 +01:00
13a7414798 Mark markers markers that are not on a way in dark red 2025-03-13 22:01:07 +01:00
3 changed files with 117 additions and 18 deletions

View file

@ -15,6 +15,8 @@ legend.onAdd = function (map) {
const map = L.map("map", { center: [52,13], zoom: 3, minZoom: 0 }); const map = L.map("map", { center: [52,13], zoom: 3, minZoom: 0 });
map.attributionControl.setPrefix('Made with <a href="https://github.com/homologic/streckenkarte" > Streckenkarte</a>' );
function update_hash() { function update_hash() {
const {lat, lng} = this.getCenter(); const {lat, lng} = this.getCenter();
@ -43,7 +45,7 @@ let pointPaintRules = [
stroke: 'white', stroke: 'white',
width: 1.5, width: 1.5,
}), }),
filter: (z,f) => { return f.props.zoom < z } filter: (z,f) => { return f.props.zoom > 0 && f.props.zoom < z }
} }
] ]
@ -68,7 +70,7 @@ let pointRules = [
return `400 ${size}px sans-serif`; return `400 ${size}px sans-serif`;
}, },
}), }),
filter: (z,f) => { return f.props.zoom < z }, filter: (z,f) => { return f.props.zoom > 0 && f.props.zoom < z },
sort: (a,b) => { return a.zoom - b.zoom } sort: (a,b) => { return a.zoom - b.zoom }
} }
]; ];
@ -94,6 +96,7 @@ fetch("layers.json")
} }
); );
const strecken = protomapsL.leafletLayer({ const strecken = protomapsL.leafletLayer({
attribution: "",
url: data["pmtiles_url"] ?? "strecken.pmtiles", url: data["pmtiles_url"] ?? "strecken.pmtiles",
maxDataZoom: data["maxZoom"] ?? 10, maxDataZoom: data["maxZoom"] ?? 10,
maxZoom: 19, maxZoom: 19,
@ -104,6 +107,7 @@ fetch("layers.json")
strecken.addTo(map); strecken.addTo(map);
if ("points_url" in data) { if ("points_url" in data) {
const points = protomapsL.leafletLayer({ const points = protomapsL.leafletLayer({
attribution: "",
url: data["points_url"], url: data["points_url"],
maxDataZoom: data["maxZoom"] ?? 10, maxDataZoom: data["maxZoom"] ?? 10,
maxZoom: 19, maxZoom: 19,
@ -120,6 +124,7 @@ fetch("layers.json")
let dirHandle; let dirHandle;
let editMode = false; let editMode = false;
let markers = []; let markers = [];
let anglemarkers = [];
let geojsons = []; let geojsons = [];
let geojson; let geojson;
let editlayer; let editlayer;
@ -137,35 +142,83 @@ function addGeoJsonToMap(dat) {
return g; return g;
} }
function computeVector(coords, i) {
return [coords[i][0] - coords[i-1][0], coords[i][1] - coords[i-1][1]];
}
function scalarProduct(a,b) {
return a[0] * b[0] + a[1] * b[1]
}
async function recompute_anglemarkers(g) {
for (k=0; k < anglemarkers.length ; k++) {
map.removeLayer(anglemarkers[k])
}
anglemarkers = [];
const limit = document.getElementById("angle").value
for (j=0; j< g.length; j++) {
const coords = g[j].geometry.coordinates;
console.log(coords);
for (let i=1; i < coords.length - 1; i++) {
const a = computeVector(coords,i);
const b = computeVector(coords,i+1);
let angle = Math.acos(scalarProduct(a,b)/Math.sqrt(scalarProduct(a,a)*scalarProduct(b,b)))
if (angle > limit ) {
let mark = new L.marker(L.latLng(coords[i][1],coords[i][0]))
anglemarkers.push(mark);
mark.addTo(map);
mark._icon.classList.add("warn");
}
console.log(angle)
}
}
}
async function updateBrouter () { async function updateBrouter () {
if (markers.length > 0) { if (markers.length > 0) {
for (i=1; i< markers.length-1; i++) { for (i=0; i< markers.length; i++) {
markers[i]._icon.classList.remove("red"); markers[i]._icon.classList.remove("red");
markers[i]._icon.classList.remove("darkred");
} }
markers[markers.length-1]._icon.classList.add("red"); markers[markers.length-1]._icon.classList.add("red");
markers[0]._icon.classList.add("green"); markers[0]._icon.classList.add("green");
} }
geojsons = []; geojsons = [];
if (markers.length < 2) { recompute_anglemarkers(geojsons);
if (geojson != undefined) { if (geojson != undefined) {
map.removeLayer(geojson); map.removeLayer(geojson);
} }
if (markers.length < 2) {
return; return;
} }
for (let i = 0; i < markers.length - 1 ; i++) { for (let i = 0; i < markers.length - 1 ; i++) {
const marker1 = markers[i].getLatLng(); const marker1 = markers[i].getLatLng();
const marker2 = markers[i+1].getLatLng(); const marker2 = markers[i+1].getLatLng();
const url = `https://brouter.de/brouter?lonlats=${marker1.lng},${marker1.lat}|${marker2.lng},${marker2.lat}&profile=rail&alternativeidx=0&format=geojson`; const url = `https://brouter.de/brouter?lonlats=${marker1.lng},${marker1.lat}|${marker2.lng},${marker2.lat}&profile=rail&alternativeidx=0&format=geojson`;
fetch(url).then((response) => response.json()) fetch(url).then((response) => {
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
return response.json()
})
.then((data) => { .then((data) => {
if (geojson != undefined) { if (geojson != undefined) {
map.removeLayer(geojson); map.removeLayer(geojson);
} }
delete data.features[0].properties.messages delete data.features[0].properties.messages;
console.log(data.features[0].geometry.coordinates)
geojsons.push(data.features[0]); geojsons.push(data.features[0]);
recompute_anglemarkers(geojsons);
const dat = {type: "FeatureCollection", features: geojsons}; const dat = {type: "FeatureCollection", features: geojsons};
geojson = addGeoJsonToMap(dat); geojson = addGeoJsonToMap(dat);
}) })
.catch(err => {
markers[i]._icon.classList.add("darkred");
markers[i+1]._icon.classList.add("darkred");
}
)
} }
} }
@ -188,6 +241,14 @@ map.on('click', function(e) {
}); });
async function quitEdit(e) {
e.stopPropagation()
L.DomEvent.preventDefault(e);
editMode = false;
document.querySelector(".edit-ui").style.display = "none";
document.getElementById("edit-mode").style.display = "block";
}
async function pickDirectory(e){ async function pickDirectory(e){
e.stopPropagation() e.stopPropagation()
L.DomEvent.preventDefault(e); L.DomEvent.preventDefault(e);
@ -196,18 +257,21 @@ async function pickDirectory(e){
if (!dirHandle) { if (!dirHandle) {
return; return;
} }
const layerspan = document.querySelector("#layername")
layerspan.innerHTML = ""
for (i = 0; i < l.length ; i++ ) { for (i = 0; i < l.length ; i++ ) {
console.log(l[i].dirname); console.log(l[i].dirname);
if (l[i].dirname === dirHandle.name) { if (l[i].dirname === dirHandle.name) {
console.log(l[i])
console.log(this)
editlayer = l[i]; editlayer = l[i];
this.innerHTML = "Editing layer " + layer_legend(l[i]) + "<br>" + this.innerHTML layerspan.innerHTML = "Editing layer " + layer_legend(l[i]) + "<br>"
break; break;
} }
} }
document.getElementById("edit-mode").style.color = "red";
document.getElementById("edit-mode").innerHTML = "save";
editMode = true; editMode = true;
document.getElementById("edit-mode").style.display = "none";
document.querySelector(".edit-ui").style.display = "block";
} else { } else {
if (geojsons.length < 1) { if (geojsons.length < 1) {
alert("There is no path to save!"); alert("There is no path to save!");
@ -218,9 +282,15 @@ async function pickDirectory(e){
return; return;
} }
const dat = {type: "FeatureCollection", features: geojsons}; const dat = {type: "FeatureCollection", features: geojsons};
const file = await dirHandle.getFileHandle(`${filename}.geojson`, { let file;
try {
file = await dirHandle.getFileHandle(`${filename}.geojson`, {
create: true create: true
}); });
} catch (error) {
alert(`Could not open file: ${error.message}`);
return
}
const blob = new Blob([JSON.stringify(dat)]); const blob = new Blob([JSON.stringify(dat)]);
const writableStream = await file.createWritable(); const writableStream = await file.createWritable();
await writableStream.write(blob); await writableStream.write(blob);
@ -243,8 +313,22 @@ if (edit) {
customButton.onAdd = () => { customButton.onAdd = () => {
const buttonDiv = L.DomUtil.create('div', 'legend'); const buttonDiv = L.DomUtil.create('div', 'legend');
if ("showDirectoryPicker" in window) { if ("showDirectoryPicker" in window) {
buttonDiv.innerHTML = `<button id="edit-mode" >Edit</button>`; // const button = L.DomUtil.create('button');
buttonDiv.addEventListener('click', pickDirectory, this) // button.id = 'edit-mode';
// button.innerHTML = 'Edit';
// buttonDiv.appendChild(button)
// button.addEventListener('click', pickDirectory, this)
L.DomEvent.disableClickPropagation(buttonDiv);
buttonDiv.addEventListener('mouseover', L.DomEvent.stopPropagation);
buttonDiv.addEventListener('click', L.DomEvent.preventDefault)
buttonDiv.addEventListener('click', L.DomEvent.stopPropagation)
buttonDiv.innerHTML = `<button id="edit-mode" onClick="pickDirectory(event)" >Edit</button>
<div class="edit-ui">
<div id="layername" ></div>
<label for="angle">Turn restriction sensitivity</label><br>
<input type="range" min="0" step="0.05" max="1" value="0.35" class="slider" id="angle" onchange="recompute_anglemarkers(geojsons)" ><br>
<button id="save" onClick="pickDirectory(event)" >Save</button><button id="quit" onclick="quitEdit(event)" >Quit</button>
</div>`
} else { } else {
buttonDiv.innerHTML = "Your browser does not support editing. <br> As of 2025, editing is supported on Chromium-based browsers only."; buttonDiv.innerHTML = "Your browser does not support editing. <br> As of 2025, editing is supported on Chromium-based browsers only.";
} }

View file

@ -1,3 +1,4 @@
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -21,3 +22,8 @@ body {
} }
img.red { filter: hue-rotate(120deg); } img.red { filter: hue-rotate(120deg); }
img.green { filter: hue-rotate(-120deg); } img.green { filter: hue-rotate(-120deg); }
img.darkred { filter: hue-rotate(160deg); }
img.warn { filter: hue-rotate(160deg); }
div.edit-ui {
display: none;
}

View file

@ -15,8 +15,17 @@ do
done done
tippecanoe -aN -z"$zoom" -o "$temp/strecken.pmtiles" $temp/*.json tippecanoe -aN -z"$zoom" -o "$temp/strecken.pmtiles" $temp/*.json
mv $temp/strecken.pmtiles "$2" if [ -f "$1/points.json" ]
then
ogr2ogr -t_srs WGS84 "$temp/points.json" "$1/points.json"
tippecanoe -aN -z12 -r1 -o "$temp/points.pmtiles" "$temp/points.json"
jq '.points_url = "points.pmtiles"' "$1/layers.json" > "$2/layers.json"
fi
mv $temp/*.pmtiles "$2"
rm -r $temp rm -r $temp