Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions MODS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2108,13 +2108,22 @@ data is flat shows no dip and no bridge.
module envelope — the same extract a hand-downloaded Overpass Turbo query returns, so a
file picked with the dialog works too. The OSM class decides what the road is made of,
and the mapper's own tags win where they say more: `surface=*` over the preset's
surface, `width=*`/`lanes=*` over its width, `oneway=yes` and `junction=roundabout` take
the centre line out (a divided road is two one-way carriageways, and an Autobahn reads as
surface — and where there is none, a field track's `tracktype=*` (`grade1` is solid and
reads as asphalt, everything below it is loose and reads as gravel) — `width=*`/`lanes=*`
over its width, `oneway=yes` and
`junction=roundabout` take the centre line out (a divided road is two one-way carriageways, and an Autobahn reads as
two of these rather than as one striped one), and any `bridge=*` but `no` flags the way as
flying. The
dialog's two checkboxes opt the many-and-thin classes in:
**field tracks** (`highway=track` — what an agricultural module is stitched with) and
**access ways** (`service`, `living_street`, `pedestrian`). Nothing is written before
**access ways** (`service`, `living_street`, `pedestrian`). `highway=track` comes in
**unpaved**: the class says what a way is used for, not what it is built of, and the
German ones are gravel far more often than concrete — the paved ones say so themselves
(`surface=*`, `tracktype=grade1`), and only those come out paved — concrete slabs only
where a mapper wrote `surface=concrete*`, because that is how rare they are next to the
asphalt ones. An unpaved
carriageway carries no markings at all, whatever its class would have painted: nobody
draws a Randlinie on gravel. Nothing is written before
the summary's Commit, and Commit is one undo step. `import-module --tracks --narrow`
runs the same query and the same filters headless (without the flags it takes the
dialog's defaults, and it replaces the road list — the module is being rebuilt).
Expand All @@ -2131,6 +2140,7 @@ dialog's defaults, and it replaces the road list — the module is being rebuilt
| Anliegerstraße | 4.5 m | edge lines only |
| Spielstraße | 3.0 m | none |
| Wirtschaftsweg (asphalt) | 3.5 m | none |
| Feldweg (gravel) | 3.0 m | none — what `highway=track` imports as |
| Feldweg (concrete slabs) | 3.0 m | none |
| Fußweg | 2.0 m | none |

Expand All @@ -2139,13 +2149,14 @@ not carry: clicks lay the centre line, Enter or a right click paves it, the pres
and the width decide what it is made of, and a click on a drawn road takes it. The
panel edits width, surface and markings of the selected road afterwards.

The look is **the program's, not the module's**: two surface scans (asphalt, concrete
ambientCG, CC0, see `THIRD_PARTY_LICENSES.md`), their normal maps for the grain, and the
The look is **the program's, not the module's**: three surface scans (asphalt, concrete,
gravel — ambientCG, CC0, see `THIRD_PARTY_LICENSES.md`), their normal maps for the grain, and the
markings drawn by the shader in real metres, per the RMS (Richtlinien für die Markierung
von Straßen): 12 cm strokes, edge lines 25 cm off the kerb, the centre dash running 6 m
on and 12 m off outside built-up areas and 3 m on and 6 m off inside them (`Dashed` vs
`DashedUrban`), and `Solid` a line that never lifts. The surface texture tiles 4 m × 4 m
on every carriageway, so the grain keeps its shape whatever the road's width, and every
on every carriageway, so the grain keeps its shape whatever the road's width; gravel is
rougher than the bound surfaces, so a wet Feldweg does not shine the way wet asphalt does. Every
marking is filtered over the pixel it falls in — a line that goes sub-pixel with distance
fades out instead of breaking into sparks. A module carries no road bitmaps, and two
clients of a multiplayer run agree on what a road looks like without a byte crossing the
Expand Down
5 changes: 5 additions & 0 deletions THIRD_PARTY_LICENSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,11 @@ same carriageway. Recompressed from the ambientCG 1K JPEGs (resize, quality
[ambientCG](https://ambientcg.com) (photogrammetry, CC0 1.0).
- `roads/concrete.jpg` and `roads/concrete_nor.jpg` —
[Concrete030](https://ambientcg.com/a/Concrete030) (ambientCG, CC0).
- `roads/gravel.jpg` and `roads/gravel_nor.jpg` —
[Gravel031](https://ambientcg.com/a/Gravel031) (ambientCG, CC0), desaturated and
slightly darkened to the tone of a German Feldweg. The scan covers 2.8 m and is read
at the same 4 m repeat as the other two, so its stones sit a shade large — a
difference nobody makes out from a cab.

## Trees in `mods/trees/`

Expand Down
170 changes: 161 additions & 9 deletions crates/content/src/import/osm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,11 @@ fn water_tag(tags: &HashMap<String, String>) -> Option<(&'static str, Option<Str
/// purposes and land on the same preset; `footway`, `cycleway`, `path` and
/// `steps` are deliberately absent — a line's footpaths are the people
/// module's business, not the road surface's.
///
/// `track` lands on the **gravel** preset, not a paved one: `highway=track`
/// says what a way is used for, not what it is built of, and the German ones
/// are unpaved far more often than not. The ones that are paved say so —
/// `surface=*` or `tracktype=grade1` — and [`surface_of`] reads them.
const ROAD_CLASSES: &[(&str, &str)] = &[
("motorway", "motorway"),
("motorway_link", "motorway"),
Expand All @@ -540,7 +545,46 @@ const ROAD_CLASSES: &[(&str, &str)] = &[
("pedestrian", "living"),
("road", "residential"),
("service", "service"),
("track", "farm-concrete"),
("track", "farm-gravel"),
];

/// What OSM's `surface=*` says a carriageway is made of, for the values a
/// German extract carries. The dark bound surfaces are asphalt; the pale hard
/// ones — the slabs, the setts, the block paving — are concrete; and the
/// unpaved family is gravel, which is what a Feldweg is. A value the table
/// does not know leaves the class preset in charge rather than guessing.
///
/// The lookup is on the value's first word: `concrete:plates` is concrete,
/// `gravel;dirt` is gravel.
const SURFACES: &[(&str, RoadSurface)] = &[
("asphalt", RoadSurface::Asphalt),
("paved", RoadSurface::Asphalt),
("bitumen", RoadSurface::Asphalt),
("chipseal", RoadSurface::Asphalt),
("tarmac", RoadSurface::Asphalt),
("concrete", RoadSurface::Concrete),
("cement", RoadSurface::Concrete),
("paving_stones", RoadSurface::Concrete),
("sett", RoadSurface::Concrete),
("cobblestone", RoadSurface::Concrete),
("bricks", RoadSurface::Concrete),
("brick", RoadSurface::Concrete),
("unpaved", RoadSurface::Gravel),
("gravel", RoadSurface::Gravel),
("fine_gravel", RoadSurface::Gravel),
("compacted", RoadSurface::Gravel),
("pebblestone", RoadSurface::Gravel),
("ground", RoadSurface::Gravel),
("dirt", RoadSurface::Gravel),
("earth", RoadSurface::Gravel),
("soil", RoadSurface::Gravel),
("mud", RoadSurface::Gravel),
("sand", RoadSurface::Gravel),
("grass", RoadSurface::Gravel),
("grass_paver", RoadSurface::Gravel),
("woodchips", RoadSurface::Gravel),
("rock", RoadSurface::Gravel),
("stone", RoadSurface::Gravel),
];

/// Roads from an Overpass extract, as [`RoadSource`]s — the route editor's
Expand All @@ -558,9 +602,10 @@ const ROAD_CLASSES: &[(&str, &str)] = &[
///
/// OSM maps a street as its centre line, so that is what a road is here: the
/// class decides the preset — width, surface and markings, all editable —
/// and where the mapper said more, it wins: `surface=*` over the preset's
/// surface, `width=*`/`lanes=*` over its width, `oneway=yes` takes the
/// centre line out, and `bridge=*` flags the way as flying (see
/// and where the mapper said more, it wins: `surface=*` and `tracktype=*` over
/// the preset's surface ([`surface_of`]), `width=*`/`lanes=*` over its width,
/// `oneway=yes` takes the centre line out, an unpaved surface takes every
/// marking out, and `bridge=*` flags the way as flying (see
/// [`crate::route::RoadSource::bridge`]). A carriageway of a divided road
/// (`oneway=yes`) carries no centre line, which is what makes an Autobahn
/// read as two carriageways rather than one striped one.
Expand Down Expand Up @@ -600,10 +645,7 @@ pub fn parse_roads(json: &str) -> Result<Vec<RoadSource>, OsmError> {
if points.len() < 2 {
continue;
}
let surface = match e.tags.get("surface").map(String::as_str) {
Some(s) if s.starts_with("concrete") => RoadSurface::Concrete,
_ => RoadSurface::Asphalt,
};
let surface = surface_of(&e.tags, preset);
let width = width_of(&e.tags)
.or_else(|| lanes_width(&e.tags))
.unwrap_or(preset.width);
Expand All @@ -614,6 +656,14 @@ pub fn parse_roads(json: &str) -> Result<Vec<RoadSource>, OsmError> {
} else {
CenterLine::None
};
// Nobody paints on a loose surface: an unpaved way carries neither
// centre line nor Randlinie, whatever its class would have had. A
// `residential` way through a village that says `surface=gravel` is a
// gravel lane, not a striped street.
let (center_line, edge_lines) = match surface {
RoadSurface::Gravel => (CenterLine::None, false),
_ => (center_line, preset.edge_lines),
};
// A bridge flies: over the hollow the way spans — a valley, a river,
// a cutting — the carriageway holds the straight line between its own
// ends instead of following the ground. Any `bridge=*` but `no`
Expand All @@ -625,7 +675,7 @@ pub fn parse_roads(json: &str) -> Result<Vec<RoadSource>, OsmError> {
width,
surface,
center_line,
edge_lines: preset.edge_lines,
edge_lines,
bridge,
tags: vec![format!("highway-{}", e.tags["highway"])],
});
Expand Down Expand Up @@ -659,6 +709,50 @@ fn road_preset_id(class: &str) -> Option<&'static str> {
.map(|(_, preset)| *preset)
}

/// What the carriageway is made of: the mapper's `surface=*` where the table
/// knows the value, else what a field track's `tracktype=*` implies, else the
/// class preset's own surface.
///
/// The tag order is what keeps a Feldweg a Feldweg. `highway=track` says
/// nothing about the material — it is the *use* of the way, and the German
/// ones run from a concrete Betonspurbahn to two ruts through a field — so the
/// class alone may not pave it. `tracktype=*` is the tag that grades how solid
/// it is, and it is on far more German tracks than `surface=*` is: `grade1`
/// is the solid one, everything below it is loose, whatever it is made of.
///
/// A solid track reads as **asphalt**, not as concrete slabs. The tag does not
/// say which, but the mappers who did write a surface do: over a Börde box of
/// 327 field tracks, the paved ones say `asphalt` or `paved` 52 times and
/// `concrete` twice. A Betonspurbahn is a real thing and it comes out of
/// `surface=concrete*` — it is not what an ungraded majority should be turned
/// into.
fn surface_of(tags: &HashMap<String, String>, preset: &crate::roads::RoadPreset) -> RoadSurface {
if let Some(surface) = tags.get("surface").and_then(|value| named_surface(value)) {
return surface;
}
match tags.get("tracktype").map(String::as_str) {
Some("grade1") => RoadSurface::Asphalt,
Some("grade2" | "grade3" | "grade4" | "grade5") => RoadSurface::Gravel,
_ => preset.surface,
}
}

/// The surface a `surface=*` value names, by its first word — `concrete:plates`
/// is concrete, `gravel;dirt` is gravel. A value [`SURFACES`] does not know is
/// no answer: the preset gives one.
fn named_surface(value: &str) -> Option<RoadSurface> {
let value = value.trim().to_ascii_lowercase();
let word = value
.split([';', ':', '/'])
.next()
.unwrap_or_default()
.trim();
SURFACES
.iter()
.find(|(name, _)| *name == word)
.map(|(_, surface)| *surface)
}

/// The carriageway width the mapper wrote [m], if it parses: a plain number
/// (`width=6.5`), else nothing — the preset answers for everything else.
fn width_of(tags: &HashMap<String, String>) -> Option<f64> {
Expand Down Expand Up @@ -1393,6 +1487,64 @@ mod tests {
assert_eq!(parse_roads(r#"{"elements": []}"#), Ok(vec![]));
}

/// A field track is unpaved until the mapper says otherwise. The class
/// alone paves nothing: `highway=track` is what the way is used for, and
/// the German ones are gravel far more often than concrete. What decides
/// is `surface=*` first, then `tracktype=*` — and a loose surface takes
/// the markings out, whatever the class would have painted.
#[test]
fn a_field_track_is_unpaved_unless_the_tags_say_so() {
let json = r#"{"elements": [
{"type": "node", "id": 1, "lat": 52.0, "lon": 10.0},
{"type": "node", "id": 2, "lat": 52.0, "lon": 10.01},
{"type": "way", "id": 10, "nodes": [1, 2], "tags": {"highway": "track"}},
{"type": "way", "id": 11, "nodes": [1, 2],
"tags": {"highway": "track", "tracktype": "grade3"}},
{"type": "way", "id": 12, "nodes": [1, 2],
"tags": {"highway": "track", "tracktype": "grade1"}},
{"type": "way", "id": 13, "nodes": [1, 2],
"tags": {"highway": "track", "tracktype": "grade1", "surface": "gravel"}},
{"type": "way", "id": 14, "nodes": [1, 2],
"tags": {"highway": "track", "surface": "Compacted"}},
{"type": "way", "id": 15, "nodes": [1, 2],
"tags": {"highway": "residential", "surface": "gravel;dirt"}},
{"type": "way", "id": 16, "nodes": [1, 2],
"tags": {"highway": "residential", "surface": "sett"}},
{"type": "way", "id": 17, "nodes": [1, 2],
"tags": {"highway": "secondary", "surface": "something_new"}}
]}"#;
let roads = parse_roads(json).expect("parses");
assert_eq!(roads.len(), 8);

// A bare track: gravel, three metres, nothing painted on it.
assert_eq!(roads[0].surface, RoadSurface::Gravel);
assert!((roads[0].width - 3.0).abs() < 1e-9);
assert_eq!(roads[0].center_line, CenterLine::None);
assert!(!roads[0].edge_lines);

assert_eq!(roads[1].surface, RoadSurface::Gravel, "grade3 is loose");
assert_eq!(roads[2].surface, RoadSurface::Asphalt, "grade1 is solid");
assert_eq!(
roads[3].surface,
RoadSurface::Gravel,
"the surface the mapper wrote beats the grade"
);
assert_eq!(roads[4].surface, RoadSurface::Gravel, "case is no matter");

// An unpaved village lane is a gravel lane, not a striped street.
assert_eq!(roads[5].surface, RoadSurface::Gravel, "the first value");
assert_eq!(roads[5].center_line, CenterLine::None);
assert!(!roads[5].edge_lines);

// Setts are hard and pale: concrete, and marked like the class says.
assert_eq!(roads[6].surface, RoadSurface::Concrete);
assert_eq!(roads[6].center_line, CenterLine::DashedUrban);
assert!(roads[6].edge_lines);

// A value the table does not know leaves the class preset in charge.
assert_eq!(roads[7].surface, RoadSurface::Asphalt, "the preset's own");
}

/// A road cut by the extract at the box stays whole — the two corners
/// the extract carried are the road, and the neighbour imports the rest.
#[test]
Expand Down
11 changes: 9 additions & 2 deletions crates/content/src/roads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ const LAYERS: usize = 12;
const LAYER_LIFT: f64 = 0.006;

/// The road presets the editor offers — the widths of the German road system,
/// from the Autobahn carriageway down to the footpath, asphalt and concrete,
/// with and without the centre line. The widths are the planning values of
/// from the Autobahn carriageway down to the footpath, asphalt, concrete and
/// the gravel of the field tracks, with and without the centre line. The widths are the planning values of
/// the German road system (RASt-06, rounded to what a builder will actually
/// pick); each carriageway of a divided road is its own preset, because OSM
/// maps the two directions of an Autobahn as their own ways.
Expand Down Expand Up @@ -194,6 +194,13 @@ pub const PRESETS: &[RoadPreset] = &[
center_line: CenterLine::None,
edge_lines: false,
},
RoadPreset {
id: "farm-gravel",
width: 3.0,
surface: RoadSurface::Gravel,
center_line: CenterLine::None,
edge_lines: false,
},
RoadPreset {
id: "farm-concrete",
width: 3.0,
Expand Down
17 changes: 12 additions & 5 deletions crates/content/src/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,10 +326,11 @@ impl WaterSource {
}

/// What a road's surface is made of. It decides the material the surface is
/// drawn with — asphalt everywhere, and the concrete of the motorway
/// carriageways and the farm roads' slabs. The markings are not part of it:
/// they travel on their own (see [`CenterLine`]), so every combination of
/// surface and markings is one road, not four kinds of road.
/// drawn with — asphalt everywhere, the concrete of the motorway carriageways
/// and the farm roads' slabs, and the gravel of everything nobody paved. The
/// markings are not part of it: they travel on their own (see [`CenterLine`]),
/// so every combination of surface and markings is one road, not four kinds of
/// road.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
Expand All @@ -339,14 +340,19 @@ pub enum RoadSurface {
Asphalt,
/// `surface=concrete` and friends — motorway sections, farm slabs.
Concrete,
/// The unpaved family — `surface=gravel`, `compacted`, `ground`, and a
/// field track that says `tracktype=grade2` and no more. A German Feldweg
/// is this, not a paved road three metres wide.
Gravel,
}

impl RoadSurface {
/// The id a line file stores (`"asphalt"` / `"concrete"`).
/// The id a line file stores (`"asphalt"` / `"concrete"` / `"gravel"`).
pub fn id(self) -> &'static str {
match self {
RoadSurface::Asphalt => "asphalt",
RoadSurface::Concrete => "concrete",
RoadSurface::Gravel => "gravel",
}
}

Expand All @@ -355,6 +361,7 @@ impl RoadSurface {
pub fn from_id(id: &str) -> Self {
match id {
"concrete" => RoadSurface::Concrete,
"gravel" => RoadSurface::Gravel,
_ => RoadSurface::Asphalt,
}
}
Expand Down
Loading
Loading