Skip to content

Commit

Permalink
Merge branch 'map-of-route'
Browse files Browse the repository at this point in the history
  • Loading branch information
lil5 committed Nov 23, 2023
2 parents 2d61207 + a3b93f7 commit 55fb3d7
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 38 deletions.
43 changes: 43 additions & 0 deletions bruno/route/coordinates.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
meta {
name: coordinates
type: http
seq: 4
}

get {
url: {{base}}/v2/route/coordinates?chain_uid={{chainUID}}
body: json
auth: none
}

query {
chain_uid: {{chainUID}}
}

body:json {
{
"chain_uid": "{{chainUID}}",
"route_order": [
"a151dcbc-d97b-46c5-bac0-90565da8e345",
"7711dc5f-396f-4c98-b86e-171f568f7ddb",
"6d71acc5-a2d5-4734-b2ac-ed5c11242dfd",
"ba6cf778-9d87-4090-bd88-52195b6aa876",
"2d5e1a55-0b56-48b0-a55e-c32a09614f80",
"11e52eea-7542-42ab-aa4e-c3a08ce51d7d"
]
}
}

body:text {
{
"chain_uid": "{{chainUID}}",
"route_order": [
"a151dcbc-d97b-46c5-bac0-90565da8e345",
"7711dc5f-396f-4c98-b86e-171f568f7ddb",
"6d71acc5-a2d5-4734-b2ac-ed5c11242dfd",
"ba6cf778-9d87-4090-bd88-52195b6aa876",
"2d5e1a55-0b56-48b0-a55e-c32a09614f80",
"11e52eea-7542-42ab-aa4e-c3a08ce51d7d"
]
}
}
1 change: 1 addition & 0 deletions frontend/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"route": "Route",
"routeOptimize": "Optimize Route",
"routeUndoOptimize": "Undo Optimize Route",
"map": "Map",
"approve": "Approve",
"pendingApproval": "Pending Host Approval",
"deny": "Deny",
Expand Down
1 change: 1 addition & 0 deletions frontend/public/locales/nl/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"route": "Route",
"routeOptimize": "Optimaliseer route",
"routeUndoOptimize": "Herstel oude route",
"map": "Kaart",
"approve": "Keur goed",
"pendingApproval": "Wacht op goedkeuring",
"deny": "Weiger",
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/api/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,16 @@ export function routeOptimizeOrder(chainUID: UID) {
params: { chain_uid: chainUID },
});
}

export interface RouteCoordinate {
user_uid: string;
latitude: number;
longitude: number;
route_order: number;
}

export function routeCoordinates(chainUID: UID) {
return window.axios.get<RouteCoordinate[]>("/v2/route/coordinates", {
params: { chain_uid: chainUID },
});
}
100 changes: 100 additions & 0 deletions frontend/src/components/RouteMap/RouteMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import mapboxjs, { GeoJSONSource } from "mapbox-gl";
import { useEffect, useState } from "react";
import { RouteCoordinate, routeCoordinates } from "../../api/route";
import { Chain, UID } from "../../api/types";
import type { FeatureCollection } from "geojson";
import { useDebouncedCallback } from "use-debounce";
const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_KEY;

export default function RouteMap(props: { chain: Chain; route: UID[] }) {
const [map, setMap] = useState<mapboxjs.Map | null>(null);
const [id] = useState(() => window.crypto.randomUUID());
useEffect(() => {
const _map = new mapboxjs.Map({
container: id,
accessToken: MAPBOX_TOKEN,
style: "mapbox://styles/mapbox/light-v11",
center: [props.chain.longitude, props.chain.latitude],
zoom: 11,
});
setMap(_map);

_map.on("load", () => {
(async () => {
const coords = (await routeCoordinates(props.chain.uid)).data;
_map.addSource("route", {
type: "geojson",
data: mapToGeoJSONCoords(coords, props.route),
});

_map.addLayer({
id: "point-bg",
type: "circle",
source: "route",
paint: {
"circle-color": ["rgba", 239, 149, 61, 0.6], // #ef953d
"circle-radius": 15,
"circle-stroke-width": 0,
},
});

_map.addLayer({
id: "point-text",
type: "symbol",
source: "route",
layout: {
"text-field": ["get", "route_order"],
"text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"],
"text-size": 12,
},
});
})();
});

return () => {
_map?.remove();
};
}, []);

const debouncedUpdateSource = useDebouncedCallback(
async () => {
const routeSource = map!.getSource("route") as GeoJSONSource;
if (!routeSource) return;
const coords = (await routeCoordinates(props.chain.uid)).data;
routeSource.setData(mapToGeoJSONCoords(coords, props.route));
},
2e3,
{}
);
useEffect(() => {
console.log("Loading", map === null ? "map is null" : "map exists");

if (!map) return;
debouncedUpdateSource();
}, [props.chain, props.route]);

return <div id={id} className="w-full h-full"></div>;
}

function mapToGeoJSONCoords(
coords: RouteCoordinate[],
route: UID[]
): FeatureCollection {
return {
type: "FeatureCollection",
features: coords.map((coord) => {
const route_order = route.indexOf(coord.user_uid) + 1;
return {
type: "Feature",
geometry: {
type: "Point",
coordinates: [coord.longitude, coord.latitude],
},
properties: {
uid: coord.user_uid,
route_order,
},
};
}),
};
}
93 changes: 93 additions & 0 deletions frontend/src/components/RouteMap/RouteMapPopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useTranslation } from "react-i18next";
import { Chain, UID } from "../../api/types";
import { useRef, MouseEvent, useState } from "react";
import RouteMap from "./RouteMap";

export default function RouteMapPopup(props: {
chain: Chain;
closeFunc: () => void;
route: UID[];
optimizeRoute: () => Promise<void>;
returnToPreviousRoute: () => void;
}) {
const { t } = useTranslation();
const ref = useRef<HTMLDialogElement>(null);
const refButtonClose = useRef<HTMLButtonElement>(null);
const [routeWasOptimized, setRouteWasOptimized] = useState(false);

function returnToPreviousRoute() {
props.returnToPreviousRoute();
setRouteWasOptimized(false);
}

function optimizeRoute() {
props.optimizeRoute().then(
() => {
setRouteWasOptimized(true);
},
() => {
setRouteWasOptimized(false);
}
);
}

function handleBackgroundClick(e: MouseEvent) {
e.preventDefault();
if (window.innerWidth > 900) {
if (e.target === e.currentTarget) {
props.closeFunc();
}
}
}

return (
<div className="absolute inset-0 p-4 bg-white/30 hidden lg:block">
<dialog
open
className="relative w-full h-full flex p-0 shadow-lg"
ref={ref}
tabIndex={-1}
onClick={handleBackgroundClick}
>
<form
className="flex flex-col bg-white w-full h-full p-6"
style={{ "--tw-shadow": "#333" } as any}
>
<div className="flex-grow">
<RouteMap chain={props.chain} route={props.route} />
</div>
<div className="mt-4 flex justify-between">
{routeWasOptimized ? (
<button
type="button"
className="btn btn-sm btn-ghost bg-teal-light text-teal"
onClick={returnToPreviousRoute}
>
{t("routeUndoOptimize")}
<span className="feather feather-corner-left-up ms-2" />
</button>
) : (
<button
type="button"
className="btn btn-sm btn-ghost bg-teal-light text-teal"
onClick={optimizeRoute}
>
{t("routeOptimize")}
<span className="feather feather-zap ms-2" />
</button>
)}
<button
key="close"
type="reset"
ref={refButtonClose}
className="btn btn-sm btn-ghost"
onClick={() => props.closeFunc()}
>
{t("close")}
</button>
</div>
</form>
</dialog>
</div>
);
}
80 changes: 42 additions & 38 deletions frontend/src/pages/ChainMemberList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { bagGetAllByChain } from "../api/bag";
import { Sleep } from "../util/sleep";
import PopoverOnHover from "../components/Popover";
import DOMPurify from "dompurify";
import RouteMapPopup from "../components/RouteMap/RouteMapPopup";

enum LoadingState {
idle,
Expand Down Expand Up @@ -73,9 +74,9 @@ export default function ChainMemberList() {
const [users, setUsers] = useState<User[] | null>(null);
const [bags, setBags] = useState<Bag[] | null>(null);
const [route, setRoute] = useState<UID[] | null>(null);
const [routeWasOptimized, setRouteWasOptimized] = useState<boolean>(false);
const [previousRoute, setPreviousRoute] = useState<UID[] | null>(null);
const [changingPublishedAuto, setChangingPublishedAuto] = useState(false);
const [isOpenRouteMapPopup, setIsOpenRouteMapPopup] = useState(false);

const [participantsSortBy, setParticipantsSortBy] =
useState<ParticipantsSortBy>("date");
Expand Down Expand Up @@ -300,26 +301,24 @@ export default function ChainMemberList() {
});
}

function optimizeRoute(chainUID: UID) {
routeOptimizeOrder(chainUID)
.then((res) => {
const optimal_path = res.data.optimal_path;

// saving current rooute before changing in the database
setPreviousRoute(route);
setRouteWasOptimized(true);
// set new order
routeSetOrder(chainUID, optimal_path);
setRoute(optimal_path);
})
.catch((err) => {
addToastError(GinParseErrors(t, err), err.status);
});
async function optimizeRoute(chainUID: UID) {
try {
const res = await routeOptimizeOrder(chainUID);
const optimal_path = res.data.optimal_path;

// saving current rooute before changing in the database
setPreviousRoute(route);
// set new order
routeSetOrder(chainUID, optimal_path);
setRoute(optimal_path);
} catch (err: any) {
addToastError(GinParseErrors(t, err), err?.status);
throw err;
}
}

function returnToPreviousRoute(chainUID: UID) {
setRoute(previousRoute);
setRouteWasOptimized(false);
routeSetOrder(chainUID, previousRoute!);
}

Expand Down Expand Up @@ -464,7 +463,7 @@ export default function ChainMemberList() {
}

if (!(chain && users && unapprovedUsers && route && bags)) {
console.log(chain, users, unapprovedUsers, route, bags);
// console.log(chain, users, unapprovedUsers, route, bags);
return null;
}

Expand Down Expand Up @@ -614,6 +613,15 @@ export default function ChainMemberList() {
chain={chain}
refresh={refresh}
/>
{isOpenRouteMapPopup ? (
<RouteMapPopup
chain={chain}
route={route}
closeFunc={() => setIsOpenRouteMapPopup(false)}
optimizeRoute={() => optimizeRoute(chain.uid)}
returnToPreviousRoute={() => returnToPreviousRoute(chain.uid)}
/>
) : null}
</div>
</section>
</div>
Expand Down Expand Up @@ -693,26 +701,22 @@ export default function ChainMemberList() {
</div>
<div className="order-2 md:justify-self-end lg:order-3 sm:-ms-8 flex flex-col xs:flex-row items-center">
{selectedTable === "route" ? (
!routeWasOptimized ? (
<button
type="button"
className="btn btn-secondary btn-outline xs:me-4 mb-4 xs:mb-0"
onClick={() => optimizeRoute(chain.uid)}
>
{t("routeOptimize")}
<span className="feather feather-zap ms-3 text-primary" />
</button>
) : (
<button
type="button"
className="btn btn-secondary btn-outline me-4"
onClick={() => returnToPreviousRoute(chain.uid)}
>
{t("routeUndoOptimize")}

<span className="feather feather-corner-left-up ms-3" />
</button>
)
<button
type="button"
className={`btn hidden lg:inline-flex me-4 ${
isOpenRouteMapPopup
? "btn-outline"
: "btn-accent text-white"
}`}
onClick={() => setIsOpenRouteMapPopup(!isOpenRouteMapPopup)}
>
{t("map")}
<span
className={`feather ${
isOpenRouteMapPopup ? "feather-x" : "feather-map"
} ms-3`}
/>
</button>
) : null}

{selectedTable !== "unapproved" ? (
Expand Down
Loading

0 comments on commit 55fb3d7

Please sign in to comment.