Improve route editor

This commit is contained in:
Araozu 2024-09-08 22:02:05 -05:00
parent 5d5d13f0b7
commit eb24c56bf6
8 changed files with 228 additions and 171 deletions

View File

@ -2,36 +2,9 @@
{
"id": 1,
"name": "1",
"district": "Hunter/Tiabaya",
"color": "#ea4fb2",
"hover_bg": "#ea4fb2",
"hover_on_bg": "white"
},
{
"id": 2,
"name": "2",
"color": "black",
"hover_bg": "black",
"hover_on_bg": "white"
},
{
"id": 3,
"name": "3",
"color": "#eab308",
"hover_bg": "#eab308",
"hover_on_bg": "white"
},
{
"id": 4,
"name": "4",
"color": "#ff7300",
"hover_bg": "#ff7300",
"hover_on_bg": "white"
},
{
"id": 5,
"name": "5",
"color": "#b8860b",
"hover_bg": "#b8860b",
"hover_on_bg": "white"
}
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,25 +5,40 @@ export interface Line {
id: number
/** E.g.: `10` */
name: string
/** name of the district of origin */
district: string
/** color of the routes on the map */
// map_color: string
/** Hex code */
color: string
"hover_bg": string
"hover_on_bg": string
hover_bg: string
hover_on_bg: string
}
export type Routes = Array<Route>
export type Point = [number, number]
export type ArrowPoints = [Point,Point,Point,Point]
export interface Route {
/** Name that differentiates this route from others
* from the same line.
*
* May contain any combinatior of letters and numbers.
*
* E.g.: `A`, `B2-A`
/**
* Name that identifies this route
*/
name: string
/** `[lat,lng]`, stored as arrays to save space in JSON */
points: Array<[number, number]>
arrows?: Array<[L.LatLng, L.LatLng, L.LatLng, L.LatLng]>
/**
* Set of points that buses running this route go to
*/
departure: Array<Point>
/**
* Arrows that indicate in which direction the buses go
*/
departure_arrows: Array<ArrowPoints>
/**
* Set of points that buses running this route go to
*/
return: Array<Point>
/**
* Arrows that indicate in which direction the buses go
*/
return_arrows: Array<ArrowPoints>
}

View File

@ -1,14 +1,22 @@
import { saveAs } from "file-saver";
import L from "leaflet";
import { createEffect, createSignal, For, onMount } from "solid-js";
import { Accessor, createEffect, createResource, createSignal, For, onMount, Setter } from "solid-js";
import { Line, Route } from "../new_types";
let map: L.Map | null = null;
export function Editor() {
const container = <div class="h-[98vh] rounded-lg dark:opacity-60" />;
const [points, setPoints] = createSignal<Array<L.LatLng>>([]);
const [route_name, set_route_name] = createSignal("");
const [departure_points, set_departure_points] = createSignal<Array<L.LatLng>>([]);
const [return_points, set_return_points] = createSignal<Array<L.LatLng>>([]);
let currentPolyline: L.Polyline | null = null;
const [lines] = createResource<Array<Line>>(async() => {
const data = await fetch("/n/lines.json");
if (!data.ok) {
throw new Error("Error fetching");
}
return data.json();
});
onMount(() => {
map = L.map(container as HTMLElement)
@ -17,52 +25,22 @@ export function Editor() {
L.tileLayer("/tiles/{z}/{x}/{y}.jpg", {
maxZoom: 18,
minZoom: 12,
attribution: "&copy; Map data <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>",
attribution: "Map data &copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>",
}).addTo(map);
// Set up click logic
map.addEventListener("click", (ev) => {
const click_lat_lng = ev.latlng;
// Update the state
setPoints((x) => [...x, click_lat_lng]);
console.log(click_lat_lng);
});
});
// Re-draws the polyline
createEffect(() => {
const p = points();
const gen_route = () => {
const new_route: Route = {
name: route_name(),
departure: departure_points().map(({lat,lng}) => [lat,lng]),
departure_arrows: [],
return: departure_points().map(({lat,lng}) => [lat,lng]),
return_arrows: [],
};
// Delete the polyline
if (p.length === 0) {
if (currentPolyline !== null) {
currentPolyline.removeFrom(map!);
}
//
} else {
// delete previous polyline, if any
if (currentPolyline !== null) {
currentPolyline.removeFrom(map!);
}
currentPolyline = L.polyline(p, {color: "red"});
currentPolyline.addTo(map!);
}
});
const saveJson = () => {
const jsonData = JSON.stringify(points().map((obj) => [obj.lat,obj.lng]));
const file = new Blob([jsonData], {type: "application/json"});
saveAs(file, "output.json");
};
const copyPoints = () => {
const jsonData = JSON.stringify(points().map((obj) => [obj.lat,obj.lng]));
navigator.clipboard.writeText(jsonData)
.then(() => alert("Copiado"))
.catch(() => alert("Error al copiar."));
navigator.clipboard.writeText(JSON.stringify(new_route))
.then(() => alert("Copied to clipboard"))
.catch(() => alert("Error copying to clipboard"));
};
return (
@ -73,55 +51,37 @@ export function Editor() {
Creador de rutas
</h2>
<p class="px-2">
Haz click en cualquier parte del mapa para empezar.
<br />
Luego, haz click izquierdo en el mapa para agregar un
punto.
<br />
Haz click en el boton "Retroceder" para borrar el último punto.
<br />
Haz click en el boton "Reiniciar" para borrar todos los puntos.
<br />
Presiona el boton "Guardar" para generar las
coordenadas de la ruta.
</p>
<div class="px-2 py-4">
Puntos:
<br />
<div class="h-40 overflow-scroll p-2 my-2 rounded border-2 border-c-primary">
<For each={points()}>
{(p) => <p class="font-mono">{p.lat},{p.lng}</p>}
<div class="px-2">
<p>Selecciona una linea:</p>
<select name="line_selection" class="bg-c-bg text-c-on-bg w-full border border-c-on-bg rounded p-2">
<For each={lines()}>
{(line) => <option value=":D">{line.id} - {line.district}</option>}
</For>
</select>
<p class="pt-2 pb-1">Nombre de la ruta (ejm: B2-A):</p>
<input type="text" class="bg-c-bg text-c-on-bg w-full border border-c-on-bg rounded p-2"
value={route_name()}
onInput={(e) => set_route_name(e.target.value)}
/>
<RouteSelector route_type="Ida" district={"TODO"} color="red"
points={departure_points}
setPoints={set_departure_points}
/>
<RouteSelector route_type="Vuelta" district={"TODO"} color="blue"
points={return_points}
setPoints={set_return_points}
/>
<div class="text-left">
<button
class="bg-green-400 text-black py-1 px-4 rounded"
onClick={gen_route}
>
Copiar JSON
</button>
</div>
<button class="bg-c-primary text-white py-2 px-4 rounded mr-2"
onClick={() => {
if (points().length >= 1) {
setPoints((x) => x.slice(0, -1));
}
}}
>
Retroceder
</button>
<button class="bg-c-primary text-white py-2 px-4 rounded mr-2"
onClick={() => {
const confirmation = confirm("¿Estas seguro? Se perderán todos los puntos");
if (confirmation) setPoints([]);
}}
>
Reiniciar
</button>
<button class="bg-c-primary text-white py-2 px-4 rounded mr-2"
onClick={copyPoints}
>
Copiar puntos
</button>
<button class="bg-c-primary text-white py-2 px-4 rounded mr-2"
onClick={saveJson}
>
Guardar
</button>
</div>
</div>
<div class="py-[0.5vh] pr-2">
@ -134,3 +94,86 @@ export function Editor() {
</div>
);
}
function RouteSelector(props: {
route_type: "Ida"|"Vuelta",
district: string,
color: string,
points: Accessor<Array<L.LatLng>>,
setPoints: Setter<Array<L.LatLng>>,
}) {
const [active, setActive] = createSignal(false);
const [points, setPoints] = [props.points, props.setPoints];
let current_polyline: L.Polyline | undefined = undefined;
const click_listener: L.LeafletMouseEventHandlerFn = (ev) => {
const latlng = ev.latlng;
setPoints((x) => [...x, latlng]);
};
const toggle_status = () => {
const new_status = !(active());
setActive(new_status);
if (new_status === true) {
map!.addEventListener("click", click_listener);
} else {
map!.removeEventListener("click", click_listener);
}
};
createEffect(() => {
const latlngs = points();
if (latlngs.length === 0) {
current_polyline?.removeFrom(map!);
current_polyline = undefined;
} else {
current_polyline?.removeFrom(map!);
current_polyline = L.polyline(latlngs, {color: props.color});
current_polyline.addTo(map!);
}
});
const pop = () => {
setPoints((p) => p.slice(0, -1));
};
const active_classes = () => `${active() ? "border-green-400" : "border-transparent"}`;
const button_classes = () => `${active() ? "animate-pulse" : ""}`;
const direction_text = () => {
if (props.route_type === "Ida") return `Desde ${props.district} hacia el centro`;
else return `Desde el centro hacia ${props.district}`;
};
return (
<div class={`p-1 rounded my-2 border-2 ${active_classes()}`}>
<h2 class="text-lg underline">Ruta de {props.route_type}</h2>
<p>{direction_text()}</p>
<div class="border border-c-on-bg rounded p-1 my-2">
Puntos:
<br />
<div class="h-32 overflow-hidden">
<For each={points()}>
{(point) => <div class="whitespace-nowrap overflow-hidden text-sm">{point.lat}; {point.lng}</div>}
</For>
</div>
</div>
<div class="text-right">
<button
class={`bg-green-400 text-black py-1 px-2 mr-1 rounded ${button_classes()}`}
onClick={pop}
>
Retroceder
</button>
<button
class={`bg-green-400 text-black py-1 px-4 rounded ${button_classes()}`}
onClick={toggle_status}
>
{active() ? "Seleccionando..." : "Empezar selección"}
</button>
</div>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { Map, map, polyline, tileLayer } from "leaflet";
import { createEffect, createResource, createSignal, For, onMount, Suspense } from "solid-js";
import { createEffect, createResource, For, onMount, Suspense } from "solid-js";
import { Line, RouteWrapper, Route, LineWrapper, PointsWrapper } from "../types";
import { LineSegmentsIcon } from "../icons/LineSegmentsIcon";
@ -85,7 +85,6 @@ function LineEl(props: { line: Line }) {
}
function RouteEl(props: { route: Route, parent_id: number, color: string }) {
const [draw, setDraw] = createSignal(false);
const [points] = createResource<PointsWrapper>(async() => {
const res = await fetch(`/data/cuenca_${props.parent_id}_ruta_${props.route.id_ruta}_ida.json`);
if (!res.ok) throw new Error("Error fetching data");
@ -98,27 +97,23 @@ function RouteEl(props: { route: Route, parent_id: number, color: string }) {
});
createEffect(() => {
if (points.state === "ready" && draw()) {
// Render the dots into the map
const coords = points()!.data[0]!.ruta_json;
// Render the dots into the map
const coords = points()!.data[0]!.ruta_json;
const line = polyline(coords, { color: props.color});
line.addTo(g_map!);
}
const line = polyline(coords, { color: props.color});
line.addTo(g_map!);
});
createEffect(() => {
if (return_points.state === "ready" && draw()) {
// Render the dots into the map
const coords = return_points()!.data[0]!.ruta_json;
// Render the dots into the map
const coords = return_points()!.data[0]!.ruta_json;
const line = polyline(coords, { color: props.color});
line.addTo(g_map!);
}
const line = polyline(coords, { color: props.color});
line.addTo(g_map!);
});
return (
<div class="pl-10" onClick={() => setDraw(true)}>
<div class="pl-10">
Ruta {props.route.cod_ruta}
</div>
);

View File

@ -3,7 +3,7 @@ import { createResource, createSignal, For, onMount, Suspense } from "solid-js";
import { LineSegmentsIcon } from "../icons/LineSegmentsIcon";
import { Line, Lines, Route, Routes } from "../new_types";
const LINE_OPACITY = 0.8;
const LINE_OPACITY = 1;
let global_map: L.Map | null = null;
@ -11,10 +11,10 @@ let global_map: L.Map | null = null;
// The first object stores Lines, the second stores routes
// Indexed by their names.
// The array is the polyline, and wether to render it (> 0) (set opacity to LINE_OPACITY)
const lines_store: Map<string, Map<string, [L.Polyline, number]>> = new Map();
const lines_store: Map<string, Map<string, [L.Polyline, Array<L.Polygon>, number]>> = new Map();
export function Index2() {
const container = <div class="h-[98vh] rounded-lg dark:opacity-60" />;
const container = <div class="md:h-[98vh] h-[65vh] md:rounded-lg dark:opacity-60" />;
onMount(() => {
global_map = L.map(container as HTMLElement)
@ -28,8 +28,8 @@ export function Index2() {
});
return (
<div class="grid grid-cols-[15rem_auto]">
<div class="h-screen overflow-scroll">
<div class="grid md:grid-cols-[15rem_auto] md:grid-rows-none grid-rows-[65vh_35vh]">
<div class="md:h-screen overflow-scroll">
<h1 class="text-c-primary text-center font-bold text-2xl py-4">AQP combi</h1>
<h2 class="bg-c-primary text-white py-2 px-2 font-bold text-lg">
@ -38,8 +38,8 @@ export function Index2() {
<LinesEl />
</div>
<div class="py-[0.5vh] pr-2">
<div class="rounded-lg overflow-hidden p-1"
<div class="md:py-[0.5vh] md:pr-2">
<div class="md:rounded-lg overflow-hidden md:p-1"
style="box-shadow: inset 0 0 5px 0px var(--main)"
>
{container}
@ -79,7 +79,7 @@ function LineEl(props: { line: Line }) {
style={`color: ${props.line.color};--hover-bg: ${props.line.hover_bg};--hover-on-bg: ${props.line.hover_on_bg}`}
>
<LineSegmentsIcon class="px-1" fill={props.line.color} />
<span class="px-2">
<span class="px-2" title={props.line.district}>
Linea {props.line.name}
</span>
<Suspense>
@ -111,36 +111,57 @@ function RouteEl(props: { line_name: string, route: Route, color: string }) {
if (!lines_store.has(line_name)) {
lines_store.set(line_name, new Map());
}
// Create the arrows, if any
const arrows = [];
if (props.route.arrows) {
for (const a of props.route.arrows) {
const arrow = L.polygon(a, {color: props.color});
arrows.push(arrow);
}
}
// store
lines_store.get(line_name)!.set(props.route.name, [line, 0]);
lines_store.get(line_name)!.set(props.route.name, [line, arrows, 0]);
const set_highlighted = () => {
const is_active = active();
const arr = lines_store.get(props.line_name)?.get(props.route.name);
if (is_active && arr !== undefined) {
arr[1] -= 1;
arr[2] -= 1;
setActive(false);
} else if (!is_active && arr !== undefined) {
arr[1] += 1;
arr[2] += 1;
setActive(true);
}
};
const active_classes = () => {
if (active()) return "underline bg-[var(--hover-bg)] text-[var(--hover-on-bg)]";
else return "";
if (active()) return "underline bg-[var(--hover-bg)] text-[var(--hover-on-bg)] border-[var(--hover-bg)]";
else return "border-transparent";
};
return (
<button
class={`inline-block w-full text-left cursor-pointer pl-10
hover:bg-[var(--hover-bg)] hover:text-[var(--hover-on-bg)] ${active_classes()}`}
class={`grid grid-cols-[auto_2.5rem_3rem] w-full text-left cursor-pointer pl-10 border-2
hover:bg-[var(--hover-br)] hover:text-[var(--hover-on-br)] hover:border-r-hover-bg ${active_classes()}`}
onClick={set_highlighted}
onMouseEnter={() => highlight_route(props.line_name, props.route.name)}
onMouseLeave={() => unhighlight_route(props.line_name, props.route.name)}
>
Ruta {props.route.name}
<span>
Ruta {props.route.name}
</span>
<span
class="hover:bg-[var(--hover-bg)] hover:text-[var(--hover-on-bg)] text-center"
>
Ida
</span>
<span
class="hover:bg-r-hover-bg hover:text-r-hover-on-bg text-center"
>
Vuelta
</span>
</button>
);
}
@ -148,7 +169,7 @@ function RouteEl(props: { line_name: string, route: Route, color: string }) {
function highlight_route(line_name: string, route_name: string) {
const arr = lines_store.get(line_name)?.get(route_name);
if (arr !== undefined) {
arr[1] += 1;
arr[2] += 1;
}
re_render_polylines();
@ -157,7 +178,7 @@ function highlight_route(line_name: string, route_name: string) {
function unhighlight_route(line_name: string, route_name: string) {
const arr = lines_store.get(line_name)?.get(route_name);
if (arr !== undefined) {
arr[1] -= 1;
arr[2] -= 1;
}
re_render_polylines();
@ -168,15 +189,21 @@ function re_render_polylines() {
// If this is 0, then all paths should be rendered.
let render_count = 0;
for (const [, line_map] of lines_store) {
for (const [, [route_polilyne, active_count]] of line_map) {
for (const [, [route_polilyne, arrows, active_count]] of line_map) {
if (active_count > 0) {
route_polilyne.setStyle({opacity: LINE_OPACITY});
render_count += 1;
// if the route has arrows and is manually toggled,
// render the arrows
for (const a of arrows) {
a.addTo(global_map!);
}
} else {
route_polilyne.setStyle({opacity: 0});
for (const a of arrows) {
a.removeFrom(global_map!);
}
}
}
}

View File

@ -7,6 +7,11 @@ module.exports = {
extend: {
colors: {
"c-primary": "var(--main)",
"c-bg": "var(--bg)",
"c-on-bg": "var(--on-bg)",
// colors for the routes colors
"r-hover-bg": "var(--hover-bg)",
"r-hover-on-bg": "var(--hover-on-bg)",
},
},
},