From 22bae347706e909d75a214ac9cd899333af4d4c4 Mon Sep 17 00:00:00 2001 From: Tobias Date: Mon, 2 Feb 2026 17:41:10 +0100 Subject: [PATCH 01/38] Refactor WayDetails by extracting sub components --- web/src/sidewalks/WayDetails.svelte | 357 ++---------------- .../way-details/CenterlineTagActions.svelte | 154 ++++++++ .../way-details/CurrentTagsTable.svelte | 35 ++ .../way-details/FootwayTagActions.svelte | 29 ++ web/src/sidewalks/way-details/Problems.svelte | 66 ++++ 5 files changed, 306 insertions(+), 335 deletions(-) create mode 100644 web/src/sidewalks/way-details/CenterlineTagActions.svelte create mode 100644 web/src/sidewalks/way-details/CurrentTagsTable.svelte create mode 100644 web/src/sidewalks/way-details/FootwayTagActions.svelte create mode 100644 web/src/sidewalks/way-details/Problems.svelte diff --git a/web/src/sidewalks/WayDetails.svelte b/web/src/sidewalks/WayDetails.svelte index 2b784f8..877f659 100644 --- a/web/src/sidewalks/WayDetails.svelte +++ b/web/src/sidewalks/WayDetails.svelte @@ -11,8 +11,12 @@ mutationCounter, refreshLoadingScreen, } from "../"; - import { Checkbox, Loading, QualitativeLegend } from "svelte-utils"; - import { kindLabels, siteColorRgba, type WayProps } from "./"; + import { Loading } from "svelte-utils"; + import { kindLabels, type WayProps } from "./"; + import Problems from "./way-details/Problems.svelte"; + import CenterlineTagActions from "./way-details/CenterlineTagActions.svelte"; + import FootwayTagActions from "./way-details/FootwayTagActions.svelte"; + import CurrentTagsTable from "./way-details/CurrentTagsTable.svelte"; let { pinnedWay, @@ -126,7 +130,6 @@ isSettingRight, ); - // Check if both left and right have the same value, convert to sidewalk:both const leftValue = tagMap.get("sidewalk:left"); const rightValue = tagMap.get("sidewalk:right"); if (leftValue && rightValue && leftValue === rightValue) { @@ -152,92 +155,21 @@ } } - // Get highlighted cell state - function getHighlightedCell( - normalized: { left?: string; right?: string; both?: string }, - row: "left" | "right", - column: "yes" | "no" | "separate", - ): "active" | "both-highlight" | null { - // For left/right rows - const value = normalized[row]; - if (value === column) { - return "active"; - } - - // Check if sidewalk:both is set - if so, highlight both left and right in lighter color - if (normalized.both && normalized.both === column) { - return "both-highlight"; - } - - return null; - } - - let footwayFixTagChoices = [ - [["footway", "sidewalk"]], - [["footway", "crossing"]], - ]; - - function getSortedTags( - tags: Record, - ): Array<[string, string]> { - const entries = Object.entries(tags); - const sidewalkTags = entries.filter(([key]) => - key.toLowerCase().startsWith("sidewalk"), - ); - const otherTags = entries.filter( - ([key]) => !key.toLowerCase().startsWith("sidewalk"), - ); - - // Sort sidewalk tags A-Z - sidewalkTags.sort(([a], [b]) => a.localeCompare(b)); - // Sort other tags A-Z - otherTags.sort(([a], [b]) => a.localeCompare(b)); - - // Return sidewalk tags first, then other tags - return [...sidewalkTags, ...otherTags] as Array<[string, string]>; - } - - function getTagsForShortcut(key: string): Array | null { - // Left: q=yes, a=no, y=separate - // Right: w=yes, s=no, x=separate - const shortcutMap: Record> = { - q: [["sidewalk:left", "yes"]], - a: [["sidewalk:left", "no"]], - y: [["sidewalk:left", "separate"]], - w: [["sidewalk:right", "yes"]], - s: [["sidewalk:right", "no"]], - x: [["sidewalk:right", "separate"]], - }; - return shortcutMap[key] || null; - } - - async function onKeyDown(e: KeyboardEvent) { - if (!pinnedWay.properties.kind.startsWith("Road")) { - // Handle footway shortcuts (old behavior) - if (pinnedWay.properties.tags.highway == "footway") { - let n = parseInt(e.key); - if (Number.isInteger(n) && n <= footwayFixTagChoices.length) { - await setTags(footwayFixTagChoices[n - 1]); - } - } - return; - } - - const tags = getTagsForShortcut(e.key.toLowerCase()); - if (tags) { - await setTags(tags); - } - } + const normalizedSidewalkTags = $derived( + $backend + ? (JSON.parse( + $backend.normalizeSidewalkTags(BigInt(pinnedWay.properties.id)), + ) as { left?: string; right?: string; both?: string }) + : { left: undefined, right: undefined, both: undefined }, + ); - -
- {#if pinnedWay.properties.problems.length} - {@const headerProblem = - pinnedWay.properties.problems.find( - (p) => - p.note === "possible separate sidewalk near way without it tagged", - ) || pinnedWay.properties.problems[0]} - {@const remainingProblems = pinnedWay.properties.problems.filter( - (p) => p.note !== headerProblem.note, - )} -
-
-
- -
-
- {headerProblem.note} -
-
- {#if remainingProblems.length} - {#each remainingProblems as problem} -

{problem.note}

- {/each} - {/if} - - {#if drawProblemDetails.features.length} - - Highlight problem on map - - - {#if showProblemDetails} - [ - f.properties.label, - f.properties.color, - ]), - )} - itemsPerRow={1} - /> - {/if} - {/if} -
- {/if} + {#if pinnedWay.properties.kind.startsWith("Road")} - {@const normalized = $backend - ? (JSON.parse( - $backend.normalizeSidewalkTags(BigInt(pinnedWay.properties.id)), - ) as { left?: string; right?: string; both?: string }) - : { left: undefined, right: undefined, both: undefined }} - - - - - - - - - - - - - - - - - - - - - -
- Left - - Right -
- - - -
- - - -
- - - -
+ {:else if pinnedWay.properties.tags.highway == "footway"} - Set these tags - - {#each footwayFixTagChoices.entries() as [idx, tags]} -
- -
- {/each} + {/if} - - - - - - - - - {#each getSortedTags(pinnedWay.properties.tags) as [key, value]} - - - - - {/each} - -
KeyValue
{key}{value}
+ {#if $debugMode}

Nodes: {pinnedWay.properties.node_ids.join(", ")}

{/if}
- - diff --git a/web/src/sidewalks/way-details/CenterlineTagActions.svelte b/web/src/sidewalks/way-details/CenterlineTagActions.svelte new file mode 100644 index 0000000..c0553e8 --- /dev/null +++ b/web/src/sidewalks/way-details/CenterlineTagActions.svelte @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
LeftRight
+ + + +
+ + + +
+ + + +
diff --git a/web/src/sidewalks/way-details/CurrentTagsTable.svelte b/web/src/sidewalks/way-details/CurrentTagsTable.svelte new file mode 100644 index 0000000..69ad857 --- /dev/null +++ b/web/src/sidewalks/way-details/CurrentTagsTable.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + + {#each getSortedTags(tags) as [key, value]} + + + + + {/each} + +
KeyValue
{key}{value}
diff --git a/web/src/sidewalks/way-details/FootwayTagActions.svelte b/web/src/sidewalks/way-details/FootwayTagActions.svelte new file mode 100644 index 0000000..3cfde32 --- /dev/null +++ b/web/src/sidewalks/way-details/FootwayTagActions.svelte @@ -0,0 +1,29 @@ + + + + +Set these tags + +{#each tagChoices.entries() as [idx, tags]} +
+ +
+{/each} diff --git a/web/src/sidewalks/way-details/Problems.svelte b/web/src/sidewalks/way-details/Problems.svelte new file mode 100644 index 0000000..34688a2 --- /dev/null +++ b/web/src/sidewalks/way-details/Problems.svelte @@ -0,0 +1,66 @@ + + +{#if problems.length} + {@const headerProblem = + problems.find( + (p) => p.note === "possible separate sidewalk near way without it tagged", + ) || problems[0]} + {@const remainingProblems = problems.filter( + (p) => p.note !== headerProblem.note, + )} +
+
+
+ +
+
+ {headerProblem.note} +
+
+ {#if remainingProblems.length} + {#each remainingProblems as problem} +

{problem.note}

+ {/each} + {/if} + + {#if drawProblemDetails.features.length} + + Highlight problem on map + + + {#if showProblemDetails} + [ + f.properties.label, + f.properties.color, + ]), + )} + itemsPerRow={1} + /> + {/if} + {/if} +
+{/if} + + From cd9dee03ff950115c8d317e7beb91f13cf202747 Mon Sep 17 00:00:00 2001 From: Tobias Date: Mon, 2 Feb 2026 17:53:39 +0100 Subject: [PATCH 02/38] WayDetails: Actions for sidepath like ways --- web/src/sidewalks/WayDetails.svelte | 12 +++- .../way-details/FootwayTagActions.svelte | 29 ---------- .../way-details/SidepathTagActions.svelte | 57 +++++++++++++++++++ 3 files changed, 66 insertions(+), 32 deletions(-) delete mode 100644 web/src/sidewalks/way-details/FootwayTagActions.svelte create mode 100644 web/src/sidewalks/way-details/SidepathTagActions.svelte diff --git a/web/src/sidewalks/WayDetails.svelte b/web/src/sidewalks/WayDetails.svelte index 877f659..52e9749 100644 --- a/web/src/sidewalks/WayDetails.svelte +++ b/web/src/sidewalks/WayDetails.svelte @@ -15,7 +15,7 @@ import { kindLabels, type WayProps } from "./"; import Problems from "./way-details/Problems.svelte"; import CenterlineTagActions from "./way-details/CenterlineTagActions.svelte"; - import FootwayTagActions from "./way-details/FootwayTagActions.svelte"; + import SidepathTagActions from "./way-details/SidepathTagActions.svelte"; import CurrentTagsTable from "./way-details/CurrentTagsTable.svelte"; let { @@ -194,8 +194,14 @@ {#if pinnedWay.properties.kind.startsWith("Road")} - {:else if pinnedWay.properties.tags.highway == "footway"} - + {:else if ["footway", "path", "cycleway"].includes(pinnedWay.properties.tags.highway ?? "")} + {/if} diff --git a/web/src/sidewalks/way-details/FootwayTagActions.svelte b/web/src/sidewalks/way-details/FootwayTagActions.svelte deleted file mode 100644 index 3cfde32..0000000 --- a/web/src/sidewalks/way-details/FootwayTagActions.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - - - -Set these tags - -{#each tagChoices.entries() as [idx, tags]} -
- -
-{/each} diff --git a/web/src/sidewalks/way-details/SidepathTagActions.svelte b/web/src/sidewalks/way-details/SidepathTagActions.svelte new file mode 100644 index 0000000..365cf51 --- /dev/null +++ b/web/src/sidewalks/way-details/SidepathTagActions.svelte @@ -0,0 +1,57 @@ + + + + +
    + {#each actions as action} +
  • + +
  • + {/each} +
From 406c776370cb0bcc75d11e9336bd86367cc79e0d Mon Sep 17 00:00:00 2001 From: Tobias Date: Mon, 2 Feb 2026 18:03:13 +0100 Subject: [PATCH 03/38] Improve CurrentTagsTable --- .../way-details/CurrentTagsTable.svelte | 61 ++++++++++++++----- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/web/src/sidewalks/way-details/CurrentTagsTable.svelte b/web/src/sidewalks/way-details/CurrentTagsTable.svelte index 69ad857..fe627f4 100644 --- a/web/src/sidewalks/way-details/CurrentTagsTable.svelte +++ b/web/src/sidewalks/way-details/CurrentTagsTable.svelte @@ -1,24 +1,48 @@ - - +
+ @@ -26,10 +50,19 @@ {#each getSortedTags(tags) as [key, value]} - - - + + + {/each}
Key Value
{key}{value}
{key}{value}
+ + From 9f2372eb55ea63549ba84c0f60a722fb1b8dca6b Mon Sep 17 00:00:00 2001 From: Tobias Date: Wed, 4 Feb 2026 07:18:45 +0100 Subject: [PATCH 04/38] Improve SidepathTagActions --- web/src/sidewalks/way-details/SidepathTagActions.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/sidewalks/way-details/SidepathTagActions.svelte b/web/src/sidewalks/way-details/SidepathTagActions.svelte index 365cf51..d22fdab 100644 --- a/web/src/sidewalks/way-details/SidepathTagActions.svelte +++ b/web/src/sidewalks/way-details/SidepathTagActions.svelte @@ -41,9 +41,9 @@ -
    +
      {#each actions as action} -
    • +
    • @@ -79,7 +204,10 @@ @@ -96,7 +224,10 @@ @@ -111,7 +242,10 @@ @@ -128,7 +262,12 @@ @@ -144,7 +283,12 @@ diff --git a/web/src/sidewalks/way-details/CurrentTagsTable.svelte b/web/src/sidewalks/way-details/CurrentTagsTable.svelte index 85fd7eb..6bb6d6c 100644 --- a/web/src/sidewalks/way-details/CurrentTagsTable.svelte +++ b/web/src/sidewalks/way-details/CurrentTagsTable.svelte @@ -2,9 +2,14 @@ let { tags, recentlyAddedTags = new Set(), + updateTags, }: { tags: Record; recentlyAddedTags?: Set; + updateTags?: ( + removeKeys: string[], + addTags: Array, + ) => Promise; } = $props(); // Order is important - tags are sorted by this order @@ -58,17 +63,25 @@ {#each getSortedTags(tags) as [key, value]} {@const isRecent = recentlyAddedTags.has(key)} - + {key} - {value} + {value} + {#if updateTags} + + {/if} {/each} diff --git a/web/src/sidewalks/way-details/SidepathTagActions.svelte b/web/src/sidewalks/way-details/SidepathTagActions.svelte index d22fdab..e69e6d7 100644 --- a/web/src/sidewalks/way-details/SidepathTagActions.svelte +++ b/web/src/sidewalks/way-details/SidepathTagActions.svelte @@ -1,31 +1,48 @@ @@ -43,15 +63,25 @@
        {#each actions as action} + {@const relevantRemovals = getRelevantRemovals(action.removes)}
      • + {#if relevantRemovals.length > 0} +
        + Removes: {relevantRemovals.join(", ")} +
        + {/if}
      • {/each}
      From 853a7155a1b6b2d20607efb504ca88d8b710af34 Mon Sep 17 00:00:00 2001 From: "Tobias (FMC)" Date: Thu, 5 Mar 2026 14:24:03 +0100 Subject: [PATCH 07/38] Rename refresh button to signal "refresh" --- web/src/common/LoadAnotherArea.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/common/LoadAnotherArea.svelte b/web/src/common/LoadAnotherArea.svelte index 5cc303b..f78ff04 100644 --- a/web/src/common/LoadAnotherArea.svelte +++ b/web/src/common/LoadAnotherArea.svelte @@ -124,5 +124,5 @@ From 9571ed919a1b6824057af11c930a932ad3ba0ec0 Mon Sep 17 00:00:00 2001 From: "Tobias (FMC)" Date: Thu, 5 Mar 2026 14:24:23 +0100 Subject: [PATCH 08/38] Rename "Disconnections" button Make it one wort so it fits with the others --- web/src/common/NavBar.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/common/NavBar.svelte b/web/src/common/NavBar.svelte index 721dfda..cc419e4 100644 --- a/web/src/common/NavBar.svelte +++ b/web/src/common/NavBar.svelte @@ -10,7 +10,7 @@ let mainActions = [ [{ kind: "sidewalks" }, "Sidewalks"], [{ kind: "crossings" }, "Crossings"], - [{ kind: "disconnections" }, "Network disconnections"], + [{ kind: "disconnections" }, "Disconnections"], [{ kind: "export" }, "Export"], ] as [Mode, string][]; From 6214b411934ed58acbc18b44c9262c366c9d3183 Mon Sep 17 00:00:00 2001 From: "Tobias (FMC)" Date: Thu, 5 Mar 2026 14:24:59 +0100 Subject: [PATCH 09/38] Jumbotron for each Page Prominent UI to explain the current page --- web/src/DisconnectionsMode.svelte | 15 ++- web/src/ExportMode.svelte | 43 ++++--- web/src/common/Jumbotron.svelte | 28 ++++ web/src/common/NetworkFilter.svelte | 70 +++++----- web/src/crossings/AuditCrossingsMode.svelte | 136 ++++++++++---------- web/src/sidewalks/SidewalksMode.svelte | 6 + 6 files changed, 171 insertions(+), 127 deletions(-) create mode 100644 web/src/common/Jumbotron.svelte diff --git a/web/src/DisconnectionsMode.svelte b/web/src/DisconnectionsMode.svelte index 828158e..447b4a1 100644 --- a/web/src/DisconnectionsMode.svelte +++ b/web/src/DisconnectionsMode.svelte @@ -11,6 +11,7 @@ import { constructMatchExpression, emptyGeojson } from "svelte-utils/map"; import { backend, map, prettyPrintDistance, networkFilter } from "./"; import NetworkFilter from "./common/NetworkFilter.svelte"; + import Jumbotron from "./common/Jumbotron.svelte"; let gj = $derived( $backend @@ -57,13 +58,15 @@ {#snippet left()} -

      Network disconnections

      + + + -

      - This shows where the network is disconnected. Click a piece to see it. -

      - - +

      Disconnected Networks

      +

      Click to highlight the selected subnetwork.

        {#each gj.component_lengths as length, idx} diff --git a/web/src/ExportMode.svelte b/web/src/ExportMode.svelte index cbeeef8..82877e0 100644 --- a/web/src/ExportMode.svelte +++ b/web/src/ExportMode.svelte @@ -1,11 +1,18 @@ + +
        +

        {title}

        + {#if lead} +

        {lead}

        + {/if} + {#if children} +
        + {@render children?.()} +
        + {/if} +
        + + diff --git a/web/src/common/NetworkFilter.svelte b/web/src/common/NetworkFilter.svelte index da108ea..d7ca5af 100644 --- a/web/src/common/NetworkFilter.svelte +++ b/web/src/common/NetworkFilter.svelte @@ -3,44 +3,44 @@ import { Checkbox } from "svelte-utils"; -
        -
        -
        - -
        +
        +
        + +
        -
        - -
        +
        + +
        -
        - -
        +
        + +
        +
        Ignore dead ends under 10m and disconnected segments under 100m diff --git a/web/src/crossings/AuditCrossingsMode.svelte b/web/src/crossings/AuditCrossingsMode.svelte index 02bacad..30e54df 100644 --- a/web/src/crossings/AuditCrossingsMode.svelte +++ b/web/src/crossings/AuditCrossingsMode.svelte @@ -13,7 +13,7 @@ import type { Feature, FeatureCollection } from "geojson"; import { emptyGeojson } from "svelte-utils/map"; import CollapsibleCard from "../common/CollapsibleCard.svelte"; - import BulkOperations from "./BulkOperations.svelte"; + import Jumbotron from "../common/Jumbotron.svelte"; import { getMapViewport, getIdUrl } from "../common/osmEditorUrls"; import type { MapGeoJSONFeature } from "maplibre-gl"; @@ -87,12 +87,26 @@ {#snippet left()} -

        Crossings audit

        - -

        - {completeJunctions.toLocaleString()} / {data.features.length.toLocaleString()} - junctions have all possible crossings mapped -

        + +

        + For each junction shown, this tool looks for crossing nodes on each arm + (road) of the junction. Please map a crossing node on each arm by + clicking to open in iD, then refreshing data here to check. If there's + no way to cross an arm, use + crossing=no + + to indicate a lack of a crossing. Please ignore cases where you would not + expect any crossing to be (and report a bug to improve this tool). And note + that there might be mid-block crossings anywhere along a road; this tool only + audits junctions. +

        +
        {#if hovered}

        @@ -118,24 +132,46 @@

        {/if} -
        - -

        - For each junction shown, this tool looks for crossing nodes on each arm - (road) of the junction. Please map a crossing node on each arm by clicking - to open in iD, then refreshing data here to check. If there's no way to - cross an arm, use - crossing=no - - to indicate a lack of a crossing. Please ignore cases where you would not expect - any crossing to be (and report a bug to improve this tool). And note that there - might be mid-block crossings anywhere along a road; this tool only audits junctions. -

        - - + + {#snippet header()}Settings{/snippet} + {#snippet body()} + + Only junctions on major roads + + + Ignore service + , + track + roads + + + Ignore cycleways + + + Ignore footway + and + path + + + Don't expect crossings on roundabouts + + + Don't expect crossings on motorways + +
        + +
        + {/snippet} +
        {/snippet} {#snippet main()} @@ -230,51 +266,13 @@ - {#snippet header()}Settings{/snippet} + {#snippet header()}Legend{/snippet} {#snippet body()} - - Only junctions on major roads - - - Ignore service - , - track - roads - - - Ignore cycleways - - - Ignore footway - and - path - - - Don't expect crossings on roundabouts - - - Don't expect crossings on motorways - -
        - -
        - -
        - -
        + {/snippet}
        diff --git a/web/src/sidewalks/SidewalksMode.svelte b/web/src/sidewalks/SidewalksMode.svelte index 45a34b7..042b14d 100644 --- a/web/src/sidewalks/SidewalksMode.svelte +++ b/web/src/sidewalks/SidewalksMode.svelte @@ -3,6 +3,7 @@ import ProblemControls from "./ProblemControls.svelte"; import ProblemLayer from "./ProblemLayer.svelte"; import CollapsibleCard from "../common/CollapsibleCard.svelte"; + import Jumbotron from "../common/Jumbotron.svelte"; import Edits from "./Edits.svelte"; import BulkOperations from "./BulkOperations.svelte"; import { @@ -114,6 +115,11 @@ {#snippet left()} + + From 9702dc22bfd65fc4deba5d2723c6400b066a86f7 Mon Sep 17 00:00:00 2001 From: "Tobias (FMC)" Date: Thu, 5 Mar 2026 14:25:30 +0100 Subject: [PATCH 10/38] Change default for Disconnected page to Routable Network --- web/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/index.ts b/web/src/index.ts index cbfa76d..4e2f072 100644 --- a/web/src/index.ts +++ b/web/src/index.ts @@ -31,7 +31,7 @@ export let networkFilter = writable<{ include: "Everything" | "OnlyExplicitFootways" | "RouteableNetwork"; ignore_deadends: boolean; }>({ - include: "OnlyExplicitFootways", + include: "RouteableNetwork", ignore_deadends: true, }); From ac037aa8a7f8ed3e0e5c23c3fc6b02a1e3bf0aa1 Mon Sep 17 00:00:00 2001 From: "Tobias (FMC)" Date: Thu, 5 Mar 2026 15:09:33 +0100 Subject: [PATCH 11/38] Introduce "Generator" page to consolidate actions --- web/src/App.svelte | 3 + web/src/common/NavBar.svelte | 1 + .../generator/GeneratorBulkOperations.svelte | 157 ++++++++++++++++++ web/src/generator/GeneratorMode.svelte | 89 ++++++++++ web/src/index.ts | 1 + web/src/sidewalks/SidewalksMode.svelte | 3 - web/src/sidewalks/WayDetails.svelte | 2 - web/src/sidewalks/index.ts | 1 + 8 files changed, 252 insertions(+), 5 deletions(-) create mode 100644 web/src/generator/GeneratorBulkOperations.svelte create mode 100644 web/src/generator/GeneratorMode.svelte diff --git a/web/src/App.svelte b/web/src/App.svelte index fe2ce98..485cebb 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -18,6 +18,7 @@ import AuditCrossingsMode from "./crossings/AuditCrossingsMode.svelte"; import DisconnectionsMode from "./DisconnectionsMode.svelte"; import ExportMode from "./ExportMode.svelte"; + import GeneratorMode from "./generator/GeneratorMode.svelte"; import StudyAreaFade from "./common/StudyAreaFade.svelte"; import NavBar from "./common/NavBar.svelte"; @@ -134,6 +135,8 @@ {:else if $mode.kind == "disconnections"} + {:else if $mode.kind == "generator"} + {:else if $mode.kind == "export"} {/if} diff --git a/web/src/common/NavBar.svelte b/web/src/common/NavBar.svelte index cc419e4..7d1a2c1 100644 --- a/web/src/common/NavBar.svelte +++ b/web/src/common/NavBar.svelte @@ -11,6 +11,7 @@ [{ kind: "sidewalks" }, "Sidewalks"], [{ kind: "crossings" }, "Crossings"], [{ kind: "disconnections" }, "Disconnections"], + [{ kind: "generator" }, "Generator"], [{ kind: "export" }, "Export"], ] as [Mode, string][]; diff --git a/web/src/generator/GeneratorBulkOperations.svelte b/web/src/generator/GeneratorBulkOperations.svelte new file mode 100644 index 0000000..7cd62ba --- /dev/null +++ b/web/src/generator/GeneratorBulkOperations.svelte @@ -0,0 +1,157 @@ + + + + +
        +
        Generate crossings
        +
        + + + +
        +
        + +
        +
        Assume old-style tags on one-ways
        +
        + Drive on the left + +
        +
        + +
        +
        Make all sidewalks
        +
        + Only for major roads + +
        +
        + +
        +
        Connect all crossing nodes
        +
        + Include crossing=no + +
        +
        diff --git a/web/src/generator/GeneratorMode.svelte b/web/src/generator/GeneratorMode.svelte new file mode 100644 index 0000000..bc82b6d --- /dev/null +++ b/web/src/generator/GeneratorMode.svelte @@ -0,0 +1,89 @@ + + + + + + {#snippet left()} + + + + {/snippet} + + {#snippet main()} + + + + + + + + {/snippet} + diff --git a/web/src/index.ts b/web/src/index.ts index 4e2f072..f558184 100644 --- a/web/src/index.ts +++ b/web/src/index.ts @@ -17,6 +17,7 @@ export type Mode = | { kind: "sidewalks" } | { kind: "crossings" } | { kind: "disconnections" } + | { kind: "generator" } | { kind: "export" }; export let mode: Writable = writable({ kind: "sidewalks" }); diff --git a/web/src/sidewalks/SidewalksMode.svelte b/web/src/sidewalks/SidewalksMode.svelte index 042b14d..5bffc6f 100644 --- a/web/src/sidewalks/SidewalksMode.svelte +++ b/web/src/sidewalks/SidewalksMode.svelte @@ -5,7 +5,6 @@ import CollapsibleCard from "../common/CollapsibleCard.svelte"; import Jumbotron from "../common/Jumbotron.svelte"; import Edits from "./Edits.svelte"; - import BulkOperations from "./BulkOperations.svelte"; import { backend, mutationCounter, @@ -127,8 +126,6 @@ {#if pinnedWay} {/if} - - {/snippet} {#snippet main()} diff --git a/web/src/sidewalks/WayDetails.svelte b/web/src/sidewalks/WayDetails.svelte index f604794..bf268c0 100644 --- a/web/src/sidewalks/WayDetails.svelte +++ b/web/src/sidewalks/WayDetails.svelte @@ -63,8 +63,6 @@ } } - - const normalizedSidewalkTags = $derived( $backend ? (JSON.parse( diff --git a/web/src/sidewalks/index.ts b/web/src/sidewalks/index.ts index 9dbe309..3538ca2 100644 --- a/web/src/sidewalks/index.ts +++ b/web/src/sidewalks/index.ts @@ -7,6 +7,7 @@ export interface NodeProps { tags?: Record; is_crossing: boolean; is_explicit_crossing_no: boolean; + is_generated_crossing?: boolean; modified: boolean; way_ids: number[]; problems: Problem[]; From fb87a901e03dbd630190765919b7bda47cc64e03 Mon Sep 17 00:00:00 2001 From: "Tobias (FMC)" Date: Thu, 5 Mar 2026 16:49:56 +0100 Subject: [PATCH 12/38] Jumbotron: Update wording Streamline wording and make it more compact. --- web/src/DisconnectionsMode.svelte | 4 ++-- web/src/ExportMode.svelte | 6 +++--- web/src/crossings/AuditCrossingsMode.svelte | 18 ++++++++---------- web/src/sidewalks/SidewalksMode.svelte | 4 ++-- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/web/src/DisconnectionsMode.svelte b/web/src/DisconnectionsMode.svelte index 447b4a1..d4cc334 100644 --- a/web/src/DisconnectionsMode.svelte +++ b/web/src/DisconnectionsMode.svelte @@ -59,8 +59,8 @@ {#snippet left()} diff --git a/web/src/ExportMode.svelte b/web/src/ExportMode.svelte index 82877e0..f4a5d64 100644 --- a/web/src/ExportMode.svelte +++ b/web/src/ExportMode.svelte @@ -7,7 +7,7 @@ LineLayer, Control, } from "svelte-maplibre"; - import { downloadGeneratedFile, QualitativeLegend } from "svelte-utils"; + import { downloadGeneratedFile, ColorLegend } from "svelte-utils"; import { SplitComponent } from "svelte-utils/top_bar_layout"; import { constructMatchExpression, emptyGeojson } from "svelte-utils/map"; import { backend, networkFilter, prettyPrintDistance } from "./"; @@ -30,7 +30,7 @@ {#snippet left()} + + + {#snippet header()} + In your current region: {inRegionSegments.length} in storage, {appliedCount} + applied + {/snippet} + {#snippet body()} + {#if notAppliedList.length > 0} +
        Could not apply
        +
          + {#each notAppliedList as seg} +
        • + + {seg.start.lat.toFixed(4)},{seg.start.lng.toFixed(4)} → {seg.end.lat.toFixed( + 4, + )},{seg.end.lng.toFixed(4)} + + + +
        • + {/each} +
        + {/if} + {#if appliedList.length > 0} +
        Applied
        +
          + {#each appliedList as seg} +
        • + + {seg.start.lat.toFixed(4)},{seg.start.lng.toFixed(4)} → {seg.end.lat.toFixed( + 4, + )},{seg.end.lng.toFixed(4)} + + + +
        • + {/each} +
        + {/if} + {#if inRegionSegments.length === 0} +

        + {#if overrides.addedCrossings.length === 0} + No manual crossings yet. + {:else} + No overwrites in this region ({overrides.addedCrossings.length} total + in storage). + {/if} +

        + {/if} + {/snippet} +
        + +
        + + +
        + {/snippet} + + {#snippet main()} + + + {#if pointA || pointB} + + + + {/if} + {#if pointA && pointB} + + + + {/if} + + {#if $backend} + + + + + + + {/if} + + + + {#snippet header()}Legend{/snippet} + {#snippet body()} + + {/snippet} + + + + {#if pointA && pointB} +
        + Red = left, blue = right. Click again to move left or right point. Press + a + + to add crossing. +
        + {:else if pointA} +
        + First point set (red, left). Click for second point (blue, right). +
        + {/if} + {/snippet} +
        diff --git a/web/src/sidewalks/index.ts b/web/src/sidewalks/index.ts index e88782f..9b64849 100644 --- a/web/src/sidewalks/index.ts +++ b/web/src/sidewalks/index.ts @@ -11,6 +11,7 @@ export interface NodeProps { is_crossing: boolean; is_explicit_crossing_no: boolean; is_generated_crossing?: boolean; + is_manual_crossing?: boolean; modified: boolean; way_ids: number[]; problems: Problem[]; @@ -33,6 +34,7 @@ export interface WayProps { node_ids: number[]; is_severance: boolean; is_service: boolean; + is_manual_crossing?: boolean; problems: Problem[]; } From e7228235c5b888e22c3585936c9ca719e7b5de4d Mon Sep 17 00:00:00 2001 From: "Tobias (FMC)" Date: Fri, 6 Mar 2026 16:07:24 +0100 Subject: [PATCH 32/38] Editing: Expand the "removes" tags --- .../way-details/SidepathTagActions.svelte | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/web/src/sidewalks/way-details/SidepathTagActions.svelte b/web/src/sidewalks/way-details/SidepathTagActions.svelte index 132d471..5b6f730 100644 --- a/web/src/sidewalks/way-details/SidepathTagActions.svelte +++ b/web/src/sidewalks/way-details/SidepathTagActions.svelte @@ -24,29 +24,57 @@ switch (highway) { case "path": return [ - { shortcut: "s", tags: [["is_sidepath", "yes"]], removes: [] }, - { shortcut: "n", tags: [["is_sidepath", "no"]], removes: [] }, - { shortcut: "c", tags: [["path", "crossing"]], removes: [] }, + { + shortcut: "s", + tags: [["is_sidepath", "yes"]], + removes: ["path"], + }, + { + shortcut: "n", + tags: [["is_sidepath", "no"]], + removes: ["path"], + }, + { + shortcut: "c", + tags: [["path", "crossing"]], + removes: ["is_sidepath"], + }, ]; case "footway": return [ { shortcut: "s", tags: [["footway", "sidewalk"]], - removes: ["is_sidepath"], + removes: ["is_sidepath", "footway"], }, { shortcut: "n", tags: [["is_sidepath", "no"]], - removes: ["footway"], + removes: ["footway", "footway"], + }, + { + shortcut: "c", + tags: [["footway", "crossing"]], + removes: ["is_sidepath"], }, - { shortcut: "c", tags: [["footway", "crossing"]], removes: [] }, ]; case "cycleway": return [ - { shortcut: "s", tags: [["is_sidepath", "yes"]], removes: [] }, - { shortcut: "n", tags: [["is_sidepath", "no"]], removes: [] }, - { shortcut: "c", tags: [["cycleway", "crossing"]], removes: [] }, + { + shortcut: "s", + tags: [["is_sidepath", "yes"]], + removes: ["cycleway"], + }, + { + shortcut: "n", + tags: [["is_sidepath", "no"]], + removes: ["cycleway"], + }, + { + shortcut: "c", + tags: [["cycleway", "crossing"]], + removes: ["is_sidepath"], + }, ]; } }); From a207df0fb64b94a048ff5b25769d32d8cbebcf2e Mon Sep 17 00:00:00 2001 From: "Tobias (FMC)" Date: Fri, 6 Mar 2026 16:49:36 +0100 Subject: [PATCH 33/38] Share network filter between export and overwrite pages Using localstorage --- web/src/ExportMode.svelte | 7 ++-- web/src/common/FilterNetworkCard.svelte | 16 ++++++++ web/src/index.ts | 13 ++++--- web/src/overwrites/OverwritesMode.svelte | 47 +++++++++++++++++++----- 4 files changed, 65 insertions(+), 18 deletions(-) create mode 100644 web/src/common/FilterNetworkCard.svelte diff --git a/web/src/ExportMode.svelte b/web/src/ExportMode.svelte index c8b9898..8a411d5 100644 --- a/web/src/ExportMode.svelte +++ b/web/src/ExportMode.svelte @@ -13,8 +13,8 @@ import { constructMatchExpression, emptyGeojson } from "svelte-utils/map"; import { backend, networkFilter, prettyPrintDistance } from "./"; import CollapsibleCard from "./common/CollapsibleCard.svelte"; + import FilterNetworkCard from "./common/FilterNetworkCard.svelte"; import Jumbotron from "./common/Jumbotron.svelte"; - import NetworkFilter from "./common/NetworkFilter.svelte"; let gj = $derived( $backend @@ -33,12 +33,13 @@ title="Export network" lead="Export the routeable walking network as GeoJSON. Choose what to include, then download." > -
        + + {#snippet header()}Details{/snippet} {#snippet body()} diff --git a/web/src/common/FilterNetworkCard.svelte b/web/src/common/FilterNetworkCard.svelte new file mode 100644 index 0000000..90a416b --- /dev/null +++ b/web/src/common/FilterNetworkCard.svelte @@ -0,0 +1,16 @@ + + + + {#snippet header()} + + Filter network + + {/snippet} + {#snippet body()} + + {/snippet} + diff --git a/web/src/index.ts b/web/src/index.ts index 044f833..73f1e78 100644 --- a/web/src/index.ts +++ b/web/src/index.ts @@ -29,13 +29,14 @@ export let enabledBulkOps = localStorageStore( ); export let debugMode = writable(false); -export let networkFilter = writable<{ - include: "Everything" | "OnlyExplicitFootways" | "RouteableNetwork"; - ignore_deadends: boolean; -}>({ - include: "RouteableNetwork", +const defaultNetworkFilter = { + include: "RouteableNetwork" as const, ignore_deadends: true, -}); +}; +export let networkFilter = localStorageStore( + "speedwalk-networkFilter", + defaultNetworkFilter, +); // TODO Upstream several of these export function sum(list: number[]): number { diff --git a/web/src/overwrites/OverwritesMode.svelte b/web/src/overwrites/OverwritesMode.svelte index 493fba2..944a8f2 100644 --- a/web/src/overwrites/OverwritesMode.svelte +++ b/web/src/overwrites/OverwritesMode.svelte @@ -6,7 +6,8 @@ type RegionOverrides, type AddedCrossingSegment, } from "../common/localOverrides"; - import { backend, map, mutationCounter, refreshLoadingScreen } from "../"; + import { backend, map, mutationCounter, networkFilter, refreshLoadingScreen } from "../"; + import { emptyGeojson } from "svelte-utils/map"; import { GeoJSON, LineLayer, @@ -18,6 +19,7 @@ import { SplitComponent } from "svelte-utils/top_bar_layout"; import Jumbotron from "../common/Jumbotron.svelte"; import CollapsibleCard from "../common/CollapsibleCard.svelte"; + import FilterNetworkCard from "../common/FilterNetworkCard.svelte"; import LegendList from "../common/LegendList.svelte"; import { downloadGeneratedFile } from "svelte-utils"; import type { FeatureCollection, LineString, Point } from "geojson"; @@ -43,17 +45,12 @@ type: "FeatureCollection", features: [], }); - let ways: FeatureCollection = $state.raw({ - type: "FeatureCollection", - features: [], - }); $effect(() => { $mutationCounter; if ($backend) { try { nodes = JSON.parse($backend.getNodes()); - ways = JSON.parse($backend.getWays()); } catch (_) {} } }); @@ -84,6 +81,36 @@ } }); + /** Filtered network (respects Filter network options); used for the map line layer. */ + const filteredNetworkGeoJSON = $derived.by(() => { + if (!$backend) return emptyGeojson(); + try { + const f = $networkFilter; + return JSON.parse($backend.exportNetwork(f)); + } catch { + return emptyGeojson(); + } + }); + + /** Nodes that appear as endpoints of edges in the filtered network, or are manual crossings (always show those). */ + const filteredNodesGeoJSON = $derived.by(() => { + const net = filteredNetworkGeoJSON; + const fc = nodes; + const ids = new Set(); + if (net.features?.length) { + for (const edge of net.features) { + const p = edge.properties as { node1?: number; node2?: number } | undefined; + if (p?.node1 != null) ids.add(p.node1); + if (p?.node2 != null) ids.add(p.node2); + } + } + const features = fc.features.filter((f) => { + const p = f.properties as { id?: number; is_manual_crossing?: boolean } | undefined; + return ids.has(p?.id as number) || p?.is_manual_crossing === true; + }); + return { type: "FeatureCollection" as const, features }; + }); + const crossingWayTags = { highway: "footway", footway: "crossing", @@ -351,6 +378,8 @@

        + + {#if !$backend}