From ecf1be3f834c7187312f8eee71f77e448c995388 Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Tue, 22 Sep 2026 22:28:10 +0200 Subject: Check period selection --- server/src/api.cpp | 11 ++--- server/src/api.cppm | 3 +- server/src/geo/wgs84.cppm | 3 -- server/src/http_server.cppm | 18 +++---- server/src/srv.cpp | 113 +++++++++++++++++++++++++++++++++++++++++++- todo.org | 3 +- web/index.html | 55 +++++++++++++++++++-- web/index.js | 97 ++++++++++++++++++++++++++++++++----- web/style.css | 21 ++++++++ 9 files changed, 283 insertions(+), 41 deletions(-) diff --git a/server/src/api.cpp b/server/src/api.cpp index f1ca24e..9a51690 100644 --- a/server/src/api.cpp +++ b/server/src/api.cpp @@ -141,20 +141,15 @@ handler::handler(log::logger const& l, datex2::situation_publication pub) auto const dur_build_s = chrono::duration{after_build - before_build}; l_.info("Indexes built in {}", dur_build_s); l_.info("BLSE index size: {}", blse_index_.size()); - l_.info("Point index size: {}", bpe_index_.size()); + l_.info("BPE index size: {}", bpe_index_.size()); } -auto handler::process_gpx(trace::id trace_id, gpx::file&& gpx_file) +auto handler::process_gpx( + trace::id trace_id, gpx::file&& gpx_file, time::period_seq check_periods) -> std::optional { auto l = l_.with("trace_id", trace_id.as_string()); - auto const now = chrono::utc_clock::now(); - auto const relevant = std::initializer_list{ - time::period{now - chrono::days(7), now + chrono::days(7)} - }; - auto const check_periods = time::period_seq{relevant.begin(), relevant.end()}; - // TODO: eliminate use of overlap segments auto segments = std::vector{}; for (auto const& track : gpx_file.tracks) diff --git a/server/src/api.cppm b/server/src/api.cppm index b2744cb..b14bbcf 100644 --- a/server/src/api.cppm +++ b/server/src/api.cppm @@ -93,7 +93,8 @@ class handler public: explicit handler(log::logger const& l, datex2::situation_publication pub); - auto process_gpx(trace::id trace_id, gpx::file&& gpx_file) + auto process_gpx( + trace::id trace_id, gpx::file&& gpx_file, time::period_seq check_periods) -> std::optional; auto sysinfo() -> sysinfo; }; diff --git a/server/src/geo/wgs84.cppm b/server/src/geo/wgs84.cppm index b53a963..2212a92 100644 --- a/server/src/geo/wgs84.cppm +++ b/server/src/geo/wgs84.cppm @@ -10,9 +10,6 @@ namespace routemon::geo::wgs84 { using cs = bgeo::cs::geographic; using point = bgeo::model::point; -// TODO: guarantee that linestring provides non-static member -// auto reserve(std::size_t) -> void -// Perhaps better yet: guarantee that the backing container is a std::vector. using linestring = bgeo::model::linestring; using box = bgeo::model::box; diff --git a/server/src/http_server.cppm b/server/src/http_server.cppm index e70110e..c111118 100644 --- a/server/src/http_server.cppm +++ b/server/src/http_server.cppm @@ -300,6 +300,7 @@ template struct routed_ctx : InnerCtx { verb_set route_methods; + boost::urls::url target; }; template @@ -687,12 +688,12 @@ class router co_return problem_rsp(ctx, problem, keep_alive{false}); } - l_.with( - "trace_id", - static_cast(ctx).trace_id.as_string()) - .debug( - "Request targets {} {}", req_base.method_string(), - req_url.path()); + // l_.with( + // "trace_id", + // static_cast(ctx).trace_id.as_string()) + // .debug( + // "Request targets {} {}", req_base.method_string(), + // req_url.path()); auto mres = match(req_url.segments()); if (!mres) @@ -720,8 +721,9 @@ class router if (auto mhdl = mres->route_handlers->lookup(*mverb)) { - auto new_ctx = - routed_ctx{std::move(ctx), mres->allowed_methods()}; + auto new_ctx = routed_ctx{ + std::move(ctx), mres->allowed_methods(), std::move(req_url) + }; co_return co_await mhdl( std::move(new_ctx), r, mres->wildcard_matches); } diff --git a/server/src/srv.cpp b/server/src/srv.cpp index dc9c814..5de0fc5 100644 --- a/server/src/srv.cpp +++ b/server/src/srv.cpp @@ -30,7 +30,7 @@ namespace routemon::srv { class gpx_parse_error_category_impl : public std::error_category { public: - char const* name() const noexcept override { return "gpx_parse"; } + auto name() const noexcept -> char const* override { return "gpx_parse"; } auto message(int condition) const noexcept -> std::string override { @@ -330,12 +330,120 @@ struct raw_deflate_body }; }; +auto parse_timestamp(std::string_view in) -> std::optional +{ + auto res = time::timestamp{}; + auto is = std::istringstream{in}; + is >> std::chrono::parse("%Y-%m-%dT%H:%MZ", res); + return is.fail() ? std::nullopt : std::make_optional(res); +} + auto handler::handle_process_gpx(l0_ctx ctx, http::readable_request r) -> net::awaitable { auto l = l_.sub("handle_process_gpx").with("trace_id", ctx.trace_id.as_string()); + auto cp_begin = time::timestamp{}; + auto cp_end = time::timestamp{}; + + auto const params = ctx.target.params(); + if (auto it = params.find("cpbegin"); it != params.end()) + { + auto mts = parse_timestamp((*it).value); + if (!mts) + { + auto tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("Query parameter has bad value"), + .type_uri = + "https://routemon.fautchen.eu/problems/query-param-bad-value", + }; + co_return http::problem_rsp( + ctx, tpl.instantiate(), http::keep_alive{false}); + } + cp_begin = *mts; + + ++it; + if (it != params.end() && (*it).key == "cpbegin") + { + auto tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("Query parameter provided too many times"), + .type_uri = "https://routemon.fautchen.eu/problems/" + "query-param-too-many-occurrences", + }; + co_return http::problem_rsp( + ctx, tpl.instantiate(), http::keep_alive{false}); + } + } + else + { + auto tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("Required query parameter not provided"), + .type_uri = "https://routemon.fautchen.eu/problems/" + "required-query-param-not-provided", + }; + co_return http::problem_rsp( + ctx, tpl.instantiate(), http::keep_alive{false}); + } + if (auto it = params.find("cpend"); it != params.end()) + { + auto mts = parse_timestamp((*it).value); + if (!mts) + { + auto tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("Query parameter has bad value"), + .type_uri = + "https://routemon.fautchen.eu/problems/query-param-bad-value", + }; + co_return http::problem_rsp( + ctx, tpl.instantiate(), http::keep_alive{false}); + } + cp_end = *mts; + + ++it; + if (it != params.end() && (*it).key == "cpend") + { + auto tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("Query parameter provided too many times"), + .type_uri = "https://routemon.fautchen.eu/problems/" + "query-param-too-many-occurrences", + }; + co_return http::problem_rsp( + ctx, tpl.instantiate(), http::keep_alive{false}); + } + } + else + { + auto tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("Required query parameter not provided"), + .type_uri = "https://routemon.fautchen.eu/problems/" + "required-query-param-not-provided", + }; + co_return http::problem_rsp( + ctx, tpl.instantiate(), http::keep_alive{false}); + } + + if (cp_begin >= cp_end) + { + auto tpl = problem::tpl{ + .status = bhttp::status::bad_request, + .title = translate("Invalid check period provided"), + .type_uri = "https://routemon.fautchen.eu/problems/invalid-check-period", + }; + co_return http::problem_rsp( + ctx, + tpl.instantiate().set_detail(translate( + "Begin of check period must be before end of check period")), + http::keep_alive{false}); + } + auto cp = time::period{cp_begin, cp_end}; + auto gpx_file = gpx::file{}; try { @@ -378,7 +486,8 @@ auto handler::handle_process_gpx(l0_ctx ctx, http::readable_request r) // TODO: catch handler exceptions and return 500 when raised? // (keep-alive depends on whether whole request was read) - auto mres = inner_.process_gpx(ctx.trace_id, std::move(gpx_file)); + auto mres = inner_.process_gpx( + ctx.trace_id, std::move(gpx_file), time::period_seq{cp}); if (!mres) { auto tpl = problem::tpl{ diff --git a/todo.org b/todo.org index 1ad2bd7..fc3b062 100644 --- a/todo.org +++ b/todo.org @@ -1,3 +1,2 @@ - [ ] Expect: 100-continue support in HTTP server? - -- [ ] UTM zome segmentation maybe not working? +- [ ] Limit on (raw-zlib-inflated) body size? diff --git a/web/index.html b/web/index.html index 877da14..42a61ff 100644 --- a/web/index.html +++ b/web/index.html @@ -14,11 +14,57 @@
- - + +
+
+ Controleer werkzaamheden gepland in periode + + + + + + + + + + + +
+ + + + + +
+ + + + + +
+
- +
@@ -38,7 +84,7 @@

-
+
Meer informatie
@@ -46,6 +92,7 @@
+ diff --git a/web/index.js b/web/index.js index e8accf0..63292aa 100644 --- a/web/index.js +++ b/web/index.js @@ -7,6 +7,50 @@ closeButton.addEventListener("click", () => { dialog.close(); }); +function handlePeriodLimitPresetSelInput(selEl, inputEl) { + selEl.addEventListener("input", (event) => { + if (selEl.value === "custom") { + inputEl.classList.remove("hidden"); + } else { + inputEl.classList.add("hidden"); + } + }); +} + +function getPeriodLimitPresetSelInput(selEl, inputEl) { + const relativeDateTime = (durationArgs) => Temporal.Now.plainDateTimeISO().add(durationArgs); + + let durationArgs = {}; + switch (selEl.value) { + case "-1m": return relativeDateTime({ months: -1 }); + case "-2w": return relativeDateTime({ weeks: -2 }); + case "-1w": return relativeDateTime({ weeks: -1 }); + case "0": return relativeDateTime({ seconds: 0 }); + case "+1w": return relativeDateTime({ weeks: 1 }); + case "+2w": return relativeDateTime({ weeks: 2 }); + case "+1m": return relativeDateTime({ months: 1 }); + case "custom": return Temporal.PlainDateTime.from(inputEl.value); + } +} + +handlePeriodLimitPresetSelInput( + document.getElementById("check-period-begin-preset-select"), + document.getElementById("check-period-begin-input")); +handlePeriodLimitPresetSelInput( + document.getElementById("check-period-end-preset-select"), + document.getElementById("check-period-end-input")); + +window.addEventListener("load", (event) => { + const oneWeek = Temporal.Duration.from({ weeks: 1 }); + const now = Temporal.Now.plainDateTimeISO(); + document.getElementById("check-period-begin-input").value = now.subtract(oneWeek).toString({ + smallestUnit: "minute", + }); + document.getElementById("check-period-end-input").value = now.add(oneWeek).toString({ + smallestUnit: "minute", + }); +}); + let map = L.map("map"); L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { @@ -19,10 +63,27 @@ document.forms["gpx-upload-form"].addEventListener("submit", (event) => { event.preventDefault(); const gpxFileInput = document.getElementById("gpx-file-input"); if (gpxFileInput.files.length !== 1) { - alert("Voor de upload moet er exact één GPX-bestand zijn geselecteerd"); + showDialog({ + title: "Geen bestand geselecteerd", + detail: "Voor de upload moet er exact één GPX-bestand zijn geselecteerd", + moreInfoEl: null, + }); return; } - load(gpxFileInput.files[0]); + + const tzidHere = Temporal.Now.timeZoneId(); + const cpBegin = getPeriodLimitPresetSelInput(document.getElementById("check-period-begin-preset-select"), + document.getElementById("check-period-begin-input")).toZonedDateTime(tzidHere).toInstant(); + const cpEnd = getPeriodLimitPresetSelInput(document.getElementById("check-period-end-preset-select"), + document.getElementById("check-period-end-input")).toZonedDateTime(tzidHere).toInstant(); + + load({ + gpxFile: gpxFileInput.files[0], + checkPeriod: { + begin: cpBegin, + end: cpEnd, + }, + }); }); function el(name, attrs, ...children) { @@ -47,15 +108,22 @@ function tbl(rowcols) { )))); } -async function showProblem(rsp) { - const problem = await rsp.json(); +function showDialog({ title, detail, moreInfoEl }) { + document.getElementById("dialog-title").innerText = title; + document.getElementById("dialog-message").innerText = detail !== null ? detail : ""; + if (moreInfoEl !== null) { + document.getElementById("dialog-more-info").replaceChildren(moreInfoEl); + document.getElementById("dialog-more-info-container").classList.remove("hidden"); + } else { + document.getElementById("dialog-more-info").replaceChildren(); + document.getElementById("dialog-more-info-container").classList.add("hidden"); + } - document.getElementById("dialog-title").innerText = problem.title; - if (problem.detail) - document.getElementById("dialog-message").innerText = problem.detail; - else - document.getElementById("dialog-message").innerText = ""; dialog.showModal(); +} + +async function showProblem(rsp) { + const problem = await rsp.json(); let rows = [ [txt("HTTP-statuscode"), @@ -70,9 +138,12 @@ async function showProblem(rsp) { rows.push([txt("Instantie"), el("code", {}, txt(problem.instance))]); } - document.getElementById("dialog-more-info").replaceChildren(tbl(rows)); - return; + showDialog({ + title: problem.title, + detail: problem.detail ? problem.detail : null, + moreInfoEl: tbl(rows), + }); } async function getSysinfo() { @@ -101,7 +172,7 @@ async function getSysinfo() { document.getElementById("sysinfo-details").replaceChildren(tbl(rows)); } -async function load(gpxFile) { +async function load({ gpxFile, checkPeriod }) { let compressedParts = []; const compressedReader = gpxFile.stream().pipeThrough(new CompressionStream("deflate-raw")).getReader(); while (true) { @@ -113,7 +184,7 @@ async function load(gpxFile) { } } - const rsp = await fetch(apiBaseUrl + "/gpx", { + const rsp = await fetch(apiBaseUrl + `/gpx?cpbegin=${encodeURI(checkPeriod.begin.toString({ smallestUnit: "minute" }))}&cpend=${encodeURI(checkPeriod.end.toString({ smallestUnit: "minute" }))}`, { method: "POST", headers: { "Accept-Language": "nl-NL", diff --git a/web/style.css b/web/style.css index a96354c..49cc502 100644 --- a/web/style.css +++ b/web/style.css @@ -34,15 +34,32 @@ th, td { padding: 5px 5px; } +table.borderless, +table.borderless th, +table.borderless td { + border: none; +} + form[name="gpx-upload-form"] { z-index: 1000; display: flex; + flex-direction: column; gap: 16pt; position: absolute; top: 10px; right: 10px; } +form fieldset { + display: flex; + flex-direction: column; + gap: 5px; +} + +form[name="gpx-upload-form"] input[type="submit"] { + float: right; +} + #sysinfo { z-index: 1000; display: flex; @@ -60,3 +77,7 @@ form[name="gpx-upload-form"] { .help-cursor { cursor: help; } + +.hidden { + display: none; +} -- cgit v1.3