use crate::geometry::{Feature, FeatureCollection, PropertyValue};
use crate::layer::LayerId;
use crate::layers::FeatureProvenance;
use crate::query::feature_id_for_feature;
use crate::symbols::{PlacedSymbol, SymbolAssetDependencies};
use crate::tile_manager::VisibleTile;
use rustial_math::TileId;
use std::collections::hash_map::DefaultHasher;
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
pub(crate) type StreamedSymbolPayloadKey = (String, Option<TileId>);
#[derive(Debug, Clone)]
pub(crate) struct StreamedVectorLayerPayloadBuild {
pub(crate) tile_payloads: Vec<TileQueryPayload>,
pub(crate) fingerprint: u64,
}
#[derive(Debug, Clone)]
pub(crate) struct StreamedVectorLayerRefreshSpec {
pub(crate) runtime_id: LayerId,
pub(crate) layer_key: String,
pub(crate) source_id: String,
pub(crate) source_layer: Option<String>,
pub(crate) visible_tiles: Vec<VisibleTile>,
}
#[derive(Debug, Clone)]
pub(crate) struct StreamedVectorLayerRefreshResult {
pub(crate) runtime_id: LayerId,
pub(crate) layer_key: String,
pub(crate) tile_payloads: Vec<TileQueryPayload>,
pub(crate) fingerprint: u64,
}
impl StreamedVectorLayerRefreshResult {
pub(crate) fn payload_view(&self) -> StreamedPayloadView<'_> {
StreamedPayloadView::new(&self.tile_payloads)
}
pub(crate) fn rebuild_feature_inputs(
&self,
) -> (FeatureCollection, Vec<Option<FeatureProvenance>>) {
self.payload_view().rebuild_feature_inputs()
}
}
pub(crate) fn resolve_streamed_vector_layer_refresh(
specs: Vec<StreamedVectorLayerRefreshSpec>,
) -> Vec<StreamedVectorLayerRefreshResult> {
specs
.into_iter()
.map(|spec| {
let resolved_payload = build_streamed_vector_layer_payload(
&spec.source_id,
spec.source_layer.as_deref(),
&spec.visible_tiles,
);
StreamedVectorLayerRefreshResult {
runtime_id: spec.runtime_id,
layer_key: spec.layer_key,
tile_payloads: resolved_payload.tile_payloads,
fingerprint: resolved_payload.fingerprint,
}
})
.collect()
}
pub(crate) fn collect_affected_symbol_payloads(
dependency_payloads: &HashMap<String, Vec<SymbolDependencyPayload>>,
matches: impl Fn(&SymbolAssetDependencies) -> bool,
) -> HashSet<StreamedSymbolPayloadKey> {
dependency_payloads
.iter()
.flat_map(|(layer_id, payloads)| {
payloads
.iter()
.filter(|&payload| matches(&payload.dependencies))
.map(|payload| (layer_id.clone(), payload.tile))
})
.collect()
}
pub(crate) fn prune_affected_symbol_payloads(
affected: &HashSet<StreamedSymbolPayloadKey>,
placed_symbols: &[PlacedSymbol],
query_payloads: &mut HashMap<String, Vec<SymbolQueryPayload>>,
dependency_payloads: &mut HashMap<String, Vec<SymbolDependencyPayload>>,
) -> Vec<PlacedSymbol> {
query_payloads.retain(|layer_id, payloads| {
payloads.retain(|payload| !affected.contains(&(layer_id.clone(), payload.tile)));
!payloads.is_empty()
});
dependency_payloads.retain(|layer_id, payloads| {
payloads.retain(|payload| !affected.contains(&(layer_id.clone(), payload.tile)));
!payloads.is_empty()
});
placed_symbols
.iter()
.filter(|symbol| {
let Some(layer_id) = symbol.layer_id.as_ref() else {
return true;
};
!affected.contains(&(layer_id.clone(), symbol.source_tile))
})
.cloned()
.collect()
}
pub(crate) fn build_streamed_vector_layer_payload(
source_id: &str,
source_layer: Option<&str>,
visible_tiles: &[VisibleTile],
) -> StreamedVectorLayerPayloadBuild {
let mut hasher = DefaultHasher::new();
source_id.hash(&mut hasher);
source_layer.hash(&mut hasher);
let mut seen_actual = HashSet::new();
let mut tile_payloads = Vec::new();
let mut next_feature_index = 0usize;
for visible in visible_tiles {
if !seen_actual.insert(visible.actual) {
continue;
}
visible.actual.hash(&mut hasher);
let Some(vector) = visible
.data
.as_ref()
.and_then(crate::tile_source::TileData::as_vector)
else {
continue;
};
let mut tile_features = Vec::new();
if let Some(source_layer_name) = source_layer {
if let Some(collection) = vector.layer(source_layer_name) {
collection.len().hash(&mut hasher);
collection.total_coords().hash(&mut hasher);
for feature in &collection.features {
let feature_index = next_feature_index;
next_feature_index += 1;
let feature_id = feature_id_for_feature(feature, feature_index);
let feature_provenance = FeatureProvenance {
source_layer: Some(source_layer_name.to_owned()),
source_tile: Some(visible.actual),
};
tile_features.push(TileQueryFeature {
feature_id,
feature_index,
geometry: feature.geometry.clone(),
properties: feature.properties.clone(),
provenance: Some(feature_provenance),
});
}
} else {
0usize.hash(&mut hasher);
}
} else {
let mut names = vector.layer_names();
names.sort_unstable();
names.hash(&mut hasher);
vector.feature_count().hash(&mut hasher);
for name in names {
if let Some(collection) = vector.layer(name) {
for feature in &collection.features {
let feature_index = next_feature_index;
next_feature_index += 1;
let feature_id = feature_id_for_feature(feature, feature_index);
let feature_provenance = FeatureProvenance {
source_layer: Some(name.to_owned()),
source_tile: Some(visible.actual),
};
tile_features.push(TileQueryFeature {
feature_id,
feature_index,
geometry: feature.geometry.clone(),
properties: feature.properties.clone(),
provenance: Some(feature_provenance),
});
}
}
}
}
if !tile_features.is_empty() {
tile_payloads.push(TileQueryPayload {
tile: visible.actual,
features: tile_features,
});
}
}
StreamedVectorLayerPayloadBuild {
tile_payloads,
fingerprint: hasher.finish(),
}
}
#[derive(Debug, Clone)]
pub(crate) struct TileQueryFeature {
pub(crate) feature_id: String,
pub(crate) feature_index: usize,
pub(crate) geometry: crate::geometry::Geometry,
pub(crate) properties: HashMap<String, PropertyValue>,
pub(crate) provenance: Option<FeatureProvenance>,
}
#[derive(Debug, Clone)]
pub(crate) struct TileQueryPayload {
pub(crate) tile: TileId,
pub(crate) features: Vec<TileQueryFeature>,
}
#[derive(Debug, Clone)]
pub(crate) struct SymbolQueryPayload {
pub(crate) tile: Option<TileId>,
pub(crate) symbols: Vec<PlacedSymbol>,
}
#[derive(Debug, Clone)]
pub(crate) struct SymbolDependencyPayload {
pub(crate) tile: Option<TileId>,
pub(crate) dependencies: SymbolAssetDependencies,
}
pub(crate) fn symbol_query_payloads_from_optional(
payloads: Option<&[SymbolQueryPayload]>,
) -> &[SymbolQueryPayload] {
payloads.unwrap_or(&[])
}
#[derive(Clone, Copy)]
pub(crate) struct VisiblePlacedSymbolRef<'a> {
pub(crate) symbol: &'a PlacedSymbol,
}
impl<'a> VisiblePlacedSymbolRef<'a> {
pub(crate) fn layer_id(&self) -> Option<&'a str> {
self.symbol.layer_id.as_deref()
}
pub(crate) fn source_tile(&self) -> Option<TileId> {
self.symbol.source_tile
}
pub(crate) fn dependencies(&self) -> SymbolAssetDependencies {
self.symbol.dependencies()
}
}
#[derive(Clone, Copy)]
pub(crate) struct VisiblePlacedSymbolView<'a> {
symbols: &'a [PlacedSymbol],
}
impl<'a> VisiblePlacedSymbolView<'a> {
pub(crate) fn new(symbols: &'a [PlacedSymbol]) -> Self {
Self { symbols }
}
pub(crate) fn iter(self) -> impl Iterator<Item = VisiblePlacedSymbolRef<'a>> + 'a {
self.symbols
.iter()
.filter(|symbol| symbol.visible && symbol.opacity > 0.0)
.map(|symbol| VisiblePlacedSymbolRef { symbol })
}
pub(crate) fn iter_all(self) -> impl Iterator<Item = VisiblePlacedSymbolRef<'a>> + 'a {
self.symbols
.iter()
.map(|symbol| VisiblePlacedSymbolRef { symbol })
}
pub(crate) fn rebuild_query_payloads(self) -> HashMap<String, Vec<SymbolQueryPayload>> {
let mut grouped: HashMap<String, HashMap<Option<TileId>, Vec<PlacedSymbol>>> =
HashMap::new();
for entry in self.iter() {
let Some(layer_id) = entry.layer_id() else {
continue;
};
grouped
.entry(layer_id.to_owned())
.or_default()
.entry(entry.source_tile())
.or_default()
.push(entry.symbol.clone());
}
grouped
.into_iter()
.map(|(layer_id, by_tile)| {
let payloads = by_tile
.into_iter()
.map(|(tile, symbols)| SymbolQueryPayload { tile, symbols })
.collect();
(layer_id, payloads)
})
.collect()
}
pub(crate) fn rebuild_dependency_payloads(
self,
) -> HashMap<String, Vec<SymbolDependencyPayload>> {
let mut grouped: HashMap<String, HashMap<Option<TileId>, SymbolAssetDependencies>> =
HashMap::new();
for entry in self.iter_all() {
let Some(layer_id) = entry.layer_id() else {
continue;
};
let deps = entry.dependencies();
let grouped_entry = grouped
.entry(layer_id.to_owned())
.or_default()
.entry(entry.source_tile())
.or_default();
grouped_entry.glyphs.extend(deps.glyphs);
grouped_entry.images.extend(deps.images);
}
grouped
.into_iter()
.map(|(layer_id, by_tile)| {
let payloads = by_tile
.into_iter()
.map(|(tile, dependencies)| SymbolDependencyPayload { tile, dependencies })
.collect();
(layer_id, payloads)
})
.collect()
}
}
#[derive(Clone, Copy)]
pub(crate) struct StreamedPayloadFeatureRef<'a> {
pub(crate) tile: TileId,
pub(crate) feature: &'a TileQueryFeature,
}
impl<'a> StreamedPayloadFeatureRef<'a> {
pub(crate) fn source_layer(&self, fallback: Option<&'a str>) -> Option<&'a str> {
self.feature
.provenance
.as_ref()
.and_then(|entry| entry.source_layer.as_deref())
.or(fallback)
}
pub(crate) fn source_tile(&self) -> Option<TileId> {
Some(self.tile).or_else(|| {
self.feature
.provenance
.as_ref()
.and_then(|entry| entry.source_tile)
})
}
pub(crate) fn cloned_feature(&self) -> Feature {
Feature {
geometry: self.feature.geometry.clone(),
properties: self.feature.properties.clone(),
}
}
}
#[derive(Clone, Copy)]
pub(crate) struct StreamedPayloadView<'a> {
payloads: &'a [TileQueryPayload],
}
impl<'a> StreamedPayloadView<'a> {
pub(crate) fn empty() -> Self {
Self { payloads: &[] }
}
pub(crate) fn new(payloads: &'a [TileQueryPayload]) -> Self {
Self { payloads }
}
pub(crate) fn from_optional(payloads: Option<&'a [TileQueryPayload]>) -> Self {
payloads.map(Self::new).unwrap_or_else(Self::empty)
}
pub(crate) fn is_empty(&self) -> bool {
self.payloads.is_empty()
}
pub(crate) fn iter(self) -> impl Iterator<Item = StreamedPayloadFeatureRef<'a>> + 'a {
self.payloads.iter().flat_map(|payload| {
payload
.features
.iter()
.map(|feature| StreamedPayloadFeatureRef {
tile: payload.tile,
feature,
})
})
}
pub(crate) fn rebuild_feature_inputs(
self,
) -> (FeatureCollection, Vec<Option<FeatureProvenance>>) {
let mut features = Vec::new();
let mut provenance = Vec::new();
for entry in self.iter() {
features.push(entry.cloned_feature());
provenance.push(entry.feature.provenance.clone());
}
(FeatureCollection { features }, provenance)
}
#[allow(clippy::type_complexity)]
pub(crate) fn rebuild_tile_buckets(
self,
) -> (
HashMap<TileId, FeatureCollection>,
HashMap<TileId, Vec<Option<FeatureProvenance>>>,
) {
let mut tile_features: HashMap<TileId, Vec<Feature>> = HashMap::new();
let mut tile_provenance: HashMap<TileId, Vec<Option<FeatureProvenance>>> = HashMap::new();
for entry in self.iter() {
tile_features
.entry(entry.tile)
.or_default()
.push(entry.cloned_feature());
tile_provenance
.entry(entry.tile)
.or_default()
.push(entry.feature.provenance.clone());
}
(
tile_features
.into_iter()
.map(|(tile, features)| (tile, FeatureCollection { features }))
.collect(),
tile_provenance,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::camera_projection::CameraProjection;
use crate::geometry::{Feature, Geometry, Point};
use crate::symbols::{
SymbolAnchor, SymbolCollisionBox, SymbolIconTextFit, SymbolPlacement, SymbolWritingMode,
};
use crate::tile_source::{TileData, VectorTileData};
use rustial_math::GeoCoord;
fn point_feature(coord: GeoCoord, name: &str) -> Feature {
let mut properties = HashMap::new();
properties.insert("name".to_owned(), PropertyValue::String(name.to_owned()));
Feature {
geometry: Geometry::Point(Point { coord }),
properties,
}
}
fn visible_vector_tile(
target: TileId,
actual: TileId,
layers: HashMap<String, FeatureCollection>,
) -> VisibleTile {
VisibleTile {
target,
actual,
data: Some(TileData::Vector(VectorTileData { layers })),
fade_opacity: 1.0,
}
}
fn placed_symbol(
layer_id: &str,
tile: TileId,
feature_id: &str,
text: Option<&str>,
icon: Option<&str>,
visible: bool,
) -> PlacedSymbol {
let anchor = GeoCoord::from_lat_lon(0.0, 0.0);
let world = CameraProjection::WebMercator.project(&anchor);
PlacedSymbol {
id: format!("symbol-{feature_id}"),
layer_id: Some(layer_id.to_owned()),
source_id: Some("streamed".to_owned()),
source_layer: Some("labels".to_owned()),
source_tile: Some(tile),
feature_id: feature_id.to_owned(),
feature_index: 0,
placement: SymbolPlacement::Point,
anchor,
world_anchor: [world.position.x, world.position.y, world.position.z],
text: text.map(ToOwned::to_owned),
icon_image: icon.map(ToOwned::to_owned),
font_stack: "Test Sans".to_owned(),
cross_tile_id: feature_id.to_owned(),
rotation_rad: 0.0,
collision_box: SymbolCollisionBox {
min: [world.position.x - 1.0, world.position.y - 1.0],
max: [world.position.x + 1.0, world.position.y + 1.0],
},
anchor_mode: SymbolAnchor::Center,
writing_mode: SymbolWritingMode::Horizontal,
offset_px: [0.0, 0.0],
radial_offset: None,
text_max_width: None,
text_line_height: None,
text_letter_spacing: None,
icon_text_fit: SymbolIconTextFit::None,
icon_text_fit_padding: [0.0, 0.0, 0.0, 0.0],
size_px: 16.0,
fill_color: [1.0, 1.0, 1.0, 1.0],
halo_color: [0.0, 0.0, 0.0, 1.0],
opacity: 1.0,
visible,
glyph_quads: Vec::new(),
}
}
#[test]
fn resolve_streamed_refresh_builds_deduped_tile_payloads() {
let tile_a = TileId::new(4, 8, 8);
let tile_b = TileId::new(4, 9, 8);
let mut layers_a = HashMap::new();
layers_a.insert(
"poi".to_owned(),
FeatureCollection {
features: vec![point_feature(GeoCoord::from_lat_lon(51.5, -0.12), "A")],
},
);
let mut layers_b = HashMap::new();
layers_b.insert(
"poi".to_owned(),
FeatureCollection {
features: vec![point_feature(GeoCoord::from_lat_lon(51.5005, -0.11), "B")],
},
);
let spec = StreamedVectorLayerRefreshSpec {
runtime_id: LayerId::next(),
layer_key: "poi-circles".to_owned(),
source_id: "streamed".to_owned(),
source_layer: Some("poi".to_owned()),
visible_tiles: vec![
visible_vector_tile(tile_a, tile_a, layers_a.clone()),
visible_vector_tile(TileId::new(4, 10, 8), tile_a, layers_a),
visible_vector_tile(tile_b, tile_b, layers_b),
],
};
let results = resolve_streamed_vector_layer_refresh(vec![spec.clone()]);
assert_eq!(results.len(), 1);
assert_eq!(results[0].runtime_id, spec.runtime_id);
assert_eq!(results[0].layer_key, "poi-circles");
assert_eq!(results[0].tile_payloads.len(), 2);
assert_eq!(
results[0]
.tile_payloads
.iter()
.map(|payload| payload.tile)
.collect::<Vec<_>>(),
vec![tile_a, tile_b]
);
let (features, provenance) = results[0].rebuild_feature_inputs();
assert_eq!(features.features.len(), 2);
assert_eq!(provenance.len(), 2);
assert_eq!(
provenance[0]
.as_ref()
.and_then(|entry| entry.source_layer.as_deref()),
Some("poi")
);
assert_eq!(
provenance[0].as_ref().and_then(|entry| entry.source_tile),
Some(tile_a)
);
let fingerprint_again = resolve_streamed_vector_layer_refresh(vec![spec])[0].fingerprint;
assert_eq!(results[0].fingerprint, fingerprint_again);
}
#[test]
fn symbol_payload_lifecycle_collects_and_prunes_affected_tiles() {
let tile_a = TileId::new(4, 8, 8);
let tile_b = TileId::new(4, 9, 8);
let placed = vec![
placed_symbol(
"poi-labels",
tile_a,
"a",
Some("Alpha"),
Some("marker-a"),
true,
),
placed_symbol(
"poi-labels",
tile_b,
"b",
Some("Beta"),
Some("marker-b"),
true,
),
placed_symbol(
"poi-labels",
TileId::new(4, 10, 8),
"c",
Some("Hidden"),
None,
false,
),
];
let view = VisiblePlacedSymbolView::new(&placed);
let mut query_payloads = view.rebuild_query_payloads();
let mut dependency_payloads = view.rebuild_dependency_payloads();
assert_eq!(query_payloads.get("poi-labels").map(Vec::len), Some(2));
let affected = collect_affected_symbol_payloads(&dependency_payloads, |deps| {
deps.images.contains("marker-a")
});
assert_eq!(affected.len(), 1);
assert!(affected.contains(&("poi-labels".to_owned(), Some(tile_a))));
let remaining = prune_affected_symbol_payloads(
&affected,
&placed,
&mut query_payloads,
&mut dependency_payloads,
);
assert_eq!(remaining.len(), 2);
assert!(remaining
.iter()
.all(|symbol| symbol.source_tile != Some(tile_a)));
assert_eq!(query_payloads.get("poi-labels").map(Vec::len), Some(1));
assert_eq!(dependency_payloads.get("poi-labels").map(Vec::len), Some(2));
assert!(dependency_payloads["poi-labels"]
.iter()
.any(|payload| payload.dependencies.images.contains("marker-b")));
}
#[test]
fn streamed_payload_view_rebuilds_feature_inputs_and_tile_buckets() {
let tile_a = TileId::new(4, 8, 8);
let tile_b = TileId::new(4, 9, 8);
let payloads = vec![
TileQueryPayload {
tile: tile_a,
features: vec![TileQueryFeature {
feature_id: "a".to_owned(),
feature_index: 0,
geometry: Geometry::Point(Point {
coord: GeoCoord::from_lat_lon(51.5, -0.12),
}),
properties: HashMap::from([(
"name".to_owned(),
PropertyValue::String("Alpha".to_owned()),
)]),
provenance: Some(FeatureProvenance {
source_layer: Some("poi".to_owned()),
source_tile: Some(tile_a),
}),
}],
},
TileQueryPayload {
tile: tile_b,
features: vec![TileQueryFeature {
feature_id: "b".to_owned(),
feature_index: 1,
geometry: Geometry::Point(Point {
coord: GeoCoord::from_lat_lon(51.5005, -0.11),
}),
properties: HashMap::from([(
"name".to_owned(),
PropertyValue::String("Beta".to_owned()),
)]),
provenance: Some(FeatureProvenance {
source_layer: Some("poi".to_owned()),
source_tile: Some(tile_b),
}),
}],
},
];
let view = StreamedPayloadView::new(&payloads);
assert!(!view.is_empty());
let entries = view.iter().collect::<Vec<_>>();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].source_layer(None), Some("poi"));
assert_eq!(entries[0].source_tile(), Some(tile_a));
let (features, provenance) = view.rebuild_feature_inputs();
assert_eq!(features.features.len(), 2);
assert_eq!(provenance.len(), 2);
assert_eq!(
features.features[0].properties.get("name"),
Some(&PropertyValue::String("Alpha".to_owned()))
);
let (tile_features, tile_provenance) = view.rebuild_tile_buckets();
assert_eq!(tile_features.len(), 2);
assert_eq!(tile_features[&tile_a].features.len(), 1);
assert_eq!(tile_features[&tile_b].features.len(), 1);
assert_eq!(
tile_provenance[&tile_a][0]
.as_ref()
.and_then(|entry| entry.source_tile),
Some(tile_a)
);
}
#[test]
fn visible_placed_symbol_view_rebuilds_query_and_dependency_payloads() {
let tile_a = TileId::new(4, 8, 8);
let tile_b = TileId::new(4, 9, 8);
let placed = vec![
placed_symbol(
"poi-labels",
tile_a,
"a",
Some("Alpha"),
Some("marker-a"),
true,
),
placed_symbol("poi-labels", tile_b, "b", Some("Beta"), None, true),
placed_symbol(
"poi-labels",
TileId::new(4, 10, 8),
"c",
Some("Hidden"),
Some("marker-c"),
false,
),
];
let view = VisiblePlacedSymbolView::new(&placed);
let visible = view.iter().collect::<Vec<_>>();
assert_eq!(visible.len(), 2);
assert_eq!(visible[0].layer_id(), Some("poi-labels"));
assert_eq!(visible[0].source_tile(), Some(tile_a));
let query_payloads = view.rebuild_query_payloads();
assert_eq!(query_payloads.get("poi-labels").map(Vec::len), Some(2));
let dependency_payloads = view.rebuild_dependency_payloads();
assert_eq!(dependency_payloads.get("poi-labels").map(Vec::len), Some(3));
assert!(dependency_payloads["poi-labels"]
.iter()
.any(|payload| payload.tile == Some(tile_a)
&& payload.dependencies.images.contains("marker-a")));
assert!(dependency_payloads["poi-labels"]
.iter()
.any(|payload| payload.tile == Some(tile_b)
&& payload
.dependencies
.glyphs
.iter()
.any(|glyph| glyph.codepoint == 'B')));
}
}