dev #160
3
.gitignore
vendored
3
.gitignore
vendored
@ -6,4 +6,7 @@ db/
|
||||
.yarn/
|
||||
public/*.js
|
||||
public/*.map
|
||||
public/*.css
|
||||
.yarnrc.yml
|
||||
.pnp*
|
||||
build/
|
@ -7,6 +7,7 @@ WORKDIR /usr/src/app
|
||||
# where available (npm@5+)
|
||||
COPY package*.json ./
|
||||
RUN yarn
|
||||
RUN yarn build
|
||||
# If you are building your code for production
|
||||
# RUN npm ci --only=production
|
||||
# Bundle app source
|
||||
|
19
package.json
19
package.json
@ -2,12 +2,14 @@
|
||||
"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 && yarn build-style",
|
||||
"build-style": "esbuild src/style/index.css --outfile=public/index.css --bundle --minify ",
|
||||
"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 --watch src --watch template --watch router --exec \"yarn build && yarn start\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -19,14 +21,13 @@
|
||||
"@fastify/leveldb": "^6.0.0",
|
||||
"@fastify/static": "^8.0.0",
|
||||
"@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",
|
||||
"nodemon": "^3.0.1",
|
||||
"pretier": "^0.0.1",
|
||||
"prettier": "^3.5.2",
|
||||
"pug": "^3.0.2"
|
||||
"pug": "^3.0.2",
|
||||
"undici": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
2928
public/css/index.css
2928
public/css/index.css
File diff suppressed because it is too large
Load Diff
103
router/api.js
103
router/api.js
@ -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('"',""").replace("'","'").replace("<","<").replace(">",">").replace("&","&").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();
|
||||
};
|
35
server.js
35
server.js
@ -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);
|
||||
});
|
122
src/api.ts
122
src/api.ts
@ -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";
|
||||
}
|
||||
};
|
13
src/app.ts
13
src/app.ts
@ -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
152
src/client/api.ts
Normal 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", "science_park", "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
4
src/client/main.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import "./types/ext";
|
||||
import "./types/format";
|
||||
import "./api";
|
||||
import "./old";
|
291
src/client/old.js
Normal file
291
src/client/old.js
Normal file
@ -0,0 +1,291 @@
|
||||
|
||||
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);
|
||||
|
||||
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, note: false, drawer: 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 () {
|
||||
if (!this.$refs.map) return undefined
|
||||
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;"></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.note = false;
|
||||
this.query.type = null;
|
||||
this.query.drawer = item ? true : false;
|
||||
setTimeout(() => this.$refs.map.mapObject.invalidateSize(), 500);
|
||||
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.drawer = true;
|
||||
setTimeout(() => this.$refs.map.mapObject.invalidateSize(), 500);
|
||||
if (f == "notes") {
|
||||
this.query.note = true;
|
||||
this.query.type = null;
|
||||
const query_in = document.getElementById('query_note')
|
||||
setTimeout(() => query_in.focus(), 500);
|
||||
return;
|
||||
}
|
||||
this.query.note = false;
|
||||
this.query.type = f;
|
||||
const query_in = document.getElementById('query_input')
|
||||
setTimeout(() => query_in.focus(), 500);
|
||||
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 left = e.pageX - c.getBoundingClientRect().left;
|
||||
const newDir =
|
||||
left < c.offsetWidth * 0.1 ? 'left' :
|
||||
(left > 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
|
||||
},
|
||||
|
||||
refreshTextAreaHeight(event) {
|
||||
console.log("AAA", event.target.scrollHeight, event.target)
|
||||
event.target.style['height'] = 'auto';
|
||||
event.target.style['height'] = event.target.scrollHeight + 'px';
|
||||
event.target.style['max-height'] = "100%";
|
||||
},
|
||||
},
|
||||
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) {
|
||||
if (this.edit_active)
|
||||
this.save_data();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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(
|
||||
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 { };
|
59
src/client/types/format.ts
Normal file
59
src/client/types/format.ts
Normal 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 leg_template: leg = {
|
||||
title: "",
|
||||
day_title: [],
|
||||
map: { zoom: 2, center: { lng: 0, lat: 0 } },
|
||||
travel: [],
|
||||
hotel: null,
|
||||
places: { restaurants: [], activities: [] },
|
||||
notes: "",
|
||||
date_range: null
|
||||
}
|
||||
const journey_template: journey = {
|
||||
fmt_ver: 1,
|
||||
title: "New Journey",
|
||||
main: [leg_template],
|
||||
}
|
||||
|
||||
|
||||
export { map, geoloc, leg, journey }
|
||||
export { journey_template, leg_template }
|
165
src/client/types/geom.ts
Normal file
165
src/client/types/geom.ts
Normal 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)
|
||||
}
|
24
src/client/types/migration.ts
Normal file
24
src/client/types/migration.ts
Normal 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;
|
||||
}
|
144
src/client/types/wrapper.ts
Normal file
144
src/client/types/wrapper.ts
Normal file
@ -0,0 +1,144 @@
|
||||
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();
|
||||
if (this.sel_leg > this.data.main.length - 1) this.leg_next();
|
||||
}
|
||||
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;
|
344
src/old.js
344
src/old.js
@ -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
90
src/server/api.ts
Normal 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('"', """).replace("'", "'").replace("<", "<").replace(">", ">").replace("&", "&").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
86
src/server/api_flight.ts
Normal 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
|
||||
};
|
||||
};
|
27
src/server/api_nominatim.ts
Normal file
27
src/server/api_nominatim.ts
Normal file
@ -0,0 +1,27 @@
|
||||
|
||||
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)
|
||||
url.searchParams.append('limit', '20')
|
||||
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
36
src/server/main.ts
Normal 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("/src/template/home.pug"));
|
||||
server.get("/:id", (req, reply) => reply.view("/src/template/journey.pug"));
|
||||
server.get("/view/:id", (req, reply) => reply.view("/src/template/view.pug"));
|
||||
server.get("/short/:id", (req, reply) => reply.view("/src/template/short.pug"));
|
||||
|
||||
server.listen({ port: 8080, host: "0.0.0.0" }, (err, address) => {
|
||||
if (err) throw err;
|
||||
console.log("Listening on", address);
|
||||
});
|
207
src/style/custom.css
Normal file
207
src/style/custom.css
Normal file
@ -0,0 +1,207 @@
|
||||
.leaflet-popup-close-button {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.p-abs {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.list-group {
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
scrollbar-width: none;
|
||||
padding: 1rem 0rem;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
border: 1px solid var(--darkdark);
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
padding: 0.5rem 0.8rem;
|
||||
}
|
||||
|
||||
.list-group-item:hover {
|
||||
filter: brightness(85%);
|
||||
}
|
||||
|
||||
.leaflet-control-attribution {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.display-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.col-0 {
|
||||
flex: 0 0 0%;
|
||||
max-width: 0%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-container,
|
||||
.drawer-container {
|
||||
transition: flex 0.5s ease-in-out, max-width 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
min-width: 52px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.drawer-container {
|
||||
height: 100%;
|
||||
right: 0;
|
||||
top: 0;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.travel-path-icon {
|
||||
margin-left: -12px;
|
||||
margin-top: -32px;
|
||||
}
|
||||
|
||||
|
||||
.ml-auto {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.mr-auto {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.input .mx-input {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.h-100 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map-menu {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
z-index: 1100;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.map-menu-top {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.map-menu-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.map-menu-center {
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.padding-1 {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.map-menu-item {
|
||||
background-color: var(--darkdark);
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.map-menu-item:hover {
|
||||
filter: brightness(150%);
|
||||
}
|
||||
|
||||
.map-menu-sub {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.map-menu-item,
|
||||
.map-menu-sub-item {
|
||||
background-color: var(--darkdark);
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vue2leaflet-map {
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 10px 20px !important;
|
||||
/* display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: auto !important;
|
||||
max-width: 100%; */
|
||||
}
|
||||
|
||||
|
||||
.leaflet-popup>.leaflet-popup-content-wrapper {
|
||||
border-radius: var(--border-radius);
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.leaflet-popup-button-group {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
margin: 2px 6px;
|
||||
}
|
||||
|
||||
.leaflet-popup-button-group>a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.leaflet-popup-button-group>a:hover {
|
||||
cursor: pointer;
|
||||
filter: brightness(150%);
|
||||
}
|
||||
|
||||
.leaflet-popup-button-group>a>i {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.query-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.query-result:hover {
|
||||
filter: brightness(85%);
|
||||
}
|
||||
|
||||
.scroll-content>div:first-child {
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.scroll-content>div:nth-last-child(3) {
|
||||
border-top-right-radius: var(--border-radius);
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.scroll-content>div:last-child {
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.mx-datepicker {
|
||||
width: 100% !important;
|
||||
}
|
19
src/style/define.css
Normal file
19
src/style/define.css
Normal file
@ -0,0 +1,19 @@
|
||||
:root {
|
||||
--black: #030B12;
|
||||
--darkdark: #0C1D2E;
|
||||
--dark: #203A53;
|
||||
--lightdark: #425F7C;
|
||||
--light: #93A9BE;
|
||||
--lightlight: #B6C5D5;
|
||||
--white: #F0F3F7;
|
||||
--orange: ##F5B97D;
|
||||
--yellow: #F5F57D;
|
||||
--green: #B9F57D;
|
||||
--turquoise: #7DF5B9;
|
||||
--blue: #7DB9F5;
|
||||
--purple: #B97DF5;
|
||||
--pink: #F57DB9;
|
||||
--red: #F57D7D;
|
||||
|
||||
--border-radius: 3px;
|
||||
}
|
13
src/style/index.css
Normal file
13
src/style/index.css
Normal file
@ -0,0 +1,13 @@
|
||||
@import './define.css';
|
||||
|
||||
@import './module/input.css';
|
||||
@import './module/load_n_spin.css';
|
||||
@import './module/typography.css';
|
||||
@import './module/layout.css';
|
||||
@import './module/general.css';
|
||||
|
||||
@import './custom.css';
|
||||
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
202
src/style/module/general.css
Normal file
202
src/style/module/general.css
Normal file
@ -0,0 +1,202 @@
|
||||
html,
|
||||
body,
|
||||
body,
|
||||
div,
|
||||
span,
|
||||
object,
|
||||
iframe,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
blockquote,
|
||||
pre,
|
||||
abbr,
|
||||
address,
|
||||
cite,
|
||||
code,
|
||||
del,
|
||||
dfn,
|
||||
em,
|
||||
ins,
|
||||
kbd,
|
||||
q,
|
||||
samp,
|
||||
small,
|
||||
strong,
|
||||
sub,
|
||||
sup,
|
||||
var,
|
||||
b,
|
||||
i,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ol,
|
||||
ul,
|
||||
li,
|
||||
fieldset,
|
||||
form,
|
||||
label,
|
||||
legend,
|
||||
table,
|
||||
caption,
|
||||
tbody,
|
||||
tfoot,
|
||||
thead,
|
||||
tr,
|
||||
th,
|
||||
td,
|
||||
article,
|
||||
aside,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
section,
|
||||
time,
|
||||
mark,
|
||||
audio,
|
||||
video {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
padding: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
article,
|
||||
aside,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
main,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
*,
|
||||
*::after,
|
||||
*::before {
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #fff;
|
||||
min-height: 100%;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
p {
|
||||
font-weight: normal;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
/**
|
||||
* BACKGROUND
|
||||
*/
|
||||
.bg-primary {
|
||||
background-color: var(--blue);
|
||||
}
|
||||
|
||||
.bg-dark {
|
||||
background-color: var(--darkdark);
|
||||
}
|
||||
|
||||
.bg-secondary {
|
||||
background-color: var(--pink);
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background-color: var(--green);
|
||||
}
|
||||
|
||||
.bg-info {
|
||||
background-color: var(--yellow);
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: var(--orange);
|
||||
}
|
||||
|
||||
.bg-error {
|
||||
background-color: var(--red);
|
||||
}
|
||||
|
||||
.bg-gray {
|
||||
background-color: var(--lightdark);
|
||||
}
|
||||
|
||||
.bg-gray-light {
|
||||
background-color: var(--lightlight);
|
||||
}
|
||||
|
||||
|
||||
.align {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fleft {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.fright {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.clearfix ::after {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
}
|
||||
|
||||
.no-wrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: var(--border-radius);
|
||||
}
|
362
src/style/module/input.css
Normal file
362
src/style/module/input.css
Normal file
@ -0,0 +1,362 @@
|
||||
input,
|
||||
textarea {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
input:-webkit-autofill {
|
||||
box-shadow: 0 0 0 1000px #eceff1 inset;
|
||||
}
|
||||
|
||||
.textarea,
|
||||
.input,
|
||||
.select {
|
||||
border: 1px solid var(--white);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: none;
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.textarea :focus,
|
||||
.input :focus,
|
||||
.select :focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.textarea.has-error,
|
||||
.input.has-error,
|
||||
.select.has-error {
|
||||
background: #eceff1;
|
||||
border: 1px solid #e74c3c;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.select {
|
||||
background-color: #eceff1;
|
||||
display: inline-block;
|
||||
margin-right: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.select:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.select-fullWidth {
|
||||
display: block;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.select select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
color: #272727;
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
line-height: 1.5em;
|
||||
margin: 0;
|
||||
padding: 8px 16px;
|
||||
padding-right: 30px;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.select select:active,
|
||||
.select select:focus {
|
||||
background-color: #fbfbfc;
|
||||
border: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.select select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select::after,
|
||||
.select::before {
|
||||
background: #03a9f4;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 2px;
|
||||
margin-top: 2px;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 50%;
|
||||
-webkit-transform-origin: 1px;
|
||||
transform-origin: 1px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.select::after {
|
||||
-webkit-transform: rotate(-135deg);
|
||||
transform: rotate(-135deg);
|
||||
}
|
||||
|
||||
.select::before {
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
background-color: #eceff1;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.textarea-fullWidth {
|
||||
display: block;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.textarea textarea {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: #272727;
|
||||
display: block;
|
||||
font-family: "Lato", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5em;
|
||||
margin: 0;
|
||||
min-height: 120px;
|
||||
padding: 8px 16px;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.textarea textarea::-webkit-input-placeholder {
|
||||
color: #969da6;
|
||||
}
|
||||
|
||||
.textarea textarea::-ms-input-placeholder {
|
||||
color: #969da6;
|
||||
}
|
||||
|
||||
.textarea textarea::placeholder {
|
||||
color: #969da6;
|
||||
}
|
||||
|
||||
.textarea textarea:focus,
|
||||
.textarea textarea:active {
|
||||
background-color: #fbfbfc;
|
||||
border: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.checkbox input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkbox input[type="checkbox"]:checked+label::after {
|
||||
-webkit-animation: checkboxAndRadioAnimation 0.25s;
|
||||
animation: checkboxAndRadioAnimation 0.25s;
|
||||
content: "";
|
||||
-webkit-transform: scale(1) rotate(45deg);
|
||||
transform: scale(1) rotate(45deg);
|
||||
}
|
||||
|
||||
.checkbox input[type="checkbox"]+label {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
padding-left: 30px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checkbox input[type="checkbox"]+label::before {
|
||||
background-color: #eceff1;
|
||||
border: 1px solid var(--white);
|
||||
border-radius: var(--border-radius);
|
||||
content: "";
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
left: 0;
|
||||
margin-top: -10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.checkbox input[type="checkbox"]+label::after {
|
||||
border-bottom: 3px solid #03a9f4;
|
||||
border-right: 3px solid #03a9f4;
|
||||
display: block;
|
||||
height: 12px;
|
||||
left: 11px;
|
||||
margin-left: -4px;
|
||||
margin-top: -7px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 7px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.radio {
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.radio input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.radio input[type="radio"]:checked+label::after {
|
||||
-webkit-animation: checkboxAndRadioAnimation 0.25s;
|
||||
animation: checkboxAndRadioAnimation 0.25s;
|
||||
content: "";
|
||||
-webkit-transform: scale(1) rotate(45deg);
|
||||
transform: scale(1) rotate(45deg);
|
||||
}
|
||||
|
||||
.radio input[type="radio"]+label {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
padding-left: 30px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.radio input[type="radio"]+label::before {
|
||||
background-color: #eceff1;
|
||||
border: 1px solid var(--white);
|
||||
border-radius: 20px;
|
||||
content: "";
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
left: 0;
|
||||
margin-top: -10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.radio input[type="radio"]+label::after {
|
||||
background-color: #03a9f4;
|
||||
border-radius: 20px;
|
||||
display: block;
|
||||
height: 10px;
|
||||
left: 11px;
|
||||
margin-left: -6px;
|
||||
margin-top: -6px;
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
width: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@-webkit-keyframes checkboxAndRadioAnimation {
|
||||
0% {
|
||||
-webkit-transform: scale(0) rotate(45deg);
|
||||
transform: scale(0) rotate(45deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: scale(1.5) rotate(45deg);
|
||||
transform: scale(1.5) rotate(45deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: scale(1) rotate(45deg);
|
||||
transform: scale(1) rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes checkboxAndRadioAnimation {
|
||||
0% {
|
||||
-webkit-transform: scale(0) rotate(45deg);
|
||||
transform: scale(0) rotate(45deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: scale(1.5) rotate(45deg);
|
||||
transform: scale(1.5) rotate(45deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: scale(1) rotate(45deg);
|
||||
transform: scale(1) rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
.input-invis {
|
||||
background-color: transparent !important;
|
||||
margin: auto !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.input {
|
||||
background-color: var(--white);
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input :focus,
|
||||
.input :active {
|
||||
background-color: var(--white);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.input input,
|
||||
.input textarea {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
color: #272727;
|
||||
font-size: 16px;
|
||||
line-height: 1.5em;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
padding: 8px 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input input::-webkit-input-placeholder {
|
||||
color: #969da6;
|
||||
}
|
||||
|
||||
.input input::-ms-input-placeholder {
|
||||
color: #969da6;
|
||||
}
|
||||
|
||||
.input input::placeholder {
|
||||
color: #969da6;
|
||||
}
|
||||
|
||||
.input input.small {
|
||||
line-height: 1em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.input-withIcon input {
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
fill: #969da6;
|
||||
height: 16px;
|
||||
margin-top: -8px;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
width: 16px;
|
||||
}
|
354
src/style/module/layout.css
Normal file
354
src/style/module/layout.css
Normal file
@ -0,0 +1,354 @@
|
||||
/**
|
||||
* LAYOUT
|
||||
*/
|
||||
.section {
|
||||
padding-bottom: 36px;
|
||||
padding-top: 36px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.section {
|
||||
padding-bottom: 72px;
|
||||
padding-top: 72px;
|
||||
}
|
||||
}
|
||||
|
||||
.section+.section {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 1380px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.container {
|
||||
max-width: 540px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.container {
|
||||
max-width: 960px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.container {
|
||||
max-width: 1140px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
max-width: 720px;
|
||||
}
|
||||
}
|
||||
|
||||
.container-medium {
|
||||
margin: 0 auto;
|
||||
max-width: 944px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container-medium {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
width: 100%;
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-right: -12px;
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
.col-1,
|
||||
.col-2,
|
||||
.col-3,
|
||||
.col-4,
|
||||
.col-5,
|
||||
.col-6,
|
||||
.col-7,
|
||||
.col-8,
|
||||
.col-9,
|
||||
.col-10,
|
||||
.col-11,
|
||||
.col-12,
|
||||
.col,
|
||||
.col-auto,
|
||||
.col-sm-1,
|
||||
.col-sm-2,
|
||||
.col-sm-3,
|
||||
.col-sm-4,
|
||||
.col-sm-5,
|
||||
.col-sm-6,
|
||||
.col-sm-7,
|
||||
.col-sm-8,
|
||||
.col-sm-9,
|
||||
.col-sm-10,
|
||||
.col-sm-11,
|
||||
.col-sm-12,
|
||||
.col-md-1,
|
||||
.col-md-2,
|
||||
.col-md-3,
|
||||
.col-md-4,
|
||||
.col-md-5,
|
||||
.col-md-6,
|
||||
.col-md-7,
|
||||
.col-md-8,
|
||||
.col-md-9,
|
||||
.col-md-10,
|
||||
.col-md-11,
|
||||
.col-md-12 {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 1px;
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.col {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.col-auto {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.col-1 {
|
||||
flex: 0 0 8.33333%;
|
||||
max-width: 8.33333%;
|
||||
}
|
||||
|
||||
.col-2 {
|
||||
flex: 0 0 16.66667%;
|
||||
max-width: 16.66667%;
|
||||
}
|
||||
|
||||
.col-3 {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
.col-4 {
|
||||
flex: 0 0 33.33333%;
|
||||
max-width: 33.33333%;
|
||||
}
|
||||
|
||||
.col-5 {
|
||||
flex: 0 0 41.66667%;
|
||||
max-width: 41.66667%;
|
||||
}
|
||||
|
||||
.col-6 {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.col-7 {
|
||||
flex: 0 0 58.33333%;
|
||||
max-width: 58.33333%;
|
||||
}
|
||||
|
||||
.col-8 {
|
||||
flex: 0 0 66.66667%;
|
||||
max-width: 66.66667%;
|
||||
}
|
||||
|
||||
.col-9 {
|
||||
flex: 0 0 75%;
|
||||
max-width: 75%;
|
||||
}
|
||||
|
||||
.col-10 {
|
||||
flex: 0 0 83.33333%;
|
||||
max-width: 83.33333%;
|
||||
}
|
||||
|
||||
.col-11 {
|
||||
flex: 0 0 91.66667%;
|
||||
max-width: 91.66667%;
|
||||
}
|
||||
|
||||
.col-12 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.col-sm {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.col-sm-auto {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.col-sm-1 {
|
||||
flex: 0 0 8.33333%;
|
||||
max-width: 8.33333%;
|
||||
}
|
||||
|
||||
.col-sm-2 {
|
||||
flex: 0 0 16.66667%;
|
||||
max-width: 16.66667%;
|
||||
}
|
||||
|
||||
.col-sm-3 {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
.col-sm-4 {
|
||||
flex: 0 0 33.33333%;
|
||||
max-width: 33.33333%;
|
||||
}
|
||||
|
||||
.col-sm-5 {
|
||||
flex: 0 0 41.66667%;
|
||||
max-width: 41.66667%;
|
||||
}
|
||||
|
||||
.col-sm-6 {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.col-sm-7 {
|
||||
flex: 0 0 58.33333%;
|
||||
max-width: 58.33333%;
|
||||
}
|
||||
|
||||
.col-sm-8 {
|
||||
flex: 0 0 66.66667%;
|
||||
max-width: 66.66667%;
|
||||
}
|
||||
|
||||
.col-sm-9 {
|
||||
flex: 0 0 75%;
|
||||
max-width: 75%;
|
||||
}
|
||||
|
||||
.col-sm-10 {
|
||||
flex: 0 0 83.33333%;
|
||||
max-width: 83.33333%;
|
||||
}
|
||||
|
||||
.col-sm-11 {
|
||||
flex: 0 0 91.66667%;
|
||||
max-width: 91.66667%;
|
||||
}
|
||||
|
||||
.col-sm-12 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.col-md {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.col-md-auto {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.col-md-1 {
|
||||
flex: 0 0 8.33333%;
|
||||
max-width: 8.33333%;
|
||||
}
|
||||
|
||||
.col-md-2 {
|
||||
flex: 0 0 16.66667%;
|
||||
max-width: 16.66667%;
|
||||
}
|
||||
|
||||
.col-md-3 {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
.col-md-4 {
|
||||
flex: 0 0 33.33333%;
|
||||
max-width: 33.33333%;
|
||||
}
|
||||
|
||||
.col-md-5 {
|
||||
flex: 0 0 41.66667%;
|
||||
max-width: 41.66667%;
|
||||
}
|
||||
|
||||
.col-md-6 {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.col-md-7 {
|
||||
flex: 0 0 58.33333%;
|
||||
max-width: 58.33333%;
|
||||
}
|
||||
|
||||
.col-md-8 {
|
||||
flex: 0 0 66.66667%;
|
||||
max-width: 66.66667%;
|
||||
}
|
||||
|
||||
.col-md-9 {
|
||||
flex: 0 0 75%;
|
||||
max-width: 75%;
|
||||
}
|
||||
|
||||
.col-md-10 {
|
||||
flex: 0 0 83.33333%;
|
||||
max-width: 83.33333%;
|
||||
}
|
||||
|
||||
.col-md-11 {
|
||||
flex: 0 0 91.66667%;
|
||||
max-width: 91.66667%;
|
||||
}
|
||||
|
||||
.col-md-12 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
166
src/style/module/load_n_spin.css
Normal file
166
src/style/module/load_n_spin.css
Normal file
@ -0,0 +1,166 @@
|
||||
/**
|
||||
* LOADING BAR
|
||||
*
|
||||
* Markup:
|
||||
* ---------
|
||||
* <div class="loadingBar"></div>
|
||||
*
|
||||
*/
|
||||
.loadingBar {
|
||||
height: 6px;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loadingBar::before {
|
||||
-webkit-animation: loading 2s linear infinite;
|
||||
animation: loading 2s linear infinite;
|
||||
background-color: #03a9f4;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 6px;
|
||||
left: -300px;
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
@-webkit-keyframes loading {
|
||||
from {
|
||||
left: -300px;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
50% {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
70% {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
80% {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
95% {
|
||||
left: 120%;
|
||||
}
|
||||
|
||||
to {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
from {
|
||||
left: -300px;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
50% {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
70% {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
80% {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
95% {
|
||||
left: 120%;
|
||||
}
|
||||
|
||||
to {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: block
|
||||
}
|
||||
|
||||
.spinner:after,
|
||||
.spinner:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -12px 0 0 -12px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 100%;
|
||||
border: 3px solid transparent;
|
||||
border-top-color: var(--blue);
|
||||
}
|
||||
|
||||
.spinner:before {
|
||||
-webkit-animation: spinning 2.4s cubic-bezier(.41, .26, .2, .62);
|
||||
animation: spinning 2.4s cubic-bezier(.41, .26, .2, .62);
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite
|
||||
}
|
||||
|
||||
.spinner:after {
|
||||
-webkit-animation: spinning 2.4s cubic-bezier(.51, .09, .21, .8);
|
||||
animation: spinning 2.4s cubic-bezier(.51, .09, .21, .8);
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite
|
||||
}
|
||||
|
||||
@-webkit-keyframes spinning {
|
||||
0% {
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
25% {
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
-webkit-transform: rotate(270deg);
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinning {
|
||||
0% {
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
148
src/style/module/typography.css
Normal file
148
src/style/module/typography.css
Normal file
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* TYPOGRAPHY
|
||||
*/
|
||||
body {
|
||||
color: #272727;
|
||||
font-family: "Lato", sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--blue);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: color-mix(in srgb, var(--color-primary), #FFF 15%);
|
||||
}
|
||||
|
||||
a:focus {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.text-huge,
|
||||
.text-big,
|
||||
.text-medium {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.text-huge {
|
||||
font-size: 36px;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
.text-big {
|
||||
font-size: 24px;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
.text-medium {
|
||||
font-size: 16px;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.text-small {
|
||||
font-size: 12px;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
.text-body {
|
||||
font-size: 16px;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #03a9f4;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #18232f;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: #e91e63;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: #5bc0de;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #f0ad4e;
|
||||
}
|
||||
|
||||
.text-error {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.text-gray {
|
||||
color: #969da6;
|
||||
}
|
||||
|
||||
.text-gray-light {
|
||||
color: #eceff1;
|
||||
}
|
||||
|
||||
.text-light {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.text-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.text-lineThrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.text-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.text-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.text-uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.text-withSubtitle {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.text-withSubtitle+.text-huge,
|
||||
.text-withSubtitle+.text-big,
|
||||
.text-withSubtitle+.text-medium,
|
||||
.text-withSubtitle+.text-small {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
32
src/template/home.pug
Normal file
32
src/template/home.pug
Normal file
@ -0,0 +1,32 @@
|
||||
doctype html
|
||||
|
||||
include module/head.pug
|
||||
main#app
|
||||
.bg-white.section.text-dark
|
||||
.container.mb-big
|
||||
.text-center
|
||||
div
|
||||
h1.text-huge.text-withSubtitle Open Tourism Map
|
||||
h2.text-big.text-gray Collaborative Holiday Planner
|
||||
.spacer
|
||||
.section.bg-dark.text-white
|
||||
.container-medium
|
||||
.row
|
||||
.col-8
|
||||
h2.text-big Your journey
|
||||
p.text-gray
|
||||
| Browse hotels, restaurants and attractions,....
|
||||
br
|
||||
|
|
||||
| Select and plan the varying elements of your journey
|
||||
.col-4
|
||||
.row.align
|
||||
.input
|
||||
input#journey.id(v-model="journey.id", placeholder="ID", type="text")
|
||||
p
|
||||
.row.align
|
||||
button.button.button--primary.button--mobileFull(
|
||||
v-on:click="start_journey"
|
||||
) Start the journey
|
||||
|
||||
include module/foot.pug
|
7
src/template/journey.pug
Normal file
7
src/template/journey.pug
Normal file
@ -0,0 +1,7 @@
|
||||
doctype html
|
||||
|
||||
include module/head.pug
|
||||
|
||||
main#app(v-cloak)
|
||||
include module/journey/main.pug
|
||||
include module/foot.pug
|
13
src/template/module/foot.pug
Normal file
13
src/template/module/foot.pug
Normal file
@ -0,0 +1,13 @@
|
||||
script(src="https://unpkg.com/leaflet")
|
||||
|
||||
script(src="https://unpkg.com/vue@2")
|
||||
script(src="https://unpkg.com/vue2-datepicker")
|
||||
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.section
|
||||
.container.text-center.text-small.text-white
|
||||
| Built with ❤ by Helcel
|
||||
br
|
||||
span.text-small.text-gray v0.0.1
|
@ -8,18 +8,10 @@ head
|
||||
href="https://fonts.googleapis.com/css?family=IBM+Plex+Sans:400,700,300",
|
||||
type="text/css"
|
||||
)
|
||||
link(rel="stylesheet", href="/public/css/index.css")
|
||||
link(rel="stylesheet", href="/public/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"
|
@ -1,4 +1,4 @@
|
||||
div
|
||||
.impexp
|
||||
.container-medium.section
|
||||
.aligner
|
||||
.input.col-sm-4
|
8
src/template/module/journey/leg/drawer-notes.pug
Normal file
8
src/template/module/journey/leg/drawer-notes.pug
Normal file
@ -0,0 +1,8 @@
|
||||
.col-12.input.text-dark()
|
||||
textarea.text-small#query_note(
|
||||
v-model="journey.leg_get().notes"
|
||||
@input="refreshTextAreaHeight"
|
||||
@focus="refreshTextAreaHeight"
|
||||
rows="1"
|
||||
placeholder="...",
|
||||
)
|
48
src/template/module/journey/leg/drawer.pug
Normal file
48
src/template/module/journey/leg/drawer.pug
Normal file
@ -0,0 +1,48 @@
|
||||
|
||||
.col-12.input.text-dark
|
||||
input#query_input(
|
||||
type="search"
|
||||
@input="search_active"
|
||||
@focus="search_active"
|
||||
placeholder="Search ... "
|
||||
style="width:85%;"
|
||||
:disabled="query.note"
|
||||
)
|
||||
.spinner(v-if="query.load")
|
||||
|
||||
div(v-if="['hotel', 'restaurant', 'place','other', 'travel'].indexOf(query.type)>=0")
|
||||
template(v-for="(item, idx) in query.res" )
|
||||
.query-result.col-12.bg-white.text-dark(
|
||||
: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" )
|
||||
.query-result.col-12.bg-white.text-dark(
|
||||
v-if="query.load==false && query.res.length==0" )
|
||||
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(v-else-if="['flight'].indexOf(query.type)>=0")
|
||||
template(v-for="(item, idx) in query.res" )
|
||||
.query-result.col-12.bg-white.text-dark(
|
||||
: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(v-else)
|
||||
template()
|
||||
.query-result.col-12.bg-white.text-dark()
|
||||
| Unsuppored Query type {{query.type}}
|
34
src/template/module/journey/leg/nav.pug
Normal file
34
src/template/module/journey/leg/nav.pug
Normal file
@ -0,0 +1,34 @@
|
||||
.scroll-handler.row(
|
||||
@mouseleave="nav_mouseleave"
|
||||
@mousemove="nav_mousemove")
|
||||
|
||||
.col-3.col-sm-2.col-md-1
|
||||
.list-group.text-dark.h-100
|
||||
.fleft.list-group-item.bg-white.text-small.rounded.h-100(v-on:click.prevent="journey.leg_prev()")
|
||||
i.fas.fa-angle-left
|
||||
.col-6.col-sm-8.col-md-10
|
||||
draggable.scroll-content.list-group.bg-dark(
|
||||
tag="div",
|
||||
:list="journey.data.main",
|
||||
handle=".handle"
|
||||
)
|
||||
.list-group-item.handle.text-dark(
|
||||
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 || "Leg "+idx}}
|
||||
i.fa.fa-times.close.fright(
|
||||
style="top: 2px; right: 2px; position: absolute",
|
||||
@click="journey.rm_leg(idx)"
|
||||
)
|
||||
.list-group-item.bg-dark
|
||||
.list-group-item.bg-white.text-dark(@click="journey.add_leg()")
|
||||
div
|
||||
i.fa.fa-plus.add()
|
||||
|
||||
.col-3.col-sm-2.col-md-1
|
||||
.list-group.text-dark.h-100
|
||||
a.fright.list-group-item.bg-white.text-small.rounded.h-100(v-on:click.prevent="journey.leg_next()")
|
||||
i.fas.fa-angle-right
|
36
src/template/module/journey/leg/top.pug
Normal file
36
src/template/module/journey/leg/top.pug
Normal file
@ -0,0 +1,36 @@
|
||||
.row.text-center.align
|
||||
.col-5.col-sm-4.col-md-2
|
||||
.input
|
||||
input(
|
||||
placeholder="Leg"
|
||||
v-model="journey.leg_get().title")
|
||||
|
||||
.col-5.col-sm-4.col-md-2.mr-auto
|
||||
.input
|
||||
input(
|
||||
placeholder="Day"
|
||||
v-model="journey.leg_get().day_title[journey.sel_day]"
|
||||
)
|
||||
.col-8.col-sm-6.col-md-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-4.col-sm-4.col-md-3.ml-auto
|
||||
.input
|
||||
input(
|
||||
disabled="",
|
||||
:value="journey.date_sel() + ' (' + journey.sel_day + ')'"
|
||||
)
|
||||
//- .col-6.list-group-item.align.center.bg-white(style="padding: 0.5rem 0;")
|
||||
//- i.fas.fa-angle-double-right(v-on:click.prevent="journey.day_next()")
|
||||
.col-sm-1.text-small
|
||||
|
||||
//- a(href="#prev", v-on:click.prevent="journey.day_prev()")
|
||||
i.fas.fa-angle-left
|
31
src/template/module/journey/main.pug
Normal file
31
src/template/module/journey/main.pug
Normal file
@ -0,0 +1,31 @@
|
||||
.row.fleft(style="position:absolute;right:0;")
|
||||
.col-1
|
||||
a(:href="'/short/' + journey.id")
|
||||
i.fas.fa-file-contract
|
||||
.col-1
|
||||
a(:href="'/view/' + journey.id")
|
||||
i.fas.fa-camera
|
||||
.col-1
|
||||
a(href="#", v-on:click.prevent="first_step")
|
||||
i.fas.fa-tools
|
||||
|
||||
|
||||
.bg-dark.text-white(v-if="journey && journey.leg_get()")
|
||||
.container
|
||||
.row.align(style="padding-top:45px;")
|
||||
.col-6.col-sm-4.col-md-3.input.text-big
|
||||
input.text-center(v-model="journey.data.name" placeholder="My Journey" type="text")
|
||||
//- input.small(type="text", :placeholder="journey.date_tot() + ' (' + journey.tot_len() + ')'" )
|
||||
|
||||
include leg/nav.pug
|
||||
include leg/top.pug
|
||||
.row(style="aspect-ratio:1.25;")
|
||||
.map-container(:class=" { 'col-2 col-sm-5 col-md-8': query.type, 'col-2 col-sm-5 col-md-6': query.note , 'col-12': (!query.type && !query.note) }" )
|
||||
include map.pug
|
||||
.row.drawer-container(:class="{ 'col-10 col-sm-7 col-md-4': query.type, 'col-10 col-sm-7 col-md-6': query.note, 'col-0': (!query.type && !query.note) }")
|
||||
.drawer-container(:class="{ 'col-12 ': query.type, 'col-0': !query.type }")
|
||||
include leg/drawer.pug
|
||||
.drawer-container(:class="{ 'col-12': query.note, 'col-0': !query.note }")
|
||||
include leg/drawer-notes.pug
|
||||
|
||||
//- include impexp.pug
|
21
src/template/module/journey/map.pug
Normal file
21
src/template/module/journey/map.pug
Normal file
@ -0,0 +1,21 @@
|
||||
l-map(
|
||||
:zoom.sync="journey.leg_get().map.zoom",
|
||||
:center.sync="journey.leg_get().map.center",
|
||||
style="height:100%"
|
||||
no-blocking-animations=true
|
||||
ref="map"
|
||||
)
|
||||
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="bottomleft", :imperial="false", :metric="true")
|
||||
include map/override.pug
|
||||
|
||||
include map/hotel.pug
|
||||
include map/activities.pug
|
||||
include map/restaurants.pug
|
||||
|
||||
include map/travel.pug
|
||||
template(v-if="edit_active")
|
||||
include map/right_menu.pug
|
6
src/template/module/journey/map/activities.pug
Normal file
6
src/template/module/journey/map/activities.pug
Normal 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)")
|
7
src/template/module/journey/map/hotel.pug
Normal file
7
src/template/module/journey/map/hotel.pug
Normal 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)")
|
37
src/template/module/journey/map/mixin-marker.pug
Normal file
37
src/template/module/journey/map/mixin-marker.pug
Normal file
@ -0,0 +1,37 @@
|
||||
|
||||
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(
|
||||
:options="{maxWidth:400, minWidth:300}")
|
||||
h1.row.text-medium.text-center {{ place.sname }}
|
||||
span.row.text-small.text-gray {{ place.display_name }}
|
||||
span(v-if="edit_active")
|
||||
.row.input()
|
||||
textarea.col-12.col-sm-12.text-small(
|
||||
placeholder="",
|
||||
v-model="place.notes",
|
||||
)
|
||||
.leaflet-popup-button-group(v-if="edit_active")
|
||||
a.text-gray(
|
||||
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')"
|
||||
)
|
||||
a.text-gray(
|
||||
v-on:click.prevent="place_delete(\""+place+"\",index)"
|
||||
v-html="generate_icon('trash', 'NA')"
|
||||
)
|
||||
span.row.text-small.text-dark(v-else) {{ place.notes }}
|
11
src/template/module/journey/map/override.pug
Normal file
11
src/template/module/journey/map/override.pug
Normal file
@ -0,0 +1,11 @@
|
||||
l-marker(
|
||||
v-if="map_override.active",
|
||||
v-for="(el, idx) in map_override.elements"
|
||||
key="'ovr'+idx"
|
||||
:lat-lng="el"
|
||||
)
|
||||
l-icon(v-html="generate_marker('plus', 'darkgreen')")
|
||||
l-polyline(
|
||||
v-if="map_override.active && map_override.elements.length>1"
|
||||
:lat-lngs="map_override.elements" :color="'darkgreen'"
|
||||
)
|
6
src/template/module/journey/map/restaurants.pug
Normal file
6
src/template/module/journey/map/restaurants.pug
Normal file
@ -0,0 +1,6 @@
|
||||
include mixin-marker.pug
|
||||
div(
|
||||
v-for="(place, index) in journey.leg_get().places.restaurants",
|
||||
:key="'restaurants'+index",
|
||||
)
|
||||
+map_marker("restaurants", "var(--dark)", "var(--dark)", "var(--dark)")
|
21
src/template/module/journey/map/right_menu.pug
Normal file
21
src/template/module/journey/map/right_menu.pug
Normal file
@ -0,0 +1,21 @@
|
||||
.map-menu.map-menu-top
|
||||
div(v-if="query.type" @click="drawer_click_item()" )
|
||||
.map-menu-item(v-html="generate_icon('close')")
|
||||
div(v-if="!query.type" @click="search_enable('hotel')")
|
||||
.map-menu-item( v-html="generate_icon('bed')")
|
||||
div(v-if="!query.type" @click="search_enable('restaurant')")
|
||||
.map-menu-item( v-html="generate_icon('utensils')")
|
||||
div(v-if="!query.type" @click="search_enable('place')")
|
||||
.map-menu-item( v-html="generate_icon('star')")
|
||||
.map-menu-sub(v-if="!query.type" @mouseenter="query.sub=true" @mouseleave="query.sub=false" )
|
||||
.map-menu-item(v-html="generate_icon('route')")
|
||||
.map-menu-item(v-if="query.sub" @click="search_enable('flight')" v-html="generate_icon('plane')")
|
||||
.map-menu-item(v-if="query.sub" @click="search_enable('train')" v-html="generate_icon('train')")
|
||||
.map-menu-item(v-if="query.sub" @click="search_enable('car')" v-html="generate_icon('car')")
|
||||
.map-menu-item(v-if="query.sub" @click="search_enable('other')" v-html="generate_icon('person-biking')")
|
||||
|
||||
.map-menu.map-menu-center
|
||||
div(v-if="query.note" @click="drawer_click_item()" )
|
||||
.map-menu-item(v-html="generate_icon('close')")
|
||||
div(v-if="!query.note" @click="search_enable('notes')")
|
||||
.map-menu-item( v-html="generate_icon('pencil')")
|
32
src/template/module/journey/map/travel.pug
Normal file
32
src/template/module/journey/map/travel.pug
Normal file
@ -0,0 +1,32 @@
|
||||
mixin flight_popup()
|
||||
l-popup(
|
||||
:options="{maxWidth:400, minWidth:300}"
|
||||
)
|
||||
h1.row.text-medium.text-center.text-uppercase {{ travel.id }}
|
||||
span.row.text-small.text-gray {{ travel.from }} - {{travel.to}}
|
||||
span(v-if="edit_active")
|
||||
.row.input(style="margin-bottom:0")
|
||||
textarea.col-12.col-sm-12.text-small(
|
||||
placeholder="",
|
||||
v-model="travel.notes",
|
||||
)
|
||||
span.row.text-small.text-dark(v-else) {{ travel.notes }}
|
||||
span(v-if="edit_active")
|
||||
.leaflet-popup-button-group(v-if="edit_active")
|
||||
a.text-gray(
|
||||
v-on:click.prevent="place_delete('flight',idx)"
|
||||
v-html="generate_icon('trash', 'NA')"
|
||||
)
|
||||
|
||||
div(v-for= "(travel, idx) in journey.leg_get().travel")
|
||||
l-polyline(:lat-lngs="travel.path" :color="travel.color || 'gray'")
|
||||
+flight_popup()
|
||||
|
||||
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')"
|
||||
)
|
||||
+flight_popup()
|
10
src/template/module/view/nav.pug
Normal file
10
src/template/module/view/nav.pug
Normal file
@ -0,0 +1,10 @@
|
||||
.row.fleft(style="position:absolute;right:0;")
|
||||
.col-1
|
||||
a(:href="'/short/' + journey.id")
|
||||
i.fas.fa-file-contract
|
||||
.col-1
|
||||
a(:href="'/view/' + journey.id")
|
||||
i.fas.fa-camera
|
||||
//- .col-1
|
||||
//- a(:href="'/' + journey.id" v-on:click.prevent="first_step")
|
||||
//- i.fas.fa-tools
|
39
src/template/module/view/short_leg.pug
Normal file
39
src/template/module/view/short_leg.pug
Normal file
@ -0,0 +1,39 @@
|
||||
.col-11.container.section
|
||||
.row.text-center.align.padding-1
|
||||
.input.col-5.col-sm-2
|
||||
input(disabled="", placeholder="Unnamed" :value="item.title")
|
||||
.col-sm-1
|
||||
.input.col-6.col-sm-4
|
||||
input(
|
||||
disabled="",
|
||||
placeholder="No Dates",
|
||||
:value="item.date_range ? item.date_range[0].toLocal() + ' - ' + item.date_range[1].toLocal() : ''"
|
||||
)
|
||||
.col-1.col-sm-2
|
||||
.input.col-5.col-sm-3.text-dark
|
||||
.text(disabled="" placeholder="No Hotel" :value="item.hotel?item.hotel.sname:''") {{item.hotel?item.hotel.sname:'No Hotel'}}
|
||||
//- .row.text-center
|
||||
.input.col-sm-3(v-if="item.travel")
|
||||
div(v-for="(item, idx) in item.travel")
|
||||
input(disabled="", placeholder="-" :value="item.map((v) => v.id).join(', ')")
|
||||
.row.align.padding-1
|
||||
.input.col-sm-10.text-dark
|
||||
.text-small(
|
||||
placeholder="No Restaurants",
|
||||
:value="item.places.restaurants.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ') || 'No Restaurants'",
|
||||
disabled=""
|
||||
) {{item.places.restaurants.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ') || 'No Restaurants'}}
|
||||
.row.align.padding-1
|
||||
.input.col-sm-10.text-dark
|
||||
.text-small(
|
||||
placeholder="No Activities",
|
||||
:value="item.places.activities.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ') || 'No Activites'",
|
||||
disabled=""
|
||||
) {{item.places.activities.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ') || 'No Activites'}}
|
||||
.row.align.padding-1
|
||||
.input.col-sm-10.text-dark
|
||||
.text-small(
|
||||
placeholder="No Notes",
|
||||
:value="item.notes || 'No Notes'",
|
||||
disabled=""
|
||||
) {{item.notes || 'No Notes'}}
|
19
src/template/module/view/view_day.pug
Normal file
19
src/template/module/view/view_day.pug
Normal 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
|
||||
.row
|
||||
.col-3.fleft.text-center.text-white.text-huge
|
||||
a(v-on:click.prevent="journey.day_prev()")
|
||||
i.fas.fa-angle-left
|
||||
.col-6.container.text-center.align
|
||||
span.small {{ journey.data.main[idx].title }} {{ journey.sel_day }}
|
||||
.text-big.text-gray {{ journey.data.main[idx].day_title[journey.sel_day] }}
|
||||
.col-3.fright.text-center.text-white.text-huge
|
||||
a(v-on:click.prevent="journey.day_next()")
|
||||
i.fas.fa-angle-right
|
||||
.row
|
||||
.col-12.col-sm-12(style="aspect-ratio:1.25;")
|
||||
include ../journey/map.pug
|
||||
.row
|
||||
.col-10
|
||||
span.small.text-gray {{journey.data.main[idx].note || '...'}}
|
16
src/template/short.pug
Normal file
16
src/template/short.pug
Normal file
@ -0,0 +1,16 @@
|
||||
doctype html
|
||||
include module/head.pug
|
||||
main#app(v-cloak)
|
||||
include module/view/nav.pug
|
||||
|
||||
.bg-dark.text-white(v-if="journey && journey.leg_get()")
|
||||
.container
|
||||
.row.align(style="padding-top:45px;")
|
||||
.col-7.col-sm-5.col-md-4.input.text-big
|
||||
input.text-center(v-model="journey.data.name" placeholder="My Journey" type="text" disabled)
|
||||
div(
|
||||
v-for="(item, idx) in journey.data.main",
|
||||
:class="idx % 2 === 0 ? 'bg-white text-dark' : 'bg-dark text-white'"
|
||||
)
|
||||
include module/view/short_leg.pug
|
||||
include module/foot.pug
|
6
src/template/view.pug
Normal file
6
src/template/view.pug
Normal file
@ -0,0 +1,6 @@
|
||||
doctype html
|
||||
include module/head.pug
|
||||
main#app(v-cloak)
|
||||
div(v-if="journey.leg_get()")
|
||||
include module/view/view_day.pug
|
||||
include module/foot.pug
|
@ -1,77 +0,0 @@
|
||||
doctype html
|
||||
|
||||
include module/head.pug
|
||||
main#app
|
||||
.container
|
||||
section.mb-big
|
||||
.text-center
|
||||
img.main-logo.mb-medium(
|
||||
src="/public/img/helcel.png",
|
||||
alt="Helcel logo"
|
||||
)
|
||||
div
|
||||
h1.text-huge.text-withSubtitle Open Tourism Map
|
||||
h2.text-big.text-gray Collaborative Holiday Planner
|
||||
p#js-header-waypoint.m-none
|
||||
a.button.button--primary.button--mobileFull(href="#go") Get started
|
||||
.bg-dark
|
||||
.container
|
||||
.row.text-center
|
||||
.col-12.col-sm-3
|
||||
.section
|
||||
img(
|
||||
src="/public/img/lightweight.png",
|
||||
alt="Lightweight",
|
||||
width="118"
|
||||
)
|
||||
br
|
||||
h2.text-withSubtitle.text-big.text-white
|
||||
| Lightweight
|
||||
br
|
||||
span.text-medium.text-gray
|
||||
| Powered By
|
||||
br
|
||||
| Fastify & Sierra
|
||||
.col-12.col-sm-4
|
||||
.section
|
||||
img(
|
||||
src="/public/img/customizable.png",
|
||||
alt="Customizable",
|
||||
width="118"
|
||||
)
|
||||
br
|
||||
h2.text-withSubtitle.text-big.text-white
|
||||
| Customizable
|
||||
br
|
||||
span.text-medium.text-gray
|
||||
| Many Templates
|
||||
br
|
||||
| to choose from
|
||||
.col-12.col-sm-4
|
||||
.section
|
||||
h2.text-withSubtitle.text-big.text-white
|
||||
img(
|
||||
src="/public/img/opensource.png",
|
||||
alt="Open Source",
|
||||
width="118"
|
||||
)
|
||||
br
|
||||
|
|
||||
| FOSS
|
||||
br
|
||||
span.text-medium.text-gray :-)
|
||||
#go.container-medium.section
|
||||
h2.text-big Your journey
|
||||
p
|
||||
| Browse hotels, restaurants and attractions,....
|
||||
br
|
||||
|
|
||||
| Select and plan the varying elements of your journey
|
||||
.aligner.aligner--contentEnd
|
||||
.input
|
||||
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
|
||||
|
||||
include module/foot.pug
|
@ -1,10 +0,0 @@
|
||||
doctype html
|
||||
|
||||
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/foot.pug
|
@ -1,26 +0,0 @@
|
||||
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/vuedraggable")
|
||||
script(src="/public/main.js", type="text/javascript", charset="utf-8")
|
||||
footer.bg-dark
|
||||
.container
|
||||
.section.text-center.text-small
|
||||
p.text-white
|
||||
img(src="/public/img/helcel.png", alt="helcel logo", width="100")
|
||||
br
|
||||
br
|
||||
|
|
||||
| Built with ❤ by Helcel
|
||||
br
|
||||
span.text-small.text-gray v0.0.1
|
||||
p.text-gray
|
||||
a(href="https://git.helcel.net") Helcel Git
|
@ -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")
|
@ -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"
|
||||
)
|
@ -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 }}
|
@ -1,38 +0,0 @@
|
||||
header.header
|
||||
.header-inner.container
|
||||
a.header-logo.text-dark(href="/")
|
||||
img.header-logoImage(
|
||||
src="/public/img/helcel.png",
|
||||
alt="Helcel logo",
|
||||
width="40"
|
||||
)
|
||||
span.hide-small OTM
|
||||
.input.input-invis.row
|
||||
input.col-6.small(v-model="journey_data.name", type="text")
|
||||
input.col-6.small(
|
||||
disabled,
|
||||
type="text",
|
||||
:placeholder="total_date() + ' (' + total_days() + ')'"
|
||||
)
|
||||
.row.header-nav.text-big(style="margin-bottom: 0")
|
||||
.col-sm-2
|
||||
a(:href="'/short/' + journey_id")
|
||||
i.fas.fa-file-contract
|
||||
.col-sm-2
|
||||
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")
|
||||
i.fas.fa-angle-double-left
|
||||
.col-sm-1
|
||||
a(href="#prev", v-on:click.prevent="prev_step")
|
||||
i.fas.fa-angle-left
|
||||
.col-sm-1
|
||||
a(href="#next", v-on:click.prevent="next_step")
|
||||
i.fas.fa-angle-right
|
||||
.col-sm-1.text-small
|
||||
a(href="#nextnext", v-on:click.prevent="nextnext_step")
|
||||
i.fas.fa-angle-double-right
|
@ -1,18 +0,0 @@
|
||||
header.header
|
||||
.header-inner.container
|
||||
a.header-logo.text-dark(href="/")
|
||||
img.header-logoImage(
|
||||
src="/public/img/helcel.png",
|
||||
alt="Helcel logo",
|
||||
width="40"
|
||||
)
|
||||
span.hide-small HOTM
|
||||
.input.input-invis
|
||||
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")
|
||||
i.fas.fa-file-contract
|
||||
.col-sm-3
|
||||
a(:href="'/view/' + journey_id")
|
||||
i.fas.fa-camera
|
@ -1,43 +0,0 @@
|
||||
.container.section
|
||||
.row.text-center
|
||||
.input.col-sm-2
|
||||
input(disabled="", :value="item.title")
|
||||
.input.col-sm-4
|
||||
input(
|
||||
disabled="",
|
||||
placeholder="No Dates",
|
||||
:value="item.dateRange ? format_date(item.dateRange[0]) + ' - ' + format_date(item.dateRange[1]) : ''"
|
||||
)
|
||||
.input.col-sm-2
|
||||
input(disabled="", placeholder="No Hotel", :value="item.hotel.sname")
|
||||
.row.text-center
|
||||
.input.col-sm-3(v-if="item.transit")
|
||||
div(v-for="(item, idx) in item.transit")
|
||||
input(disabled="", :value="item.map((v) => v.id).join(', ')")
|
||||
.row.text-center
|
||||
.input.col-sm-8(v-if="item.places && item.places.restaurants")
|
||||
textarea-autosize.text-small(
|
||||
placeholder="No Restaurants",
|
||||
:value="item.places.restaurants.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ')",
|
||||
:min-height="30",
|
||||
:max-height="350",
|
||||
disabled=""
|
||||
)
|
||||
.row.text-center
|
||||
.input.col-sm-8(v-if="item.places && item.places.activities")
|
||||
textarea-autosize.text-small(
|
||||
placeholder="No Activities",
|
||||
:value="item.places.activities.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ')",
|
||||
:min-height="30",
|
||||
:max-height="350",
|
||||
disabled=""
|
||||
)
|
||||
.row.text-center
|
||||
.input.col-sm-8(v-if="item.notes")
|
||||
textarea-autosize.text-small(
|
||||
placeholder="No Notes",
|
||||
:value="item.notes",
|
||||
:min-height="30",
|
||||
:max-height="350",
|
||||
disabled=""
|
||||
)
|
@ -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
|
@ -1,10 +0,0 @@
|
||||
doctype html
|
||||
include module/head.pug
|
||||
main#app(v-cloak)
|
||||
include module/nav_pub.pug
|
||||
div(
|
||||
v-for="(item, idx) in journey_data.main",
|
||||
:class="idx % 2 === 0 ? 'bg-dark text-white' : ''"
|
||||
)
|
||||
include module/short_sec.pug
|
||||
include module/foot.pug
|
@ -1,6 +0,0 @@
|
||||
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
|
||||
include module/foot.pug
|
17
tsconfig-client.json
Normal file
17
tsconfig-client.json
Normal 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",
|
||||
}
|
||||
}
|
@ -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",
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user