Compare commits

...

85 Commits

Author SHA1 Message Date
102b873e79 Add template/module/journey/map/mixin-marker.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-28 17:53:12 +01:00
2fbf2ec3e9 Update template/module/journey/map/hotel.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-28 17:52:59 +01:00
828b2ae3cb Update template/module/journey/map/activities.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-28 17:52:49 +01:00
22f4cbda18 Update template/module/journey/leg/top.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-28 17:52:24 +01:00
8820e4990a Update template/module/journey/leg/old_cfg.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-28 17:52:14 +01:00
4a10318b74 Update template/module/journey/leg/nav.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-28 17:52:03 +01:00
d68a148b40 Update template/module/journey/leg/drawer.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-28 17:51:42 +01:00
0ece752fd4 Update template/module/journey/map.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-28 17:51:32 +01:00
6260d396e5 Update src/client/types/migration.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-28 17:50:51 +01:00
208bf1805e Update src/client/types/geom.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-28 17:50:32 +01:00
b030286f9c Update src/client/types/format.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-28 17:50:21 +01:00
3ea48d69ab Update src/client/old.js
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-28 17:49:59 +01:00
62956dd4b1 Update src/client/api.ts 2025-02-28 17:49:41 +01:00
soraefir
7af6d04dd1 wip
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-28 01:43:32 +01:00
soraefir
1960036980 wip
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-28 01:05:10 +01:00
9060814609 Update template/journey.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-27 17:33:19 +01:00
50a98b9a31 Update template/module/map.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-27 17:32:59 +01:00
1f3db375a4 Update template/module/journey_leg.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-27 17:32:45 +01:00
f3381bf8f0 Update template/module/journey_nav.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-27 17:32:32 +01:00
503ff92d5e Update template/module/foot.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-27 17:31:54 +01:00
92032f60e9 Update src/client/types/wrapper.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-27 17:31:19 +01:00
1814a8a87d Update src/client/types/geom.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-27 17:31:09 +01:00
fe4190313b Update src/client/types/format.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-27 17:30:58 +01:00
52b3d98fec Update src/client/old.js
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-27 17:30:44 +01:00
366ca8b97f Update src/client/main.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-27 17:30:33 +01:00
soraefir
f1c702bc14 Wip
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-27 00:55:37 +01:00
b7b87507e3 Update src/client/types/geom.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-26 17:01:07 +01:00
f5b9ad05c7 Update template/module/map.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-26 16:58:21 +01:00
be46114dd3 Update template/module/journey_step.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-26 16:58:11 +01:00
951172ede3 Update src/server/main.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-26 16:57:27 +01:00
38f1300095 Update src/server/api_nominatim.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-26 16:56:53 +01:00
cbaaf4de5b Update src/client/types/wrapper.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-26 16:56:21 +01:00
d184068ae2 Add src/client/types/geoms.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-26 16:56:10 +01:00
a69d334782 Update src/client/old.js
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-26 16:55:50 +01:00
ad420b4528 Update src/client/api.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-26 16:55:29 +01:00
60508b99ae Update template/module/foot.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 18:03:22 +01:00
6ec4d426c9 Update src/server/api.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 18:03:07 +01:00
d63845ed9a Update src/server/api_nominatim.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 18:02:57 +01:00
58c6eaa868 Update src/server/api_flight.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 18:02:48 +01:00
dc23d5ba83 Update src/server/main.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 18:02:38 +01:00
c307dd0a22 Update src/client/old.js
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 18:02:19 +01:00
1df6f14f1a Update src/server/main.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:11:34 +01:00
e0ec2c0f46 Add src/server/api_nominatim.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:10:57 +01:00
ca9edace2f Add src/server/api_flight.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:10:41 +01:00
5a45dcb1b9 Update src/server/api.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:10:18 +01:00
7fdd467c8c Update src/server/main.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:10:01 +01:00
e8f0516ccb Update src/server/api.js
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:09:39 +01:00
b995cd45f0 Update tsconfig-client.json
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:09:16 +01:00
bfd13fd782 Update package.json
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:08:53 +01:00
3568de823d Update src/client/types/wrapper.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:08:18 +01:00
8e8ab398c9 Update src/client/types/format.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:08:00 +01:00
25a6fcf90d Update src/client/types/migration.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:07:04 +01:00
4083c4268f Update src/client/types/format.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:06:36 +01:00
aacefdce94 Update src/client/types/ext.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:06:20 +01:00
ca2e2c11d2 Update src/client/old.js
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:05:37 +01:00
a782f61e0b Update src/client/main.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:05:22 +01:00
5bb1622a0b Update src/client/api.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 16:05:00 +01:00
soraefir
0cc9d235ed Better nominatim
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-25 01:38:34 +01:00
b554eba76c Update template/module/map.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 17:31:48 +01:00
97f2a5ebb8 Add src/types/migration.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 17:31:32 +01:00
a13b3acbe5 Update src/types/format.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 17:31:15 +01:00
c2d1858e68 Update src/old.js
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 17:31:02 +01:00
f3bed6d21c Update template/module/view_step.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 16:46:42 +01:00
ecc9e83369 Update src/types/ext.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 16:46:22 +01:00
d689ceb511 Update src/api.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 16:46:11 +01:00
059dfec50d Update router/api.js
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 16:45:49 +01:00
cc527bc1ab Update template/module/view_step.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:16:59 +01:00
9d9e79b07d Update template/module/short_sec.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:16:48 +01:00
6048b30a18 Update template/module/nav.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:16:36 +01:00
8e547fd0cf Update template/module/nav_pub.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:16:21 +01:00
b3867d42b3 Update template/module/map.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:16:11 +01:00
923263e27f Update template/module/journey_step.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:15:59 +01:00
63eb47f855 Update template/module/journey_sec.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:15:49 +01:00
17cfcc77da Update template/module/foot.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:15:35 +01:00
0949635148 Update template/view.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:15:17 +01:00
6f96bff5b5 Update template/short.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:15:06 +01:00
c724167803 Update template/home.pug
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:14:54 +01:00
b8e71cd6fa Add src/types/wrapper.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:14:38 +01:00
c4537ca1b0 Add src/types/format.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:14:19 +01:00
d295c15504 Update src/types/ext.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:13:53 +01:00
ef00ba536d Update src/old.js
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:13:35 +01:00
d6890940d0 Update src/app.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:13:16 +01:00
9f88d80b68 Update src/api.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:13:02 +01:00
1e10109bbe Update router/api.js
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:12:46 +01:00
8a46f473eb Update package.json
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 15:12:04 +01:00
52 changed files with 2217 additions and 998 deletions

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@ db/
public/*.js
public/*.map
.yarnrc.yml
.pnp*
build/

View File

@@ -2,12 +2,13 @@
"name": "volp",
"version": "1.0.0",
"description": "Open Travel Mapper",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "esbuild src/app.ts --bundle --outfile=public/main.js --bundle --minify --sourcemap --tsconfig=tsconfig.json",
"start": "node server.js",
"demon": "nodemon -e ts,pug --watch src --exec \"yarn build && yarn start\""
"build": "yarn build-server && yarn build-client",
"build-client": "esbuild src/client/main.ts --outfile=public/main.js --tree-shaking=true --bundle --minify --sourcemap --tsconfig=tsconfig-client.json",
"build-server": "esbuild src/server/**/*.ts --outdir=build --platform=node --format=cjs",
"start": "node build/main.js",
"demon": "nodemon -e ts,js,pug --watch src --watch template --watch router --exec \"yarn build && yarn start\""
},
"repository": {
"type": "git",
@@ -21,12 +22,17 @@
"@fastify/view": "^10.0.0",
"@prettier/plugin-pug": "^3.0.0",
"@types/node": "^22.13.5",
"axios": "^1.7.9",
"esbuild": "^0.25.0",
"fastify": "^5.0.0",
"fastify": "^5.2.1",
"jsdom": "^26.0.0",
"leaflet": "^1.9.4",
"nodemon": "^3.0.1",
"pretier": "^0.0.1",
"prettier": "^3.5.2",
"pug": "^3.0.2"
"pug": "^3.0.2",
"undici": "^7.3.0",
"vue": "2",
"vue-multiselect": "2",
"vue-textarea-autosize": "^1.1.1",
"vue2-leaflet": "^2.7.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,103 +0,0 @@
const axios = require("axios");
module.exports = (fastify, opts, done) => {
fastify.get("/flight/:id", async (req, reply) => {
const ENDPOINT = "https://www.flightradar24.com/v1/search/web/find";
const FORMAT = "-";
if (req.params.id) {
axios
.get(ENDPOINT, {
params: {
format: FORMAT,
query: req.params.id,
limit: 16,
type: "schedule",
},
})
.then((res) => reply.send(res.data));
} else {
return reply.send([]);
}
return reply;
});
fastify.get("/place/:id", async (req, reply) => {
const ENDPOINT = "https://nominatim.openstreetmap.org/";
const FORMAT = "jsonv2";
if (req.params.id) {
axios
.get(ENDPOINT, {
params: {
format: FORMAT,
q: req.params.id,
},
})
.then((res) => reply.send(res.data));
} else {
return reply.send([]);
}
return reply;
});
fastify.get("/gpx/:id", async (req, reply) => {
if (req.params.id == undefined)
return reply.code(400).send({ error: "No ID query parameter" });
fastify.level.db.get(req.params.id, (err, val) => {
if (err) {
console.warn(err);
reply.code(500).send();
} else {
let file = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><gpx xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/1" version="1.1" creator="EMTAC BTGPS Trine II DataLog Dump 1.0 - http://www.ayeltd.biz" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
const data = JSON.parse(val);
const gen_wpt = (name,desc,latlon,icon="Flag") => `<wpt lat="${latlon[0]}" lon="${latlon[1]}"><ele>0</ele><name>${name}</name><cmt>-</cmt><desc>${desc}</desc><sym>${icon}</sym></wpt>`
const esc_str = (str) => (str||"Undefined").replace('"',"&quot;").replace("'","&apos;").replace("<","&lt;").replace(">","&gt;").replace("&","&amp;").replace("\n","...")
data.main.forEach(a => {
file+= gen_wpt(esc_str(a.hotel.name), esc_str(a.hotel.notes), a.hotel.latlon, icon="Hotel");
a.places.restaurants.forEach(b => {
file+= gen_wpt(esc_str(b.name), esc_str(b.notes), b.latlon, icon="Restaurant");
});
a.places.activities.forEach(b => {
file+= gen_wpt(esc_str(b.name), esc_str(b.notes), b.latlon, icon="Tree");
});
});
file+="</gpx>";
reply.header('Content-Type', 'application/gpx+xml');
reply.header('Content-Disposition', `attachment; filename=${req.params.id}.gpx`);
reply.send(file);
}
});
return reply;
});
fastify.get("/:id", async (req, reply) => {
if (req.params.id == undefined)
return reply.code(400).send({ error: "No ID query parameter" });
fastify.level.db.get(req.params.id, (err, val) => {
if (err) {
console.warn(err);
reply.code(500).send();
} else {
reply.send(JSON.parse(val));
}
});
return reply;
});
fastify.post("/:id", async (req, reply) => {
if (req.params.id == undefined)
return reply.code(400).send({ error: "No ID query parameter" });
fastify.level.db.put(req.params.id, JSON.stringify(req.body), (err) => {
if (err) {
console.warn(err);
reply.code(500).send({ error: "Error with DB" });
} else {
reply.send({ content: "ok" });
}
});
return reply;
});
done();
};

View File

@@ -1,35 +0,0 @@
const fastify = require("fastify")(); //{ logger: true });
const path = require("path");
fastify.register(require("@fastify/static"), {
root: path.join(__dirname, "public"),
prefix: "/public/",
});
fastify.register(
require("@fastify/leveldb"),
{
name: "db",
},
(err) => {
if (err) throw err;
}
);
fastify.register(require("@fastify/view"), {
engine: {
pug: require("pug"),
},
});
fastify.register(require("./router/api"), { prefix: "/api" });
fastify.get("/", (req, reply) => reply.view("/template/home.pug"));
fastify.get("/:id", (req, reply) => reply.view("/template/journey.pug"));
fastify.get("/view/:id", (req, reply) => reply.view("/template/view.pug"));
fastify.get("/short/:id", (req, reply) => reply.view("/template/short.pug"));
fastify.listen({ port: 8080, host: "0.0.0.0" }, (err, address) => {
if (err) throw err;
console.log("Listening on", address);
});

View File

@@ -1,122 +0,0 @@
import axios from "axios";
export const load = (id: string) =>
axios.get("/api/" + id).then((response) => {
if (response.data == "") throw "Invalid Journey Data Received";
let res = response.data;
for (let e of res.main) {
if (e.dateRange) {
e.dateRange[0] = new Date(e.dateRange[0]);
e.dateRange[1] = new Date(e.dateRange[1]);
}
e.step_title = e.step_title || [];
}
return res;
});
export const save = (id: string, v: string) =>
axios
.post("/api/" + id, v)
.then((response) => {
console.log("Saved...");
})
.catch((error) => {
console.warn("Error! Could not reach the API.");
});
export const query_nominatim = (
q: string,
f: (v: string) => Boolean = () => true,
) =>
axios
.get("/api/place/" + q)
.then((res) => res.data)
.then((res) => res.filter(f));
export const query_flight = (q: string) =>
axios.get("/api/flight/" + q).then((res) => res.data);
type NominatimResult = {
type: string;
category: string;
display_name: string; // DEBUG ONLY
};
export const is_restauration_type = (e: NominatimResult) =>
["restaurant", "cafe", "pub", "bar", "fast_food", "food_court"].indexOf(
e.type,
) != -1;
export const is_attraction_type = (e: NominatimResult): boolean =>
[
"tourism",
"leisure",
"place",
"amenity",
"highway",
"historic",
"natural",
"waterway",
].indexOf(e.category) != -1 ||
[
"place_of_worship",
"national_park",
"nature_reserve",
"protected_area",
].indexOf(e.type) != -1;
export const icon_type = (item: NominatimResult): string => {
let t = item.type;
let c = item.category;
const arr = ["restaurant", "cafe", "pub", "bar", "fast_food", "food_court"];
if (arr.indexOf(t) != -1) {
return "utensils";
} else if (t == "hotel" || t == "hostel" || t == "guest_house") {
return "bed";
} else if (t == "museum" || c == "historic" || t == "place_of_worship") {
return "landmark";
} else if (t == "peak" || t == "viewpoint") {
return "mountain";
} else if (t == "parking") {
return "parking";
} else if (
t == "water" ||
t == "river" ||
t == "lake" ||
t == "torrent" ||
t == "aquarium"
) {
return "water";
} else if (t == "community_centre" || t == "locality") {
return "building";
} else if (t == "attraction") {
return "landmark";
} else if (t == "information" || t == "university") {
return "landmark";
} else if (t == "bridge") {
return "archway";
} else if (
t == "woodland" ||
t == "shieling" ||
t == "national_park" ||
t == "zoo" ||
t == "park" ||
t == "garden" ||
0
) {
return "tree";
} else if (t == "water_park" || t == "theme_park") {
return "dice-five";
} else if (
t == "?" ||
t == "neighbourhood" ||
t == "quarter" ||
c == "highway"
) {
return "";
} else {
console.log(item.display_name, item.category, item.type);
return "question";
}
};

View File

@@ -1,13 +0,0 @@
import "./types/ext";
import "./api";
import "./old.js";
console.log("TEST");
if (false) {
console.log("B");
}
function test() {
console.log("CC");
}

152
src/client/api.ts Normal file
View File

@@ -0,0 +1,152 @@
export const throttle = (func: () => void, wait: number) => {
var lastTime = 0;
var timeoutId: ReturnType<typeof setTimeout> | undefined;
var lastArgs: any[];
return function (...args: any[]) {
const now = Date.now();
lastArgs = args;
if (timeoutId) clearTimeout(timeoutId);
timeoutId = undefined;
if (now - lastTime >= wait) {
lastTime = now;
func.apply(this, lastArgs);
} else {
timeoutId = setTimeout(
() => {
lastTime = Date.now();
func.apply(this, lastArgs);
},
wait - (now - lastTime)
);
}
};
};
export const load = (id: string) =>
fetch("/api/" + id)
.then((res) => {
if (!res.ok) throw new Error("Error " + res.statusText);
return res.json();
})
.then((res) => {
for (let e of res.main) {
if (e.date_range) {
e.date_range[0] = new Date(e.date_range[0]);
e.date_range[1] = new Date(e.date_range[1]);
}
e.day_title = e.day_title || [];
}
return res;
});
export const save = (id: string, v: journey) =>
fetch("/api/" + id, { method: "post", body: JSON.stringify(v) })
.then((res) => {
if (!res.ok) throw new Error("Error " + res.statusText);
return res.json();
})
.then((_res) => {
console.log("Saved...");
});
export const query_nominatim = (
q: string,
bb: any,
f: (v: string) => Boolean = () => true
) => {
if (q.length == 0) return Promise.resolve([])
let url = new URL("/api/place/" + q, window.location.origin);
url.searchParams.append("id", q);
url.searchParams.append("bb", JSON.stringify(bb));
return fetch(url)
.then((res) => (res.status == 200 ? res.json() : []))
.then((res) => res.filter(f));
};
export const query_flight = (q: string) =>
fetch("/api/flight/" + q).then((res) => res.json());
type NominatimResult = {
type: string;
category: string;
display_name: string; // DEBUG ONLY
};
export const is_restauration_type = (e: NominatimResult) =>
["restaurant", "cafe", "pub", "bar", "fast_food", "food_court"].indexOf(
e.type
) != -1;
export const is_attraction_type = (e: NominatimResult): boolean =>
[
"tourism",
"leisure",
"place",
"amenity",
// "highway",
"historic",
"natural",
"waterway",
].indexOf(e.category) != -1 ||
[
"place_of_worship",
"national_park",
"nature_reserve",
"protected_area",
].indexOf(e.type) != -1 || is_travel_type(e);
export const is_hotel_type = (e: NominatimResult): boolean =>
["hotel", "hostel", "guest_house"].indexOf(e.type) != -1
export const is_travel_type = (e: NominatimResult): boolean =>
["bus_stop", "tram_stop", "station", , "aerodrome", "parking"].indexOf(e.type) != -1
export const icon_type = (item: string | NominatimResult): string => {
if (typeof (item) == "string") {
return item
}
let t = item.type;
let c = item.category;
let types = {
utensils: [
"restaurant",
"cafe",
"pub",
"bar",
"fast_food",
"food_court",
],
bed: ["hotel", "hostel", "guest_house"],
landmark: [
"museum",
"historic",
"place_of_worship",
"attraction",
"information",
"university", "theatre", "opera"
],
mountain: ["peak", "viewpoint"],
parking: ["parking"],
water: ["water", "river", "lake", "torrent", "aquarium"],
building: ["community_center", "locality"],
archway: ["bridge"],
tree: [
"woodland",
"shieling",
"national_park",
"park",
"zoo",
"garden",
"nature_reserve",
],
"dice-five": ["water_park", "theme_park", "casino"],
"": ["?", "neighbourhood", "quarter", "highway"],
};
for (let k in types) {
if (types[k].indexOf(t) >= 0 || types[k].indexOf(c) >= 0) return k;
}
console.log(item.display_name, item.category, item.type);
return "question";
};

4
src/client/main.ts Normal file
View File

@@ -0,0 +1,4 @@
import "./types/ext";
import "./types/format";
import "./api";
import "./old";

273
src/client/old.js Normal file
View File

@@ -0,0 +1,273 @@
import * as api from "./api";
import journey_wrapper from "./types/wrapper";
import { migrator } from "./types/migration";
import { getGeoLine } from "./types/geom";
Vue.component("l-map", window.Vue2Leaflet.LMap);
Vue.component("l-tile-layer", window.Vue2Leaflet.LTileLayer);
Vue.component("l-marker", window.Vue2Leaflet.LMarker);
Vue.component("l-icon", window.Vue2Leaflet.LIcon);
Vue.component("l-popup", window.Vue2Leaflet.LPopup);
Vue.component("l-tooltip", window.Vue2Leaflet.LTooltip);
Vue.component("l-polyline", window.Vue2Leaflet.LPolyline);
Vue.component("l-control-scale", window.Vue2Leaflet.LControlScale);
// Vue.component("multiselect", window.VueMultiselect.default);
Vue.use(window.VueTextareaAutosize);
var first_boot = true
const app = new Vue({
el: "#app",
data: {
edit_active: ["view", "short"].indexOf(window.location.pathname.split("/")[1]) == -1,
journey: new journey_wrapper(window.location.pathname.split("/").pop() || String.gen_id(16)),
map_override: { active: false, elements: [] },
query: {
type: "", res: [], load: false, sub: false,
},
leg_nav: {
scrollInterval: null,
scrollDir: null
},
impexp: "",
lang: {
format: "ddd D MMM",
formatLocale: {
firstDayOfWeek: 1,
},
monthBeforeYear: true,
},
},
methods: {
start_journey: function () { window.location.href = "/" + this.journey.id },
compute_bb: function () {
const bounds = this.$refs.map.mapObject.getBounds();
return [[bounds.getSouthWest().lng, bounds.getSouthWest().lat],
[bounds.getNorthEast().lng, bounds.getNorthEast().lat]]
},
generate_rotation: function (index, list) {
if (index < 0 || index >= list.length) return 0;
const c0 = list[(index == 0) ? index : (index - 1)]
const c1 = list[(index == list.length - 1) ? index : (index + 1)]
const brng = Math.atan2(c1[1] - c0[1], c1[0] - c0[0]);
return `rotate:${brng - Math.PI / 2}rad`;
},
generate_marker: function (item, fcolor) {
return `
<div style="position: absolute;top: -30px;left: -6px;">
<i class=" fa fa-${api.icon_type(item) || "star"} fa-lg icon-white" style="position: absolute;text-align:center; width:24px;height:24px; line-height:1.5em"></i>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 36" width="24" height="36">
<circle cx="12" cy="12" r="12" fill="${fcolor || item.color || "blue"}"/>
<polygon points="4,12 20,12 12,36" fill="${fcolor || item.color || "blue"}" /></svg>`
},
generate_icon: function (item, fcolor = "", styling = "", classes = "") {
return `<i class="fa fa-${api.icon_type(item) || "star"} fa-2x ${classes}" style="${styling}; color:${fcolor || "white"}; text-align:center; align-content:center; width:32px; height:32px;"></i>`;
},
import_data: function () {
this.journey.data = Object.assign(
{},
JSON.parse(this.impexp.toDecoded()),
);
this.journey.data.main.forEach((e) => {
if (e.date_range) {
e.date_range[0] = new Date(e.date_range[0]);
e.date_range[1] = new Date(e.date_range[1]);
}
});
},
export_data: function () {
this.impexp = JSON.stringify(this.journey.data).toEncoded();
},
filter_selected: function (list, step) {
return list.filter((e) =>
step ? e.step == this.journey.sel_day : e.step >= 0,
);
},
filter_unselected: function (list) {
return list.filter((e) => e.step == undefined || e.step < 0);
},
remove_item: function (list, idx) {
list[idx].step = -1;
list.splice(idx, 1);
},
log: function (e) {
console.log(e);
},
place_delete: function (f, idx) {
switch (f) {
case "hotel": return this.journey.leg_get().hotel = null;
case "restaurant": return this.journey.leg_get().places.restaurants.splice(idx, 1);
case "activities": return this.journey.leg_get().places.activities.splice(idx, 1);
case "other": return;
case "flight": return this.journey.leg_get().travel.splice(idx, 1);
default: return true;
}
},
get_filter: function (f) {
switch (f) {
case "hotel": return api.is_hotel_type;
case "restaurant": return api.is_restauration_type;
case "place": return api.is_attraction_type;
case "other":
default: return () => true;
}
},
search_nominatim: function (f) {
return (q) => api.query_nominatim(q, this.compute_bb(), this.get_filter(f)).catch((_err) => []).then((r) => {
r.forEach((rr) => {
rr.latlon = [parseFloat(rr.lat), parseFloat(rr.lon)];
rr.sname = rr.display_name.split(",")[0];
});
r = r.filter(e => {
if (this.journey.leg_get().hotel && this.journey.leg_get().hotel.osm_id == e.osm_id) return false;
if (this.journey.leg_get().places.restaurants.find(i => i.osm_id == e.osm_id)) return false;
if (this.journey.leg_get().places.activities.find(i => i.osm_id == e.osm_id)) return false;
return true
})
this.query.load = false;
this.query.res = r;
return r
});
},
search_travel: function (f) {
return (q) => api.query_flight(q).then((r) => {
r.forEach(el => {
el.path = getGeoLine(
{ lat: el.from_geo.lat, lng: el.from_geo.lon },
{ lat: el.to_geo.lat, lng: el.to_geo.lon }, { dist: 2_500_000 }).map(v => [v.lat, v.lng])
el.type = "flight";
});
r = r.filter(e => {
if (this.journey.leg_get().travel.find(i => `${i.from}->${i.to}` == `${e.from}->${e.to}`)) return false;
return true
})
this.query.load = false;
this.query.res = r;
return r;
});
},
drawer_hover_item: function (item) {
if (item) {
this.map_override.active = true
if (item.type == 'flight') {
this.map_override.elements = [[item.from_geo.lat, item.from_geo.lon], [item.to_geo.lat, item.to_geo.lon]]
} else {
this.map_override.elements = [[item.lat, item.lon]]
}
} else {
this.map_override.active = false
}
},
drawer_click_item: function (item) {
const tpe = this.query.type;
this.query.res = [];
this.query.type = null;
this.query.sub = false;
this.drawer_hover_item()
if (item) {
item.step = -1;
switch (tpe) {
case 'hotel': return this.journey.leg_get().hotel = item;
case 'restaurant': return this.journey.leg_get().places.restaurants.push(item);
case 'place': return this.journey.leg_get().places.activities.push(item);
case 'other': return;
case 'flight': return this.journey.leg_get().travel.push(item);
}
}
},
search_active: function (q) {
const txt = q.target.value
this.query.load = true;
switch (this.query.type) {
case 'hotel': return this.search_hotel(txt);
case 'restaurant': return this.search_restaurant(txt);
case 'place': return this.search_place(txt);
case 'other': return this.search_other(txt);
case 'flight': return this.search_flight(txt);
}
},
search_enable: function (f) {
this.query.type = f;
const query_in = document.getElementById('query_input')
query_in.focus()
this.search_active({ target: query_in })
},
sideScroll: function (element, direction, speed, step) {
this.leg_nav.scrollDir = direction
if (direction == 'none') return;
this.leg_nav.scrollInterval = setInterval(() => {
element.scrollLeft += (direction == 'left') ? -step : step;
}, speed);
},
keyboardEvent(e) {
if (e.which === 13) {
}
},
nav_mousemove(e) {
const c = document.querySelector('.scroll-content')
const newDir =
e.pageX < c.offsetWidth * 0.1 ? 'left' :
(e.pageX > c.offsetWidth * 0.9 ? 'right' : 'none')
if (!this.leg_nav.scrollInterval || this.leg_nav.scrollDir != newDir) {
if (this.leg_nav.scrollInterval) clearInterval(this.leg_nav.scrollInterval)
this.sideScroll(c, newDir, 25, 10);
}
},
nav_mouseleave(e) {
clearInterval(this.leg_nav.scrollInterval);
this.leg_nav.scrollDir = 'none'
this.leg_nav.scrollInterval = null
},
},
created: function () {
this.search_hotel = api.throttle(this.search_nominatim("hotel"), 1000)
this.search_restaurant = api.throttle(this.search_nominatim("restaurant"), 1000)
this.search_place = api.throttle(this.search_nominatim("place"), 1000)
this.save_data = api.throttle(() => {
this.impexp = JSON.stringify(this.journey.data).toEncoded();
api.save(this.journey.id, this.journey.data);
}, 1000);
this.search_flight = api.throttle(this.search_travel("flight"), 2000)
window.addEventListener("keydown", (e) => {
switch (e.key) {
case "ArrowLeft":
this.journey.day_prev();
break;
case "ArrowRight":
this.journey.day_next();
break;
default:
console.log(e.key);
}
});
api.load(this.journey.id).then((r) => {
app.journey.data = migrator(r)
});
},
watch: {
journey: {
handler: function (ndata, odata) {
this.save_data();
},
deep: true,
},
},
});

View File

@@ -2,8 +2,10 @@
declare global {
interface Date {
toJSONLocal: () => string;
toLocal: () => string;
}
}
Date.prototype.toJSONLocal = function () {
function addZ(n: number): string {
return n <= 9 ? `0${n}` : `${n}`;
@@ -13,7 +15,27 @@ Date.prototype.toJSONLocal = function () {
addZ(this.getMonth() + 1),
addZ(this.getDate()),
].join("-");
};
}
Date.prototype.toLocal = function () {
return [["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][this.getDay()],
this.getDate(),
[
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
][this.getMonth()]].join(" ")
;
}
// ARRAY EXTENTION
declare global {
@@ -34,27 +56,20 @@ Array.prototype.foldl = function <T, B>(f: (x: T, acc: B) => B, acc: B): B {
// STRING EXTENTION
declare global {
interface String {
btoa: () => String;
toEncoded: () => String;
toDecoded: () => String;
}
}
String.prototype.btoa = function () {
return window.btoa(this);
};
String.prototype.toEncoded = function () {
return window.btoa(
Array.from(this as string, (c) => c.charCodeAt(0)).foldl(
(e, v) => v + String.fromCharCode(e),
"",
),
);
return window.btoa(encodeURIComponent(Array.from(this as string, (c) => c.charCodeAt(0)).foldl(
(e, v) => v + String.fromCharCode(e),
"",
)))
};
String.prototype.toDecoded = function () {
return Array.from(window.atob(this), (c) => c.charCodeAt(0)).foldl(
return Array.from(decodeURIComponent(window.atob(this as string)), (c) => c.charCodeAt(0)).foldl(
(e, v) => v + String.fromCharCode(e),
"",
);
@@ -76,4 +91,4 @@ String.gen_id = function (length) {
.join("");
};
export {};
export { };

View File

@@ -0,0 +1,59 @@
declare global {
interface LatLng {
lat: number
lng: number
}
interface geoloc {
latlon: [number, number]
notes: string
step: -1
}
interface map {
zoom: number
center: LatLng
}
interface leg {
title: string
day_title: string[]
date_range: [Date, Date] | null
map: map
travel: unknown[]
hotel: geoloc | null
places: {
restaurants: geoloc[]
activities: geoloc[]
}
notes: string
}
interface journey {
fmt_ver: number
title: string
main: leg[]
}
}
const journey_template: journey = {
fmt_ver: 1,
title: "New Journey",
main: [],
}
const leg_template: leg = {
title: "New Leg",
day_title: [],
map: { zoom: 2, center: { lng: 0, lat: 0 } },
travel: [],
hotel: null,
places: { restaurants: [], activities: [] },
notes: "",
date_range: null
}
export { map, geoloc, leg, journey }
export { journey_template, leg_template }

165
src/client/types/geom.ts Normal file
View File

@@ -0,0 +1,165 @@
const ellipsoid = {
a: 6378137,
b: 6356752.3142,
f: 1 / 298.257223563
};
function mod(n: number, p: number): number {
const r = n % p;
return r < 0 ? r + p : r;
}
function wrap(degrees: number, max = 360) {
if (-max <= degrees && degrees <= max) {
return degrees;
} else {
return mod(degrees + max, 2 * max) - max;
}
}
function dist(src: LatLng, dst: LatLng, itr = 100, mit = true): number {
const p1 = src,
p2 = dst;
const φ1 = toRadians(p1.lat),
λ1 = toRadians(p1.lng);
const φ2 = toRadians(p2.lat),
λ2 = toRadians(p2.lng);
const π = Math.PI;
const ε = Number.EPSILON;
// allow alternative ellipsoid to be specified
const { a, b, f } = ellipsoid;
const dL = λ2 - λ1; // L = difference in longitude, U = reduced latitude, defined by tan U = (1-f)·tanφ.
const tanU1 = (1 - f) * Math.tan(φ1),
cosU1 = 1 / Math.sqrt(1 + tanU1 * tanU1),
sinU1 = tanU1 * cosU1;
const tanU2 = (1 - f) * Math.tan(φ2),
cosU2 = 1 / Math.sqrt(1 + tanU2 * tanU2),
sinU2 = tanU2 * cosU2;
const antipodal = Math.abs(dL) > π / 2 || Math.abs(φ2 - φ1) > π / 2;
let λ = dL,
sinλ: number | null = null,
cosλ: number | null = null; // λ = difference in longitude on an auxiliary sphere
let σ = antipodal ? π : 0,
sinσ = 0,
cosσ = antipodal ? -1 : 1,
sinSqσ: number | null = null; // σ = angular distance P₁ P₂ on the sphere
let cos2σ = 1; // σₘ = angular distance on the sphere from the equator to the midpoint of the line
let sinα: number | null = null,
cosSqα = 1; // α = azimuth of the geodesic at the equator
let C: number | null = null;
let λʹ: number | null = null,
iterations = 0;
do {
sinλ = Math.sin(λ);
cosλ = Math.cos(λ);
sinSqσ =
cosU2 * sinλ * (cosU2 * sinλ) +
(cosU1 * sinU2 - sinU1 * cosU2 * cosλ) * (cosU1 * sinU2 - sinU1 * cosU2 * cosλ);
if (Math.abs(sinSqσ) < ε) {
break; // co-incident/antipodal points (falls back on λ/σ = L)
}
sinσ = Math.sqrt(sinSqσ);
cosσ = sinU1 * sinU2 + cosU1 * cosU2 * cosλ;
σ = Math.atan2(sinσ, cosσ);
sinα = (cosU1 * cosU2 * sinλ) / sinσ;
cosSqα = 1 - sinα * sinα;
cos2σ = cosSqα !== 0 ? cosσ - (2 * sinU1 * sinU2) / cosSqα : 0; // on equatorial line cos²α = 0 (§6)
C = (f / 16) * cosSqα * (4 + f * (4 - 3 * cosSqα));
λʹ = λ;
λ = dL + (1 - C) * f * sinα * (σ + C * sinσ * (cos2σ + C * cosσ * (-1 + 2 * cos2σ * cos2σ)));
const iterationCheck = antipodal ? Math.abs(λ) - π : Math.abs(λ);
if (iterationCheck > π) {
throw new EvalError("λ > π");
}
} while (Math.abs(λ - λʹ) > 1e-12 && ++iterations < itr);
if (iterations >= itr) {
if (mit) {
return dist(
src,
{ lat: dst.lat, lng: dst.lng - 0.01 },
itr,
mit
);
} else {
throw new EvalError(`Inverse vincenty formula failed to converge after ${itr} iterations
(start=${src.lat}/${src.lng}; dest=${dst.lat}/${dst.lng})`);
}
}
const uSq = (cosSqα * (a * a - b * b)) / (b * b);
const A = 1 + (uSq / 16384) * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
const B = (uSq / 1024) * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
const Δσ =
B *
sinσ *
(cos2σ +
(B / 4) *
(cosσ * (-1 + 2 * cos2σ * cos2σ) -
(B / 6) * cos2σ * (-3 + 4 * sinσ * sinσ) * (-3 + 4 * cos2σ * cos2σ)));
const s = b * A * (σ - Δσ); // s = length of the geodesic
return s
}
function pointDistance(src: LatLng, dst: LatLng): number {
return dist(
{ lat: src.lat, lng: wrap(src.lng, 180) },
{ lat: dst.lat, lng: wrap(dst.lng, 180) }
);
}
function toRadians(degree: number): number {
return (degree * Math.PI) / 180;
}
function toDegrees(radians: number): number {
return (radians * 180) / Math.PI;
}
function midpoint(src: LatLng, dst: LatLng): LatLng {
// φm = atan2( sinφ1 + sinφ2, √( (cosφ1 + cosφ2⋅cosΔλ)² + cos²φ2⋅sin²Δλ ) )
// λm = λ1 + atan2(cosφ2⋅sinΔλ, cosφ1 + cosφ2⋅cosΔλ)
// midpoint is sum of vectors to two points: mathforum.org/library/drmath/view/51822.html
const φ1 = toRadians(src.lat);
const λ1 = toRadians(src.lng);
const φ2 = toRadians(dst.lat);
const Δλ = toRadians(dst.lng - src.lng);
// get cartesian coordinates for the two points
const A = { x: Math.cos(φ1), y: 0, z: Math.sin(φ1) }; // place point A on prime meridian y=0
const B = { x: Math.cos(φ2) * Math.cos(Δλ), y: Math.cos(φ2) * Math.sin(Δλ), z: Math.sin(φ2) };
// vector to midpoint is sum of vectors to two points (no need to normalise)
const C = { x: A.x + B.x, y: A.y + B.y, z: A.z + B.z };
const φm = Math.atan2(C.z, Math.sqrt(C.x * C.x + C.y * C.y));
const λm = λ1 + Math.atan2(C.y, C.x);
return { lat: toDegrees(φm), lng: toDegrees(λm) };
}
function recursiveMidPoint(src: LatLng, dst: LatLng, opt: { step?: number, dist?: number } = {}, curr = 0): LatLng[] {
const geom: LatLng[] = [src, dst];
const mp = midpoint(src, dst);
const split_step = (opt.step != undefined && (opt.step > 0 || curr == 0))
const split_dist = (opt.dist != undefined && (pointDistance(src, dst) > opt.dist || curr == 0))
const next_opt = split_step ? { step: (opt.step || 0) - 1 } : { dist: opt.dist };
if (split_step || split_dist) {
geom.splice(0, 1, ...recursiveMidPoint(src, mp, next_opt, curr + 1));
geom.splice(geom.length - 2, 2, ...recursiveMidPoint(mp, dst, next_opt, curr + 1));
} else {
geom.splice(1, 0, mp);
}
return geom;
}
export function getGeoLine(src: LatLng, dst: LatLng, opt: { step?: number, dist?: number }) {
return recursiveMidPoint(src, dst, opt, 1)
}

View File

@@ -0,0 +1,24 @@
const FMT_VER_0 = 0
const FMT_VER_LATEST = FMT_VER_0
function migrate_A_to_0(e: journey): journey {
e.title = (e as any).name;
e.main.forEach((v) => {
v.date_range = v.date_range || (v as any).dateRange;
v.day_title = v.day_title || (v as any).step_title;
v.places.activities = v.places.activities || (v as any).places.places;
v.travel = v.travel || [];
})
console.log(e)
return e;
}
export const migrator = (e: journey): journey => {
if (e.fmt_ver == FMT_VER_LATEST) return e;
switch (e.fmt_ver) {
case FMT_VER_0: break; // Update when FMT_VER_1 releases
default:
return migrate_A_to_0(e)
}
return e;
}

143
src/client/types/wrapper.ts Normal file
View File

@@ -0,0 +1,143 @@
import { journey_template, leg_template } from "./format"
const date_day_diff = (d0: Date, d1: Date): number =>
(d1.getTime() - d0.getTime()) / (1000 * 60 * 60 * 24)
class journey_wrapper {
id: String
data: journey = journey_template;
sel_leg: number = 0;
sel_day: number = 0;
constructor(id: String) {
this.id = id;
}
leg_first = () => this.data.main[0]
leg_last = () => this.data.main[this.leg_count() - 1]
leg_count(): number {
return this.data.main.length;
}
leg_len(idx?: number): number {
let d = this.leg_get(idx == undefined ? this.sel_leg : idx).date_range;
return d ? date_day_diff(d[0], d[1]) + 1 : 1;
}
add_leg(): void {
if (this.data.main == undefined) this.data.main = [];
this.data.main.push(leg_template);
}
rm_leg(idx: number): void {
this.data.main.splice(idx, 1);
if (this.sel_leg == idx) this.leg_prev();
}
tot_len(): number | "?" {
if (this.leg_count() == 0) return 0;
let lf = this.leg_first(), ll = this.leg_last();
if (lf.date_range && ll.date_range) {
let d0 = lf.date_range[0]
let d1 = ll.date_range[1]
return date_day_diff(d0, d1);
}
return "?";
}
leg_sel(idx: number): void {
this.sel_leg = idx;
this.sel_day = 0;
}
leg_get(idx?: number): leg {
return this.data.main[idx != undefined ? idx : this.sel_leg]
}
leg_next(): void {
this.sel_leg = Math.min(this.sel_leg + 1, this.leg_count() - 1);
this.sel_day = 0;
}
leg_prev(): void {
this.sel_leg = Math.max(this.sel_leg - 1, 0);
this.sel_day = 0;
}
day_next() {
this.sel_day += 1
if (this.sel_day > this.leg_len() - 1) {
if (this.sel_leg < this.leg_count() - 1) {
this.leg_next()
this.sel_day = 0;
} else {
this.sel_day = this.leg_len() - 1;
}
}
}
day_prev() {
this.sel_day -= 1
if (this.sel_day < 0) {
if (this.sel_leg > 0) {
this.leg_prev()
this.sel_day = this.leg_len() - 1;
} else {
this.sel_day = 0;
}
}
}
date_sel(): string {
if (this.sel_day < 0) return "?";
let leg = this.leg_get()
if (!leg.date_range)
return "?";
var date = new Date(leg.date_range[0]);
date.setDate(date.getDate() + this.sel_day);
return date.toLocal();
}
date_tot() {
if (this.leg_count() == 0) return "";
let lf = this.leg_first(), ll = this.leg_last();
if (lf.date_range && ll.date_range)
return `${lf.date_range[0].toLocal()} - ${ll.date_range[1].toLocal()}`;
return "?";
}
date_update(idx: number) {
let date_range = this.leg_get(idx).date_range;
if (!date_range) return;
let start_end = [0, 0];
let step_len = 0;
let last_start = date_range[0];
for (let i = idx - 1; i >= 0; --i) {
step_len = this.leg_len(i) - 1;
if (this.leg_get(i).date_range) {
start_end = [last_start.getDate() - step_len, last_start.getDate()];
} else {
this.leg_get(i).date_range = [new Date(), new Date()];
start_end = [last_start.getDate() - step_len, last_start.getDate()];
}
let leg = this.leg_get(i)
if (leg.date_range) {
leg.date_range[0].setTime(last_start.getTime());
leg.date_range[0].setDate(start_end[0]);
leg.date_range[1].setTime(last_start.getTime());
leg.date_range[1].setDate(start_end[1]);
last_start = leg.date_range[0];
}
}
let last_end = date_range[1];
for (let i = idx + 1; i < this.leg_count(); ++i) {
step_len = this.leg_len(i) - 1;
if (this.leg_get(i).date_range) {
start_end = [last_end.getDate(), last_end.getDate() + step_len];
} else {
this.leg_get(i).date_range = [new Date(), new Date()];
start_end = [last_end.getDate(), last_end.getDate() + step_len];
}
let leg = this.leg_get(i)
if (leg.date_range) {
leg.date_range[0].setTime(last_end.getTime());
leg.date_range[0].setDate(start_end[0]);
leg.date_range[1].setTime(last_end.getTime());
leg.date_range[1].setDate(start_end[1]);
last_end = leg.date_range[1];
}
}
}
}
export default journey_wrapper;

View File

@@ -1,344 +0,0 @@
import * as api from "./api";
Vue.component("l-map", window.Vue2Leaflet.LMap);
Vue.component("l-tile-layer", window.Vue2Leaflet.LTileLayer);
Vue.component("l-marker", window.Vue2Leaflet.LMarker);
Vue.component("l-icon", window.Vue2Leaflet.LIcon);
Vue.component("l-popup", window.Vue2Leaflet.LPopup);
Vue.component("l-tooltip", window.Vue2Leaflet.LTooltip);
Vue.component("l-control-scale", window.Vue2Leaflet.LControlScale);
Vue.component("multiselect", window.VueMultiselect.default);
Vue.use(window.VueTextareaAutosize);
const app = new Vue({
el: "#app",
data: {
journey_edit:
["view", "short"].indexOf(window.location.pathname.split("/")[1]) == -1,
journey_id: window.location.pathname.split("/").pop() || String.gen_id(16),
journey_step_data: { day: 1, section: 0 },
journey_data: {
name: "New Journey",
main: [],
},
query: { hotel: [], flight: [], nominatim: [] },
querying: { hotel: false, flight: false, place: false, food: false },
impexp: "",
lang: {
format: "ddd D MMM",
formatLocale: {
firstDayOfWeek: 1,
},
monthBeforeYear: true,
},
},
methods: {
start_journey: function (event) {
window.location.href = "/" + this.journey_id;
},
add_section: function (event) {
if (this.journey_data.main == undefined) this.journey_data.main = [];
this.journey_data.main.push({
title: "?",
step_title: [],
map: { zoom: 2 },
hotel: { latlon: [0, 0] },
places: { restaurants: [], places: [] },
});
},
step_len: function (idx) {
return this.journey_data.main[idx].dateRange
? (this.journey_data.main[idx].dateRange[1] -
this.journey_data.main[idx].dateRange[0]) /
(1000 * 60 * 60 * 24) +
1
: 1;
},
next_step: function () {
this.journey_step_data.day += 1;
let s = this.journey_step_data.section;
let cd = this.step_len(s);
if (this.journey_step_data.day > cd) {
this.journey_step_data.section += 1;
if (this.journey_step_data.section >= this.journey_data.main.length) {
this.journey_step_data.section = this.journey_data.main.length - 1;
this.journey_step_data.day = cd;
} else {
this.journey_step_data.day = 1;
}
}
},
prev_step: function () {
this.journey_step_data.day -= 1;
if (this.journey_step_data.day <= 0) {
this.journey_step_data.section -= 1;
if (this.journey_step_data.section < 0) {
this.first_step();
} else {
let s = this.journey_step_data.section;
let cd = this.step_len(s);
this.journey_step_data.day = cd;
}
}
},
nextnext_step: function () {
this.journey_step_data.section += 1;
this.journey_step_data.day = 1;
if (this.journey_step_data.section >= this.journey_data.main.length)
this.first_step();
},
prevprev_step: function () {
this.journey_step_data.section -= 1;
this.journey_step_data.day = 1;
if (this.journey_step_data.section < 0) this.first_step();
},
first_step: function () {
this.journey_step_data.section = 0;
this.journey_step_data.day = 1;
},
active_date: function () {
if (this.journey_step_data.day < 0) return "?";
if (!this.journey_data.main[this.journey_step_data.section].dateRange)
return "?";
var date = new Date(
this.journey_data.main[this.journey_step_data.section].dateRange[0],
);
date.setDate(date.getDate() + this.journey_step_data.day - 1);
return this.format_date(date);
},
format_date: function (d) {
return (
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()] +
" " +
d.getDate() +
" " +
[
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
][d.getMonth()]
);
},
total_days: function () {
if (this.journey_data.main.length == 0) return 0;
try {
return (
(this.journey_data.main[this.journey_data.main.length - 1]
.dateRange[1] -
this.journey_data.main[0].dateRange[0]) /
(1000 * 60 * 60 * 24)
);
} catch {
return "?";
}
},
total_date: function () {
if (this.journey_data.main.length == 0) return "";
try {
return `${this.format_date(
this.journey_data.main[0].dateRange[0],
)} - ${this.format_date(
this.journey_data.main[this.journey_data.main.length - 1]
.dateRange[1],
)}`;
} catch {
return "?";
}
},
update_date: function (idx) {
let dateRange = this.journey_data.main[idx].dateRange;
let start_end = [0, 0];
let step_len = 0;
let last_start = dateRange[0];
for (let i = idx - 1; i >= 0; --i) {
step_len = this.step_len(i) - 1;
if (this.journey_data.main[i].dateRange) {
start_end = [last_start.getDate() - step_len, last_start.getDate()];
} else {
this.journey_data.main[i].dateRange = [new Date(), new Date()];
start_end = [last_start.getDate() - step_len, last_start.getDate()];
}
this.journey_data.main[i].dateRange[0].setTime(last_start.getTime());
this.journey_data.main[i].dateRange[0].setDate(start_end[0]);
this.journey_data.main[i].dateRange[1].setTime(last_start.getTime());
this.journey_data.main[i].dateRange[1].setDate(start_end[1]);
last_start = this.journey_data.main[i].dateRange[0];
}
let last_end = dateRange[1];
for (let i = idx + 1; i < this.journey_data.main.length; ++i) {
step_len = this.step_len(i) - 1;
if (this.journey_data.main[i].dateRange) {
start_end = [last_end.getDate(), last_end.getDate() + step_len];
} else {
this.journey_data.main[i].dateRange = [new Date(), new Date()];
start_end = [last_end.getDate(), last_end.getDate() + step_len];
}
this.journey_data.main[i].dateRange[0].setTime(last_end.getTime());
this.journey_data.main[i].dateRange[0].setDate(start_end[0]);
this.journey_data.main[i].dateRange[1].setTime(last_end.getTime());
this.journey_data.main[i].dateRange[1].setDate(start_end[1]);
last_end = this.journey_data.main[i].dateRange[1];
}
},
rm_section: function (idx) {
this.journey_data.main.splice(idx, 1);
if (this.journey_step_data.section == idx) {
this.prevprev_step();
}
},
sel_section: function (idx) {
this.journey_step_data.section = idx;
this.journey_step_data.day = 1;
},
search_nominatim: function (txt, f) {
if (txt == "") {
this.query.nominatim = [];
return Promise.resolve([]);
}
return query_nominatim(txt, f).then((results) => {
results.forEach((r) => {
r.latlon = [parseFloat(r.lat), parseFloat(r.lon)];
r.sname = r.display_name.split(",")[0];
});
this.query.nominatim = results;
});
},
search_flight: function (txt) {
if (txt == "") return;
this.querying.flight = true;
query_flight(txt.replace(" ", "")).then((results) => {
if (results.results == "") {
this.query.flight = [];
this.querying.flight = false;
return;
}
this.query.flight = results.results;
this.querying.flight = false;
});
},
generate_icon: function (item, fcolor) {
return L.AwesomeMarkers.icon({
icon: api.icon_type(item) || "star",
prefix: "fa",
markerColor: fcolor || item.color || "blue",
}).createIcon().outerHTML;
},
save_data: function () {
this.impexp = JSON.stringify(this.journey_data).toEncoded();
api.save(this.journey_id, this.journey_data);
},
import_data: function () {
this.journey_data = Object.assign(
{},
JSON.parse(this.impexp.toDecoded()),
);
this.journey_data.main.forEach((e) => {
if (e.dateRange) {
e.dateRange[0] = new Date(e.dateRange[0]);
e.dateRange[1] = new Date(e.dateRange[1]);
}
});
},
export_data: function () {
this.impexp = JSON.stringify(this.journey_data).toEncoded();
},
filter_selected: function (list, step) {
return list.filter((e) =>
step ? e.step == this.journey_step_data.day : e.step >= 0,
);
},
filter_unselected: function (list) {
return list.filter((e) => e.step == undefined || e.step < 0);
},
remove_item: function (list, idx) {
list[idx].step = -1;
list.splice(idx, 1);
},
log: function (e) {
console.log(e);
},
keyboardEvent(e) {
if (e.which === 13) {
}
},
},
created: function () {
window.addEventListener("keydown", (e) => {
switch (e.key) {
case "ArrowLeft":
this.prev_step();
break;
case "ArrowRight":
this.next_step();
break;
default:
console.log(e.key);
}
});
api.load(this.journey_id).then((r) => (app.journey_data = r));
this.debounceSave = _.debounce(this.save_data, 500);
this.debounceSearch = {
hotel: _.debounce((q) => {
this.querying.hotel = true;
this.search_nominatim(
q,
(r) =>
r.type == "hotel" || r.type == "hostel" || r.type == "guest_house",
).then((r) => {
this.querying.hotel = false;
});
}, 500),
restaurants: _.debounce((q) => {
this.querying.food = true;
this.search_nominatim(q, (r) => api.is_restauration_type(r)).then(
(r) => {
this.querying.food = false;
},
);
}, 500),
places: _.debounce((q) => {
this.querying.place = true;
this.search_nominatim(q, (r) => api.is_attraction_type(r)).then((r) => {
this.querying.place = false;
});
}, 500),
other: _.debounce((q) => {
this.querying.any = true;
this.search_nominatim(q, (r) => true).then((r) => {
this.querying.any = false;
});
}, 500),
flight: _.debounce((q) => this.search_flight(q), 500),
};
},
watch: {
journey_data: {
handler: function (ndata, odata) {
this.debounceSave();
},
deep: true,
},
},
});

90
src/server/api.ts Normal file
View File

@@ -0,0 +1,90 @@
//import { ProxyAgent, setGlobalDispatcher } from 'undici';
import { flight_get_data } from './api_flight'
import { nominatim_get_data } from './api_nominatim';
//setGlobalDispatcher(new ProxyAgent(process.env.HTTPS_PROXY as string));
export default function (server, opts, done) {
server.get("/flight/:id", async (req, reply) =>
flight_get_data(req.params.id)
.then(res => {
let wait_for_all: Promise<any>[] = []
res.forEach(r => {
wait_for_all.push(nominatim_get_data(r.from).then(geo => (r as any).from_geo = geo[0]));
wait_for_all.push(nominatim_get_data(r.to).then(geo => (r as any).to_geo = geo[0]));
});
return Promise.all(wait_for_all).then(_ => res)
})
.then(res => reply.send(res))
);
server.get("/place/:id", async (req, reply) =>
nominatim_get_data(req.params.id, JSON.parse(req.query.bb))
.then(res => reply.send(res))
);
server.get("/gpx/:id", async (req, reply) => {
if (req.params.id == undefined)
return reply.code(400).send({ error: "No ID query parameter" });
server.level.db.get(req.params.id, (err, val) => {
if (err) {
console.warn(err);
reply.code(500).send();
} else {
let file = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><gpx xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/1" version="1.1" creator="EMTAC BTGPS Trine II DataLog Dump 1.0 - http://www.ayeltd.biz" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
const data = JSON.parse(val);
const gen_wpt = (name, desc, latlon, icon = "Flag") => `<wpt lat="${latlon[0]}" lon="${latlon[1]}"><ele>0</ele><name>${name}</name><cmt>-</cmt><desc>${desc}</desc><sym>${icon}</sym></wpt>`
const esc_str = (str) => (str || "Undefined").replace('"', "&quot;").replace("'", "&apos;").replace("<", "&lt;").replace(">", "&gt;").replace("&", "&amp;").replace("\n", "...")
data.main.forEach(a => {
file += gen_wpt(esc_str(a.hotel.name), esc_str(a.hotel.notes), a.hotel.latlon, "Hotel");
a.places.restaurants.forEach(b => {
file += gen_wpt(esc_str(b.name), esc_str(b.notes), b.latlon, "Restaurant");
});
a.places.activities.forEach(b => {
file += gen_wpt(esc_str(b.name), esc_str(b.notes), b.latlon, "Tree");
});
});
file += "</gpx>";
reply.header('Content-Type', 'application/gpx+xml');
reply.header('Content-Disposition', `attachment; filename=${req.params.id}.gpx`);
reply.send(file);
}
});
return reply;
});
server.get("/:id", async (req, reply) => {
if (req.params.id == undefined)
return reply.code(400).send({ error: "No ID query parameter" });
server.level.db.get(req.params.id, (err, val) => {
if (err) {
console.warn(err);
reply.code(500).send();
} else {
reply.send(JSON.parse(val));
}
});
return reply;
});
server.post("/:id", async (req, reply) => {
if (req.params.id == undefined)
return reply.code(400).send({ error: "No ID query parameter" });
server.level.db.put(req.params.id, req.body, (err) => {
if (err) {
console.warn(err);
reply.code(500).send({ error: "Error with DB" });
} else {
reply.send({ content: "ok" });
}
});
return reply;
});
done();
};

86
src/server/api_flight.ts Normal file
View File

@@ -0,0 +1,86 @@
import { JSDOM } from 'jsdom';
interface FlightData {
id: string;
date: string;
from: string;
to: string;
std: string;
atd: string | string[] | null;
sta: string;
ata: string | string[] | null;
}
function clean_times(s: string): string | null {
if (s == "—" || s == "Scheduled") return null
if (s.indexOf("Estimated departure") == 0) return null
if (s.indexOf("Landed") == 0) return s.replace("Landed ", "")
return s
}
export function flight_get_data(flightId: string): Promise<FlightData[]> {
const url = new URL(`https://www.flightradar24.com/data/flights/${flightId}`);
return fetch(url).then(res => res.text()).then(res => new JSDOM(res)).then(dom => {
const rows = dom.window.document.querySelectorAll('table tbody tr');
const flightData: FlightData[] = [];
rows.forEach(row => {
const columns = row.querySelectorAll('td');
/*
* 2/6 = Date/Duration
* 3/4 = From/To
* 7/8 = STD/ATD
* 9/11 = STA/ATA
*/
const flight: FlightData = {
id: flightId,
date: columns[2].textContent.trim(),
from: columns[3].textContent.trim(),
to: columns[4].textContent.trim(),
std: columns[7].textContent.trim(),
atd: clean_times(columns[8].textContent.trim()),
sta: columns[9].textContent.trim(),
ata: clean_times(columns[11].textContent.trim())
};
flightData.push(flight);
});
return groupByPair(flightData).map(v => groupByFrequency(v));
})
}
function groupByPair(flightData: FlightData[]): FlightData[][] {
const flightMap: { [key: string]: FlightData[] } = {};
flightData.forEach(flight => {
const key = `${flight.from}-${flight.to}-${flight.std}`;
if (!flightMap[key])
flightMap[key] = [];
flightMap[key].push(flight);
});
return Object.values(flightMap);
}
function groupByFrequency(flightData: FlightData[]): FlightData {
let data = 'OOOOOOO'; // Initialize with no flights
const atdArray: string[] = [];
const ataArray: string[] = [];
flightData.forEach(flight => {
const dayOfWeek = (new Date(flight.date).getDay() + 6) % 7;
data = data.substring(0, dayOfWeek) + 'X' + data.substring(dayOfWeek + 1);
if (flight.atd)
atdArray.push(flight.atd as string);
if (flight.ata)
ataArray.push(flight.ata as string);
});
return {
id: flightData[0].id,
date: data,
from: flightData[0].from,
to: flightData[0].to,
std: flightData[0].std,
sta: flightData[0].sta,
atd: atdArray,
ata: ataArray
};
};

View File

@@ -0,0 +1,26 @@
function drop_fields(results) {
results.forEach(e => {
delete e.licence;
delete e.place_rank;
delete e.importance;
delete e.boundingbox;
});
return results
}
export function nominatim_get_data(id: string, bb: string[][] | null = null): Promise<any> {
if (!id) return Promise.resolve([])
const url = new URL("https://nominatim.openstreetmap.org/search");
url.searchParams.append('format', 'jsonv2')
url.searchParams.append('q', id)
if (bb) {
url.searchParams.append('viewbox', `${bb[0][0]},${bb[0][1]},${bb[1][0]},${bb[1][1]}`)
url.searchParams.append('bounded', `1`)
}
return fetch(url).then((res) => {
if (!res.ok) throw new Error("Nominatim Error")
return res.json().then(r => drop_fields(r))
})
}

36
src/server/main.ts Normal file
View File

@@ -0,0 +1,36 @@
import fastify from 'fastify'
import fastify_static from '@fastify/static'
import fastify_db from '@fastify/leveldb'
import fastify_view from '@fastify/view';
import pug from 'pug'
import { join as pathJoin } from "path";
import api from "./api"
const server = fastify(); //{ logger: true });
server.register(fastify_static, {
root: pathJoin(__dirname, "../public"),
prefix: "/public/",
});
server.register(
fastify_db as any,
{ name: "db" }
);
server.register(fastify_view, {
engine: { pug: pug },
});
server.register(api, { prefix: "/api" });
server.get("/", (req, reply) => reply.view("/template/home.pug"));
server.get("/:id", (req, reply) => reply.view("/template/journey.pug"));
server.get("/view/:id", (req, reply) => reply.view("/template/view.pug"));
server.get("/short/:id", (req, reply) => reply.view("/template/short.pug"));
server.listen({ port: 8080, host: "0.0.0.0" }, (err, address) => {
if (err) throw err;
console.log("Listening on", address);
});

View File

@@ -69,7 +69,7 @@ main#app
| Select and plan the varying elements of your journey
.aligner.aligner--contentEnd
.input
input#journey_id(v-model="journey_id", placeholder="ID", type="text")
input#journey.id(v-model="journey.id", placeholder="ID", type="text")
button.button.button--primary.button--mobileFull(
v-on:click="start_journey"
) Start the journey

View File

@@ -4,7 +4,5 @@ include module/head.pug
main#app(v-cloak)
include module/nav.pug
include module/journey_sec.pug
include module/journey_step.pug
include module/importexport.pug
include module/journey.pug
include module/foot.pug

View File

@@ -1,14 +1,10 @@
script(src="https://unpkg.com/leaflet")
script(src="https://unpkg.com/leaflet.awesome-markers")
//- script(src="https://unpkg.com/axios")
script(src="https://unpkg.com/lodash")
script(src="https://unpkg.com/sortablejs")
script(src="https://unpkg.com/vue@2")
script(src="https://unpkg.com/vue2-datepicker")
script(src="https://unpkg.com/vue-textarea-autosize")
script(src="https://unpkg.com/vue-multiselect@2")
script(src="https://unpkg.com/vue2-leaflet")
script(src="https://unpkg.com/sortablejs")
script(src="https://unpkg.com/vuedraggable")
script(src="/public/main.js", type="text/javascript", charset="utf-8")
footer.bg-dark

View File

@@ -11,15 +11,7 @@ head
link(rel="stylesheet", href="/public/css/index.css")
link(rel="stylesheet", href="https://unpkg.com/vue2-datepicker/index.css")
link(
rel="stylesheet",
href="https://unpkg.com/vue-multiselect@2/dist/vue-multiselect.min.css"
)
link(rel="stylesheet", href="https://unpkg.com/leaflet/dist/leaflet.css")
link(
rel="stylesheet",
href="https://unpkg.com/leaflet.awesome-markers/dist/leaflet.awesome-markers.css"
)
link(
rel="stylesheet",
href="https://unpkg.com/@fortawesome/fontawesome-free/css/all.min.css"

View File

@@ -0,0 +1,3 @@
.journey
include journey/leg.pug
include journey/impexp.pug

View File

@@ -1,4 +1,4 @@
div
.impexp
.container-medium.section
.aligner
.input.col-sm-4
@@ -10,4 +10,4 @@ div
.col-sm-2
button.button.button--primary.button--mobileFull(
v-on:click="export_data"
) Export
) Export

View File

@@ -0,0 +1,11 @@
include leg/nav.pug
.bg-dark.text-white(v-if="journey && journey.leg_get()")
.container.section
include leg/top.pug
.row(style="aspect-ratio:1.25;")
.map-container(:class="{ 'col-9': query.type, 'col-12': !query.type }" )
include map.pug
.drawer-container(:class="{ 'col-3': query.type, 'col-0': !query.type }")
include leg/drawer.pug
include leg/old_cfg.pug

View File

@@ -0,0 +1,50 @@
.input.text-dark(style="width: 100%")
input#query_input(
type="search"
@input="search_active"
placeholder="Search ... "
style="width:85%;"
)
.spinner(v-if="query.load")
div(style="width:100%"
v-if="['hotel', 'restaurant', 'place','other', 'travel'].indexOf(query.type)>=0")
template(v-for="(item, idx) in query.res" )
.col-12.bg-white.text-dark(
style="display:flex;align-items:center; border-radius:3px;"
:key="'q'+idx"
@mouseover="drawer_hover_item(item)"
@mouseleave="drawer_hover_item()"
@click="drawer_click_item(item)" )
div( v-html="generate_icon(item, 'var(--dark)')")
.col-10()
| {{ item.name }}
.bg-dark.divider(
:key="'qdiv'+idx" style="height:1px" )
.col-12.bg-white.text-dark(
v-if="query.load==false && query.res.length==0"
style="display:flex;align-items:center; border-radius:3px;" )
div( v-html="generate_icon('star', 'var(--dark)')")
.col-10()
| Add custom
.col-12.text-white.text-center(
) {{query.load? `Loading ...` : `Found ${query.res.length} results`}}
div(style="width:100%"
v-else-if="['flight'].indexOf(query.type)>=0")
template(v-for="(item, idx) in query.res" )
.col-12.bg-white.text-dark(
style="display:flex;align-items:center; border-radius:3px;"
:key="'q'+idx"
@mouseover="drawer_hover_item(item)"
@mouseleave="drawer_hover_item()"
@click="drawer_click_item(item)" )
div( v-html="generate_icon('plane', 'var(--dark)')")
.col-10()
| {{ item.from }} => {{item.to}}
.bg-dark.divider(
:key="'qdiv'+idx" style="height:1px" )
div(style="width:100%" v-else)
template()
.col-12.bg-white.text-dark(
style="display:flex;align-items:center; border-radius:3px;")
| Unsuppored Query type {{query.type}}

View File

@@ -0,0 +1,23 @@
.scroll-handler(
@mouseleave="nav_mouseleave"
@mousemove="nav_mousemove")
draggable.scroll-content.list-group.bg-dark(
tag="div",
:list="journey.data.main",
handle=".handle"
)
.list-group-item.handle(
v-for="(element, idx) in journey.data.main",
:key="idx",
@click="journey.leg_sel(idx)",
:class="journey.sel_leg == idx ? 'bg-primary' : 'bg-white'"
)
.text {{ element.title }}
i.fa.fa-times.close.fright(
style="top: 2px; right: 2px; position: absolute",
@click="journey.rm_leg(idx)"
)
.list-group-item.bg-white(@click="journey.add_leg()")
.text Add Leg
i.fa.fa-plus.add(style="top: 12px; right: 5px; position: absolute")

View File

@@ -0,0 +1,12 @@
div
div
.row.text-center
div
label Notes
.input.text-dark(style="width: 100%")
textarea-autosize.text-small(
v-model="journey.leg_get().notes",
placeholder="Notes",
:min-height="30",
:max-height="350"
)

View File

@@ -0,0 +1,29 @@
.row.text-center
.col-sm-2
.input
input(v-model="journey.leg_get().title")
.col-sm-2
.input
input(
placeholder="Day title",
v-model="journey.leg_get().day_title[journey.sel_day]"
)
.col-sm-4
.input
//- label Date Range ({{ journey.leg_len() }})
date-picker(
:lang="lang",
v-model="journey.leg_get().date_range",
range="",
format="ddd D MMM",
placeholder="Date Range",
v-on:change="journey.date_update(journey.sel_leg)"
)
.col-sm-2
.right.col-sm-2
.input
input(
disabled="",
:value="journey.date_sel() + ' (' + journey.sel_day + ')'"
)

View File

@@ -0,0 +1,20 @@
l-map(
:zoom.sync="journey.leg_get().map.zoom",
:center.sync="journey.leg_get().map.center",
style="height:100%"
no-blocking-animations=true
)
l-tile-layer(
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution="© <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors"
)
l-control-scale(position="bottomright", :imperial="false", :metric="true")
include map/override.pug
include map/hotel.pug
include map/activities.pug
include map/restaurants.pug
include map/travel.pug
include map/right_menu.pug

View File

@@ -0,0 +1,6 @@
include mixin-marker.pug
div(
v-for="(place, index) in journey.leg_get().places.activities",
:key="'activities'+index",
)
+map_marker("activities", "var(--lightdark)", "var(--light)", "var(--lightlight)")

View File

@@ -0,0 +1,7 @@
include mixin-marker.pug
div(
v-if="journey.leg_get().hotel",
v-for="(place, index) in [journey.leg_get().hotel]",
:key="'hotel'+index",
)
+map_marker("hotel", "var(--darkdark)", "var(--darkdark)", "var(--darkdark)")

View File

@@ -0,0 +1,41 @@
mixin map_marker(place, color_sel_c, color_sel_o, color_else)
l-marker(
:lat-lng="place.latlon"
)
l-icon(
v-if="(place.step == journey.sel_day)"
v-html="generate_marker(place, \""+color_sel_c+"\")"
)
l-icon(
v-else-if="(place.step >=0)"
v-html="generate_marker(place, \""+color_sel_o+"\")"
)
l-icon(
v-else
v-html="generate_marker(place, \""+color_else+"\")"
)
l-popup()
h1.row.text-medium.text-center {{ place.sname }}
span.row.text-small.text-gray {{ place.display_name }}
span(v-if="edit_active")
.row.input(style="margin-bottom:0")
textarea-autosize.col-12.col-sm-12.text-small(
placeholder="Notes",
v-model="place.notes",
:min-height="30",
:max-height="350"
)
a.leaflet-popup-close-button.text-gray(
style="right: 4px; visibility: visible",
href="#rm",
v-on:click.prevent="place_delete(\""+place+"\",index)"
v-html="generate_icon('trash', 'NA')"
)
a.leaflet-popup-close-button.text-gray(
style="right: 20px; visibility: visible",
href="#toggle_day",
v-on:click.prevent="place.step = ((place.step==journey.sel_day)?-1:journey.sel_day)"
v-html="generate_icon(((place.step==journey.sel_day)?'calendar-xmark':'calendar-plus'), 'NA')"
)
span.row.text-small.text-dark(v-else) {{ place.notes }}

View File

@@ -0,0 +1,5 @@
l-marker(
v-if="map_override.active",
:lat-lng="map_override.center"
)
l-icon(v-html="generate_marker('plus', 'darkgreen')")

View File

@@ -0,0 +1,18 @@
l-marker(
v-for="(place, index) in journey.leg_get().places.restaurants",
:key="'restaurants'+index"
:lat-lng="place.latlon"
)
l-icon(v-html="generate_marker(place, 'cadetblue')")
l-popup
h1.row.text-medium.text-center {{ place.sname }}
span.row.text-small.text-gray {{ place.display_name }}
span(v-if="edit_active")
.row.input(style="margin-bottom:0")
textarea-autosize.col-12.col-sm-12.text-small(
placeholder="Notes"
v-model="place.notes"
:min-height="30"
:max-height="350"
)
span.row.text-small.text-dark(v-else) {{ place.notes }}

View File

@@ -0,0 +1,6 @@
.map-menu
.map-menu-item( v-if="query.type" @click="query.type=''" v-html="generate_icon('close')")
.map-menu-item(v-if="!query.type" @click="query.type='hotel'" v-html="generate_icon('bed')")
.map-menu-item(v-if="!query.type" @click="query.type='restaurant'" v-html="generate_icon('utensils')")
.map-menu-item(v-if="!query.type" @click="query.type='place'" v-html="generate_icon('star')")
.map-menu-item(v-if="!query.type" @click="query.type='flight'" v-html="generate_icon('plane')")

View File

@@ -0,0 +1,9 @@
div(v-for= "travel in journey.leg_get().travel")
l-polyline(:lat-lngs="travel.path" :color="travel.color || 'gray'")
l-marker(
v-for="(place, index) in travel.path"
:key="'plane'+index"
:lat-lng="place"
)
l-icon(v-html="generate_icon('plane', travel.color || 'gray', generate_rotation(index,travel.path), 'travel-path-icon')"
)

View File

@@ -1,20 +0,0 @@
draggable.list-group.bg-dark(
tag="div",
:list="journey_data.main",
handle=".handle"
)
.list-group-item.handle(
v-for="(element, idx) in journey_data.main",
:key="idx",
@click="sel_section(idx)",
:class="journey_step_data.section == idx ? 'bg-primary' : 'bg-white'"
)
.text {{ element.title }}
i.fa.fa-times.close.fright(
style="top: 2px; right: 2px; position: absolute",
@click="rm_section(idx)"
)
.list-group-item.bg-white(@click="add_section()")
.text Add Section
i.fa.fa-plus.add(style="top: 12px; right: 5px; position: absolute")

View File

@@ -1,102 +0,0 @@
div(v-for="(e, idx) in journey_data.main", :key="idx")
.bg-dark.text-white(v-if="journey_step_data.section == idx")
.container.section
.row.text-center
.input.col-sm-2
input(v-model="journey_data.main[idx].title")
.input.col-sm-2
input(
placeholder="Day title",
v-model="journey_data.main[idx].step_title[journey_step_data.day]"
)
.col-sm-3
.right.input.col-sm-2
input(
disabled="",
:value="active_date() + ' (' + journey_step_data.day + ')'"
)
.row
.col-9.col-ssm-12
include map.pug
.col-3.col-ssm-12
.row.text-center
div
label Date Range ({{ step_len(idx) }})
.input.text-dark
date-picker(
:lang="lang",
v-model="journey_data.main[idx].dateRange",
range="",
format="ddd D MMM",
placeholder="Date Range",
v-on:change="update_date(idx)"
)
.row.text-center
div
label Hotel
multiselect#ajax(
v-model="journey_data.main[idx].hotel",
label="sname",
track-by="place_id",
placeholder="Type to search",
open-direction="bottom",
:options="query.nominatim",
:searchable="true",
:loading="querying.hotel",
:internal-search="false",
:clear-on-select="false",
:options-limit="50",
:limit="1",
:max-height="600",
@search-change="debounceSearch.hotel"
)
.row.text-center
div
label Restoration
multiselect#ajax(
v-model="journey_data.main[idx].places.restaurants",
label="sname",
track-by="place_id",
placeholder="Type to search",
open-direction="bottom",
:multiple="true",
:options="query.nominatim",
:searchable="true",
:loading="querying.food",
:internal-search="false",
:clear-on-select="false",
:options-limit="50",
:limit="10",
:max-height="600",
@search-change="debounceSearch.restaurants"
)
.row.text-center
div
label Activities
multiselect#ajax(
v-model="journey_data.main[idx].places.activities",
label="sname",
track-by="place_id",
placeholder="Type to search",
open-direction="bottom",
:multiple="true",
:options="query.nominatim",
:searchable="true",
:loading="querying.place",
:internal-search="false",
:clear-on-select="false",
:options-limit="50",
:limit="10",
:max-height="600",
@search-change="debounceSearch.places"
)
.row.text-center
div
label Notes
.input.text-dark(style="width: 100%")
textarea-autosize.text-small(
v-model="journey_data.main[idx].notes",
placeholder="Notes",
:min-height="30",
:max-height="350"
)

View File

@@ -1,84 +0,0 @@
l-map(
:zoom.sync="journey_data.main[idx].map.zoom",
:center.sync="journey_data.main[idx].map.center",
style="padding-top: 100%"
)
l-tile-layer(
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution="© <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors"
)
l-control-scale(position="topright", :imperial="false", :metric="true")
l-marker(
v-if="journey_data.main[idx].hotel",
:lat-lng="journey_data.main[idx].hotel.latlon"
)
l-icon
div(v-html="generate_icon(journey_data.main[idx].hotel, 'darkblue')")
l-popup
h1.row.text-medium.text-center {{ journey_data.main[idx].hotel.sname }}
span.row.text-small.text-gray {{ journey_data.main[idx].hotel.display_name }}
span(v-if="journey_edit")
.row.input
textarea-autosize.col-12.col-sm-12.text-small(
placeholder="Notes",
v-model="journey_data.main[idx].hotel.notes",
:min-height="30",
:max-height="350"
)
span.row.text-small.text-white(v-else) {{ journey_data.main[idx].hotel.notes }}
l-marker(
v-for="place in journey_data.main[idx].places.activities",
:lat-lng="place.latlon"
)
l-icon
div(
v-if="place.step == journey_step_data.day",
v-html="generate_icon(place)"
)
div(
v-else-if="place.step == -1 || place.step == undefined",
v-html="generate_icon(place, 'gray')"
)
div(v-else-if="journey_edit", v-html="generate_icon(place, 'lightgray')")
div(v-else)
l-popup
h1.row.text-medium.text-center {{ place.sname }}
span.row.text-small.text-gray {{ place.display_name }}
span(v-if="journey_edit")
.row.input
textarea-autosize.col-12.col-sm-12.text-small(
placeholder="Notes",
v-model="place.notes",
:min-height="30",
:max-height="350"
)
a.leaflet-popup-close-button.text-gray(
style="right: 0px; visibility: visible",
href="#rm",
v-on:click.prevent="place.step = -1"
) -
a.leaflet-popup-close-button.text-gray(
style="right: 16px; visibility: visible",
href="#ad",
v-on:click.prevent="place.step = journey_step_data.day"
) +
span.row.text-small.text-dark(v-else) {{ place.notes }}
l-marker(
v-for="place in journey_data.main[idx].places.restaurants",
:lat-lng.sync="place.latlon"
)
l-icon
div(v-html="generate_icon(place, 'cadetblue')")
l-popup
h1.row.text-medium.text-center {{ place.sname }}
span.row.text-small.text-gray {{ place.display_name }}
span(v-if="journey_edit")
.row.input
textarea-autosize.col-12.col-sm-12.text-small(
placeholder="Notes",
v-model="place.notes",
:min-height="30",
:max-height="350"
)
span.row.text-small.text-dark(v-else) {{ place.notes }}

View File

@@ -8,31 +8,31 @@ header.header
)
span.hide-small OTM
.input.input-invis.row
input.col-6.small(v-model="journey_data.name", type="text")
input.col-6.small(v-model="journey.data.name", type="text")
input.col-6.small(
disabled,
type="text",
:placeholder="total_date() + ' (' + total_days() + ')'"
:placeholder="journey.date_tot() + ' (' + journey.tot_len() + ')'"
)
.row.header-nav.text-big(style="margin-bottom: 0")
.col-sm-2
a(:href="'/short/' + journey_id")
a(:href="'/short/' + journey.id")
i.fas.fa-file-contract
.col-sm-2
a(:href="'/view/' + journey_id")
a(:href="'/view/' + journey.id")
i.fas.fa-camera
.col-sm-2
a(href="#main", v-on:click.prevent="first_step")
i.fas.fa-tools
.col-sm-1.text-small
a(href="#prevprev", v-on:click.prevent="prevprev_step")
a(href="#prevprev", v-on:click.prevent="journey.leg_prev()")
i.fas.fa-angle-double-left
.col-sm-1
a(href="#prev", v-on:click.prevent="prev_step")
a(href="#prev", v-on:click.prevent="journey.day_prev()")
i.fas.fa-angle-left
.col-sm-1
a(href="#next", v-on:click.prevent="next_step")
a(href="#next", v-on:click.prevent="journey.day_next()")
i.fas.fa-angle-right
.col-sm-1.text-small
a(href="#nextnext", v-on:click.prevent="nextnext_step")
a(href="#nextnext", v-on:click.prevent="journey.leg_next()")
i.fas.fa-angle-double-right

View File

@@ -8,11 +8,11 @@ header.header
)
span.hide-small HOTM
.input.input-invis
input.small(:value="journey_data.name", type="text", disabled="")
input.small(:value="journey.data.name", type="text", disabled="")
.row.header-nav.text-big(style="margin-bottom: 0")
.col-sm-3
a(:href="'/short/' + journey_id")
a(:href="'/short/' + journey.id")
i.fas.fa-file-contract
.col-sm-3
a(:href="'/view/' + journey_id")
a(:href="'/view/' + journey.id")
i.fas.fa-camera

View File

@@ -6,7 +6,7 @@
input(
disabled="",
placeholder="No Dates",
:value="item.dateRange ? format_date(item.dateRange[0]) + ' - ' + format_date(item.dateRange[1]) : ''"
:value="item.date_range ? format_date(item.date_range[0]) + ' - ' + format_date(item.date_range[1]) : ''"
)
.input.col-sm-2
input(disabled="", placeholder="No Hotel", :value="item.hotel.sname")

View File

@@ -0,0 +1,19 @@
div(v-for="(e, idx) in journey.data.main", :key="idx")
.bg-dark.text-white(v-if="journey.sel_leg == idx")
.container.section
.aligner.text-center.text-white.text-huge(style="margin-bottom: 5px")
.aligner--itemTop.fleft
a(href="#prev", v-on:click.prevent="journey.day_prev()")
i.fas.fa-angle-left
span.container
span.small {{ journey.data.main[idx].title }} {{ journey.sel_day }}
.text-big.text-gray {{ journey.data.main[idx].day_title[journey.sel_day] }}
.aligner--itemEnd.fright
a(href="#next", v-on:click.prevent="journey.day_next()")
i.fas.fa-angle-right
.row
.col-12.col-sm-12
include map.pug
.row
.col-12.col-sm-12
.container

View File

@@ -1,19 +0,0 @@
div(v-for="(e, idx) in journey_data.main", :key="idx")
.bg-dark.text-white(v-if="journey_step_data.section == idx")
.container.section
.aligner.text-center.text-white.text-huge(style="margin-bottom: 5px")
.aligner--itemTop.fleft
a(href="#prev", v-on:click.prevent="prev_step")
i.fas.fa-angle-left
span.container
span.small {{ journey_data.main[idx].title }} {{ journey_step_data.day }}
.text-big.text-gray {{ journey_data.main[idx].step_title[journey_step_data.day] }}
.aligner--itemEnd.fright
a(href="#next", v-on:click.prevent="next_step")
i.fas.fa-angle-right
.row
.col-12.col-sm-12
include map.pug
.row
.col-12.col-sm-12
.container

View File

@@ -1,10 +1,10 @@
doctype html
include module/head.pug
main#app(v-cloak)
include module/nav_pub.pug
include module/view/nav.pug
div(
v-for="(item, idx) in journey_data.main",
v-for="(item, idx) in journey.data.main",
:class="idx % 2 === 0 ? 'bg-dark text-white' : ''"
)
include module/short_sec.pug
include module/view/short_leg.pug
include module/foot.pug

View File

@@ -1,6 +1,6 @@
doctype html
include module/head.pug
main#app(v-cloak)
div(v-if="journey_data.main[journey_step_data.section] != undefined")
include module/view_step.pug
div(v-if="journey.data.main[journey.sel_leg] != undefined")
include module/view/view_day.pug
include module/foot.pug

17
tsconfig-client.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "esnext",
"typeRoots": [
"./node_modules/@types",
"./types/ext"
],
"lib": [
"esnext",
"DOM"
],
"noEmit": true, // Disable emitting output (use esbuild to handle this)
"skipLibCheck": true, // Skip type checking of all declaration files (*.d.ts)
"strict": false, // Disable strict type checks if needed
"moduleResolution": "node",
}
}

View File

@@ -1,12 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"typeRoots": ["./node_modules/@types", "./types/ext"],
"lib": ["esnext", "DOM"],
"noEmit": true, // Disable emitting output (use esbuild to handle this)
"skipLibCheck": true, // Skip type checking of all declaration files (*.d.ts)
"strict": false, // Disable strict type checks if needed
"moduleResolution": "node",
}
}

480
yarn.lock
View File

@@ -5,6 +5,19 @@ __metadata:
version: 8
cacheKey: 10c0
"@asamuzakjp/css-color@npm:^2.8.2":
version: 2.8.3
resolution: "@asamuzakjp/css-color@npm:2.8.3"
dependencies:
"@csstools/css-calc": "npm:^2.1.1"
"@csstools/css-color-parser": "npm:^3.0.7"
"@csstools/css-parser-algorithms": "npm:^3.0.4"
"@csstools/css-tokenizer": "npm:^3.0.3"
lru-cache: "npm:^10.4.3"
checksum: 10c0/e108c92ee5de6d8510c9aaca8375c0aeab730dc9b6d4bd287aea2a0379cfbaa09f0814dcacb3e2ddc5c79d7deedf3f82ec8d1ce0effd4a8fac8415b1fe553798
languageName: node
linkType: hard
"@babel/helper-string-parser@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-string-parser@npm:7.25.9"
@@ -19,7 +32,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/parser@npm:^7.6.0, @babel/parser@npm:^7.9.6":
"@babel/parser@npm:^7.23.5, @babel/parser@npm:^7.6.0, @babel/parser@npm:^7.9.6":
version: 7.26.9
resolution: "@babel/parser@npm:7.26.9"
dependencies:
@@ -40,6 +53,52 @@ __metadata:
languageName: node
linkType: hard
"@csstools/color-helpers@npm:^5.0.2":
version: 5.0.2
resolution: "@csstools/color-helpers@npm:5.0.2"
checksum: 10c0/bebaddb28b9eb58b0449edd5d0c0318fa88f3cb079602ee27e88c9118070d666dcc4e09a5aa936aba2fde6ba419922ade07b7b506af97dd7051abd08dfb2959b
languageName: node
linkType: hard
"@csstools/css-calc@npm:^2.1.1, @csstools/css-calc@npm:^2.1.2":
version: 2.1.2
resolution: "@csstools/css-calc@npm:2.1.2"
peerDependencies:
"@csstools/css-parser-algorithms": ^3.0.4
"@csstools/css-tokenizer": ^3.0.3
checksum: 10c0/34ced30553968ef5d5f9e00e3b90b48c47480cf130e282e99d57ec9b09f803aab8bc06325683e72a1518b5e7180a3da8b533f1b462062757c21989a53b482e1a
languageName: node
linkType: hard
"@csstools/css-color-parser@npm:^3.0.7":
version: 3.0.8
resolution: "@csstools/css-color-parser@npm:3.0.8"
dependencies:
"@csstools/color-helpers": "npm:^5.0.2"
"@csstools/css-calc": "npm:^2.1.2"
peerDependencies:
"@csstools/css-parser-algorithms": ^3.0.4
"@csstools/css-tokenizer": ^3.0.3
checksum: 10c0/90722c5a62ca94e9d578ddf59be604a76400b932bd3d4bd23cb1ae9b7ace8fcf83c06995d2b31f96f4afef24a7cefba79beb11ed7ee4999d7ecfec3869368359
languageName: node
linkType: hard
"@csstools/css-parser-algorithms@npm:^3.0.4":
version: 3.0.4
resolution: "@csstools/css-parser-algorithms@npm:3.0.4"
peerDependencies:
"@csstools/css-tokenizer": ^3.0.3
checksum: 10c0/d411f07765e14eede17bccc6bd4f90ff303694df09aabfede3fd104b2dfacfd4fe3697cd25ddad14684c850328f3f9420ebfa9f78380892492974db24ae47dbd
languageName: node
linkType: hard
"@csstools/css-tokenizer@npm:^3.0.3":
version: 3.0.3
resolution: "@csstools/css-tokenizer@npm:3.0.3"
checksum: 10c0/c31bf410e1244b942e71798e37c54639d040cb59e0121b21712b40015fced2b0fb1ffe588434c5f8923c9cd0017cfc1c1c8f3921abc94c96edf471aac2eba5e5
languageName: node
linkType: hard
"@esbuild/aix-ppc64@npm:0.25.0":
version: 0.25.0
resolution: "@esbuild/aix-ppc64@npm:0.25.0"
@@ -403,6 +462,21 @@ __metadata:
languageName: node
linkType: hard
"@vue/compiler-sfc@npm:2.7.16":
version: 2.7.16
resolution: "@vue/compiler-sfc@npm:2.7.16"
dependencies:
"@babel/parser": "npm:^7.23.5"
postcss: "npm:^8.4.14"
prettier: "npm:^1.18.2 || ^2.0.0"
source-map: "npm:^0.6.1"
dependenciesMeta:
prettier:
optional: true
checksum: 10c0/eaeeef054c939e6cd7591199e2b998ae33d0afd65dc1b5675b54361f0c657c08ae82945791a1a8ca76762e1c1f8e69a00595daf280b854cbc3370ed5c5a34bcd
languageName: node
linkType: hard
"abbrev@npm:^3.0.0":
version: 3.0.0
resolution: "abbrev@npm:3.0.0"
@@ -551,17 +625,6 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.7.9":
version: 1.7.9
resolution: "axios@npm:1.7.9"
dependencies:
follow-redirects: "npm:^1.15.6"
form-data: "npm:^4.0.0"
proxy-from-env: "npm:^1.1.0"
checksum: 10c0/b7a41e24b59fee5f0f26c1fc844b45b17442832eb3a0fb42dd4f1430eb4abc571fe168e67913e8a1d91c993232bd1d1ab03e20e4d1fee8c6147649b576fc1b0b
languageName: node
linkType: hard
"babel-walk@npm:3.0.0-canary-5":
version: 3.0.0-canary-5
resolution: "babel-walk@npm:3.0.0-canary-5"
@@ -770,6 +833,13 @@ __metadata:
languageName: node
linkType: hard
"core-js@npm:^2.6.5":
version: 2.6.12
resolution: "core-js@npm:2.6.12"
checksum: 10c0/00128efe427789120a06b819adc94cc72b96955acb331cb71d09287baf9bd37bebd191d91f1ee4939c893a050307ead4faea08876f09115112612b6a05684b63
languageName: node
linkType: hard
"cross-spawn@npm:^7.0.0":
version: 7.0.6
resolution: "cross-spawn@npm:7.0.6"
@@ -781,6 +851,33 @@ __metadata:
languageName: node
linkType: hard
"cssstyle@npm:^4.2.1":
version: 4.2.1
resolution: "cssstyle@npm:4.2.1"
dependencies:
"@asamuzakjp/css-color": "npm:^2.8.2"
rrweb-cssom: "npm:^0.8.0"
checksum: 10c0/02ba8c47c0caaab57acadacb3eb6c0f5f009000f55d61f6563670e07d389b26edefeed497e6c1847fcd2e6bbe0b6974c2d4291f97fa0c6ec6add13a7fa926d84
languageName: node
linkType: hard
"csstype@npm:^3.1.0":
version: 3.1.3
resolution: "csstype@npm:3.1.3"
checksum: 10c0/80c089d6f7e0c5b2bd83cf0539ab41474198579584fa10d86d0cafe0642202343cbc119e076a0b1aece191989477081415d66c9fefbf3c957fc2fc4b7009f248
languageName: node
linkType: hard
"data-urls@npm:^5.0.0":
version: 5.0.0
resolution: "data-urls@npm:5.0.0"
dependencies:
whatwg-mimetype: "npm:^4.0.0"
whatwg-url: "npm:^14.0.0"
checksum: 10c0/1b894d7d41c861f3a4ed2ae9b1c3f0909d4575ada02e36d3d3bc584bdd84278e20709070c79c3b3bff7ac98598cb191eb3e86a89a79ea4ee1ef360e1694f92ad
languageName: node
linkType: hard
"debug@npm:4, debug@npm:^4, debug@npm:^4.3.4":
version: 4.4.0
resolution: "debug@npm:4.4.0"
@@ -793,6 +890,13 @@ __metadata:
languageName: node
linkType: hard
"decimal.js@npm:^10.4.3":
version: 10.5.0
resolution: "decimal.js@npm:10.5.0"
checksum: 10c0/785c35279df32762143914668df35948920b6c1c259b933e0519a69b7003fc0a5ed2a766b1e1dda02574450c566b21738a45f15e274b47c2ac02072c0d1f3ac3
languageName: node
linkType: hard
"deferred-leveldown@npm:^7.0.0":
version: 7.0.0
resolution: "deferred-leveldown@npm:7.0.0"
@@ -884,6 +988,13 @@ __metadata:
languageName: node
linkType: hard
"entities@npm:^4.5.0":
version: 4.5.0
resolution: "entities@npm:4.5.0"
checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250
languageName: node
linkType: hard
"env-paths@npm:^2.2.0":
version: 2.2.1
resolution: "env-paths@npm:2.2.1"
@@ -1091,7 +1202,7 @@ __metadata:
languageName: node
linkType: hard
"fastify@npm:^5.0.0":
"fastify@npm:^5.2.1":
version: 5.2.1
resolution: "fastify@npm:5.2.1"
dependencies:
@@ -1143,16 +1254,6 @@ __metadata:
languageName: node
linkType: hard
"follow-redirects@npm:^1.15.6":
version: 1.15.9
resolution: "follow-redirects@npm:1.15.9"
peerDependenciesMeta:
debug:
optional: true
checksum: 10c0/5829165bd112c3c0e82be6c15b1a58fa9dcfaede3b3c54697a82fe4a62dd5ae5e8222956b448d2f98e331525f05d00404aba7d696de9e761ef6e42fdc780244f
languageName: node
linkType: hard
"foreground-child@npm:^3.1.0":
version: 3.3.0
resolution: "foreground-child@npm:3.3.0"
@@ -1163,7 +1264,7 @@ __metadata:
languageName: node
linkType: hard
"form-data@npm:^4.0.0":
"form-data@npm:^4.0.1":
version: 4.0.2
resolution: "form-data@npm:4.0.2"
dependencies:
@@ -1325,6 +1426,15 @@ __metadata:
languageName: node
linkType: hard
"html-encoding-sniffer@npm:^4.0.0":
version: 4.0.0
resolution: "html-encoding-sniffer@npm:4.0.0"
dependencies:
whatwg-encoding: "npm:^3.1.1"
checksum: 10c0/523398055dc61ac9b34718a719cb4aa691e4166f29187e211e1607de63dc25ac7af52ca7c9aead0c4b3c0415ffecb17326396e1202e2e86ff4bca4c0ee4c6140
languageName: node
linkType: hard
"http-cache-semantics@npm:^4.1.1":
version: 4.1.1
resolution: "http-cache-semantics@npm:4.1.1"
@@ -1345,7 +1455,7 @@ __metadata:
languageName: node
linkType: hard
"http-proxy-agent@npm:^7.0.0":
"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.2":
version: 7.0.2
resolution: "http-proxy-agent@npm:7.0.2"
dependencies:
@@ -1355,7 +1465,7 @@ __metadata:
languageName: node
linkType: hard
"https-proxy-agent@npm:^7.0.1":
"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.6":
version: 7.0.6
resolution: "https-proxy-agent@npm:7.0.6"
dependencies:
@@ -1365,7 +1475,7 @@ __metadata:
languageName: node
linkType: hard
"iconv-lite@npm:^0.6.2":
"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
dependencies:
@@ -1484,6 +1594,13 @@ __metadata:
languageName: node
linkType: hard
"is-potential-custom-element-name@npm:^1.0.1":
version: 1.0.1
resolution: "is-potential-custom-element-name@npm:1.0.1"
checksum: 10c0/b73e2f22bc863b0939941d369486d308b43d7aef1f9439705e3582bfccaa4516406865e32c968a35f97a99396dac84e2624e67b0a16b0a15086a785e16ce7db9
languageName: node
linkType: hard
"is-promise@npm:^2.0.0":
version: 2.2.2
resolution: "is-promise@npm:2.2.2"
@@ -1553,6 +1670,40 @@ __metadata:
languageName: node
linkType: hard
"jsdom@npm:^26.0.0":
version: 26.0.0
resolution: "jsdom@npm:26.0.0"
dependencies:
cssstyle: "npm:^4.2.1"
data-urls: "npm:^5.0.0"
decimal.js: "npm:^10.4.3"
form-data: "npm:^4.0.1"
html-encoding-sniffer: "npm:^4.0.0"
http-proxy-agent: "npm:^7.0.2"
https-proxy-agent: "npm:^7.0.6"
is-potential-custom-element-name: "npm:^1.0.1"
nwsapi: "npm:^2.2.16"
parse5: "npm:^7.2.1"
rrweb-cssom: "npm:^0.8.0"
saxes: "npm:^6.0.0"
symbol-tree: "npm:^3.2.4"
tough-cookie: "npm:^5.0.0"
w3c-xmlserializer: "npm:^5.0.0"
webidl-conversions: "npm:^7.0.0"
whatwg-encoding: "npm:^3.1.1"
whatwg-mimetype: "npm:^4.0.0"
whatwg-url: "npm:^14.1.0"
ws: "npm:^8.18.0"
xml-name-validator: "npm:^5.0.0"
peerDependencies:
canvas: ^3.0.0
peerDependenciesMeta:
canvas:
optional: true
checksum: 10c0/e48725ba4027edcfc9bca5799eaec72c6561ecffe3675a8ff87fe9c3541ca4ff9f82b4eff5b3d9c527302da0d859b2f60e9364347a5d42b77f5c76c436c569dc
languageName: node
linkType: hard
"json-schema-ref-resolver@npm:^2.0.0":
version: 2.0.1
resolution: "json-schema-ref-resolver@npm:2.0.1"
@@ -1579,6 +1730,13 @@ __metadata:
languageName: node
linkType: hard
"leaflet@npm:^1.9.4":
version: 1.9.4
resolution: "leaflet@npm:1.9.4"
checksum: 10c0/f639441dbb7eb9ae3fcd29ffd7d3508f6c6106892441634b0232fafb9ffb1588b05a8244ec7085de2c98b5ed703894df246898477836cfd0ce5b96d4717b5ca1
languageName: node
linkType: hard
"level-codec@npm:^10.0.0":
version: 10.0.0
resolution: "level-codec@npm:10.0.0"
@@ -1658,7 +1816,7 @@ __metadata:
languageName: node
linkType: hard
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0":
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0, lru-cache@npm:^10.4.3":
version: 10.4.3
resolution: "lru-cache@npm:10.4.3"
checksum: 10c0/ebd04fbca961e6c1d6c0af3799adcc966a1babe798f685bb84e6599266599cd95d94630b10262f5424539bc4640107e8a33aa28585374abf561d30d16f4b39fb
@@ -1843,6 +2001,15 @@ __metadata:
languageName: node
linkType: hard
"nanoid@npm:^3.3.8":
version: 3.3.8
resolution: "nanoid@npm:3.3.8"
bin:
nanoid: bin/nanoid.cjs
checksum: 10c0/4b1bb29f6cfebf3be3bc4ad1f1296fb0a10a3043a79f34fbffe75d1621b4318319211cd420549459018ea3592f0d2f159247a6f874911d6d26eaaadda2478120
languageName: node
linkType: hard
"napi-macros@npm:~2.0.0":
version: 2.0.0
resolution: "napi-macros@npm:2.0.0"
@@ -1926,6 +2093,13 @@ __metadata:
languageName: node
linkType: hard
"nwsapi@npm:^2.2.16":
version: 2.2.16
resolution: "nwsapi@npm:2.2.16"
checksum: 10c0/0aa0637f4d51043d0183d994e08336bae996b03b42984381bf09ebdf3ff4909c018eda6b2a8aba0a08f3ea8303db8a0dad0608b38dc0bff15fd87017286ae21a
languageName: node
linkType: hard
"object-assign@npm:^4.1.1":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
@@ -1954,6 +2128,15 @@ __metadata:
languageName: node
linkType: hard
"parse5@npm:^7.2.1":
version: 7.2.1
resolution: "parse5@npm:7.2.1"
dependencies:
entities: "npm:^4.5.0"
checksum: 10c0/829d37a0c709215a887e410a7118d754f8e1afd7edb529db95bc7bbf8045fb0266a7b67801331d8e8d9d073ea75793624ec27ce9ff3b96862c3b9008f4d68e80
languageName: node
linkType: hard
"path-key@npm:^3.1.0":
version: 3.1.1
resolution: "path-key@npm:3.1.1"
@@ -1988,6 +2171,13 @@ __metadata:
languageName: node
linkType: hard
"picocolors@npm:^1.1.1":
version: 1.1.1
resolution: "picocolors@npm:1.1.1"
checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58
languageName: node
linkType: hard
"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1":
version: 2.3.1
resolution: "picomatch@npm:2.3.1"
@@ -2032,10 +2222,23 @@ __metadata:
languageName: node
linkType: hard
"pretier@npm:^0.0.1":
version: 0.0.1
resolution: "pretier@npm:0.0.1"
checksum: 10c0/206f5b353c32a9ad0e38243ad2caf7c8859ef43c975514e1c725088e4925d8b1480f73b96ebfd5ceaa1a563a77a3b8c893db7bb3642585bfa0c635f7ee149885
"postcss@npm:^8.4.14":
version: 8.5.3
resolution: "postcss@npm:8.5.3"
dependencies:
nanoid: "npm:^3.3.8"
picocolors: "npm:^1.1.1"
source-map-js: "npm:^1.2.1"
checksum: 10c0/b75510d7b28c3ab728c8733dd01538314a18c52af426f199a3c9177e63eb08602a3938bfb66b62dc01350b9aed62087eabbf229af97a1659eb8d3513cec823b3
languageName: node
linkType: hard
"prettier@npm:^1.18.2 || ^2.0.0":
version: 2.8.8
resolution: "prettier@npm:2.8.8"
bin:
prettier: bin-prettier.js
checksum: 10c0/463ea8f9a0946cd5b828d8cf27bd8b567345cf02f56562d5ecde198b91f47a76b7ac9eae0facd247ace70e927143af6135e8cf411986b8cb8478784a4d6d724a
languageName: node
linkType: hard
@@ -2081,13 +2284,6 @@ __metadata:
languageName: node
linkType: hard
"proxy-from-env@npm:^1.1.0":
version: 1.1.0
resolution: "proxy-from-env@npm:1.1.0"
checksum: 10c0/fe7dd8b1bdbbbea18d1459107729c3e4a2243ca870d26d34c2c1bcd3e4425b7bcc5112362df2d93cc7fb9746f6142b5e272fd1cc5c86ddf8580175186f6ad42b
languageName: node
linkType: hard
"pstree.remy@npm:^1.1.8":
version: 1.1.8
resolution: "pstree.remy@npm:1.1.8"
@@ -2222,6 +2418,13 @@ __metadata:
languageName: node
linkType: hard
"punycode@npm:^2.3.1":
version: 2.3.1
resolution: "punycode@npm:2.3.1"
checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9
languageName: node
linkType: hard
"queue-microtask@npm:^1.2.3":
version: 1.2.3
resolution: "queue-microtask@npm:1.2.3"
@@ -2335,6 +2538,13 @@ __metadata:
languageName: node
linkType: hard
"rrweb-cssom@npm:^0.8.0":
version: 0.8.0
resolution: "rrweb-cssom@npm:0.8.0"
checksum: 10c0/56f2bfd56733adb92c0b56e274c43f864b8dd48784d6fe946ef5ff8d438234015e59ad837fc2ad54714b6421384141c1add4eb569e72054e350d1f8a50b8ac7b
languageName: node
linkType: hard
"safe-buffer@npm:5.2.1, safe-buffer@npm:~5.2.0":
version: 5.2.1
resolution: "safe-buffer@npm:5.2.1"
@@ -2365,6 +2575,15 @@ __metadata:
languageName: node
linkType: hard
"saxes@npm:^6.0.0":
version: 6.0.0
resolution: "saxes@npm:6.0.0"
dependencies:
xmlchars: "npm:^2.2.0"
checksum: 10c0/3847b839f060ef3476eb8623d099aa502ad658f5c40fd60c105ebce86d244389b0d76fcae30f4d0c728d7705ceb2f7e9b34bb54717b6a7dbedaf5dad2d9a4b74
languageName: node
linkType: hard
"secure-json-parse@npm:^3.0.1":
version: 3.0.2
resolution: "secure-json-parse@npm:3.0.2"
@@ -2464,6 +2683,20 @@ __metadata:
languageName: node
linkType: hard
"source-map-js@npm:^1.2.1":
version: 1.2.1
resolution: "source-map-js@npm:1.2.1"
checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf
languageName: node
linkType: hard
"source-map@npm:^0.6.1":
version: 0.6.1
resolution: "source-map@npm:0.6.1"
checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011
languageName: node
linkType: hard
"split2@npm:^4.0.0":
version: 4.2.0
resolution: "split2@npm:4.2.0"
@@ -2559,6 +2792,13 @@ __metadata:
languageName: node
linkType: hard
"symbol-tree@npm:^3.2.4":
version: 3.2.4
resolution: "symbol-tree@npm:3.2.4"
checksum: 10c0/dfbe201ae09ac6053d163578778c53aa860a784147ecf95705de0cd23f42c851e1be7889241495e95c37cabb058edb1052f141387bef68f705afc8f9dd358509
languageName: node
linkType: hard
"tar@npm:^7.4.3":
version: 7.4.3
resolution: "tar@npm:7.4.3"
@@ -2582,6 +2822,24 @@ __metadata:
languageName: node
linkType: hard
"tldts-core@npm:^6.1.78":
version: 6.1.78
resolution: "tldts-core@npm:6.1.78"
checksum: 10c0/aea5e664da879cd862ccf5df9286531ddf4c34a9ca832480188bf6cd165cd45654f5b0a0f0f5315e16203ebfb87d52f8630b9419e729b3cfe5eff073c398693e
languageName: node
linkType: hard
"tldts@npm:^6.1.32":
version: 6.1.78
resolution: "tldts@npm:6.1.78"
dependencies:
tldts-core: "npm:^6.1.78"
bin:
tldts: bin/cli.js
checksum: 10c0/966f3f5a63405db6abb49b479784baa677510993f21ffbd67571f3d819451d70a603f1246b13f1c309a7573c4d9fbe0241aca6ff6e8399cbe7d2dd70b7ee4052
languageName: node
linkType: hard
"to-regex-range@npm:^5.0.1":
version: 5.0.1
resolution: "to-regex-range@npm:5.0.1"
@@ -2621,6 +2879,24 @@ __metadata:
languageName: node
linkType: hard
"tough-cookie@npm:^5.0.0":
version: 5.1.1
resolution: "tough-cookie@npm:5.1.1"
dependencies:
tldts: "npm:^6.1.32"
checksum: 10c0/84fe18b7c28ce273c916d95028c00ffff58c285d58e90fbd44eb9380dd1bc21892c675cd1bbd4bfbc95108fe833c406b285844757d41636248bfe264655a6ef8
languageName: node
linkType: hard
"tr46@npm:^5.0.0":
version: 5.0.0
resolution: "tr46@npm:5.0.0"
dependencies:
punycode: "npm:^2.3.1"
checksum: 10c0/1521b6e7bbc8adc825c4561480f9fe48eb2276c81335eed9fa610aa4c44a48a3221f78b10e5f18b875769eb3413e30efbf209ed556a17a42aa8d690df44b7bee
languageName: node
linkType: hard
"undefsafe@npm:^2.0.5":
version: 2.0.5
resolution: "undefsafe@npm:2.0.5"
@@ -2635,6 +2911,13 @@ __metadata:
languageName: node
linkType: hard
"undici@npm:^7.3.0":
version: 7.3.0
resolution: "undici@npm:7.3.0"
checksum: 10c0/62c5e335725cadb02e19950932c7823fc330cbfd80106e6836daa6db1379aa727510b77de0a4e6f912087b288ded93f7daf4b8c154ad36fd5c9c4b96b26888b8
languageName: node
linkType: hard
"unique-filename@npm:^4.0.0":
version: 4.0.0
resolution: "unique-filename@npm:4.0.0"
@@ -2676,16 +2959,100 @@ __metadata:
"@fastify/view": "npm:^10.0.0"
"@prettier/plugin-pug": "npm:^3.0.0"
"@types/node": "npm:^22.13.5"
axios: "npm:^1.7.9"
esbuild: "npm:^0.25.0"
fastify: "npm:^5.0.0"
fastify: "npm:^5.2.1"
jsdom: "npm:^26.0.0"
leaflet: "npm:^1.9.4"
nodemon: "npm:^3.0.1"
pretier: "npm:^0.0.1"
prettier: "npm:^3.5.2"
pug: "npm:^3.0.2"
undici: "npm:^7.3.0"
vue: "npm:2"
vue-multiselect: "npm:2"
vue-textarea-autosize: "npm:^1.1.1"
vue2-leaflet: "npm:^2.7.1"
languageName: unknown
linkType: soft
"vue-multiselect@npm:2":
version: 2.1.9
resolution: "vue-multiselect@npm:2.1.9"
checksum: 10c0/baecfbb97b4b225bd2e2054f2eed640917b47f448a76a43f084d344764c813a58e825ea2131f6803c125e6e8d4e0b4edb5712da49dd1b8a082f29ac845cc7c2b
languageName: node
linkType: hard
"vue-textarea-autosize@npm:^1.1.1":
version: 1.1.1
resolution: "vue-textarea-autosize@npm:1.1.1"
dependencies:
core-js: "npm:^2.6.5"
checksum: 10c0/22614d412b7e592b68c9b127cfc4257985058571d185ef1885acd3666b77381c87a2006b5084d5687b5e3729ad5b8c53ff962eaf43bbefa1dc5ae0e58cada9db
languageName: node
linkType: hard
"vue2-leaflet@npm:^2.7.1":
version: 2.7.1
resolution: "vue2-leaflet@npm:2.7.1"
peerDependencies:
"@types/leaflet": ^1.5.7
leaflet: ^1.3.4
vue: ^2.5.17
checksum: 10c0/34ae5cc4b78deaf3ee7f73a210b2e45f9d80b92860d5ac5d831d74fd8a94d06983845f01e6203b4e61bad7f6385715f0ea3cd23b2e39fb6c8ce912fe4d096af6
languageName: node
linkType: hard
"vue@npm:2":
version: 2.7.16
resolution: "vue@npm:2.7.16"
dependencies:
"@vue/compiler-sfc": "npm:2.7.16"
csstype: "npm:^3.1.0"
checksum: 10c0/15bf536c131a863d03c42386a4bbc82316262129421ef70e88d1758bcf951446ef51edeff42e3b27d026015330fe73d90155fca270eb5eadd30b0290735f2c3e
languageName: node
linkType: hard
"w3c-xmlserializer@npm:^5.0.0":
version: 5.0.0
resolution: "w3c-xmlserializer@npm:5.0.0"
dependencies:
xml-name-validator: "npm:^5.0.0"
checksum: 10c0/8712774c1aeb62dec22928bf1cdfd11426c2c9383a1a63f2bcae18db87ca574165a0fbe96b312b73652149167ac6c7f4cf5409f2eb101d9c805efe0e4bae798b
languageName: node
linkType: hard
"webidl-conversions@npm:^7.0.0":
version: 7.0.0
resolution: "webidl-conversions@npm:7.0.0"
checksum: 10c0/228d8cb6d270c23b0720cb2d95c579202db3aaf8f633b4e9dd94ec2000a04e7e6e43b76a94509cdb30479bd00ae253ab2371a2da9f81446cc313f89a4213a2c4
languageName: node
linkType: hard
"whatwg-encoding@npm:^3.1.1":
version: 3.1.1
resolution: "whatwg-encoding@npm:3.1.1"
dependencies:
iconv-lite: "npm:0.6.3"
checksum: 10c0/273b5f441c2f7fda3368a496c3009edbaa5e43b71b09728f90425e7f487e5cef9eb2b846a31bd760dd8077739c26faf6b5ca43a5f24033172b003b72cf61a93e
languageName: node
linkType: hard
"whatwg-mimetype@npm:^4.0.0":
version: 4.0.0
resolution: "whatwg-mimetype@npm:4.0.0"
checksum: 10c0/a773cdc8126b514d790bdae7052e8bf242970cebd84af62fb2f35a33411e78e981f6c0ab9ed1fe6ec5071b09d5340ac9178e05b52d35a9c4bcf558ba1b1551df
languageName: node
linkType: hard
"whatwg-url@npm:^14.0.0, whatwg-url@npm:^14.1.0":
version: 14.1.1
resolution: "whatwg-url@npm:14.1.1"
dependencies:
tr46: "npm:^5.0.0"
webidl-conversions: "npm:^7.0.0"
checksum: 10c0/de1e9cc2f04cb000f232c839d4999384ba41b680ef8a89e7c61c9bc40354ba8593c775d068faaf0819f5866e4d6ca3e7b9b386e2123aa475bfd33da02316f476
languageName: node
linkType: hard
"which@npm:^2.0.1":
version: 2.0.2
resolution: "which@npm:2.0.2"
@@ -2742,6 +3109,35 @@ __metadata:
languageName: node
linkType: hard
"ws@npm:^8.18.0":
version: 8.18.1
resolution: "ws@npm:8.18.1"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ">=5.0.2"
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
checksum: 10c0/e498965d6938c63058c4310ffb6967f07d4fa06789d3364829028af380d299fe05762961742971c764973dce3d1f6a2633fe8b2d9410c9b52e534b4b882a99fa
languageName: node
linkType: hard
"xml-name-validator@npm:^5.0.0":
version: 5.0.0
resolution: "xml-name-validator@npm:5.0.0"
checksum: 10c0/3fcf44e7b73fb18be917fdd4ccffff3639373c7cb83f8fc35df6001fecba7942f1dbead29d91ebb8315e2f2ff786b508f0c9dc0215b6353f9983c6b7d62cb1f5
languageName: node
linkType: hard
"xmlchars@npm:^2.2.0":
version: 2.2.0
resolution: "xmlchars@npm:2.2.0"
checksum: 10c0/b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593
languageName: node
linkType: hard
"yallist@npm:^4.0.0":
version: 4.0.0
resolution: "yallist@npm:4.0.0"