dev (#160)
Some checks failed
continuous-integration/drone/push Build is failing

Co-authored-by: sora-ext
Co-authored-by: soraefir
Reviewed-on: #160
This commit is contained in:
2025-03-02 01:09:29 +01:00
parent 977751d517
commit aee79aac75
67 changed files with 6060 additions and 5661 deletions

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

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

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

291
src/client/old.js Normal file
View 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,
},
},
});

94
src/client/types/ext.ts Normal file
View File

@ -0,0 +1,94 @@
// DATE EXTENTION
declare global {
interface Date {
toJSONLocal: () => string;
toLocal: () => string;
}
}
Date.prototype.toJSONLocal = function () {
function addZ(n: number): string {
return n <= 9 ? `0${n}` : `${n}`;
}
return [
this.getFullYear(),
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 {
interface Array<T> {
foldl<B>(f: (x: T, acc: B) => B, acc: B): B;
foldr<B>(f: (x: T, acc: B) => B, acc: B): B;
}
}
Array.prototype.foldr = function <T, B>(f: (x: T, acc: B) => B, acc: B): B {
return this.reverse().foldl(f, acc);
};
Array.prototype.foldl = function <T, B>(f: (x: T, acc: B) => B, acc: B): B {
for (let i = 0; i < this.length; i++) acc = f(this[i], acc);
return acc;
};
// STRING EXTENTION
declare global {
interface String {
toEncoded: () => String;
toDecoded: () => String;
}
}
String.prototype.toEncoded = function () {
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(decodeURIComponent(window.atob(this as string)), (c) => c.charCodeAt(0)).foldl(
(e, v) => v + String.fromCharCode(e),
"",
);
};
declare global {
interface StringConstructor {
gen_id: (l: Number) => String;
}
}
String.gen_id = function (length) {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return Array.from(Array(length))
.map((_v) =>
characters.charAt(Math.floor(Math.random() * characters.length)),
)
.join("");
};
export { };

View File

@ -0,0 +1,59 @@
declare global {
interface LatLng {
lat: number
lng: number
}
interface geoloc {
latlon: [number, number]
notes: string
step: -1
}
interface map {
zoom: number
center: LatLng
}
interface leg {
title: string
day_title: string[]
date_range: [Date, Date] | null
map: map
travel: unknown[]
hotel: geoloc | null
places: {
restaurants: geoloc[]
activities: geoloc[]
}
notes: string
}
interface journey {
fmt_ver: number
title: string
main: leg[]
}
}
const 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
View File

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

View File

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

144
src/client/types/wrapper.ts Normal file
View 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;