1use crate::camera_projection::CameraProjection;
21use crate::expression::{ExprEvalContext, Expression};
22use crate::geometry::{Feature, FeatureCollection, Geometry};
23use crate::layer::{Layer, LayerId};
24use crate::query::feature_id_for_feature;
25use crate::symbols::{
26 SymbolAnchor, SymbolCandidate, SymbolIconTextFit, SymbolPlacement, SymbolTextJustify,
27 SymbolTextTransform, SymbolWritingMode,
28};
29use crate::terrain::TerrainManager;
30use crate::tessellator;
31use crate::visualization::ColorRamp;
32use rustial_math::{GeoCoord, TileId, WorldCoord};
33use std::any::Any;
34use std::sync::Arc;
35
36const METERS_PER_DEGREE: f64 = 111_319.49;
49const DEGREES_PER_PIXEL_APPROX: f64 = 0.00001;
50const METERS_PER_PIXEL_APPROX: f64 = METERS_PER_DEGREE * DEGREES_PER_PIXEL_APPROX;
51const DEFAULT_CIRCLE_SEGMENTS: usize = 20;
52
53#[derive(Debug, Clone)]
64pub struct PatternImage {
65 pub width: u32,
67 pub height: u32,
69 pub data: Vec<u8>,
71}
72
73impl PatternImage {
74 pub fn new(width: u32, height: u32, data: Vec<u8>) -> Self {
80 assert_eq!(
81 data.len(),
82 (width * height * 4) as usize,
83 "RGBA8 data length must be width × height × 4"
84 );
85 Self { width, height, data }
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
91pub enum VectorRenderMode {
92 #[default]
94 Generic,
95 Fill,
97 Line,
99 Circle,
101 Heatmap,
103 FillExtrusion,
105 Symbol,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
111pub enum LineCap {
112 #[default]
114 Butt,
115 Round,
117 Square,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
123pub enum LineJoin {
124 #[default]
127 Miter,
128 Bevel,
130 Round,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct FeatureProvenance {
137 pub source_layer: Option<String>,
139 pub source_tile: Option<TileId>,
141}
142
143#[derive(Debug, Clone)]
145pub struct VectorStyle {
146 pub render_mode: VectorRenderMode,
148 pub fill_color: [f32; 4],
150 pub stroke_color: [f32; 4],
152 pub stroke_width: f32,
154 pub point_radius: f32,
156 pub line_cap: LineCap,
158 pub line_join: LineJoin,
160 pub miter_limit: f32,
164 pub dash_array: Option<Vec<f32>>,
166 pub heatmap_radius: f32,
168 pub heatmap_intensity: f32,
170 pub extrusion_base: f32,
172 pub extrusion_height: f32,
174 pub symbol_size: f32,
176 pub symbol_halo_color: [f32; 4],
178 pub symbol_text_field: Option<String>,
180 pub symbol_icon_image: Option<String>,
182 pub symbol_font_stack: String,
184 pub symbol_padding: f32,
186 pub symbol_allow_overlap: bool,
193 pub symbol_text_allow_overlap: bool,
195 pub symbol_icon_allow_overlap: bool,
197 pub symbol_text_optional: bool,
199 pub symbol_icon_optional: bool,
201 pub symbol_text_ignore_placement: bool,
203 pub symbol_icon_ignore_placement: bool,
205 pub symbol_text_anchor: SymbolAnchor,
207 pub symbol_text_justify: SymbolTextJustify,
209 pub symbol_text_transform: SymbolTextTransform,
211 pub symbol_text_max_width: Option<f32>,
218 pub symbol_text_line_height: Option<f32>,
224 pub symbol_text_letter_spacing: Option<f32>,
230 pub symbol_icon_text_fit: SymbolIconTextFit,
232 pub symbol_icon_text_fit_padding: [f32; 4],
234 pub symbol_sort_key: Option<f32>,
240 pub symbol_anchors: Vec<SymbolAnchor>,
242 pub symbol_placement: SymbolPlacement,
244 pub symbol_spacing: f32,
250 pub symbol_max_angle: f32,
257 pub symbol_keep_upright: bool,
263 pub symbol_text_radial_offset: Option<f32>,
270 pub symbol_variable_anchor_offsets: Option<Vec<(SymbolAnchor, [f32; 2])>>,
276 pub symbol_writing_mode: SymbolWritingMode,
278 pub symbol_offset: [f32; 2],
280 pub fill_translate: [f32; 2],
282 pub fill_opacity: f32,
284 pub fill_antialias: bool,
286 pub fill_outline_color: Option<[f32; 4]>,
288
289 pub fill_pattern: Option<Arc<PatternImage>>,
296
297 pub line_pattern: Option<Arc<PatternImage>>,
305
306 pub width_expr: Option<Expression<f32>>,
314 pub stroke_color_expr: Option<Expression<[f32; 4]>>,
319 pub eval_zoom: f32,
324
325 pub line_gradient: Option<ColorRamp>,
334}
335
336impl Default for VectorStyle {
337 fn default() -> Self {
338 Self {
339 render_mode: VectorRenderMode::Generic,
340 fill_color: [0.2, 0.5, 0.8, 0.5],
341 stroke_color: [0.0, 0.0, 0.0, 1.0],
342 stroke_width: 2.0,
343 point_radius: 6.0,
344 line_cap: LineCap::Butt,
345 line_join: LineJoin::Miter,
346 miter_limit: 2.0,
347 dash_array: None,
348 heatmap_radius: 18.0,
349 heatmap_intensity: 1.0,
350 extrusion_base: 0.0,
351 extrusion_height: 30.0,
352 symbol_size: 10.0,
353 symbol_halo_color: [1.0, 1.0, 1.0, 0.85],
354 symbol_text_field: None,
355 symbol_icon_image: None,
356 symbol_font_stack: "Noto Sans Regular".into(),
357 symbol_padding: 2.0,
358 symbol_allow_overlap: false,
359 symbol_text_allow_overlap: false,
360 symbol_icon_allow_overlap: false,
361 symbol_text_optional: false,
362 symbol_icon_optional: false,
363 symbol_text_ignore_placement: false,
364 symbol_icon_ignore_placement: false,
365 symbol_text_anchor: SymbolAnchor::Center,
366 symbol_text_justify: SymbolTextJustify::Auto,
367 symbol_text_transform: SymbolTextTransform::None,
368 symbol_text_max_width: None,
369 symbol_text_line_height: None,
370 symbol_text_letter_spacing: None,
371 symbol_icon_text_fit: SymbolIconTextFit::None,
372 symbol_icon_text_fit_padding: [0.0, 0.0, 0.0, 0.0],
373 symbol_sort_key: None,
374 symbol_anchors: vec![SymbolAnchor::Center],
375 symbol_placement: SymbolPlacement::Point,
376 symbol_spacing: 250.0,
377 symbol_max_angle: 45.0,
378 symbol_keep_upright: true,
379 symbol_text_radial_offset: None,
380 symbol_variable_anchor_offsets: None,
381 symbol_writing_mode: SymbolWritingMode::Horizontal,
382 symbol_offset: [0.0, 0.0],
383 fill_translate: [0.0, 0.0],
384 fill_opacity: 1.0,
385 fill_antialias: true,
386 fill_outline_color: None,
387 fill_pattern: None,
388 line_pattern: None,
389 width_expr: None,
390 stroke_color_expr: None,
391 eval_zoom: 0.0,
392 line_gradient: None,
393 }
394 }
395}
396
397impl VectorStyle {
398 pub fn fill(fill_color: [f32; 4], outline_color: [f32; 4], outline_width: f32) -> Self {
400 Self {
401 render_mode: VectorRenderMode::Fill,
402 fill_color,
403 stroke_color: outline_color,
404 stroke_width: outline_width,
405 ..Self::default()
406 }
407 }
408
409 pub fn fill_pattern(pattern: Arc<PatternImage>) -> Self {
414 Self {
415 render_mode: VectorRenderMode::Fill,
416 fill_pattern: Some(pattern),
417 ..Self::default()
418 }
419 }
420
421 pub fn line_pattern(width: f32, pattern: Arc<PatternImage>) -> Self {
427 Self {
428 render_mode: VectorRenderMode::Line,
429 stroke_width: width,
430 line_pattern: Some(pattern),
431 ..Self::default()
432 }
433 }
434
435 pub fn line(color: [f32; 4], width: f32) -> Self {
437 Self {
438 render_mode: VectorRenderMode::Line,
439 stroke_color: color,
440 stroke_width: width,
441 ..Self::default()
442 }
443 }
444
445 pub fn line_styled(
447 color: [f32; 4],
448 width: f32,
449 cap: LineCap,
450 join: LineJoin,
451 miter_limit: f32,
452 dash_array: Option<Vec<f32>>,
453 ) -> Self {
454 Self {
455 render_mode: VectorRenderMode::Line,
456 stroke_color: color,
457 stroke_width: width,
458 line_cap: cap,
459 line_join: join,
460 miter_limit,
461 dash_array,
462 ..Self::default()
463 }
464 }
465
466 pub fn line_gradient(width: f32, ramp: ColorRamp) -> Self {
472 Self {
473 render_mode: VectorRenderMode::Line,
474 stroke_color: [1.0, 1.0, 1.0, 1.0],
475 stroke_width: width,
476 line_gradient: Some(ramp),
477 ..Self::default()
478 }
479 }
480
481 pub fn circle(color: [f32; 4], radius: f32, stroke_color: [f32; 4], stroke_width: f32) -> Self {
483 Self {
484 render_mode: VectorRenderMode::Circle,
485 fill_color: color,
486 point_radius: radius,
487 stroke_color,
488 stroke_width,
489 ..Self::default()
490 }
491 }
492
493 pub fn heatmap(color: [f32; 4], radius: f32, intensity: f32) -> Self {
495 Self {
496 render_mode: VectorRenderMode::Heatmap,
497 fill_color: color,
498 heatmap_radius: radius,
499 heatmap_intensity: intensity,
500 ..Self::default()
501 }
502 }
503
504 pub fn fill_extrusion(color: [f32; 4], base: f32, height: f32) -> Self {
506 Self {
507 render_mode: VectorRenderMode::FillExtrusion,
508 fill_color: color,
509 extrusion_base: base,
510 extrusion_height: height,
511 ..Self::default()
512 }
513 }
514
515 pub fn symbol(color: [f32; 4], halo_color: [f32; 4], size: f32) -> Self {
517 Self {
518 render_mode: VectorRenderMode::Symbol,
519 fill_color: color,
520 symbol_halo_color: halo_color,
521 symbol_size: size,
522 ..Self::default()
523 }
524 }
525
526 pub fn tessellation_fingerprint(&self) -> u64 {
535 use std::hash::{Hash, Hasher};
536 let mut h = std::collections::hash_map::DefaultHasher::new();
537 std::mem::discriminant(&self.render_mode).hash(&mut h);
539 self.fill_color.iter().for_each(|v| v.to_bits().hash(&mut h));
541 self.stroke_color.iter().for_each(|v| v.to_bits().hash(&mut h));
542 self.stroke_width.to_bits().hash(&mut h);
543 self.point_radius.to_bits().hash(&mut h);
544 self.heatmap_radius.to_bits().hash(&mut h);
545 self.heatmap_intensity.to_bits().hash(&mut h);
546 self.extrusion_base.to_bits().hash(&mut h);
547 self.extrusion_height.to_bits().hash(&mut h);
548 self.symbol_size.to_bits().hash(&mut h);
549 self.symbol_halo_color.iter().for_each(|v| v.to_bits().hash(&mut h));
550 self.fill_translate.iter().for_each(|v| v.to_bits().hash(&mut h));
551 self.fill_opacity.to_bits().hash(&mut h);
552 self.fill_antialias.hash(&mut h);
553 if let Some(ref c) = self.fill_outline_color {
554 c.iter().for_each(|v| v.to_bits().hash(&mut h));
555 }
556 let has_dd_width = self.width_expr.as_ref().is_some_and(|e| e.is_data_driven());
561 let has_dd_color = self.stroke_color_expr.as_ref().is_some_and(|e| e.is_data_driven());
562 has_dd_width.hash(&mut h);
563 has_dd_color.hash(&mut h);
564 if has_dd_width || has_dd_color {
565 self.eval_zoom.to_bits().hash(&mut h);
566 }
567 h.finish()
568 }
569
570 #[inline]
572 pub fn is_width_data_driven(&self) -> bool {
573 self.width_expr.as_ref().is_some_and(|e| e.is_data_driven())
574 }
575
576 #[inline]
578 pub fn is_stroke_color_data_driven(&self) -> bool {
579 self.stroke_color_expr.as_ref().is_some_and(|e| e.is_data_driven())
580 }
581
582 pub fn evaluate_width(&self, feature: &Feature) -> f32 {
586 match &self.width_expr {
587 Some(expr) if expr.is_data_driven() => {
588 let ctx = ExprEvalContext::with_feature(self.eval_zoom, &feature.properties);
589 expr.evaluate_with_properties(&ctx)
590 }
591 _ => self.stroke_width,
592 }
593 }
594
595 pub fn evaluate_stroke_color(&self, feature: &Feature) -> [f32; 4] {
599 match &self.stroke_color_expr {
600 Some(expr) if expr.is_data_driven() => {
601 let ctx = ExprEvalContext::with_feature(self.eval_zoom, &feature.properties);
602 expr.evaluate_with_properties(&ctx)
603 }
604 _ => self.stroke_color,
605 }
606 }
607}
608
609#[derive(Clone, Copy)]
610struct LinePlacementAnchor {
611 coord: GeoCoord,
612 rotation_rad: f32,
613 distance: f64,
614}
615
616#[derive(Debug, Clone, Copy, Default)]
626pub struct CircleInstanceData {
627 pub center: [f64; 3],
629 pub radius: f32,
631 pub color: [f32; 4],
633 pub stroke_color: [f32; 4],
635 pub stroke_width: f32,
637 pub blur: f32,
639}
640
641#[derive(Debug, Clone)]
649pub struct VectorMeshData {
650 pub positions: Vec<[f64; 3]>,
652 pub colors: Vec<[f32; 4]>,
654 pub normals: Vec<[f32; 3]>,
661 pub indices: Vec<u32>,
663 pub render_mode: VectorRenderMode,
668
669 pub line_distances: Vec<f32>,
675 pub line_normals: Vec<[f32; 2]>,
679 pub line_cap_joins: Vec<f32>,
684 pub line_params: [f32; 4],
688
689 pub circle_instances: Vec<CircleInstanceData>,
694
695 pub heatmap_points: Vec<[f64; 4]>,
699 pub heatmap_intensity: f32,
701
702 pub fill_translate: [f32; 2],
706 pub fill_opacity: f32,
708 pub fill_antialias: bool,
710 pub fill_outline_color: [f32; 4],
713 pub fill_pattern: Option<Arc<PatternImage>>,
719 pub fill_pattern_uvs: Vec<[f32; 2]>,
726
727 pub line_pattern: Option<Arc<PatternImage>>,
735 pub line_pattern_uvs: Vec<[f32; 2]>,
742}
743
744impl VectorMeshData {
745 #[inline]
747 pub fn is_empty(&self) -> bool {
748 self.indices.is_empty()
749 }
750
751 #[inline]
753 pub fn vertex_count(&self) -> usize {
754 self.positions.len()
755 }
756
757 #[inline]
759 pub fn index_count(&self) -> usize {
760 self.indices.len()
761 }
762
763 #[inline]
765 pub fn triangle_count(&self) -> usize {
766 self.indices.len() / 3
767 }
768
769 #[inline]
771 pub fn has_normals(&self) -> bool {
772 !self.normals.is_empty()
773 }
774
775 pub fn merge(&mut self, other: &VectorMeshData) {
777 let base = self.positions.len() as u32;
778 self.positions.extend_from_slice(&other.positions);
779 self.colors.extend_from_slice(&other.colors);
780 self.normals.extend_from_slice(&other.normals);
781 self.line_distances.extend_from_slice(&other.line_distances);
782 self.line_normals.extend_from_slice(&other.line_normals);
783 self.fill_pattern_uvs.extend_from_slice(&other.fill_pattern_uvs);
784 self.line_pattern_uvs.extend_from_slice(&other.line_pattern_uvs);
785 self.indices.extend(other.indices.iter().map(|i| base + i));
786 self.circle_instances.extend_from_slice(&other.circle_instances);
787 self.heatmap_points.extend_from_slice(&other.heatmap_points);
788 }
789
790 pub fn clear(&mut self) {
792 self.positions.clear();
793 self.colors.clear();
794 self.normals.clear();
795 self.line_distances.clear();
796 self.line_normals.clear();
797 self.indices.clear();
798 self.circle_instances.clear();
799 self.heatmap_points.clear();
800 self.fill_translate = [0.0, 0.0];
801 self.fill_opacity = 1.0;
802 self.fill_antialias = true;
803 self.fill_outline_color = [0.0, 0.0, 0.0, 0.0];
804 self.fill_pattern = None;
805 self.fill_pattern_uvs.clear();
806 self.line_pattern = None;
807 self.line_pattern_uvs.clear();
808 }
809}
810
811impl Default for VectorMeshData {
812 fn default() -> Self {
813 Self {
814 positions: Vec::new(),
815 colors: Vec::new(),
816 normals: Vec::new(),
817 indices: Vec::new(),
818 render_mode: VectorRenderMode::Generic,
819 line_distances: Vec::new(),
820 line_normals: Vec::new(),
821 line_cap_joins: Vec::new(),
822 line_params: [0.0; 4],
823 circle_instances: Vec::new(),
824 heatmap_points: Vec::new(),
825 heatmap_intensity: 0.0,
826 fill_translate: [0.0, 0.0],
827 fill_opacity: 1.0,
828 fill_antialias: true,
829 fill_outline_color: [0.0, 0.0, 0.0, 0.0],
830 fill_pattern: None,
831 fill_pattern_uvs: Vec::new(),
832 line_pattern: None,
833 line_pattern_uvs: Vec::new(),
834 }
835 }
836}
837
838pub struct VectorLayer {
847 id: LayerId,
848 name: String,
849 visible: bool,
850 opacity: f32,
851 pub query_layer_id: Option<String>,
853 pub query_source_id: Option<String>,
855 pub query_source_layer: Option<String>,
857 pub features: FeatureCollection,
859 pub feature_provenance: Vec<Option<FeatureProvenance>>,
861 pub style: VectorStyle,
863 data_generation: u64,
867}
868
869impl std::fmt::Debug for VectorLayer {
870 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
871 f.debug_struct("VectorLayer")
872 .field("id", &self.id)
873 .field("name", &self.name)
874 .field("visible", &self.visible)
875 .field("opacity", &self.opacity)
876 .field("feature_count", &self.features.len())
877 .field("style", &self.style)
878 .finish()
879 }
880}
881
882impl VectorLayer {
883 pub fn new(name: impl Into<String>, features: FeatureCollection, style: VectorStyle) -> Self {
885 Self {
886 id: LayerId::next(),
887 name: name.into(),
888 visible: true,
889 opacity: 1.0,
890 query_layer_id: None,
891 query_source_id: None,
892 query_source_layer: None,
893 feature_provenance: vec![None; features.len()],
894 features,
895 style,
896 data_generation: 0,
897 }
898 }
899
900 pub fn with_query_metadata(
902 mut self,
903 layer_id: impl Into<Option<String>>,
904 source_id: impl Into<Option<String>>,
905 ) -> Self {
906 self.query_layer_id = layer_id.into();
907 self.query_source_id = source_id.into();
908 self
909 }
910
911 pub fn with_source_layer(mut self, source_layer: Option<String>) -> Self {
913 self.query_source_layer = source_layer;
914 self
915 }
916
917 pub fn set_features_with_provenance(
919 &mut self,
920 features: FeatureCollection,
921 mut provenance: Vec<Option<FeatureProvenance>>,
922 ) {
923 if provenance.len() < features.len() {
924 provenance.resize(features.len(), None);
925 } else if provenance.len() > features.len() {
926 provenance.truncate(features.len());
927 }
928 self.features = features;
929 self.feature_provenance = provenance;
930 self.data_generation = self.data_generation.wrapping_add(1);
931 }
932
933 #[inline]
938 pub fn data_generation(&self) -> u64 {
939 self.data_generation
940 }
941
942 pub fn set_query_metadata(&mut self, layer_id: Option<String>, source_id: Option<String>) {
944 self.query_layer_id = layer_id;
945 self.query_source_id = source_id;
946 }
947
948 #[inline]
952 pub fn feature_count(&self) -> usize {
953 self.features.len()
954 }
955
956 #[inline]
958 pub fn total_coords(&self) -> usize {
959 self.features.total_coords()
960 }
961
962 pub fn drape_on_terrain(&mut self, terrain: &TerrainManager) {
971 if !terrain.enabled() {
972 return;
973 }
974 for feature in &mut self.features.features {
975 drape_geometry(&mut feature.geometry, terrain);
976 }
977 }
978
979 pub fn tessellate(&self, projection: CameraProjection) -> VectorMeshData {
996 let mut mesh = VectorMeshData {
997 render_mode: self.style.render_mode,
998 ..VectorMeshData::default()
999 };
1000
1001 let dd_width = self.style.is_width_data_driven();
1002 let dd_color = self.style.is_stroke_color_data_driven();
1003 let default_half_width = self.style.stroke_width as f64 * DEGREES_PER_PIXEL_APPROX;
1004
1005 if dd_width || dd_color {
1006 for feature in &self.features.features {
1008 let half_width = if dd_width {
1009 self.style.evaluate_width(feature) as f64 * DEGREES_PER_PIXEL_APPROX
1010 } else {
1011 default_half_width
1012 };
1013 if dd_color {
1014 let color = self.style.evaluate_stroke_color(feature);
1015 let mut feature_style = self.style.clone();
1016 feature_style.stroke_color = color;
1017 tessellate_geometry(
1018 &feature.geometry,
1019 &feature_style,
1020 projection,
1021 half_width,
1022 &mut mesh,
1023 );
1024 } else {
1025 tessellate_geometry(
1026 &feature.geometry,
1027 &self.style,
1028 projection,
1029 half_width,
1030 &mut mesh,
1031 );
1032 }
1033 }
1034 } else {
1035 for feature in &self.features.features {
1037 tessellate_geometry(
1038 &feature.geometry,
1039 &self.style,
1040 projection,
1041 default_half_width,
1042 &mut mesh,
1043 );
1044 }
1045 }
1046
1047 if self.style.render_mode == VectorRenderMode::Fill {
1050 mesh.fill_translate = self.style.fill_translate;
1051 mesh.fill_opacity = self.style.fill_opacity;
1052 mesh.fill_antialias = self.style.fill_antialias;
1053 mesh.fill_outline_color = self.style.fill_outline_color
1054 .unwrap_or(self.style.stroke_color);
1055 }
1056
1057 if self.style.render_mode == VectorRenderMode::Line {
1060 let (dash_len, gap_len) = match &self.style.dash_array {
1061 Some(arr) if arr.len() >= 2 => (arr[0], arr[1]),
1062 _ => (0.0, 0.0),
1063 };
1064 let cap_round = match self.style.line_cap {
1065 LineCap::Round => 1.0,
1066 _ => 0.0,
1067 };
1068 mesh.line_params = [dash_len, gap_len, cap_round, 0.0];
1069 }
1070
1071 mesh
1072 }
1073
1074 pub fn symbol_candidates(&self) -> Vec<SymbolCandidate> {
1076 self.symbol_candidates_for_features(&self.features, &self.feature_provenance)
1077 }
1078
1079 pub fn symbol_candidates_for_features(
1081 &self,
1082 features: &FeatureCollection,
1083 feature_provenance: &[Option<FeatureProvenance>],
1084 ) -> Vec<SymbolCandidate> {
1085 if self.style.render_mode != VectorRenderMode::Symbol {
1086 return Vec::new();
1087 }
1088
1089 let mut out = Vec::new();
1090 for (feature_index, feature) in features.features.iter().enumerate() {
1091 collect_symbol_candidates_from_geometry(
1092 self.id,
1093 self.query_layer_id.as_deref(),
1094 self.query_source_id.as_deref(),
1095 feature_provenance.get(feature_index).and_then(|p| p.as_ref()),
1096 feature_index,
1097 0,
1098 feature,
1099 &feature.geometry,
1100 &self.style,
1101 &mut out,
1102 );
1103 }
1104 out
1105 }
1106}
1107
1108fn collect_symbol_candidates_from_geometry(
1109 layer_id: LayerId,
1110 query_layer_id: Option<&str>,
1111 query_source_id: Option<&str>,
1112 provenance: Option<&FeatureProvenance>,
1113 feature_index: usize,
1114 point_index: usize,
1115 feature: &Feature,
1116 geometry: &Geometry,
1117 style: &VectorStyle,
1118 out: &mut Vec<SymbolCandidate>,
1119) -> usize {
1120 if style.symbol_placement == SymbolPlacement::Line {
1121 return collect_line_symbol_candidates_from_geometry(
1122 layer_id,
1123 query_layer_id,
1124 query_source_id,
1125 provenance,
1126 feature_index,
1127 point_index,
1128 feature,
1129 geometry,
1130 style,
1131 out,
1132 );
1133 }
1134
1135 match geometry {
1136 Geometry::Point(point) => {
1137 let candidates = symbol_candidates_for_point(
1138 layer_id,
1139 query_layer_id,
1140 query_source_id,
1141 provenance,
1142 feature_index,
1143 point_index,
1144 feature,
1145 point.coord,
1146 style,
1147 );
1148 out.extend(candidates);
1149 point_index + 1
1150 }
1151 Geometry::MultiPoint(points) => {
1152 let mut next = point_index;
1153 for point in &points.points {
1154 next = collect_symbol_candidates_from_geometry(
1155 layer_id,
1156 query_layer_id,
1157 query_source_id,
1158 provenance,
1159 feature_index,
1160 next,
1161 feature,
1162 &Geometry::Point(point.clone()),
1163 style,
1164 out,
1165 );
1166 }
1167 next
1168 }
1169 Geometry::GeometryCollection(geometries) => {
1170 let mut next = point_index;
1171 for geometry in geometries {
1172 next = collect_symbol_candidates_from_geometry(
1173 layer_id,
1174 query_layer_id,
1175 query_source_id,
1176 provenance,
1177 feature_index,
1178 next,
1179 feature,
1180 geometry,
1181 style,
1182 out,
1183 );
1184 }
1185 next
1186 }
1187 _ => point_index,
1188 }
1189}
1190
1191fn collect_line_symbol_candidates_from_geometry(
1192 layer_id: LayerId,
1193 query_layer_id: Option<&str>,
1194 query_source_id: Option<&str>,
1195 provenance: Option<&FeatureProvenance>,
1196 feature_index: usize,
1197 point_index: usize,
1198 feature: &Feature,
1199 geometry: &Geometry,
1200 style: &VectorStyle,
1201 out: &mut Vec<SymbolCandidate>,
1202) -> usize {
1203 match geometry {
1204 Geometry::LineString(line) => {
1205 let mut next = point_index;
1206 let label_length = estimated_line_label_length_meters(feature, style);
1207 for (slot_index, anchor) in line_placement_anchors(line, style, label_length).into_iter().enumerate() {
1208 let candidates = symbol_candidates_at_anchor(
1209 layer_id,
1210 query_layer_id,
1211 query_source_id,
1212 provenance,
1213 feature_index,
1214 next,
1215 feature,
1216 anchor.coord,
1217 style,
1218 anchor.rotation_rad,
1219 Some(slot_index),
1220 );
1221 next += candidates.len();
1222 out.extend(candidates);
1223 }
1224 next
1225 }
1226 Geometry::MultiLineString(lines) => {
1227 let mut next = point_index;
1228 for line in &lines.lines {
1229 next = collect_line_symbol_candidates_from_geometry(
1230 layer_id,
1231 query_layer_id,
1232 query_source_id,
1233 provenance,
1234 feature_index,
1235 next,
1236 feature,
1237 &Geometry::LineString(line.clone()),
1238 style,
1239 out,
1240 );
1241 }
1242 next
1243 }
1244 Geometry::GeometryCollection(geometries) => {
1245 let mut next = point_index;
1246 for geometry in geometries {
1247 next = collect_line_symbol_candidates_from_geometry(
1248 layer_id,
1249 query_layer_id,
1250 query_source_id,
1251 provenance,
1252 feature_index,
1253 next,
1254 feature,
1255 geometry,
1256 style,
1257 out,
1258 );
1259 }
1260 next
1261 }
1262 _ => point_index,
1263 }
1264}
1265
1266fn symbol_candidates_for_point(
1267 layer_id: LayerId,
1268 query_layer_id: Option<&str>,
1269 query_source_id: Option<&str>,
1270 provenance: Option<&FeatureProvenance>,
1271 feature_index: usize,
1272 point_index: usize,
1273 feature: &Feature,
1274 anchor: GeoCoord,
1275 style: &VectorStyle,
1276) -> Vec<SymbolCandidate> {
1277 symbol_candidates_at_anchor(
1278 layer_id,
1279 query_layer_id,
1280 query_source_id,
1281 provenance,
1282 feature_index,
1283 point_index,
1284 feature,
1285 anchor,
1286 style,
1287 0.0,
1288 None,
1289 )
1290}
1291
1292fn symbol_candidates_at_anchor(
1293 layer_id: LayerId,
1294 query_layer_id: Option<&str>,
1295 query_source_id: Option<&str>,
1296 provenance: Option<&FeatureProvenance>,
1297 feature_index: usize,
1298 point_index: usize,
1299 feature: &Feature,
1300 anchor: GeoCoord,
1301 style: &VectorStyle,
1302 rotation_rad: f32,
1303 line_slot_index: Option<usize>,
1304) -> Vec<SymbolCandidate> {
1305 let feature_id = feature_id_for_feature(feature, feature_index);
1306 let text = style
1307 .symbol_text_field
1308 .as_deref()
1309 .and_then(|field| feature.property(field))
1310 .and_then(symbol_text_from_property)
1311 .map(|text| transform_symbol_text(text, style.symbol_text_transform));
1312 let icon_image = style.symbol_icon_image.clone();
1313
1314 if text.is_none() && icon_image.is_none() {
1315 return Vec::new();
1316 }
1317
1318fn transform_symbol_text(text: String, transform: SymbolTextTransform) -> String {
1325 match transform {
1326 SymbolTextTransform::None => text,
1327 SymbolTextTransform::Uppercase => text.to_uppercase(),
1328 SymbolTextTransform::Lowercase => text.to_lowercase(),
1329 }
1330}
1331
1332 let cross_tile_id = symbol_cross_tile_id(
1333 query_layer_id,
1334 query_source_id,
1335 &feature_id,
1336 text.as_deref(),
1337 icon_image.as_deref(),
1338 anchor,
1339 style.symbol_placement,
1340 line_slot_index,
1341 style,
1342 );
1343
1344 let base_id = format!("{}:{feature_index}:{point_index}", layer_id.as_u64());
1345 let text_present = text.is_some();
1346 let icon_present = icon_image.is_some();
1347 let text_optional = style.symbol_text_optional;
1348 let icon_optional = style.symbol_icon_optional;
1349
1350 let mut variants = Vec::new();
1351 let base_candidate = make_symbol_candidate(
1352 &base_id,
1353 &base_id,
1354 query_layer_id,
1355 query_source_id,
1356 provenance,
1357 &feature_id,
1358 feature_index,
1359 style.symbol_placement,
1360 anchor,
1361 text.clone(),
1362 icon_image.clone(),
1363 style,
1364 cross_tile_id.clone(),
1365 rotation_rad,
1366 );
1367 variants.push(base_candidate);
1368
1369 if text_present && icon_present {
1374 if text_optional {
1375 variants.push(make_symbol_candidate(
1376 &format!("{base_id}:icon-only"),
1377 &base_id,
1378 query_layer_id,
1379 query_source_id,
1380 provenance,
1381 &feature_id,
1382 feature_index,
1383 style.symbol_placement,
1384 anchor,
1385 None,
1386 icon_image.clone(),
1387 style,
1388 cross_tile_id.clone(),
1389 rotation_rad,
1390 ));
1391 }
1392 if icon_optional {
1393 variants.push(make_symbol_candidate(
1394 &format!("{base_id}:text-only"),
1395 &base_id,
1396 query_layer_id,
1397 query_source_id,
1398 provenance,
1399 &feature_id,
1400 feature_index,
1401 style.symbol_placement,
1402 anchor,
1403 text,
1404 None,
1405 style,
1406 cross_tile_id,
1407 rotation_rad,
1408 ));
1409 }
1410 }
1411
1412 variants
1413}
1414
1415fn make_symbol_candidate(
1416 id: &str,
1417 placement_group_id: &str,
1418 query_layer_id: Option<&str>,
1419 query_source_id: Option<&str>,
1420 provenance: Option<&FeatureProvenance>,
1421 feature_id: &str,
1422 feature_index: usize,
1423 placement: SymbolPlacement,
1424 anchor: GeoCoord,
1425 text: Option<String>,
1426 icon_image: Option<String>,
1427 style: &VectorStyle,
1428 cross_tile_id: String,
1429 rotation_rad: f32,
1430) -> SymbolCandidate {
1431 let has_text = text.is_some();
1432 let has_icon = icon_image.is_some();
1433
1434 SymbolCandidate {
1435 id: id.to_owned(),
1436 layer_id: query_layer_id.map(ToOwned::to_owned),
1437 source_id: query_source_id.map(ToOwned::to_owned),
1438 source_layer: provenance.and_then(|p| p.source_layer.clone()),
1439 source_tile: provenance.and_then(|p| p.source_tile),
1440 feature_id: feature_id.to_owned(),
1441 feature_index,
1442 placement_group_id: placement_group_id.to_owned(),
1443 placement,
1444 anchor,
1445 text,
1446 icon_image,
1447 font_stack: style.symbol_font_stack.clone(),
1448 cross_tile_id,
1449 rotation_rad,
1450 size_px: style.symbol_size,
1451 padding_px: style.symbol_padding,
1452 allow_overlap: effective_symbol_overlap(style, has_text, has_icon),
1453 ignore_placement: effective_symbol_ignore_placement(style, has_text, has_icon),
1454 sort_key: style.symbol_sort_key,
1455 radial_offset: style.symbol_text_radial_offset,
1456 variable_anchor_offsets: style.symbol_variable_anchor_offsets.clone(),
1457 text_max_width: style.symbol_text_max_width,
1458 text_line_height: style.symbol_text_line_height,
1459 text_letter_spacing: style.symbol_text_letter_spacing,
1460 icon_text_fit: style.symbol_icon_text_fit,
1461 icon_text_fit_padding: style.symbol_icon_text_fit_padding,
1462 anchors: if style.symbol_variable_anchor_offsets.is_some() {
1463 style
1464 .symbol_variable_anchor_offsets
1465 .as_ref()
1466 .map(|offsets| offsets.iter().map(|(anchor, _)| *anchor).collect())
1467 .unwrap_or_default()
1468 } else {
1469 style.symbol_anchors.clone()
1470 },
1471 writing_mode: style.symbol_writing_mode,
1472 offset_px: style.symbol_offset,
1473 fill_color: style.fill_color,
1474 halo_color: style.symbol_halo_color,
1475 }
1476}
1477
1478fn effective_symbol_ignore_placement(style: &VectorStyle, has_text: bool, has_icon: bool) -> bool {
1487 match (has_text, has_icon) {
1488 (true, true) => {
1489 style.symbol_text_ignore_placement && style.symbol_icon_ignore_placement
1490 }
1491 (true, false) => style.symbol_text_ignore_placement,
1492 (false, true) => style.symbol_icon_ignore_placement,
1493 (false, false) => false,
1494 }
1495}
1496
1497fn effective_symbol_overlap(style: &VectorStyle, has_text: bool, has_icon: bool) -> bool {
1506 match (has_text, has_icon) {
1507 (true, true) => style.symbol_text_allow_overlap && style.symbol_icon_allow_overlap,
1508 (true, false) => style.symbol_text_allow_overlap,
1509 (false, true) => style.symbol_icon_allow_overlap,
1510 (false, false) => style.symbol_allow_overlap,
1511 }
1512}
1513
1514fn symbol_cross_tile_id(
1526 query_layer_id: Option<&str>,
1527 query_source_id: Option<&str>,
1528 feature_id: &str,
1529 text: Option<&str>,
1530 icon_image: Option<&str>,
1531 anchor: GeoCoord,
1532 placement: SymbolPlacement,
1533 line_slot_index: Option<usize>,
1534 style: &VectorStyle,
1535) -> String {
1536 match placement {
1537 SymbolPlacement::Point => format!(
1538 "{}|{}|{:.6}|{:.6}",
1539 text.unwrap_or(""),
1540 icon_image.unwrap_or(""),
1541 anchor.lat,
1542 anchor.lon,
1543 ),
1544 SymbolPlacement::Line => {
1545 let slot = line_slot_index.unwrap_or(0);
1546 let world = CameraProjection::WebMercator.project(&anchor);
1547 let coarse_bucket = ((style.symbol_spacing.max(style.symbol_size).max(1.0) as f64)
1554 * METERS_PER_PIXEL_APPROX
1555 * 2.0)
1556 .max(1.0);
1557 let bucket_x = (world.position.x / coarse_bucket).round() as i64;
1558 let bucket_y = (world.position.y / coarse_bucket).round() as i64;
1559 format!(
1560 "line|{}|{}|{}|{}|{}|{}|{}|{}",
1561 query_source_id.unwrap_or(""),
1562 query_layer_id.unwrap_or(""),
1563 feature_id,
1564 slot,
1565 bucket_x,
1566 bucket_y,
1567 text.unwrap_or(""),
1568 icon_image.unwrap_or(""),
1569 )
1570 }
1571 }
1572}
1573
1574fn line_placement_anchors(
1583 line: &crate::geometry::LineString,
1584 style: &VectorStyle,
1585 label_length: f64,
1586) -> Vec<LinePlacementAnchor> {
1587 if line.coords.len() < 2 {
1588 return Vec::new();
1589 }
1590
1591 let projected: Vec<_> = line
1592 .coords
1593 .iter()
1594 .map(|coord| CameraProjection::WebMercator.project(coord))
1595 .collect();
1596 let total_length: f64 = projected
1597 .windows(2)
1598 .map(|segment| {
1599 let a = segment[0].position;
1600 let b = segment[1].position;
1601 let dx = b.x - a.x;
1602 let dy = b.y - a.y;
1603 let dz = b.z - a.z;
1604 (dx * dx + dy * dy + dz * dz).sqrt()
1605 })
1606 .sum();
1607 if total_length <= f64::EPSILON {
1608 return line
1609 .coords
1610 .first()
1611 .copied()
1612 .map(|coord| LinePlacementAnchor {
1613 coord,
1614 rotation_rad: 0.0,
1615 distance: total_length * 0.5,
1616 })
1617 .into_iter()
1618 .collect();
1619 }
1620
1621 let spacing = (style.symbol_spacing.max(style.symbol_size).max(1.0) as f64)
1626 * METERS_PER_PIXEL_APPROX;
1627 if spacing <= f64::EPSILON || total_length <= spacing {
1628 return interpolate_line_anchor_at_distance(
1629 line,
1630 &projected,
1631 total_length * 0.5,
1632 style.symbol_keep_upright,
1633 )
1634 .filter(|anchor| line_anchor_passes_max_angle(&projected, anchor.distance, label_length, style))
1635 .into_iter()
1636 .collect();
1637 }
1638
1639 let mut anchors = Vec::new();
1640 let mut target = spacing * 0.5;
1641 while target < total_length {
1642 if let Some(anchor) =
1643 interpolate_line_anchor_at_distance(line, &projected, target, style.symbol_keep_upright)
1644 {
1645 if line_anchor_passes_max_angle(&projected, anchor.distance, label_length, style) {
1646 anchors.push(anchor);
1647 }
1648 }
1649 target += spacing;
1650 }
1651
1652 if anchors.is_empty() {
1653 interpolate_line_anchor_at_distance(
1654 line,
1655 &projected,
1656 total_length * 0.5,
1657 style.symbol_keep_upright,
1658 )
1659 .filter(|anchor| line_anchor_passes_max_angle(&projected, anchor.distance, label_length, style))
1660 .into_iter()
1661 .collect()
1662 } else {
1663 anchors
1664 }
1665}
1666
1667fn estimated_line_label_length_meters(feature: &Feature, style: &VectorStyle) -> f64 {
1674 let text = style
1675 .symbol_text_field
1676 .as_deref()
1677 .and_then(|field| feature.property(field))
1678 .and_then(symbol_text_from_property);
1679 let icon = style.symbol_icon_image.as_deref();
1680 let size_px = style.symbol_size.max(1.0) as f64;
1681 let text_width_px = text
1682 .as_deref()
1683 .map(|value| value.chars().count() as f64 * size_px * 0.6)
1684 .unwrap_or(0.0);
1685 let icon_width_px = if icon.is_some() { size_px * 1.2 } else { 0.0 };
1686 (text_width_px.max(size_px) + icon_width_px + style.symbol_padding.max(0.0) as f64 * 2.0)
1687 * METERS_PER_PIXEL_APPROX
1688}
1689
1690fn line_anchor_passes_max_angle(
1697 projected: &[WorldCoord],
1698 anchor_distance: f64,
1699 label_length: f64,
1700 style: &VectorStyle,
1701) -> bool {
1702 if projected.len() < 3 {
1703 return true;
1704 }
1705 let half_label = label_length * 0.5;
1706 if anchor_distance - half_label < 0.0 || anchor_distance + half_label > line_total_length(projected) {
1707 return false;
1708 }
1709
1710 let max_angle = style.symbol_max_angle.max(0.0) as f64 * std::f64::consts::PI / 180.0;
1711 if max_angle >= std::f64::consts::PI {
1712 return true;
1713 }
1714
1715 let start = anchor_distance - half_label;
1716 let end = anchor_distance + half_label;
1717 let mut distance = 0.0;
1718 let mut accumulated_turn = 0.0;
1719
1720 for index in 1..projected.len() - 1 {
1721 let prev = projected[index - 1].position;
1722 let current = projected[index].position;
1723 let next = projected[index + 1].position;
1724 let segment_dx = current.x - prev.x;
1725 let segment_dy = current.y - prev.y;
1726 let segment_dz = current.z - prev.z;
1727 distance += (segment_dx * segment_dx + segment_dy * segment_dy + segment_dz * segment_dz).sqrt();
1728 if distance < start || distance > end {
1729 continue;
1730 }
1731
1732 let prev_angle = (current.y - prev.y).atan2(current.x - prev.x);
1733 let next_angle = (next.y - current.y).atan2(next.x - current.x);
1734 accumulated_turn += normalize_angle_delta(next_angle - prev_angle).abs();
1735 if accumulated_turn > max_angle {
1736 return false;
1737 }
1738 }
1739
1740 true
1741}
1742
1743fn normalize_angle_delta(angle: f64) -> f64 {
1744 ((angle + std::f64::consts::PI * 3.0) % (std::f64::consts::PI * 2.0))
1745 - std::f64::consts::PI
1746}
1747
1748fn line_total_length(projected: &[WorldCoord]) -> f64 {
1749 projected
1750 .windows(2)
1751 .map(|segment| {
1752 let a = segment[0].position;
1753 let b = segment[1].position;
1754 let dx = b.x - a.x;
1755 let dy = b.y - a.y;
1756 let dz = b.z - a.z;
1757 (dx * dx + dy * dy + dz * dz).sqrt()
1758 })
1759 .sum()
1760}
1761
1762fn interpolate_line_anchor_at_distance(
1763 line: &crate::geometry::LineString,
1764 projected: &[WorldCoord],
1765 target: f64,
1766 keep_upright: bool,
1767) -> Option<LinePlacementAnchor> {
1768 let mut traversed = 0.0;
1769 for (coords, segment) in line.coords.windows(2).zip(projected.windows(2)) {
1770 let a = segment[0].position;
1771 let b = segment[1].position;
1772 let dx = b.x - a.x;
1773 let dy = b.y - a.y;
1774 let dz = b.z - a.z;
1775 let segment_length = (dx * dx + dy * dy + dz * dz).sqrt();
1776 if segment_length <= f64::EPSILON {
1777 continue;
1778 }
1779 if traversed + segment_length >= target {
1780 let t = ((target - traversed) / segment_length).clamp(0.0, 1.0);
1781 let start = coords[0];
1782 let end = coords[1];
1783 let coord = GeoCoord::new(
1784 start.lat + (end.lat - start.lat) * t,
1785 start.lon + (end.lon - start.lon) * t,
1786 start.alt + (end.alt - start.alt) * t,
1787 );
1788 let rotation_rad = normalize_line_label_rotation(
1791 (b.y - a.y).atan2(b.x - a.x) as f32,
1792 keep_upright,
1793 );
1794 return Some(LinePlacementAnchor {
1795 coord,
1796 rotation_rad,
1797 distance: target,
1798 });
1799 }
1800 traversed += segment_length;
1801 }
1802
1803 line.coords.last().copied().map(|coord| LinePlacementAnchor {
1804 coord,
1805 rotation_rad: 0.0,
1806 distance: target,
1807 })
1808}
1809
1810fn normalize_line_label_rotation(rotation_rad: f32, keep_upright: bool) -> f32 {
1817 if !keep_upright {
1818 return rotation_rad;
1819 }
1820
1821 let mut normalized = rotation_rad;
1822 while normalized > std::f32::consts::PI {
1823 normalized -= std::f32::consts::TAU;
1824 }
1825 while normalized < -std::f32::consts::PI {
1826 normalized += std::f32::consts::TAU;
1827 }
1828 if normalized > std::f32::consts::FRAC_PI_2 {
1829 normalized -= std::f32::consts::PI;
1830 } else if normalized < -std::f32::consts::FRAC_PI_2 {
1831 normalized += std::f32::consts::PI;
1832 }
1833 normalized
1834}
1835
1836fn symbol_text_from_property(value: &crate::geometry::PropertyValue) -> Option<String> {
1837 match value {
1838 crate::geometry::PropertyValue::Null => None,
1839 crate::geometry::PropertyValue::Bool(value) => Some(value.to_string()),
1840 crate::geometry::PropertyValue::Number(value) => Some(value.to_string()),
1841 crate::geometry::PropertyValue::String(value) => Some(value.clone()),
1842 }
1843}
1844
1845fn tessellate_geometry(
1854 geometry: &Geometry,
1855 style: &VectorStyle,
1856 projection: CameraProjection,
1857 half_width: f64,
1858 mesh: &mut VectorMeshData,
1859) {
1860 match style.render_mode {
1861 VectorRenderMode::Generic => tessellate_generic_geometry(geometry, style, projection, half_width, mesh),
1862 VectorRenderMode::Fill => tessellate_fill_geometry(geometry, style, projection, mesh),
1863 VectorRenderMode::Line => tessellate_line_geometry(geometry, style, projection, half_width, mesh),
1864 VectorRenderMode::Circle => tessellate_circle_geometry(geometry, style, projection, mesh),
1865 VectorRenderMode::Heatmap => tessellate_heatmap_geometry(geometry, style, projection, mesh),
1866 VectorRenderMode::FillExtrusion => tessellate_fill_extrusion_geometry(geometry, style, projection, mesh),
1867 VectorRenderMode::Symbol => tessellate_symbol_geometry(geometry, style, projection, mesh),
1868 }
1869}
1870
1871fn tessellate_generic_geometry(
1872 geometry: &Geometry,
1873 style: &VectorStyle,
1874 projection: CameraProjection,
1875 half_width: f64,
1876 mesh: &mut VectorMeshData,
1877) {
1878 match geometry {
1879 Geometry::Point(p) => append_square_marker(mesh, &p.coord, projection, half_width * METERS_PER_DEGREE, style.fill_color),
1880 Geometry::LineString(ls) => append_stroked_line(mesh, &ls.coords, projection, half_width, style.stroke_color, style),
1881 Geometry::Polygon(poly) => append_polygon_fill(mesh, &poly.exterior, projection, style.fill_color, None),
1882 Geometry::MultiPoint(mp) => {
1883 for p in &mp.points {
1884 tessellate_generic_geometry(&Geometry::Point(p.clone()), style, projection, half_width, mesh);
1885 }
1886 }
1887 Geometry::MultiLineString(mls) => {
1888 for ls in &mls.lines {
1889 tessellate_generic_geometry(&Geometry::LineString(ls.clone()), style, projection, half_width, mesh);
1890 }
1891 }
1892 Geometry::MultiPolygon(mpoly) => {
1893 for poly in &mpoly.polygons {
1894 tessellate_generic_geometry(&Geometry::Polygon(poly.clone()), style, projection, half_width, mesh);
1895 }
1896 }
1897 Geometry::GeometryCollection(geoms) => {
1898 for g in geoms {
1899 tessellate_generic_geometry(g, style, projection, half_width, mesh);
1900 }
1901 }
1902 }
1903}
1904
1905fn tessellate_fill_geometry(geometry: &Geometry, style: &VectorStyle, projection: CameraProjection, mesh: &mut VectorMeshData) {
1906 if style.fill_pattern.is_some() && mesh.fill_pattern.is_none() {
1908 mesh.fill_pattern = style.fill_pattern.clone();
1909 }
1910
1911 match geometry {
1912 Geometry::Polygon(poly) => {
1913 append_polygon_fill(mesh, &poly.exterior, projection, style.fill_color, style.fill_pattern.as_deref());
1914 if style.stroke_width > 0.0 {
1915 append_stroked_line(mesh, &poly.exterior, projection, style.stroke_width as f64 * DEGREES_PER_PIXEL_APPROX, style.stroke_color, style);
1916 for hole in &poly.interiors {
1917 append_stroked_line(mesh, hole, projection, style.stroke_width as f64 * DEGREES_PER_PIXEL_APPROX, style.stroke_color, style);
1918 }
1919 }
1920 }
1921 Geometry::MultiPolygon(mpoly) => {
1922 for poly in &mpoly.polygons {
1923 tessellate_fill_geometry(&Geometry::Polygon(poly.clone()), style, projection, mesh);
1924 }
1925 }
1926 Geometry::GeometryCollection(geoms) => {
1927 for g in geoms {
1928 tessellate_fill_geometry(g, style, projection, mesh);
1929 }
1930 }
1931 _ => {}
1932 }
1933}
1934
1935fn tessellate_line_geometry(
1936 geometry: &Geometry,
1937 style: &VectorStyle,
1938 projection: CameraProjection,
1939 half_width: f64,
1940 mesh: &mut VectorMeshData,
1941) {
1942 if style.line_pattern.is_some() && mesh.line_pattern.is_none() {
1944 mesh.line_pattern = style.line_pattern.clone();
1945 }
1946
1947 match geometry {
1948 Geometry::LineString(ls) => append_stroked_line(mesh, &ls.coords, projection, half_width, style.stroke_color, style),
1949 Geometry::Polygon(poly) => {
1950 append_stroked_line(mesh, &poly.exterior, projection, half_width, style.stroke_color, style);
1951 for hole in &poly.interiors {
1952 append_stroked_line(mesh, hole, projection, half_width, style.stroke_color, style);
1953 }
1954 }
1955 Geometry::MultiLineString(mls) => {
1956 for ls in &mls.lines {
1957 tessellate_line_geometry(&Geometry::LineString(ls.clone()), style, projection, half_width, mesh);
1958 }
1959 }
1960 Geometry::MultiPolygon(mpoly) => {
1961 for poly in &mpoly.polygons {
1962 tessellate_line_geometry(&Geometry::Polygon(poly.clone()), style, projection, half_width, mesh);
1963 }
1964 }
1965 Geometry::GeometryCollection(geoms) => {
1966 for g in geoms {
1967 tessellate_line_geometry(g, style, projection, half_width, mesh);
1968 }
1969 }
1970 _ => {}
1971 }
1972}
1973
1974fn tessellate_circle_geometry(geometry: &Geometry, style: &VectorStyle, projection: CameraProjection, mesh: &mut VectorMeshData) {
1975 match geometry {
1976 Geometry::Point(p) => {
1977 let radius = style.point_radius as f64 * DEGREES_PER_PIXEL_APPROX * METERS_PER_DEGREE;
1978 let stroke_w = style.stroke_width.max(0.0) as f64 * DEGREES_PER_PIXEL_APPROX * METERS_PER_DEGREE;
1979 append_circle(mesh, &p.coord, projection, radius, style.fill_color, Some((style.stroke_color, stroke_w)));
1980
1981 let w = projection.project(&p.coord);
1985 mesh.circle_instances.push(CircleInstanceData {
1986 center: [w.position.x, w.position.y, w.position.z],
1987 radius: radius as f32,
1988 color: style.fill_color,
1989 stroke_color: style.stroke_color,
1990 stroke_width: stroke_w as f32,
1991 blur: 0.0,
1992 });
1993 }
1994 Geometry::MultiPoint(mp) => {
1995 for p in &mp.points {
1996 tessellate_circle_geometry(&Geometry::Point(p.clone()), style, projection, mesh);
1997 }
1998 }
1999 Geometry::GeometryCollection(geoms) => {
2000 for g in geoms {
2001 tessellate_circle_geometry(g, style, projection, mesh);
2002 }
2003 }
2004 _ => {}
2005 }
2006}
2007
2008fn tessellate_heatmap_geometry(geometry: &Geometry, style: &VectorStyle, projection: CameraProjection, mesh: &mut VectorMeshData) {
2009 match geometry {
2010 Geometry::Point(p) => {
2011 let radius = style.heatmap_radius.max(0.0) as f64 * DEGREES_PER_PIXEL_APPROX * METERS_PER_DEGREE;
2012 append_heat_blob(mesh, &p.coord, projection, radius, style.fill_color, style.heatmap_intensity.max(0.0));
2013
2014 let w = projection.project(&p.coord);
2017 let weight = style.heatmap_intensity.max(0.0) as f64;
2018 mesh.heatmap_points.push([w.position.x, w.position.y, weight, radius]);
2019 }
2020 Geometry::MultiPoint(mp) => {
2021 for p in &mp.points {
2022 tessellate_heatmap_geometry(&Geometry::Point(p.clone()), style, projection, mesh);
2023 }
2024 }
2025 Geometry::GeometryCollection(geoms) => {
2026 for g in geoms {
2027 tessellate_heatmap_geometry(g, style, projection, mesh);
2028 }
2029 }
2030 _ => {}
2031 }
2032}
2033
2034fn tessellate_fill_extrusion_geometry(geometry: &Geometry, style: &VectorStyle, projection: CameraProjection, mesh: &mut VectorMeshData) {
2035 match geometry {
2036 Geometry::Polygon(poly) => append_extruded_polygon(mesh, &poly.exterior, projection, style),
2037 Geometry::MultiPolygon(mpoly) => {
2038 for poly in &mpoly.polygons {
2039 append_extruded_polygon(mesh, &poly.exterior, projection, style);
2040 }
2041 }
2042 Geometry::GeometryCollection(geoms) => {
2043 for g in geoms {
2044 tessellate_fill_extrusion_geometry(g, style, projection, mesh);
2045 }
2046 }
2047 _ => {}
2048 }
2049}
2050
2051fn tessellate_symbol_geometry(geometry: &Geometry, style: &VectorStyle, projection: CameraProjection, mesh: &mut VectorMeshData) {
2052 match geometry {
2053 Geometry::Point(p) => {
2054 let size = style.symbol_size.max(0.0) as f64 * DEGREES_PER_PIXEL_APPROX * METERS_PER_DEGREE;
2055 append_square_marker(mesh, &p.coord, projection, size * 1.35, style.symbol_halo_color);
2056 append_square_marker(mesh, &p.coord, projection, size, style.fill_color);
2057 }
2058 Geometry::MultiPoint(mp) => {
2059 for p in &mp.points {
2060 tessellate_symbol_geometry(&Geometry::Point(p.clone()), style, projection, mesh);
2061 }
2062 }
2063 Geometry::GeometryCollection(geoms) => {
2064 for g in geoms {
2065 tessellate_symbol_geometry(g, style, projection, mesh);
2066 }
2067 }
2068 _ => {}
2069 }
2070}
2071
2072fn append_polygon_fill(mesh: &mut VectorMeshData, coords: &[GeoCoord], projection: CameraProjection, color: [f32; 4], pattern: Option<&PatternImage>) {
2073 let ring = normalized_ring(coords);
2074 if ring.len() < 3 {
2075 return;
2076 }
2077 let indices = tessellator::triangulate_polygon(&ring);
2078 let base = mesh.positions.len() as u32;
2079 for coord in &ring {
2080 let w = projection.project(coord);
2081 mesh.positions.push([w.position.x, w.position.y, w.position.z]);
2082 mesh.colors.push(color);
2083
2084 if let Some(pat) = pattern {
2088 let u = w.position.x as f32 / pat.width.max(1) as f32;
2089 let v = w.position.y as f32 / pat.height.max(1) as f32;
2090 mesh.fill_pattern_uvs.push([u, v]);
2091 }
2092 }
2093 for idx in indices {
2094 mesh.indices.push(base + idx);
2095 }
2096}
2097
2098fn append_stroked_line(mesh: &mut VectorMeshData, coords: &[GeoCoord], projection: CameraProjection, half_width: f64, color: [f32; 4], style: &VectorStyle) {
2099 let result = tessellator::stroke_line_styled(
2100 coords,
2101 half_width,
2102 style.line_cap,
2103 style.line_join,
2104 style.miter_limit,
2105 );
2106 if result.positions.is_empty() {
2107 return;
2108 }
2109
2110 let gradient_max_dist = if style.line_gradient.is_some() {
2112 result
2113 .distances
2114 .iter()
2115 .cloned()
2116 .fold(0.0_f64, f64::max)
2117 .max(f64::EPSILON)
2118 } else {
2119 1.0
2120 };
2121
2122 let has_pattern = style.line_pattern.is_some();
2127 let pat_width = style.line_pattern.as_ref().map_or(1.0_f32, |p| p.width.max(1) as f32);
2128 let mut body_side = false; let base = mesh.positions.len() as u32;
2131 for (i, pos) in result.positions.iter().enumerate() {
2132 let coord = GeoCoord::from_lat_lon(pos[1], pos[0]);
2133 let w = projection.project(&coord);
2134 mesh.positions.push([w.position.x, w.position.y, w.position.z]);
2135
2136 let vertex_color = if let Some(ref ramp) = style.line_gradient {
2139 let t = (result.distances[i] / gradient_max_dist) as f32;
2140 ramp.evaluate(t)
2141 } else {
2142 color
2143 };
2144 mesh.colors.push(vertex_color);
2145
2146 mesh.line_normals.push([result.normals[i][0] as f32, result.normals[i][1] as f32]);
2147 let dist_meters = (result.distances[i] * METERS_PER_DEGREE) as f32;
2149 mesh.line_distances.push(dist_meters);
2150 mesh.line_cap_joins.push(result.cap_join[i]);
2151
2152 if has_pattern {
2156 let u = dist_meters / pat_width;
2157 let v = if result.cap_join[i] > 0.5 {
2158 0.5
2160 } else {
2161 let v = if body_side { 1.0 } else { 0.0 };
2163 body_side = !body_side;
2164 v
2165 };
2166 mesh.line_pattern_uvs.push([u, v]);
2167 }
2168 }
2169 for idx in &result.indices {
2170 mesh.indices.push(base + idx);
2171 }
2172}
2173
2174fn append_square_marker(mesh: &mut VectorMeshData, coord: &GeoCoord, projection: CameraProjection, half_size: f64, color: [f32; 4]) {
2175 let w = projection.project(coord);
2176 let base = mesh.positions.len() as u32;
2177 mesh.positions.push([w.position.x - half_size, w.position.y - half_size, w.position.z]);
2178 mesh.positions.push([w.position.x + half_size, w.position.y - half_size, w.position.z]);
2179 mesh.positions.push([w.position.x + half_size, w.position.y + half_size, w.position.z]);
2180 mesh.positions.push([w.position.x - half_size, w.position.y + half_size, w.position.z]);
2181 for _ in 0..4 {
2182 mesh.colors.push(color);
2183 }
2184 mesh.indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
2185}
2186
2187fn append_circle(
2188 mesh: &mut VectorMeshData,
2189 coord: &GeoCoord,
2190 projection: CameraProjection,
2191 radius: f64,
2192 fill_color: [f32; 4],
2193 outline: Option<([f32; 4], f64)>,
2194) {
2195 append_radial_fan(mesh, coord, projection, radius, fill_color, fill_color);
2196 if let Some((outline_color, outline_width)) = outline {
2197 if outline_width > 0.0 {
2198 let outer = radius + outline_width;
2199 append_ring(mesh, coord, projection, radius, outer, outline_color);
2200 }
2201 }
2202}
2203
2204fn append_heat_blob(
2205 mesh: &mut VectorMeshData,
2206 coord: &GeoCoord,
2207 projection: CameraProjection,
2208 radius: f64,
2209 color: [f32; 4],
2210 intensity: f32,
2211) {
2212 let mut center_color = color;
2213 center_color[3] = (center_color[3] * intensity).clamp(0.0, 1.0);
2214 let mut edge_color = color;
2215 edge_color[3] = 0.0;
2216 append_radial_fan(mesh, coord, projection, radius, center_color, edge_color);
2217}
2218
2219fn append_radial_fan(
2220 mesh: &mut VectorMeshData,
2221 coord: &GeoCoord,
2222 projection: CameraProjection,
2223 radius: f64,
2224 center_color: [f32; 4],
2225 edge_color: [f32; 4],
2226) {
2227 let center = projection.project(coord);
2228 let base = mesh.positions.len() as u32;
2229 mesh.positions.push([center.position.x, center.position.y, center.position.z]);
2230 mesh.colors.push(center_color);
2231 for i in 0..=DEFAULT_CIRCLE_SEGMENTS {
2232 let t = i as f64 / DEFAULT_CIRCLE_SEGMENTS as f64;
2233 let angle = std::f64::consts::TAU * t;
2234 mesh.positions.push([
2235 center.position.x + radius * angle.cos(),
2236 center.position.y + radius * angle.sin(),
2237 center.position.z,
2238 ]);
2239 mesh.colors.push(edge_color);
2240 }
2241 for i in 1..=DEFAULT_CIRCLE_SEGMENTS as u32 {
2242 mesh.indices.extend_from_slice(&[base, base + i, base + i + 1]);
2243 }
2244}
2245
2246fn append_ring(
2247 mesh: &mut VectorMeshData,
2248 coord: &GeoCoord,
2249 projection: CameraProjection,
2250 inner_radius: f64,
2251 outer_radius: f64,
2252 color: [f32; 4],
2253) {
2254 if outer_radius <= inner_radius {
2255 return;
2256 }
2257 let center = projection.project(coord);
2258 let base = mesh.positions.len() as u32;
2259 for i in 0..=DEFAULT_CIRCLE_SEGMENTS {
2260 let t = i as f64 / DEFAULT_CIRCLE_SEGMENTS as f64;
2261 let angle = std::f64::consts::TAU * t;
2262 let (sin, cos) = angle.sin_cos();
2263 mesh.positions.push([
2264 center.position.x + inner_radius * cos,
2265 center.position.y + inner_radius * sin,
2266 center.position.z,
2267 ]);
2268 mesh.colors.push(color);
2269 mesh.positions.push([
2270 center.position.x + outer_radius * cos,
2271 center.position.y + outer_radius * sin,
2272 center.position.z,
2273 ]);
2274 mesh.colors.push(color);
2275 }
2276 for i in 0..DEFAULT_CIRCLE_SEGMENTS as u32 {
2277 let a = base + i * 2;
2278 mesh.indices.extend_from_slice(&[a, a + 1, a + 2, a + 1, a + 3, a + 2]);
2279 }
2280}
2281
2282fn append_extruded_polygon(mesh: &mut VectorMeshData, coords: &[GeoCoord], projection: CameraProjection, style: &VectorStyle) {
2283 let ring = normalized_ring(coords);
2284 if ring.len() < 3 {
2285 return;
2286 }
2287
2288 let top_base = mesh.positions.len() as u32;
2290 for coord in &ring {
2291 let w = projection.project(coord);
2292 mesh.positions.push([
2293 w.position.x,
2294 w.position.y,
2295 coord.alt + style.extrusion_base as f64 + style.extrusion_height as f64,
2296 ]);
2297 mesh.colors.push(style.fill_color);
2298 mesh.normals.push([0.0, 0.0, 1.0]); }
2300 for idx in tessellator::triangulate_polygon(&ring) {
2301 mesh.indices.push(top_base + idx);
2302 }
2303
2304 let side_color = [
2306 style.fill_color[0] * 0.75,
2307 style.fill_color[1] * 0.75,
2308 style.fill_color[2] * 0.75,
2309 style.fill_color[3],
2310 ];
2311
2312 for i in 0..ring.len() {
2313 let a = &ring[i];
2314 let b = &ring[(i + 1) % ring.len()];
2315 let wa = projection.project(a);
2316 let wb = projection.project(b);
2317 let base_z_a = a.alt + style.extrusion_base as f64;
2318 let base_z_b = b.alt + style.extrusion_base as f64;
2319 let top_z_a = base_z_a + style.extrusion_height as f64;
2320 let top_z_b = base_z_b + style.extrusion_height as f64;
2321
2322 let dx = (wb.position.x - wa.position.x) as f32;
2325 let dy = (wb.position.y - wa.position.y) as f32;
2326 let len = (dx * dx + dy * dy).sqrt().max(1e-12);
2327 let normal = [dy / len, -dx / len, 0.0];
2328
2329 let base = mesh.positions.len() as u32;
2330 mesh.positions.push([wa.position.x, wa.position.y, base_z_a]);
2331 mesh.positions.push([wb.position.x, wb.position.y, base_z_b]);
2332 mesh.positions.push([wb.position.x, wb.position.y, top_z_b]);
2333 mesh.positions.push([wa.position.x, wa.position.y, top_z_a]);
2334 for _ in 0..4 {
2335 mesh.colors.push(side_color);
2336 mesh.normals.push(normal);
2337 }
2338 mesh.indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
2339 }
2340}
2341
2342fn normalized_ring(coords: &[GeoCoord]) -> Vec<GeoCoord> {
2343 if coords.len() > 1 {
2344 let first = coords.first().expect("ring first");
2345 let last = coords.last().expect("ring last");
2346 if (first.lat - last.lat).abs() < 1e-12
2347 && (first.lon - last.lon).abs() < 1e-12
2348 && (first.alt - last.alt).abs() < 1e-6
2349 {
2350 return coords[..coords.len() - 1].to_vec();
2351 }
2352 }
2353 coords.to_vec()
2354}
2355
2356fn drape_geometry(geometry: &mut Geometry, terrain: &TerrainManager) {
2362 match geometry {
2363 Geometry::Point(p) => {
2364 if let Some(elev) = terrain.elevation_at(&p.coord) {
2365 p.coord.alt = elev;
2366 }
2367 }
2368 Geometry::LineString(ls) => {
2369 drape_coords(&mut ls.coords, terrain);
2370 }
2371 Geometry::Polygon(poly) => {
2372 drape_coords(&mut poly.exterior, terrain);
2373 for hole in &mut poly.interiors {
2374 drape_coords(hole, terrain);
2375 }
2376 }
2377 Geometry::MultiPoint(mp) => {
2378 for p in &mut mp.points {
2379 if let Some(elev) = terrain.elevation_at(&p.coord) {
2380 p.coord.alt = elev;
2381 }
2382 }
2383 }
2384 Geometry::MultiLineString(mls) => {
2385 for ls in &mut mls.lines {
2386 drape_coords(&mut ls.coords, terrain);
2387 }
2388 }
2389 Geometry::MultiPolygon(mpoly) => {
2390 for poly in &mut mpoly.polygons {
2391 drape_coords(&mut poly.exterior, terrain);
2392 for hole in &mut poly.interiors {
2393 drape_coords(hole, terrain);
2394 }
2395 }
2396 }
2397 Geometry::GeometryCollection(geoms) => {
2398 for g in geoms {
2399 drape_geometry(g, terrain);
2400 }
2401 }
2402 }
2403}
2404
2405fn drape_coords(coords: &mut [GeoCoord], terrain: &TerrainManager) {
2407 for coord in coords.iter_mut() {
2408 if let Some(elev) = terrain.elevation_at(coord) {
2409 coord.alt = elev;
2410 }
2411 }
2412}
2413
2414impl Layer for VectorLayer {
2419 fn id(&self) -> LayerId {
2420 self.id
2421 }
2422
2423 fn kind(&self) -> crate::layer::LayerKind {
2424 crate::layer::LayerKind::Vector
2425 }
2426
2427 fn name(&self) -> &str {
2428 &self.name
2429 }
2430
2431 fn visible(&self) -> bool {
2432 self.visible
2433 }
2434
2435 fn set_visible(&mut self, visible: bool) {
2436 self.visible = visible;
2437 }
2438
2439 fn opacity(&self) -> f32 {
2440 self.opacity
2441 }
2442
2443 fn set_opacity(&mut self, opacity: f32) {
2444 self.opacity = opacity.clamp(0.0, 1.0);
2445 }
2446
2447 fn as_any(&self) -> &dyn Any {
2448 self
2449 }
2450
2451 fn as_any_mut(&mut self) -> &mut dyn Any {
2452 self
2453 }
2454}
2455
2456#[cfg(test)]
2461mod tests {
2462 use super::*;
2463 use crate::geometry::{
2464 Feature, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon,
2465 };
2466 use crate::layer::Layer;
2467 use crate::camera_projection::CameraProjection;
2468 use crate::terrain::{FlatElevationSource, TerrainConfig, TerrainManager};
2469 use rustial_math::{WebMercator, WorldBounds, WorldCoord};
2470 use std::collections::HashMap;
2471
2472 fn make_layer(geometry: Geometry) -> VectorLayer {
2477 let features = FeatureCollection {
2478 features: vec![Feature {
2479 geometry,
2480 properties: HashMap::new(),
2481 }],
2482 };
2483 VectorLayer::new("test", features, VectorStyle::default())
2484 }
2485
2486 fn square_polygon() -> Polygon {
2487 Polygon {
2488 exterior: vec![
2489 GeoCoord::from_lat_lon(0.0, 0.0),
2490 GeoCoord::from_lat_lon(0.0, 1.0),
2491 GeoCoord::from_lat_lon(1.0, 1.0),
2492 GeoCoord::from_lat_lon(1.0, 0.0),
2493 ],
2494 interiors: vec![],
2495 }
2496 }
2497
2498 fn two_point_line() -> LineString {
2499 LineString {
2500 coords: vec![
2501 GeoCoord::from_lat_lon(0.0, 0.0),
2502 GeoCoord::from_lat_lon(0.0, 1.0),
2503 ],
2504 }
2505 }
2506
2507 fn origin_point() -> Point {
2508 Point {
2509 coord: GeoCoord::from_lat_lon(0.0, 0.0),
2510 }
2511 }
2512
2513 fn flat_terrain_manager() -> TerrainManager {
2514 let config = TerrainConfig {
2515 enabled: true,
2516 mesh_resolution: 4,
2517 source: Box::new(FlatElevationSource::new(4, 4)),
2518 ..TerrainConfig::default()
2519 };
2520 let mut mgr = TerrainManager::new(config, 100);
2521 let extent = WebMercator::max_extent();
2522 let bounds = WorldBounds::new(
2523 WorldCoord::new(-extent, -extent, 0.0),
2524 WorldCoord::new(extent, extent, 0.0),
2525 );
2526 mgr.update(&bounds, 0, (0.0, 0.0), CameraProjection::WebMercator, 10_000_000.0, 0.0);
2528 mgr.update(&bounds, 0, (0.0, 0.0), CameraProjection::WebMercator, 10_000_000.0, 0.0);
2529 mgr
2530 }
2531
2532 #[test]
2537 fn new_layer_defaults() {
2538 let layer = make_layer(Geometry::Point(origin_point()));
2539 assert_eq!(layer.name(), "test");
2540 assert!(layer.visible());
2541 assert!((layer.opacity() - 1.0).abs() < f32::EPSILON);
2542 assert_eq!(layer.feature_count(), 1);
2543 assert_eq!(layer.total_coords(), 1);
2544 }
2545
2546 #[test]
2547 fn layer_trait_visibility() {
2548 let mut layer = make_layer(Geometry::Point(origin_point()));
2549 layer.set_visible(false);
2550 assert!(!layer.visible());
2551 layer.set_visible(true);
2552 assert!(layer.visible());
2553 }
2554
2555 #[test]
2556 fn layer_trait_opacity_clamped() {
2557 let mut layer = make_layer(Geometry::Point(origin_point()));
2558 layer.set_opacity(1.5);
2559 assert!((layer.opacity() - 1.0).abs() < f32::EPSILON);
2560 layer.set_opacity(-0.5);
2561 assert!((layer.opacity() - 0.0).abs() < f32::EPSILON);
2562 }
2563
2564 #[test]
2565 fn debug_impl() {
2566 let layer = make_layer(Geometry::Point(origin_point()));
2567 let dbg = format!("{layer:?}");
2568 assert!(dbg.contains("VectorLayer"));
2569 assert!(dbg.contains("test"));
2570 }
2571
2572 #[test]
2577 fn tessellate_polygon() {
2578 let layer = make_layer(Geometry::Polygon(square_polygon()));
2579 let mesh = layer.tessellate(CameraProjection::WebMercator);
2580 assert_eq!(mesh.vertex_count(), 4);
2581 assert_eq!(mesh.index_count(), 6); assert_eq!(mesh.triangle_count(), 2);
2583 assert_eq!(mesh.colors.len(), 4);
2584 assert!(!mesh.is_empty());
2585 }
2586
2587 #[test]
2592 fn tessellate_linestring() {
2593 let layer = make_layer(Geometry::LineString(two_point_line()));
2594 let mesh = layer.tessellate(CameraProjection::WebMercator);
2595 assert_eq!(mesh.vertex_count(), 4); assert_eq!(mesh.index_count(), 6);
2597 }
2598
2599 #[test]
2604 fn tessellate_point() {
2605 let layer = make_layer(Geometry::Point(origin_point()));
2606 let mesh = layer.tessellate(CameraProjection::WebMercator);
2607 assert_eq!(mesh.vertex_count(), 4); assert_eq!(mesh.index_count(), 6); assert_eq!(mesh.colors[0], VectorStyle::default().fill_color);
2611 }
2612
2613 #[test]
2618 fn tessellate_multi_point() {
2619 let mp = Geometry::MultiPoint(MultiPoint {
2620 points: vec![origin_point(), origin_point()],
2621 });
2622 let layer = make_layer(mp);
2623 let mesh = layer.tessellate(CameraProjection::WebMercator);
2624 assert_eq!(mesh.vertex_count(), 8);
2626 assert_eq!(mesh.index_count(), 12);
2627 }
2628
2629 #[test]
2630 fn tessellate_multi_linestring() {
2631 let mls = Geometry::MultiLineString(MultiLineString {
2632 lines: vec![two_point_line(), two_point_line()],
2633 });
2634 let layer = make_layer(mls);
2635 let mesh = layer.tessellate(CameraProjection::WebMercator);
2636 assert_eq!(mesh.vertex_count(), 8); assert_eq!(mesh.index_count(), 12);
2638 }
2639
2640 #[test]
2641 fn tessellate_multi_polygon() {
2642 let mpoly = Geometry::MultiPolygon(MultiPolygon {
2643 polygons: vec![square_polygon(), square_polygon()],
2644 });
2645 let layer = make_layer(mpoly);
2646 let mesh = layer.tessellate(CameraProjection::WebMercator);
2647 assert_eq!(mesh.vertex_count(), 8); assert_eq!(mesh.index_count(), 12);
2649 }
2650
2651 #[test]
2652 fn tessellate_geometry_collection() {
2653 let gc = Geometry::GeometryCollection(vec![
2654 Geometry::Point(origin_point()),
2655 Geometry::Polygon(square_polygon()),
2656 ]);
2657 let layer = make_layer(gc);
2658 let mesh = layer.tessellate(CameraProjection::WebMercator);
2659 assert_eq!(mesh.vertex_count(), 8);
2661 assert_eq!(mesh.index_count(), 12);
2662 }
2663
2664 #[test]
2669 fn tessellate_empty_collection() {
2670 let features = FeatureCollection { features: vec![] };
2671 let layer = VectorLayer::new("empty", features, VectorStyle::default());
2672 let mesh = layer.tessellate(CameraProjection::WebMercator);
2673 assert!(mesh.is_empty());
2674 assert_eq!(mesh.vertex_count(), 0);
2675 }
2676
2677 #[test]
2682 fn mesh_data_merge() {
2683 let mut a = VectorMeshData {
2684 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
2685 colors: vec![[1.0, 0.0, 0.0, 1.0], [1.0, 0.0, 0.0, 1.0]],
2686 indices: vec![0, 1, 0],
2687 ..Default::default()
2688 };
2689 let b = VectorMeshData {
2690 positions: vec![[2.0, 0.0, 0.0], [3.0, 0.0, 0.0]],
2691 colors: vec![[0.0, 1.0, 0.0, 1.0], [0.0, 1.0, 0.0, 1.0]],
2692 indices: vec![0, 1, 0],
2693 ..Default::default()
2694 };
2695 a.merge(&b);
2696 assert_eq!(a.vertex_count(), 4);
2697 assert_eq!(a.index_count(), 6);
2698 assert_eq!(a.indices, vec![0, 1, 0, 2, 3, 2]);
2700 }
2701
2702 #[test]
2703 fn mesh_data_clear() {
2704 let mut mesh = VectorMeshData {
2705 positions: vec![[0.0, 0.0, 0.0]],
2706 colors: vec![[1.0, 0.0, 0.0, 1.0]],
2707 indices: vec![0],
2708 ..Default::default()
2709 };
2710 mesh.clear();
2711 assert!(mesh.is_empty());
2712 assert_eq!(mesh.vertex_count(), 0);
2713 }
2714
2715 #[test]
2720 fn drape_sets_altitude() {
2721 let mgr = flat_terrain_manager();
2722
2723 let features = FeatureCollection {
2724 features: vec![Feature {
2725 geometry: Geometry::Point(Point {
2726 coord: GeoCoord::new(10.0, 20.0, 999.0),
2727 }),
2728 properties: HashMap::new(),
2729 }],
2730 };
2731
2732 let mut layer = VectorLayer::new("test", features, VectorStyle::default());
2733 layer.drape_on_terrain(&mgr);
2734
2735 match &layer.features.features[0].geometry {
2737 Geometry::Point(p) => assert!((p.coord.alt - 0.0).abs() < 1e-3),
2738 _ => panic!("expected Point"),
2739 }
2740 }
2741
2742 #[test]
2743 fn drape_skipped_when_terrain_disabled() {
2744 let config = TerrainConfig::default(); let mgr = TerrainManager::new(config, 100);
2746
2747 let features = FeatureCollection {
2748 features: vec![Feature {
2749 geometry: Geometry::Point(Point {
2750 coord: GeoCoord::new(10.0, 20.0, 999.0),
2751 }),
2752 properties: HashMap::new(),
2753 }],
2754 };
2755
2756 let mut layer = VectorLayer::new("test", features, VectorStyle::default());
2757 layer.drape_on_terrain(&mgr);
2758
2759 match &layer.features.features[0].geometry {
2761 Geometry::Point(p) => assert!((p.coord.alt - 999.0).abs() < 1e-3),
2762 _ => panic!("expected Point"),
2763 }
2764 }
2765
2766 #[test]
2767 fn drape_linestring_coords() {
2768 let mgr = flat_terrain_manager();
2769
2770 let features = FeatureCollection {
2771 features: vec![Feature {
2772 geometry: Geometry::LineString(LineString {
2773 coords: vec![
2774 GeoCoord::new(10.0, 20.0, 500.0),
2775 GeoCoord::new(11.0, 21.0, 600.0),
2776 ],
2777 }),
2778 properties: HashMap::new(),
2779 }],
2780 };
2781
2782 let mut layer = VectorLayer::new("test", features, VectorStyle::default());
2783 layer.drape_on_terrain(&mgr);
2784
2785 match &layer.features.features[0].geometry {
2786 Geometry::LineString(ls) => {
2787 for coord in &ls.coords {
2788 assert!(
2789 coord.alt.abs() < 1e-3,
2790 "expected flat terrain, got alt={}",
2791 coord.alt
2792 );
2793 }
2794 }
2795 _ => panic!("expected LineString"),
2796 }
2797 }
2798
2799 #[test]
2800 fn tessellate_circle_mode() {
2801 let mut style = VectorStyle::default();
2802 style.render_mode = VectorRenderMode::Circle;
2803 let layer = make_layer(Geometry::Point(origin_point()));
2804 let layer = VectorLayer::new("circle", layer.features, style);
2805 let mesh = layer.tessellate(CameraProjection::WebMercator);
2806 assert!(mesh.vertex_count() > 8);
2807 assert!(mesh.index_count() >= DEFAULT_CIRCLE_SEGMENTS * 3);
2808 }
2809
2810 #[test]
2811 fn tessellate_heatmap_mode_has_faded_edges() {
2812 let mut style = VectorStyle::heatmap([1.0, 0.0, 0.0, 0.5], 24.0, 1.0);
2813 style.render_mode = VectorRenderMode::Heatmap;
2814 let layer = make_layer(Geometry::Point(origin_point()));
2815 let layer = VectorLayer::new("heatmap", layer.features, style);
2816 let mesh = layer.tessellate(CameraProjection::WebMercator);
2817 assert_eq!(mesh.colors.first().map(|c| c[3]), Some(0.5));
2818 assert_eq!(mesh.colors.last().map(|c| c[3]), Some(0.0));
2819 }
2820
2821 #[test]
2822 fn tessellate_fill_extrusion_mode_produces_vertical_geometry() {
2823 let style = VectorStyle::fill_extrusion([0.5, 0.5, 0.8, 1.0], 0.0, 50.0);
2824 let layer = make_layer(Geometry::Polygon(square_polygon()));
2825 let layer = VectorLayer::new("extrusion", layer.features, style);
2826 let mesh = layer.tessellate(CameraProjection::WebMercator);
2827 let min_z = mesh.positions.iter().map(|p| p[2]).fold(f64::INFINITY, f64::min);
2828 let max_z = mesh.positions.iter().map(|p| p[2]).fold(f64::NEG_INFINITY, f64::max);
2829 assert!(max_z > min_z);
2830 }
2831
2832 #[test]
2833 fn fill_extrusion_tessellation_produces_normals() {
2834 let style = VectorStyle::fill_extrusion([0.5, 0.5, 0.8, 1.0], 0.0, 50.0);
2835 let layer = make_layer(Geometry::Polygon(square_polygon()));
2836 let layer = VectorLayer::new("extrusion", layer.features, style);
2837 let mesh = layer.tessellate(CameraProjection::WebMercator);
2838
2839 assert!(mesh.has_normals());
2840 assert_eq!(mesh.normals.len(), mesh.positions.len());
2841 assert_eq!(mesh.render_mode, VectorRenderMode::FillExtrusion);
2842 }
2843
2844 #[test]
2845 fn fill_extrusion_top_normals_point_up() {
2846 let style = VectorStyle::fill_extrusion([1.0, 0.0, 0.0, 1.0], 0.0, 100.0);
2847 let layer = make_layer(Geometry::Polygon(square_polygon()));
2848 let layer = VectorLayer::new("extrusion", layer.features, style);
2849 let mesh = layer.tessellate(CameraProjection::WebMercator);
2850
2851 let ring_len = 4; for i in 0..ring_len {
2855 let n = mesh.normals[i];
2856 assert!((n[0]).abs() < 1e-6, "top normal x should be 0, got {}", n[0]);
2857 assert!((n[1]).abs() < 1e-6, "top normal y should be 0, got {}", n[1]);
2858 assert!((n[2] - 1.0).abs() < 1e-6, "top normal z should be 1, got {}", n[2]);
2859 }
2860 }
2861
2862 #[test]
2863 fn fill_extrusion_side_normals_are_horizontal() {
2864 let style = VectorStyle::fill_extrusion([1.0, 0.0, 0.0, 1.0], 0.0, 100.0);
2865 let layer = make_layer(Geometry::Polygon(square_polygon()));
2866 let layer = VectorLayer::new("extrusion", layer.features, style);
2867 let mesh = layer.tessellate(CameraProjection::WebMercator);
2868
2869 let ring_len = 4;
2871 for i in ring_len..mesh.normals.len() {
2872 let n = mesh.normals[i];
2873 assert!((n[2]).abs() < 1e-6, "side normal z should be 0, got {} at vertex {}", n[2], i);
2875 let len = (n[0] * n[0] + n[1] * n[1]).sqrt();
2877 assert!((len - 1.0).abs() < 0.01, "side normal length should be ~1, got {} at vertex {}", len, i);
2878 }
2879 }
2880
2881 #[test]
2882 fn flat_fill_tessellation_has_no_normals() {
2883 let style = VectorStyle {
2884 render_mode: VectorRenderMode::Fill,
2885 fill_color: [0.0, 1.0, 0.0, 1.0],
2886 ..VectorStyle::default()
2887 };
2888 let layer = make_layer(Geometry::Polygon(square_polygon()));
2889 let layer = VectorLayer::new("fill", layer.features, style);
2890 let mesh = layer.tessellate(CameraProjection::WebMercator);
2891
2892 assert!(!mesh.has_normals());
2893 assert!(mesh.normals.is_empty());
2894 assert_eq!(mesh.render_mode, VectorRenderMode::Fill);
2895 }
2896
2897 #[test]
2898 fn symbol_candidates_point_placement_skips_lines() {
2899 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
2900 style.symbol_text_field = Some("name".to_owned());
2901 let mut properties = HashMap::new();
2902 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Road".to_owned()));
2903 let layer = VectorLayer::new(
2904 "symbol",
2905 FeatureCollection {
2906 features: vec![Feature {
2907 geometry: Geometry::LineString(two_point_line()),
2908 properties,
2909 }],
2910 },
2911 style,
2912 );
2913
2914 assert!(layer.symbol_candidates().is_empty());
2915 }
2916
2917 #[test]
2918 fn symbol_candidates_text_only_use_text_overlap_flag() {
2919 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
2920 style.symbol_text_field = Some("name".to_owned());
2921 style.symbol_text_allow_overlap = true;
2922 style.symbol_icon_allow_overlap = false;
2923
2924 let mut properties = HashMap::new();
2925 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Label".to_owned()));
2926 let layer = VectorLayer::new(
2927 "symbol",
2928 FeatureCollection {
2929 features: vec![Feature {
2930 geometry: Geometry::Point(origin_point()),
2931 properties,
2932 }],
2933 },
2934 style,
2935 );
2936
2937 let candidates = layer.symbol_candidates();
2938 assert_eq!(candidates.len(), 1);
2939 assert!(candidates[0].allow_overlap);
2940 }
2941
2942 #[test]
2943 fn symbol_candidates_use_fixed_text_anchor_when_variable_anchors_absent() {
2944 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
2945 style.symbol_text_field = Some("name".to_owned());
2946 style.symbol_text_anchor = SymbolAnchor::TopRight;
2947 style.symbol_anchors = vec![SymbolAnchor::TopRight];
2948
2949 let mut properties = HashMap::new();
2950 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Label".to_owned()));
2951 let layer = VectorLayer::new(
2952 "symbol",
2953 FeatureCollection {
2954 features: vec![Feature {
2955 geometry: Geometry::Point(origin_point()),
2956 properties,
2957 }],
2958 },
2959 style,
2960 );
2961
2962 let candidates = layer.symbol_candidates();
2963 assert_eq!(candidates.len(), 1);
2964 assert_eq!(candidates[0].anchors, vec![SymbolAnchor::TopRight]);
2965 }
2966
2967 #[test]
2968 fn symbol_candidates_propagate_text_radial_offset() {
2969 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
2970 style.symbol_text_field = Some("name".to_owned());
2971 style.symbol_text_anchor = SymbolAnchor::Top;
2972 style.symbol_anchors = vec![SymbolAnchor::Top];
2973 style.symbol_text_radial_offset = Some(2.0);
2974
2975 let mut properties = HashMap::new();
2976 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Label".to_owned()));
2977 let layer = VectorLayer::new(
2978 "symbol",
2979 FeatureCollection {
2980 features: vec![Feature {
2981 geometry: Geometry::Point(origin_point()),
2982 properties,
2983 }],
2984 },
2985 style,
2986 );
2987
2988 let candidates = layer.symbol_candidates();
2989 assert_eq!(candidates.len(), 1);
2990 assert_eq!(candidates[0].radial_offset, Some(2.0));
2991 }
2992
2993 #[test]
2994 fn symbol_candidates_propagate_text_max_width() {
2995 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
2996 style.symbol_text_field = Some("name".to_owned());
2997 style.symbol_text_max_width = Some(6.0);
2998
2999 let mut properties = HashMap::new();
3000 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Long label".to_owned()));
3001 let layer = VectorLayer::new(
3002 "symbol",
3003 FeatureCollection {
3004 features: vec![Feature {
3005 geometry: Geometry::Point(origin_point()),
3006 properties,
3007 }],
3008 },
3009 style,
3010 );
3011
3012 let candidates = layer.symbol_candidates();
3013 assert_eq!(candidates.len(), 1);
3014 assert_eq!(candidates[0].text_max_width, Some(6.0));
3015 }
3016
3017 #[test]
3018 fn symbol_candidates_propagate_text_line_height() {
3019 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3020 style.symbol_text_field = Some("name".to_owned());
3021 style.symbol_text_line_height = Some(1.5);
3022
3023 let mut properties = HashMap::new();
3024 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Long label".to_owned()));
3025 let layer = VectorLayer::new(
3026 "symbol",
3027 FeatureCollection {
3028 features: vec![Feature {
3029 geometry: Geometry::Point(origin_point()),
3030 properties,
3031 }],
3032 },
3033 style,
3034 );
3035
3036 let candidates = layer.symbol_candidates();
3037 assert_eq!(candidates.len(), 1);
3038 assert_eq!(candidates[0].text_line_height, Some(1.5));
3039 }
3040
3041 #[test]
3042 fn symbol_candidates_propagate_text_letter_spacing() {
3043 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3044 style.symbol_text_field = Some("name".to_owned());
3045 style.symbol_text_letter_spacing = Some(0.25);
3046
3047 let mut properties = HashMap::new();
3048 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Long label".to_owned()));
3049 let layer = VectorLayer::new(
3050 "symbol",
3051 FeatureCollection {
3052 features: vec![Feature {
3053 geometry: Geometry::Point(origin_point()),
3054 properties,
3055 }],
3056 },
3057 style,
3058 );
3059
3060 let candidates = layer.symbol_candidates();
3061 assert_eq!(candidates.len(), 1);
3062 assert_eq!(candidates[0].text_letter_spacing, Some(0.25));
3063 }
3064
3065 #[test]
3066 fn symbol_candidates_apply_uppercase_text_transform() {
3067 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3068 style.symbol_text_field = Some("name".to_owned());
3069 style.symbol_text_transform = SymbolTextTransform::Uppercase;
3070
3071 let mut properties = HashMap::new();
3072 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Main Street".to_owned()));
3073 let layer = VectorLayer::new(
3074 "symbol",
3075 FeatureCollection {
3076 features: vec![Feature {
3077 geometry: Geometry::Point(origin_point()),
3078 properties,
3079 }],
3080 },
3081 style,
3082 );
3083
3084 let candidates = layer.symbol_candidates();
3085 assert_eq!(candidates.len(), 1);
3086 assert_eq!(candidates[0].text.as_deref(), Some("MAIN STREET"));
3087 }
3088
3089 #[test]
3090 fn symbol_candidates_apply_lowercase_text_transform() {
3091 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3092 style.symbol_text_field = Some("name".to_owned());
3093 style.symbol_text_transform = SymbolTextTransform::Lowercase;
3094
3095 let mut properties = HashMap::new();
3096 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Main Street".to_owned()));
3097 let layer = VectorLayer::new(
3098 "symbol",
3099 FeatureCollection {
3100 features: vec![Feature {
3101 geometry: Geometry::Point(origin_point()),
3102 properties,
3103 }],
3104 },
3105 style,
3106 );
3107
3108 let candidates = layer.symbol_candidates();
3109 assert_eq!(candidates.len(), 1);
3110 assert_eq!(candidates[0].text.as_deref(), Some("main street"));
3111 }
3112
3113 #[test]
3114 fn symbol_candidates_keep_variable_anchor_priority_over_fixed_anchor() {
3115 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3116 style.symbol_text_field = Some("name".to_owned());
3117 style.symbol_text_anchor = SymbolAnchor::BottomLeft;
3118 style.symbol_anchors = vec![SymbolAnchor::Center, SymbolAnchor::Top];
3119
3120 let mut properties = HashMap::new();
3121 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Label".to_owned()));
3122 let layer = VectorLayer::new(
3123 "symbol",
3124 FeatureCollection {
3125 features: vec![Feature {
3126 geometry: Geometry::Point(origin_point()),
3127 properties,
3128 }],
3129 },
3130 style,
3131 );
3132
3133 let candidates = layer.symbol_candidates();
3134 assert_eq!(candidates.len(), 1);
3135 assert_eq!(candidates[0].anchors, vec![SymbolAnchor::Center, SymbolAnchor::Top]);
3136 }
3137
3138 #[test]
3139 fn symbol_candidates_use_variable_anchor_offset_order_when_present() {
3140 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3141 style.symbol_text_field = Some("name".to_owned());
3142 style.symbol_text_anchor = SymbolAnchor::BottomLeft;
3143 style.symbol_anchors = vec![SymbolAnchor::Center];
3144 style.symbol_variable_anchor_offsets = Some(vec![
3145 (SymbolAnchor::Top, [1.0, 2.0]),
3146 (SymbolAnchor::Right, [3.0, 4.0]),
3147 ]);
3148
3149 let mut properties = HashMap::new();
3150 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Label".to_owned()));
3151 let layer = VectorLayer::new(
3152 "symbol",
3153 FeatureCollection {
3154 features: vec![Feature {
3155 geometry: Geometry::Point(origin_point()),
3156 properties,
3157 }],
3158 },
3159 style,
3160 );
3161
3162 let candidates = layer.symbol_candidates();
3163 assert_eq!(candidates.len(), 1);
3164 assert_eq!(candidates[0].anchors, vec![SymbolAnchor::Top, SymbolAnchor::Right]);
3165 assert_eq!(
3166 candidates[0].variable_anchor_offsets,
3167 Some(vec![
3168 (SymbolAnchor::Top, [1.0, 2.0]),
3169 (SymbolAnchor::Right, [3.0, 4.0]),
3170 ])
3171 );
3172 }
3173
3174 #[test]
3175 fn symbol_candidates_text_and_icon_require_both_overlap_flags() {
3176 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3177 style.symbol_text_field = Some("name".to_owned());
3178 style.symbol_icon_image = Some("marker".to_owned());
3179 style.symbol_text_allow_overlap = true;
3180 style.symbol_icon_allow_overlap = false;
3181
3182 let mut properties = HashMap::new();
3183 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Label".to_owned()));
3184 let layer = VectorLayer::new(
3185 "symbol",
3186 FeatureCollection {
3187 features: vec![Feature {
3188 geometry: Geometry::Point(origin_point()),
3189 properties,
3190 }],
3191 },
3192 style,
3193 );
3194
3195 let candidates = layer.symbol_candidates();
3196 assert_eq!(candidates.len(), 1);
3197 assert!(!candidates[0].allow_overlap);
3198 }
3199
3200 #[test]
3201 fn symbol_candidates_text_only_use_text_ignore_placement_flag() {
3202 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3203 style.symbol_text_field = Some("name".to_owned());
3204 style.symbol_text_ignore_placement = true;
3205 style.symbol_icon_ignore_placement = false;
3206
3207 let mut properties = HashMap::new();
3208 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Label".to_owned()));
3209 let layer = VectorLayer::new(
3210 "symbol",
3211 FeatureCollection {
3212 features: vec![Feature {
3213 geometry: Geometry::Point(origin_point()),
3214 properties,
3215 }],
3216 },
3217 style,
3218 );
3219
3220 let candidates = layer.symbol_candidates();
3221 assert_eq!(candidates.len(), 1);
3222 assert!(candidates[0].ignore_placement);
3223 }
3224
3225 #[test]
3226 fn symbol_candidates_text_and_icon_require_both_ignore_flags() {
3227 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3228 style.symbol_text_field = Some("name".to_owned());
3229 style.symbol_icon_image = Some("marker".to_owned());
3230 style.symbol_text_ignore_placement = true;
3231 style.symbol_icon_ignore_placement = false;
3232
3233 let mut properties = HashMap::new();
3234 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Label".to_owned()));
3235 let layer = VectorLayer::new(
3236 "symbol",
3237 FeatureCollection {
3238 features: vec![Feature {
3239 geometry: Geometry::Point(origin_point()),
3240 properties,
3241 }],
3242 },
3243 style,
3244 );
3245
3246 let candidates = layer.symbol_candidates();
3247 assert_eq!(candidates.len(), 1);
3248 assert!(!candidates[0].ignore_placement);
3249 }
3250
3251 #[test]
3252 fn symbol_candidates_emit_icon_fallback_when_text_is_optional() {
3253 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3254 style.symbol_text_field = Some("name".to_owned());
3255 style.symbol_icon_image = Some("marker".to_owned());
3256 style.symbol_text_optional = true;
3257
3258 let mut properties = HashMap::new();
3259 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Label".to_owned()));
3260 let layer = VectorLayer::new(
3261 "symbol",
3262 FeatureCollection {
3263 features: vec![Feature {
3264 geometry: Geometry::Point(origin_point()),
3265 properties,
3266 }],
3267 },
3268 style,
3269 );
3270
3271 let candidates = layer.symbol_candidates();
3272 assert_eq!(candidates.len(), 2);
3273 assert_eq!(candidates[0].text.as_deref(), Some("Label"));
3274 assert_eq!(candidates[0].icon_image.as_deref(), Some("marker"));
3275 assert!(candidates[1].text.is_none());
3276 assert_eq!(candidates[1].icon_image.as_deref(), Some("marker"));
3277 assert_eq!(candidates[0].cross_tile_id, candidates[1].cross_tile_id);
3278 assert_eq!(candidates[0].placement_group_id, candidates[1].placement_group_id);
3279 }
3280
3281 #[test]
3282 fn symbol_candidates_emit_text_fallback_when_icon_is_optional() {
3283 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3284 style.symbol_text_field = Some("name".to_owned());
3285 style.symbol_icon_image = Some("marker".to_owned());
3286 style.symbol_icon_optional = true;
3287
3288 let mut properties = HashMap::new();
3289 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Label".to_owned()));
3290 let layer = VectorLayer::new(
3291 "symbol",
3292 FeatureCollection {
3293 features: vec![Feature {
3294 geometry: Geometry::Point(origin_point()),
3295 properties,
3296 }],
3297 },
3298 style,
3299 );
3300
3301 let candidates = layer.symbol_candidates();
3302 assert_eq!(candidates.len(), 2);
3303 assert_eq!(candidates[0].text.as_deref(), Some("Label"));
3304 assert_eq!(candidates[0].icon_image.as_deref(), Some("marker"));
3305 assert_eq!(candidates[1].text.as_deref(), Some("Label"));
3306 assert!(candidates[1].icon_image.is_none());
3307 assert_eq!(candidates[0].cross_tile_id, candidates[1].cross_tile_id);
3308 assert_eq!(candidates[0].placement_group_id, candidates[1].placement_group_id);
3309 }
3310
3311 #[test]
3312 fn symbol_candidates_line_placement_repeats_anchors_with_spacing() {
3313 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3314 style.symbol_text_field = Some("name".to_owned());
3315 style.symbol_placement = SymbolPlacement::Line;
3316 style.symbol_spacing = 1000.0;
3317
3318 let start = GeoCoord::from_lat_lon(0.0, 0.0);
3319 let end = GeoCoord::from_lat_lon(0.0, 0.05);
3320 let mut properties = HashMap::new();
3321 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Road".to_owned()));
3322 let layer = VectorLayer::new(
3323 "symbol",
3324 FeatureCollection {
3325 features: vec![Feature {
3326 geometry: Geometry::LineString(LineString {
3327 coords: vec![start, end],
3328 }),
3329 properties,
3330 }],
3331 },
3332 style,
3333 );
3334
3335 let candidates = layer.symbol_candidates();
3336 assert!(candidates.len() > 1, "longer lines should produce repeated anchors");
3337 assert!(candidates.iter().all(|candidate| (candidate.anchor.lat - 0.0).abs() < 1e-9));
3338 assert!(candidates.windows(2).all(|pair| pair[0].anchor.lon < pair[1].anchor.lon));
3339 assert!(candidates.iter().all(|candidate| candidate.rotation_rad.abs() < 1e-6));
3340 assert_eq!(candidates[0].text.as_deref(), Some("Road"));
3341 }
3342
3343 #[test]
3344 fn symbol_candidates_line_placement_rotates_vertical_lines() {
3345 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3346 style.symbol_text_field = Some("name".to_owned());
3347 style.symbol_placement = SymbolPlacement::Line;
3348
3349 let mut properties = HashMap::new();
3350 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("North".to_owned()));
3351 let layer = VectorLayer::new(
3352 "symbol",
3353 FeatureCollection {
3354 features: vec![Feature {
3355 geometry: Geometry::LineString(LineString {
3356 coords: vec![
3357 GeoCoord::from_lat_lon(0.0, 0.0),
3358 GeoCoord::from_lat_lon(1.0, 0.0),
3359 ],
3360 }),
3361 properties,
3362 }],
3363 },
3364 style,
3365 );
3366
3367 let candidates = layer.symbol_candidates();
3368 assert!(!candidates.is_empty());
3369 assert!(candidates.iter().all(|candidate| {
3370 (candidate.rotation_rad.abs() - std::f32::consts::FRAC_PI_2).abs() < 0.05
3371 }));
3372 }
3373
3374 #[test]
3375 fn symbol_candidates_line_placement_keeps_reversed_lines_upright() {
3376 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3377 style.symbol_text_field = Some("name".to_owned());
3378 style.symbol_placement = SymbolPlacement::Line;
3379 style.symbol_keep_upright = true;
3380
3381 let mut properties = HashMap::new();
3382 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("West".to_owned()));
3383 let layer = VectorLayer::new(
3384 "symbol",
3385 FeatureCollection {
3386 features: vec![Feature {
3387 geometry: Geometry::LineString(LineString {
3388 coords: vec![
3389 GeoCoord::from_lat_lon(0.0, 1.0),
3390 GeoCoord::from_lat_lon(0.0, 0.0),
3391 ],
3392 }),
3393 properties,
3394 }],
3395 },
3396 style,
3397 );
3398
3399 let candidates = layer.symbol_candidates();
3400 assert!(!candidates.is_empty());
3401 assert!(candidates.iter().all(|candidate| candidate.rotation_rad.abs() < 0.05));
3402 }
3403
3404 #[test]
3405 fn symbol_candidates_line_placement_can_disable_keep_upright() {
3406 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3407 style.symbol_text_field = Some("name".to_owned());
3408 style.symbol_placement = SymbolPlacement::Line;
3409 style.symbol_keep_upright = false;
3410
3411 let mut properties = HashMap::new();
3412 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("West".to_owned()));
3413 let layer = VectorLayer::new(
3414 "symbol",
3415 FeatureCollection {
3416 features: vec![Feature {
3417 geometry: Geometry::LineString(LineString {
3418 coords: vec![
3419 GeoCoord::from_lat_lon(0.0, 1.0),
3420 GeoCoord::from_lat_lon(0.0, 0.0),
3421 ],
3422 }),
3423 properties,
3424 }],
3425 },
3426 style,
3427 );
3428
3429 let candidates = layer.symbol_candidates();
3430 assert!(!candidates.is_empty());
3431 assert!(candidates.iter().all(|candidate| {
3432 (candidate.rotation_rad.abs() - std::f32::consts::PI).abs() < 0.05
3433 }));
3434 }
3435
3436 #[test]
3437 fn symbol_candidates_line_placement_cross_tile_id_stays_stable_across_small_anchor_shifts() {
3438 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3439 style.symbol_text_field = Some("name".to_owned());
3440 style.symbol_placement = SymbolPlacement::Line;
3441 style.symbol_spacing = 1000.0;
3442
3443 let mut properties = HashMap::new();
3444 properties.insert("id".to_owned(), crate::geometry::PropertyValue::String("road-1".to_owned()));
3445 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Main".to_owned()));
3446
3447 let base = VectorLayer::new(
3448 "symbol",
3449 FeatureCollection {
3450 features: vec![Feature {
3451 geometry: Geometry::LineString(LineString {
3452 coords: vec![
3453 GeoCoord::from_lat_lon(0.0, 0.0),
3454 GeoCoord::from_lat_lon(0.0, 0.05),
3455 ],
3456 }),
3457 properties: properties.clone(),
3458 }],
3459 },
3460 style.clone(),
3461 );
3462 let shifted = VectorLayer::new(
3463 "symbol",
3464 FeatureCollection {
3465 features: vec![Feature {
3466 geometry: Geometry::LineString(LineString {
3467 coords: vec![
3468 GeoCoord::from_lat_lon(0.0, 0.0),
3469 GeoCoord::from_lat_lon(0.0002, 0.0502),
3470 ],
3471 }),
3472 properties,
3473 }],
3474 },
3475 style,
3476 );
3477
3478 let base_ids = base
3479 .symbol_candidates()
3480 .into_iter()
3481 .map(|candidate| candidate.cross_tile_id)
3482 .collect::<Vec<_>>();
3483 let shifted_ids = shifted
3484 .symbol_candidates()
3485 .into_iter()
3486 .map(|candidate| candidate.cross_tile_id)
3487 .collect::<Vec<_>>();
3488
3489 assert_eq!(base_ids, shifted_ids);
3490 }
3491
3492 #[test]
3493 fn symbol_candidates_line_placement_cross_tile_id_changes_for_shifted_line_windows() {
3494 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3495 style.symbol_text_field = Some("name".to_owned());
3496 style.symbol_placement = SymbolPlacement::Line;
3497 style.symbol_spacing = 1000.0;
3498
3499 let mut properties = HashMap::new();
3500 properties.insert("id".to_owned(), crate::geometry::PropertyValue::String("road-2".to_owned()));
3501 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Main".to_owned()));
3502
3503 let base = VectorLayer::new(
3504 "symbol",
3505 FeatureCollection {
3506 features: vec![Feature {
3507 geometry: Geometry::LineString(LineString {
3508 coords: vec![
3509 GeoCoord::from_lat_lon(0.0, 0.0),
3510 GeoCoord::from_lat_lon(0.0, 0.05),
3511 ],
3512 }),
3513 properties: properties.clone(),
3514 }],
3515 },
3516 style.clone(),
3517 );
3518 let shifted_window = VectorLayer::new(
3519 "symbol",
3520 FeatureCollection {
3521 features: vec![Feature {
3522 geometry: Geometry::LineString(LineString {
3523 coords: vec![
3524 GeoCoord::from_lat_lon(0.0, 0.01),
3525 GeoCoord::from_lat_lon(0.0, 0.06),
3526 ],
3527 }),
3528 properties,
3529 }],
3530 },
3531 style,
3532 );
3533
3534 let base_ids = base
3535 .symbol_candidates()
3536 .into_iter()
3537 .map(|candidate| candidate.cross_tile_id)
3538 .collect::<Vec<_>>();
3539 let shifted_ids = shifted_window
3540 .symbol_candidates()
3541 .into_iter()
3542 .map(|candidate| candidate.cross_tile_id)
3543 .collect::<Vec<_>>();
3544
3545 assert_ne!(base_ids.first(), shifted_ids.first());
3546 assert!(base_ids.iter().any(|id| shifted_ids.contains(id)));
3547 }
3548
3549 #[test]
3550 fn symbol_candidates_line_placement_filters_sharp_turns_with_max_angle() {
3551 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3552 style.symbol_text_field = Some("name".to_owned());
3553 style.symbol_placement = SymbolPlacement::Line;
3554 style.symbol_spacing = 10_000.0;
3555 style.symbol_max_angle = 10.0;
3556
3557 let mut properties = HashMap::new();
3558 properties.insert("name".to_owned(), crate::geometry::PropertyValue::String("Turn".to_owned()));
3559 let layer = VectorLayer::new(
3560 "symbol",
3561 FeatureCollection {
3562 features: vec![Feature {
3563 geometry: Geometry::LineString(LineString {
3564 coords: vec![
3565 GeoCoord::from_lat_lon(0.0, 0.0),
3566 GeoCoord::from_lat_lon(0.0, 0.03),
3567 GeoCoord::from_lat_lon(0.03, 0.03),
3568 ],
3569 }),
3570 properties,
3571 }],
3572 },
3573 style,
3574 );
3575
3576 assert!(layer.symbol_candidates().is_empty());
3577 }
3578
3579 #[test]
3580 fn tessellate_symbol_mode_stacks_halo_and_fill() {
3581 let style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3582 let layer = make_layer(Geometry::Point(origin_point()));
3583 let layer = VectorLayer::new("symbol", layer.features, style);
3584 let mesh = layer.tessellate(CameraProjection::WebMercator);
3585 assert_eq!(mesh.vertex_count(), 8);
3586 assert_eq!(mesh.index_count(), 12);
3587 }
3588
3589 #[test]
3590 fn tessellate_equirectangular_changes_xy_positions() {
3591 let layer = make_layer(Geometry::Polygon(square_polygon()));
3592 let merc = layer.tessellate(CameraProjection::WebMercator);
3593 let eq = layer.tessellate(CameraProjection::Equirectangular);
3594
3595 assert_eq!(merc.positions.len(), eq.positions.len());
3596 assert!(merc
3597 .positions
3598 .iter()
3599 .zip(eq.positions.iter())
3600 .any(|(a, b)| (a[0] - b[0]).abs() > 1.0 || (a[1] - b[1]).abs() > 1.0));
3601 }
3602
3603 #[test]
3608 fn tessellate_line_mode_populates_normals_and_distances() {
3609 let style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 4.0);
3610 let layer = VectorLayer::new("line", make_layer(Geometry::LineString(two_point_line())).features, style);
3611 let mesh = layer.tessellate(CameraProjection::WebMercator);
3612 assert!(!mesh.is_empty());
3613 assert_eq!(mesh.line_normals.len(), mesh.positions.len(),
3614 "line_normals must have one entry per vertex");
3615 assert_eq!(mesh.line_distances.len(), mesh.positions.len(),
3616 "line_distances must have one entry per vertex");
3617 assert!(mesh.line_distances.iter().any(|&d| d > 0.0),
3619 "at least one distance should be positive");
3620 }
3621
3622 #[test]
3623 fn tessellate_line_mode_propagates_dash_params() {
3624 let style = VectorStyle::line_styled(
3625 [1.0, 0.0, 0.0, 1.0],
3626 4.0,
3627 LineCap::Round,
3628 LineJoin::Bevel,
3629 2.0,
3630 Some(vec![10.0, 5.0]),
3631 );
3632 let layer = VectorLayer::new("dashed", make_layer(Geometry::LineString(two_point_line())).features, style);
3633 let mesh = layer.tessellate(CameraProjection::WebMercator);
3634 assert_eq!(mesh.line_params[0], 10.0, "dash_length");
3635 assert_eq!(mesh.line_params[1], 5.0, "gap_length");
3636 assert_eq!(mesh.line_params[2], 1.0, "cap_round flag");
3637 }
3638
3639 #[test]
3640 fn tessellate_line_mode_round_join_adds_vertices() {
3641 let line = crate::geometry::LineString {
3642 coords: vec![
3643 GeoCoord::from_lat_lon(0.0, 0.0),
3644 GeoCoord::from_lat_lon(0.0, 1.0),
3645 GeoCoord::from_lat_lon(1.0, 1.0),
3646 ],
3647 };
3648 let miter_style = VectorStyle::line_styled(
3649 [1.0, 0.0, 0.0, 1.0], 4.0,
3650 LineCap::Butt, LineJoin::Miter, 10.0, None,
3651 );
3652 let round_style = VectorStyle::line_styled(
3653 [1.0, 0.0, 0.0, 1.0], 4.0,
3654 LineCap::Butt, LineJoin::Round, 2.0, None,
3655 );
3656 let miter_layer = VectorLayer::new("m", make_layer(Geometry::LineString(line.clone())).features, miter_style);
3657 let round_layer = VectorLayer::new("r", make_layer(Geometry::LineString(line)).features, round_style);
3658 let miter_mesh = miter_layer.tessellate(CameraProjection::WebMercator);
3659 let round_mesh = round_layer.tessellate(CameraProjection::WebMercator);
3660 assert!(
3661 round_mesh.vertex_count() > miter_mesh.vertex_count(),
3662 "round join should produce more vertices than miter: {} vs {}",
3663 round_mesh.vertex_count(), miter_mesh.vertex_count(),
3664 );
3665 }
3666
3667 fn two_feature_dd_width_layer() -> VectorLayer {
3675 use crate::geometry::PropertyValue;
3676
3677 let narrow_feature = Feature {
3678 geometry: Geometry::LineString(two_point_line()),
3679 properties: {
3680 let mut p = HashMap::new();
3681 p.insert("width".into(), PropertyValue::Number(2.0));
3682 p
3683 },
3684 };
3685 let wide_feature = Feature {
3686 geometry: Geometry::LineString(two_point_line()),
3687 properties: {
3688 let mut p = HashMap::new();
3689 p.insert("width".into(), PropertyValue::Number(10.0));
3690 p
3691 },
3692 };
3693 let features = FeatureCollection {
3694 features: vec![narrow_feature, wide_feature],
3695 };
3696
3697 let mut style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 2.0);
3698 style.width_expr = Some(Expression::GetProperty {
3699 key: "width".into(),
3700 fallback: 2.0,
3701 });
3702 style.eval_zoom = 10.0;
3703
3704 VectorLayer::new("dd_width", features, style)
3705 }
3706
3707 #[test]
3708 fn data_driven_width_produces_different_ribbon_widths() {
3709 let layer = two_feature_dd_width_layer();
3710 let mesh = layer.tessellate(CameraProjection::WebMercator);
3711
3712 assert!(!mesh.is_empty());
3714
3715 let narrow_style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 2.0);
3717 let wide_style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 10.0);
3718
3719 let narrow_layer = VectorLayer::new(
3720 "narrow",
3721 FeatureCollection {
3722 features: vec![layer.features.features[0].clone()],
3723 },
3724 narrow_style,
3725 );
3726 let wide_layer = VectorLayer::new(
3727 "wide",
3728 FeatureCollection {
3729 features: vec![layer.features.features[1].clone()],
3730 },
3731 wide_style,
3732 );
3733
3734 let narrow_mesh = narrow_layer.tessellate(CameraProjection::WebMercator);
3735 let wide_mesh = wide_layer.tessellate(CameraProjection::WebMercator);
3736
3737 let narrow_span = position_y_span(&narrow_mesh);
3739 let wide_span = position_y_span(&wide_mesh);
3740 assert!(
3741 wide_span > narrow_span,
3742 "wide ribbon span ({wide_span}) must exceed narrow ribbon span ({narrow_span})",
3743 );
3744
3745 let combined_span = position_y_span(&mesh);
3748 assert!(
3749 combined_span >= wide_span * 0.99,
3750 "combined mesh span ({combined_span}) should be >= wide span ({wide_span})",
3751 );
3752 }
3753
3754 #[test]
3755 fn data_driven_color_assigns_per_feature_colors() {
3756 use crate::geometry::PropertyValue;
3757
3758 let red_feature = Feature {
3759 geometry: Geometry::LineString(two_point_line()),
3760 properties: {
3761 let mut p = HashMap::new();
3762 p.insert("kind".into(), PropertyValue::String("highway".into()));
3763 p
3764 },
3765 };
3766 let blue_feature = Feature {
3767 geometry: Geometry::LineString(two_point_line()),
3768 properties: {
3769 let mut p = HashMap::new();
3770 p.insert("kind".into(), PropertyValue::String("local".into()));
3771 p
3772 },
3773 };
3774 let features = FeatureCollection {
3775 features: vec![red_feature, blue_feature],
3776 };
3777
3778 let mut style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 4.0);
3779 style.stroke_color_expr = Some(Expression::Match {
3780 input: Box::new(crate::expression::StringExpression::GetProperty {
3781 key: "kind".into(),
3782 fallback: String::new(),
3783 }),
3784 cases: vec![
3785 ("highway".into(), [1.0, 0.0, 0.0, 1.0]),
3786 ("local".into(), [0.0, 0.0, 1.0, 1.0]),
3787 ],
3788 fallback: [0.5, 0.5, 0.5, 1.0],
3789 });
3790 style.eval_zoom = 10.0;
3791
3792 let layer = VectorLayer::new("dd_color", features, style);
3793 let mesh = layer.tessellate(CameraProjection::WebMercator);
3794
3795 assert!(!mesh.is_empty());
3796
3797 let has_red = mesh.colors.iter().any(|c| c[0] > 0.9 && c[1] < 0.1 && c[2] < 0.1);
3799 let has_blue = mesh.colors.iter().any(|c| c[0] < 0.1 && c[1] < 0.1 && c[2] > 0.9);
3800 assert!(has_red, "mesh should contain red vertices from highway feature");
3801 assert!(has_blue, "mesh should contain blue vertices from local feature");
3802 }
3803
3804 #[test]
3805 fn non_data_driven_width_expr_uses_uniform_value() {
3806 let features = FeatureCollection {
3809 features: vec![
3810 Feature {
3811 geometry: Geometry::LineString(two_point_line()),
3812 properties: HashMap::new(),
3813 },
3814 Feature {
3815 geometry: Geometry::LineString(two_point_line()),
3816 properties: HashMap::new(),
3817 },
3818 ],
3819 };
3820 let mut style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 4.0);
3821 style.width_expr = Some(Expression::Constant(4.0));
3822 style.eval_zoom = 10.0;
3823
3824 let layer = VectorLayer::new("uniform", features, style);
3825 let mesh = layer.tessellate(CameraProjection::WebMercator);
3826 assert!(!mesh.is_empty());
3827
3828 let first_color = mesh.colors[0];
3830 assert!(mesh.colors.iter().all(|c| *c == first_color));
3831 }
3832
3833 #[test]
3834 fn data_driven_width_fingerprint_changes_with_zoom() {
3835 let mut style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 4.0);
3836 style.width_expr = Some(Expression::GetProperty {
3837 key: "width".into(),
3838 fallback: 4.0,
3839 });
3840
3841 style.eval_zoom = 5.0;
3842 let fp1 = style.tessellation_fingerprint();
3843
3844 style.eval_zoom = 10.0;
3845 let fp2 = style.tessellation_fingerprint();
3846
3847 assert_ne!(fp1, fp2, "data-driven fingerprint should differ by zoom");
3848 }
3849
3850 fn position_y_span(mesh: &VectorMeshData) -> f64 {
3852 let ys: Vec<f64> = mesh.positions.iter().map(|p| p[1]).collect();
3853 let min = ys.iter().cloned().fold(f64::INFINITY, f64::min);
3854 let max = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
3855 max - min
3856 }
3857
3858 fn blue_to_red_ramp() -> crate::visualization::ColorRamp {
3863 use crate::visualization::{ColorRamp, ColorStop};
3864 ColorRamp::new(vec![
3865 ColorStop { value: 0.0, color: [0.0, 0.0, 1.0, 1.0] },
3866 ColorStop { value: 1.0, color: [1.0, 0.0, 0.0, 1.0] },
3867 ])
3868 }
3869
3870 #[test]
3871 fn line_gradient_overrides_vertex_colors() {
3872 let ramp = blue_to_red_ramp();
3873 let style = VectorStyle::line_gradient(4.0, ramp);
3874 let layer = VectorLayer::new(
3875 "grad",
3876 FeatureCollection {
3877 features: vec![Feature {
3878 geometry: Geometry::LineString(LineString {
3879 coords: vec![
3880 GeoCoord::from_lat_lon(0.0, 0.0),
3881 GeoCoord::from_lat_lon(0.0, 1.0),
3882 ],
3883 }),
3884 properties: HashMap::new(),
3885 }],
3886 },
3887 style,
3888 );
3889 let mesh = layer.tessellate(CameraProjection::WebMercator);
3890 assert!(!mesh.colors.is_empty());
3891 let has_blue = mesh.colors.iter().any(|c| c[2] > 0.8 && c[0] < 0.2);
3894 let has_red = mesh.colors.iter().any(|c| c[0] > 0.8 && c[2] < 0.2);
3895 assert!(has_blue, "expected some blue-ish vertices near start");
3896 assert!(has_red, "expected some red-ish vertices near end");
3897 }
3898
3899 #[test]
3900 fn line_gradient_without_ramp_uses_solid_color() {
3901 let solid = [0.5, 0.5, 0.5, 1.0];
3902 let style = VectorStyle::line(solid, 4.0);
3903 let layer = VectorLayer::new(
3904 "solid",
3905 FeatureCollection {
3906 features: vec![Feature {
3907 geometry: Geometry::LineString(LineString {
3908 coords: vec![
3909 GeoCoord::from_lat_lon(0.0, 0.0),
3910 GeoCoord::from_lat_lon(0.0, 1.0),
3911 ],
3912 }),
3913 properties: HashMap::new(),
3914 }],
3915 },
3916 style,
3917 );
3918 let mesh = layer.tessellate(CameraProjection::WebMercator);
3919 assert!(!mesh.colors.is_empty());
3920 for c in &mesh.colors {
3921 assert_eq!(*c, solid, "all vertices should share the solid colour");
3922 }
3923 }
3924
3925 #[test]
3926 fn line_gradient_midpoint_is_interpolated() {
3927 use crate::visualization::{ColorRamp, ColorStop};
3928 let ramp = ColorRamp::new(vec![
3929 ColorStop { value: 0.0, color: [0.0, 0.0, 0.0, 1.0] },
3930 ColorStop { value: 1.0, color: [1.0, 1.0, 1.0, 1.0] },
3931 ]);
3932 let style = VectorStyle::line_gradient(2.0, ramp);
3933 let layer = VectorLayer::new(
3934 "mid",
3935 FeatureCollection {
3936 features: vec![Feature {
3937 geometry: Geometry::LineString(LineString {
3938 coords: vec![
3939 GeoCoord::from_lat_lon(0.0, 0.0),
3940 GeoCoord::from_lat_lon(0.0, 0.5),
3941 GeoCoord::from_lat_lon(0.0, 1.0),
3942 ],
3943 }),
3944 properties: HashMap::new(),
3945 }],
3946 },
3947 style,
3948 );
3949 let mesh = layer.tessellate(CameraProjection::WebMercator);
3950 let has_mid = mesh.colors.iter().any(|c| c[0] > 0.3 && c[0] < 0.7);
3953 assert!(has_mid, "expected midpoint vertices with interpolated colour");
3954 }
3955
3956 #[test]
3957 fn line_gradient_style_roundtrips_through_line_style_layer() {
3958 use crate::style::{LineStyleLayer, StyleEvalContextFull, line_style_with_state};
3959 let ramp = blue_to_red_ramp();
3960 let mut layer = LineStyleLayer::new("grad", "src");
3961 layer.line_gradient = Some(ramp.clone());
3962 let state = HashMap::new();
3963 let ctx = StyleEvalContextFull::new(5.0, &state);
3964 let vs = line_style_with_state(&layer, &ctx);
3965 assert!(vs.line_gradient.is_some());
3966 }
3967
3968 fn checkerboard_2x2() -> Arc<PatternImage> {
3972 #[rustfmt::skip]
3974 let data = vec![
3975 0, 0, 0, 255, 255, 255, 255, 255,
3976 255, 255, 255, 255, 0, 0, 0, 255,
3977 ];
3978 Arc::new(PatternImage::new(2, 2, data))
3979 }
3980
3981 #[test]
3982 fn pattern_image_validates_data_length() {
3983 let img = PatternImage::new(2, 2, vec![0u8; 16]);
3985 assert_eq!(img.width, 2);
3986 assert_eq!(img.height, 2);
3987 }
3988
3989 #[test]
3990 #[should_panic(expected = "RGBA8 data length")]
3991 fn pattern_image_rejects_wrong_data_length() {
3992 let _img = PatternImage::new(2, 2, vec![0u8; 10]);
3993 }
3994
3995 #[test]
3996 fn fill_pattern_generates_uvs() {
3997 let pattern = checkerboard_2x2();
3998 let style = VectorStyle::fill_pattern(pattern);
3999
4000 let geom = Geometry::Polygon(square_polygon());
4001 let features = FeatureCollection {
4002 features: vec![Feature {
4003 geometry: geom,
4004 properties: HashMap::new(),
4005 }],
4006 };
4007 let layer = VectorLayer::new("pat", features, style);
4008 let mesh = layer.tessellate(CameraProjection::WebMercator);
4009
4010 assert!(mesh.fill_pattern.is_some());
4012 assert!(
4013 !mesh.fill_pattern_uvs.is_empty(),
4014 "expected fill_pattern_uvs to be non-empty"
4015 );
4016 assert!(
4020 mesh.fill_pattern_uvs.len() <= mesh.positions.len(),
4021 "fill_pattern_uvs should not exceed positions count"
4022 );
4023 }
4024
4025 #[test]
4026 fn solid_fill_has_no_pattern_uvs() {
4027 let style = VectorStyle::fill([0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 0.0, 1.0], 1.0);
4028
4029 let geom = Geometry::Polygon(square_polygon());
4030 let features = FeatureCollection {
4031 features: vec![Feature {
4032 geometry: geom,
4033 properties: HashMap::new(),
4034 }],
4035 };
4036 let layer = VectorLayer::new("solid", features, style);
4037 let mesh = layer.tessellate(CameraProjection::WebMercator);
4038
4039 assert!(mesh.fill_pattern.is_none());
4040 assert!(
4041 mesh.fill_pattern_uvs.is_empty(),
4042 "solid fills should not generate pattern UVs"
4043 );
4044 }
4045
4046 #[test]
4047 fn fill_pattern_style_roundtrips_through_fill_style_layer() {
4048 use crate::style::{FillStyleLayer, StyleEvalContextFull, fill_style_with_state};
4049 let pattern = checkerboard_2x2();
4050 let mut layer = FillStyleLayer::new("fp", "src");
4051 layer.fill_pattern = Some(pattern.clone());
4052 let state = HashMap::new();
4053 let ctx = StyleEvalContextFull::new(5.0, &state);
4054 let vs = fill_style_with_state(&layer, &ctx);
4055 assert!(vs.fill_pattern.is_some());
4056 assert_eq!(vs.fill_pattern.as_ref().unwrap().width, 2);
4057 }
4058
4059 fn line_feature() -> FeatureCollection {
4064 FeatureCollection {
4065 features: vec![Feature {
4066 geometry: Geometry::LineString(LineString {
4067 coords: vec![
4068 GeoCoord::from_lat_lon(0.0, 0.0),
4069 GeoCoord::from_lat_lon(0.0, 1.0),
4070 ],
4071 }),
4072 properties: HashMap::new(),
4073 }],
4074 }
4075 }
4076
4077 #[test]
4078 fn line_pattern_generates_uvs() {
4079 let pattern = checkerboard_2x2();
4080 let style = VectorStyle::line_pattern(4.0, pattern);
4081 let layer = VectorLayer::new("lp", line_feature(), style);
4082 let mesh = layer.tessellate(CameraProjection::WebMercator);
4083
4084 assert!(mesh.line_pattern.is_some(), "pattern image should be carried to mesh");
4085 assert!(
4086 !mesh.line_pattern_uvs.is_empty(),
4087 "expected line_pattern_uvs to be non-empty"
4088 );
4089 assert_eq!(
4090 mesh.line_pattern_uvs.len(),
4091 mesh.positions.len(),
4092 "each position should have a corresponding pattern UV"
4093 );
4094 }
4095
4096 #[test]
4097 fn line_pattern_uvs_have_correct_v_range() {
4098 let pattern = checkerboard_2x2();
4099 let style = VectorStyle::line_pattern(4.0, pattern);
4100 let layer = VectorLayer::new("lp", line_feature(), style);
4101 let mesh = layer.tessellate(CameraProjection::WebMercator);
4102
4103 let has_left = mesh.line_pattern_uvs.iter().any(|uv| (uv[1] - 0.0).abs() < 0.01);
4106 let has_right = mesh.line_pattern_uvs.iter().any(|uv| (uv[1] - 1.0).abs() < 0.01);
4107 assert!(has_left, "expected some V=0.0 vertices (left edge)");
4108 assert!(has_right, "expected some V=1.0 vertices (right edge)");
4109
4110 for uv in &mesh.line_pattern_uvs {
4112 assert!(
4113 uv[1] >= -0.01 && uv[1] <= 1.01,
4114 "V coordinate {:.3} outside [0, 1]",
4115 uv[1]
4116 );
4117 }
4118 }
4119
4120 #[test]
4121 fn solid_line_has_no_pattern_uvs() {
4122 let style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 4.0);
4123 let layer = VectorLayer::new("solid", line_feature(), style);
4124 let mesh = layer.tessellate(CameraProjection::WebMercator);
4125
4126 assert!(mesh.line_pattern.is_none());
4127 assert!(
4128 mesh.line_pattern_uvs.is_empty(),
4129 "solid lines should not generate pattern UVs"
4130 );
4131 }
4132
4133 #[test]
4134 fn line_pattern_style_constructor() {
4135 let pattern = checkerboard_2x2();
4136 let style = VectorStyle::line_pattern(6.0, pattern.clone());
4137 assert_eq!(style.render_mode, VectorRenderMode::Line);
4138 assert_eq!(style.stroke_width, 6.0);
4139 assert!(style.line_pattern.is_some());
4140 assert_eq!(style.line_pattern.as_ref().unwrap().width, 2);
4141 }
4142
4143 #[test]
4144 fn line_pattern_style_roundtrips_through_line_style_layer() {
4145 use crate::style::{LineStyleLayer, StyleEvalContextFull, line_style_with_state};
4146 let pattern = checkerboard_2x2();
4147 let mut layer = LineStyleLayer::new("lp", "src");
4148 layer.line_pattern = Some(pattern.clone());
4149 let state = HashMap::new();
4150 let ctx = StyleEvalContextFull::new(5.0, &state);
4151 let vs = line_style_with_state(&layer, &ctx);
4152 assert!(vs.line_pattern.is_some());
4153 assert_eq!(vs.line_pattern.as_ref().unwrap().width, 2);
4154 }
4155
4156 #[test]
4157 fn line_pattern_u_increases_along_line() {
4158 let pattern = checkerboard_2x2();
4159 let style = VectorStyle::line_pattern(4.0, pattern);
4160
4161 let features = FeatureCollection {
4163 features: vec![Feature {
4164 geometry: Geometry::LineString(LineString {
4165 coords: vec![
4166 GeoCoord::from_lat_lon(0.0, 0.0),
4167 GeoCoord::from_lat_lon(0.0, 5.0),
4168 ],
4169 }),
4170 properties: HashMap::new(),
4171 }],
4172 };
4173 let layer = VectorLayer::new("lp", features, style);
4174 let mesh = layer.tessellate(CameraProjection::WebMercator);
4175
4176 let body_us: Vec<f32> = mesh
4178 .line_pattern_uvs
4179 .iter()
4180 .filter(|uv| (uv[1] - 0.0).abs() < 0.01 || (uv[1] - 1.0).abs() < 0.01)
4181 .map(|uv| uv[0])
4182 .collect();
4183 assert!(!body_us.is_empty(), "should have body vertices with pattern UVs");
4184 let max_u = body_us.iter().cloned().fold(0.0_f32, f32::max);
4186 assert!(max_u > 0.0, "max U along line should be > 0, got {max_u}");
4187 }
4188
4189 #[test]
4190 fn heatmap_tessellation_populates_heatmap_points() {
4191 let gc = |lat: f64, lon: f64| rustial_math::GeoCoord::from_lat_lon(lat, lon);
4192 let features = FeatureCollection {
4193 features: vec![Feature {
4194 geometry: Geometry::Point(Point { coord: gc(0.0, 0.0) }),
4195 properties: HashMap::new(),
4196 }],
4197 };
4198 let style = VectorStyle::heatmap([1.0, 0.0, 0.0, 1.0], 20.0, 1.0);
4199 let layer = VectorLayer::new("hm", features, style);
4200 let mesh = layer.tessellate(CameraProjection::WebMercator);
4201 assert!(
4202 !mesh.heatmap_points.is_empty(),
4203 "heatmap tessellation should populate heatmap_points"
4204 );
4205 let pt = &mesh.heatmap_points[0];
4206 assert!(pt[3] > 0.0, "heatmap radius should be positive: {}", pt[3]);
4207 }
4208
4209 #[test]
4210 fn circle_tessellation_populates_circle_instances() {
4211 let gc = |lat: f64, lon: f64| rustial_math::GeoCoord::from_lat_lon(lat, lon);
4212 let features = FeatureCollection {
4213 features: vec![Feature {
4214 geometry: Geometry::Point(Point { coord: gc(0.0, 0.0) }),
4215 properties: HashMap::new(),
4216 }],
4217 };
4218 let style = VectorStyle::circle([0.0, 1.0, 0.0, 1.0], 10.0, [0.0, 0.0, 0.0, 1.0], 2.0);
4219 let layer = VectorLayer::new("cc", features, style);
4220 let mesh = layer.tessellate(CameraProjection::WebMercator);
4221 assert!(
4222 !mesh.circle_instances.is_empty(),
4223 "circle tessellation should populate circle_instances"
4224 );
4225 assert!(
4226 mesh.circle_instances[0].radius > 0.0,
4227 "circle radius should be positive"
4228 );
4229 }
4230}