Skip to main content

osmic_repl/
apply.rs

1use geo_types::{Coord, LineString, Point, Polygon};
2
3use osmic_core::error::OsmicResult;
4use osmic_core::geometry::Geometry;
5use osmic_core::NodeLocationStore;
6use osmic_osm::classify::classify;
7use osmic_osm::feature::Feature;
8use osmic_osm::tags::{TagStore, Tags};
9use osmic_osm::LayerSet;
10use osmic_tiles::TileGeneratorConfig;
11use tracing::info;
12
13use crate::dirty::DirtyTileSet;
14use crate::osc::{ChangeAction, OscChange, OscElement};
15use crate::store::FeatureStore;
16
17/// Apply a set of OSC changes to the feature store.
18/// Returns the set of tiles that need regeneration.
19pub fn apply_changes(
20    changes: &[OscChange],
21    store: &FeatureStore,
22    node_store: &dyn NodeLocationStore,
23    tag_store: &TagStore,
24    layers: &LayerSet,
25    config: &TileGeneratorConfig,
26) -> OsmicResult<DirtyTileSet> {
27    let mut dirty = DirtyTileSet::new();
28    let mut created = 0u64;
29    let mut modified = 0u64;
30    let mut deleted = 0u64;
31    let mut skipped = 0u64;
32
33    for change in changes {
34        match change.action {
35            ChangeAction::Create | ChangeAction::Modify => {
36                if let Some(feature) = build_feature(&change.element, node_store, tag_store, layers)
37                {
38                    let new_bbox = feature.bbox();
39
40                    // Upsert returns old bbox if feature existed
41                    let old_bbox = store.upsert(feature.id, &new_bbox)?;
42
43                    // Mark both old and new tile regions as dirty
44                    if let Some(old) = old_bbox {
45                        dirty.mark_bbox(&old, config.min_zoom, config.max_zoom);
46                    }
47                    dirty.mark_bbox(&new_bbox, config.min_zoom, config.max_zoom);
48
49                    if change.action == ChangeAction::Create {
50                        created += 1;
51                    } else {
52                        modified += 1;
53                    }
54                } else {
55                    skipped += 1;
56                }
57            }
58            ChangeAction::Delete => {
59                let id = change.element.id();
60                if let Some(old_bbox) = store.delete(id)? {
61                    dirty.mark_bbox(&old_bbox, config.min_zoom, config.max_zoom);
62                    deleted += 1;
63                }
64            }
65        }
66    }
67
68    info!(
69        created,
70        modified,
71        deleted,
72        skipped,
73        dirty_tiles = dirty.len(),
74        "Changes applied"
75    );
76
77    Ok(dirty)
78}
79
80/// Build an osmic Feature from an OSC element.
81/// Returns None if the element doesn't classify into a known feature type.
82fn build_feature(
83    element: &OscElement,
84    node_store: &dyn NodeLocationStore,
85    tag_store: &TagStore,
86    layers: &LayerSet,
87) -> Option<Feature> {
88    match element {
89        OscElement::Node {
90            id, lon, lat, tags, ..
91        } => {
92            let interned_tags = intern_tags(tags, tag_store);
93            let kind = classify(&interned_tags, tag_store, layers)?;
94            Some(Feature {
95                id: *id,
96                kind,
97                geometry: Geometry::Point(Point::new(*lon, *lat)),
98                tags: interned_tags,
99            })
100        }
101        OscElement::Way {
102            id,
103            node_refs,
104            tags,
105            ..
106        } => {
107            // Resolve node references to coordinates
108            let coords: Vec<Coord<f64>> = node_refs
109                .iter()
110                .filter_map(|&nid| {
111                    let lonlat = node_store.get(nid)?;
112                    Some(Coord {
113                        x: lonlat.lon,
114                        y: lonlat.lat,
115                    })
116                })
117                .collect();
118
119            if coords.len() < 2 {
120                return None;
121            }
122
123            let interned_tags = intern_tags(tags, tag_store);
124            let kind = classify(&interned_tags, tag_store, layers)?;
125
126            // Closed ways that are area types become polygons
127            let is_closed = coords.len() >= 4 && coords.first() == coords.last();
128            let mut geometry = if is_closed && kind.is_area() {
129                Geometry::Polygon(Polygon::new(LineString::new(coords), vec![]))
130            } else {
131                Geometry::Line(LineString::new(coords))
132            };
133            osmic_geo::orient::orient_geometry(&mut geometry);
134
135            Some(Feature {
136                id: *id,
137                kind,
138                geometry,
139                tags: interned_tags,
140            })
141        }
142        OscElement::Relation { .. } => {
143            // Multipolygon relations require resolving member ways,
144            // which needs a second pass. Skip in incremental updates.
145            // TODO: Support relation updates
146            None
147        }
148    }
149}
150
151/// Intern tag string pairs into the TagStore's compact representation.
152fn intern_tags(tags: &[(String, String)], tag_store: &TagStore) -> Tags {
153    let mut interned = Tags::new();
154    for (k, v) in tags {
155        let key = tag_store.intern_key(k);
156        let val = tag_store.intern_value(v);
157        interned.push(key, val);
158    }
159    interned
160}