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 {
86 width,
87 height,
88 data,
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
95pub enum VectorRenderMode {
96 #[default]
98 Generic,
99 Fill,
101 Line,
103 Circle,
105 Heatmap,
107 FillExtrusion,
109 Symbol,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
115pub enum LineCap {
116 #[default]
118 Butt,
119 Round,
121 Square,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
127pub enum LineJoin {
128 #[default]
131 Miter,
132 Bevel,
134 Round,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct FeatureProvenance {
141 pub source_layer: Option<String>,
143 pub source_tile: Option<TileId>,
145}
146
147#[derive(Debug, Clone)]
149pub struct VectorStyle {
150 pub render_mode: VectorRenderMode,
152 pub fill_color: [f32; 4],
154 pub stroke_color: [f32; 4],
156 pub stroke_width: f32,
158 pub point_radius: f32,
160 pub line_cap: LineCap,
162 pub line_join: LineJoin,
164 pub miter_limit: f32,
168 pub dash_array: Option<Vec<f32>>,
170 pub heatmap_radius: f32,
172 pub heatmap_intensity: f32,
174 pub extrusion_base: f32,
176 pub extrusion_height: f32,
178 pub symbol_size: f32,
180 pub symbol_halo_color: [f32; 4],
182 pub symbol_text_field: Option<String>,
184 pub symbol_icon_image: Option<String>,
186 pub symbol_font_stack: String,
188 pub symbol_padding: f32,
190 pub symbol_allow_overlap: bool,
197 pub symbol_text_allow_overlap: bool,
199 pub symbol_icon_allow_overlap: bool,
201 pub symbol_text_optional: bool,
203 pub symbol_icon_optional: bool,
205 pub symbol_text_ignore_placement: bool,
207 pub symbol_icon_ignore_placement: bool,
209 pub symbol_text_anchor: SymbolAnchor,
211 pub symbol_text_justify: SymbolTextJustify,
213 pub symbol_text_transform: SymbolTextTransform,
215 pub symbol_text_max_width: Option<f32>,
222 pub symbol_text_line_height: Option<f32>,
228 pub symbol_text_letter_spacing: Option<f32>,
234 pub symbol_icon_text_fit: SymbolIconTextFit,
236 pub symbol_icon_text_fit_padding: [f32; 4],
238 pub symbol_sort_key: Option<f32>,
244 pub symbol_anchors: Vec<SymbolAnchor>,
246 pub symbol_placement: SymbolPlacement,
248 pub symbol_spacing: f32,
254 pub symbol_max_angle: f32,
261 pub symbol_keep_upright: bool,
267 pub symbol_text_radial_offset: Option<f32>,
274 pub symbol_variable_anchor_offsets: Option<Vec<(SymbolAnchor, [f32; 2])>>,
280 pub symbol_writing_mode: SymbolWritingMode,
282 pub symbol_offset: [f32; 2],
284 pub fill_translate: [f32; 2],
286 pub fill_opacity: f32,
288 pub fill_antialias: bool,
290 pub fill_outline_color: Option<[f32; 4]>,
292
293 pub fill_pattern: Option<Arc<PatternImage>>,
300
301 pub line_pattern: Option<Arc<PatternImage>>,
309
310 pub width_expr: Option<Expression<f32>>,
317 pub stroke_color_expr: Option<Expression<[f32; 4]>>,
322 pub eval_zoom: f32,
327
328 pub line_gradient: Option<ColorRamp>,
336}
337
338impl Default for VectorStyle {
339 fn default() -> Self {
340 Self {
341 render_mode: VectorRenderMode::Generic,
342 fill_color: [0.2, 0.5, 0.8, 0.5],
343 stroke_color: [0.0, 0.0, 0.0, 1.0],
344 stroke_width: 2.0,
345 point_radius: 6.0,
346 line_cap: LineCap::Butt,
347 line_join: LineJoin::Miter,
348 miter_limit: 2.0,
349 dash_array: None,
350 heatmap_radius: 18.0,
351 heatmap_intensity: 1.0,
352 extrusion_base: 0.0,
353 extrusion_height: 30.0,
354 symbol_size: 10.0,
355 symbol_halo_color: [1.0, 1.0, 1.0, 0.85],
356 symbol_text_field: None,
357 symbol_icon_image: None,
358 symbol_font_stack: "Noto Sans Regular".into(),
359 symbol_padding: 2.0,
360 symbol_allow_overlap: false,
361 symbol_text_allow_overlap: false,
362 symbol_icon_allow_overlap: false,
363 symbol_text_optional: false,
364 symbol_icon_optional: false,
365 symbol_text_ignore_placement: false,
366 symbol_icon_ignore_placement: false,
367 symbol_text_anchor: SymbolAnchor::Center,
368 symbol_text_justify: SymbolTextJustify::Auto,
369 symbol_text_transform: SymbolTextTransform::None,
370 symbol_text_max_width: None,
371 symbol_text_line_height: None,
372 symbol_text_letter_spacing: None,
373 symbol_icon_text_fit: SymbolIconTextFit::None,
374 symbol_icon_text_fit_padding: [0.0, 0.0, 0.0, 0.0],
375 symbol_sort_key: None,
376 symbol_anchors: vec![SymbolAnchor::Center],
377 symbol_placement: SymbolPlacement::Point,
378 symbol_spacing: 250.0,
379 symbol_max_angle: 45.0,
380 symbol_keep_upright: true,
381 symbol_text_radial_offset: None,
382 symbol_variable_anchor_offsets: None,
383 symbol_writing_mode: SymbolWritingMode::Horizontal,
384 symbol_offset: [0.0, 0.0],
385 fill_translate: [0.0, 0.0],
386 fill_opacity: 1.0,
387 fill_antialias: true,
388 fill_outline_color: None,
389 fill_pattern: None,
390 line_pattern: None,
391 width_expr: None,
392 stroke_color_expr: None,
393 eval_zoom: 0.0,
394 line_gradient: None,
395 }
396 }
397}
398
399impl VectorStyle {
400 pub fn fill(fill_color: [f32; 4], outline_color: [f32; 4], outline_width: f32) -> Self {
402 Self {
403 render_mode: VectorRenderMode::Fill,
404 fill_color,
405 stroke_color: outline_color,
406 stroke_width: outline_width,
407 ..Self::default()
408 }
409 }
410
411 pub fn fill_pattern(pattern: Arc<PatternImage>) -> Self {
416 Self {
417 render_mode: VectorRenderMode::Fill,
418 fill_pattern: Some(pattern),
419 ..Self::default()
420 }
421 }
422
423 pub fn line_pattern(width: f32, pattern: Arc<PatternImage>) -> Self {
429 Self {
430 render_mode: VectorRenderMode::Line,
431 stroke_width: width,
432 line_pattern: Some(pattern),
433 ..Self::default()
434 }
435 }
436
437 pub fn line(color: [f32; 4], width: f32) -> Self {
439 Self {
440 render_mode: VectorRenderMode::Line,
441 stroke_color: color,
442 stroke_width: width,
443 ..Self::default()
444 }
445 }
446
447 pub fn line_styled(
449 color: [f32; 4],
450 width: f32,
451 cap: LineCap,
452 join: LineJoin,
453 miter_limit: f32,
454 dash_array: Option<Vec<f32>>,
455 ) -> Self {
456 Self {
457 render_mode: VectorRenderMode::Line,
458 stroke_color: color,
459 stroke_width: width,
460 line_cap: cap,
461 line_join: join,
462 miter_limit,
463 dash_array,
464 ..Self::default()
465 }
466 }
467
468 pub fn line_gradient(width: f32, ramp: ColorRamp) -> Self {
474 Self {
475 render_mode: VectorRenderMode::Line,
476 stroke_color: [1.0, 1.0, 1.0, 1.0],
477 stroke_width: width,
478 line_gradient: Some(ramp),
479 ..Self::default()
480 }
481 }
482
483 pub fn circle(color: [f32; 4], radius: f32, stroke_color: [f32; 4], stroke_width: f32) -> Self {
485 Self {
486 render_mode: VectorRenderMode::Circle,
487 fill_color: color,
488 point_radius: radius,
489 stroke_color,
490 stroke_width,
491 ..Self::default()
492 }
493 }
494
495 pub fn heatmap(color: [f32; 4], radius: f32, intensity: f32) -> Self {
497 Self {
498 render_mode: VectorRenderMode::Heatmap,
499 fill_color: color,
500 heatmap_radius: radius,
501 heatmap_intensity: intensity,
502 ..Self::default()
503 }
504 }
505
506 pub fn fill_extrusion(color: [f32; 4], base: f32, height: f32) -> Self {
508 Self {
509 render_mode: VectorRenderMode::FillExtrusion,
510 fill_color: color,
511 extrusion_base: base,
512 extrusion_height: height,
513 ..Self::default()
514 }
515 }
516
517 pub fn symbol(color: [f32; 4], halo_color: [f32; 4], size: f32) -> Self {
519 Self {
520 render_mode: VectorRenderMode::Symbol,
521 fill_color: color,
522 symbol_halo_color: halo_color,
523 symbol_size: size,
524 ..Self::default()
525 }
526 }
527
528 pub fn tessellation_fingerprint(&self) -> u64 {
537 use std::hash::{Hash, Hasher};
538 let mut h = std::collections::hash_map::DefaultHasher::new();
539 std::mem::discriminant(&self.render_mode).hash(&mut h);
541 self.fill_color
543 .iter()
544 .for_each(|v| v.to_bits().hash(&mut h));
545 self.stroke_color
546 .iter()
547 .for_each(|v| v.to_bits().hash(&mut h));
548 self.stroke_width.to_bits().hash(&mut h);
549 self.point_radius.to_bits().hash(&mut h);
550 self.heatmap_radius.to_bits().hash(&mut h);
551 self.heatmap_intensity.to_bits().hash(&mut h);
552 self.extrusion_base.to_bits().hash(&mut h);
553 self.extrusion_height.to_bits().hash(&mut h);
554 self.symbol_size.to_bits().hash(&mut h);
555 self.symbol_halo_color
556 .iter()
557 .for_each(|v| v.to_bits().hash(&mut h));
558 self.fill_translate
559 .iter()
560 .for_each(|v| v.to_bits().hash(&mut h));
561 self.fill_opacity.to_bits().hash(&mut h);
562 self.fill_antialias.hash(&mut h);
563 if let Some(ref c) = self.fill_outline_color {
564 c.iter().for_each(|v| v.to_bits().hash(&mut h));
565 }
566 let has_dd_width = self.width_expr.as_ref().is_some_and(|e| e.is_data_driven());
571 let has_dd_color = self
572 .stroke_color_expr
573 .as_ref()
574 .is_some_and(|e| e.is_data_driven());
575 has_dd_width.hash(&mut h);
576 has_dd_color.hash(&mut h);
577 if has_dd_width || has_dd_color {
578 self.eval_zoom.to_bits().hash(&mut h);
579 }
580 h.finish()
581 }
582
583 #[inline]
585 pub fn is_width_data_driven(&self) -> bool {
586 self.width_expr.as_ref().is_some_and(|e| e.is_data_driven())
587 }
588
589 #[inline]
591 pub fn is_stroke_color_data_driven(&self) -> bool {
592 self.stroke_color_expr
593 .as_ref()
594 .is_some_and(|e| e.is_data_driven())
595 }
596
597 pub fn evaluate_width(&self, feature: &Feature) -> f32 {
601 match &self.width_expr {
602 Some(expr) if expr.is_data_driven() => {
603 let ctx = ExprEvalContext::with_feature(self.eval_zoom, &feature.properties);
604 expr.evaluate_with_properties(&ctx)
605 }
606 _ => self.stroke_width,
607 }
608 }
609
610 pub fn evaluate_stroke_color(&self, feature: &Feature) -> [f32; 4] {
614 match &self.stroke_color_expr {
615 Some(expr) if expr.is_data_driven() => {
616 let ctx = ExprEvalContext::with_feature(self.eval_zoom, &feature.properties);
617 expr.evaluate_with_properties(&ctx)
618 }
619 _ => self.stroke_color,
620 }
621 }
622}
623
624#[derive(Clone, Copy)]
625struct LinePlacementAnchor {
626 coord: GeoCoord,
627 rotation_rad: f32,
628 distance: f64,
629}
630
631#[derive(Debug, Clone, Copy, Default)]
641pub struct CircleInstanceData {
642 pub center: [f64; 3],
644 pub radius: f32,
646 pub color: [f32; 4],
648 pub stroke_color: [f32; 4],
650 pub stroke_width: f32,
652 pub blur: f32,
654}
655
656#[derive(Debug, Clone)]
664pub struct VectorMeshData {
665 pub positions: Vec<[f64; 3]>,
667 pub colors: Vec<[f32; 4]>,
669 pub normals: Vec<[f32; 3]>,
676 pub indices: Vec<u32>,
678 pub render_mode: VectorRenderMode,
683
684 pub line_distances: Vec<f32>,
689 pub line_normals: Vec<[f32; 2]>,
693 pub line_cap_joins: Vec<f32>,
698 pub line_params: [f32; 4],
702
703 pub circle_instances: Vec<CircleInstanceData>,
707
708 pub heatmap_points: Vec<[f64; 4]>,
711 pub heatmap_intensity: f32,
713
714 pub fill_translate: [f32; 2],
717 pub fill_opacity: f32,
719 pub fill_antialias: bool,
721 pub fill_outline_color: [f32; 4],
724 pub fill_pattern: Option<Arc<PatternImage>>,
730 pub fill_pattern_uvs: Vec<[f32; 2]>,
737
738 pub line_pattern: Option<Arc<PatternImage>>,
745 pub line_pattern_uvs: Vec<[f32; 2]>,
752}
753
754impl VectorMeshData {
755 #[inline]
757 pub fn is_empty(&self) -> bool {
758 self.indices.is_empty()
759 }
760
761 #[inline]
763 pub fn vertex_count(&self) -> usize {
764 self.positions.len()
765 }
766
767 #[inline]
769 pub fn index_count(&self) -> usize {
770 self.indices.len()
771 }
772
773 #[inline]
775 pub fn triangle_count(&self) -> usize {
776 self.indices.len() / 3
777 }
778
779 #[inline]
781 pub fn has_normals(&self) -> bool {
782 !self.normals.is_empty()
783 }
784
785 pub fn merge(&mut self, other: &VectorMeshData) {
787 let base = self.positions.len() as u32;
788 self.positions.extend_from_slice(&other.positions);
789 self.colors.extend_from_slice(&other.colors);
790 self.normals.extend_from_slice(&other.normals);
791 self.line_distances.extend_from_slice(&other.line_distances);
792 self.line_normals.extend_from_slice(&other.line_normals);
793 self.fill_pattern_uvs
794 .extend_from_slice(&other.fill_pattern_uvs);
795 self.line_pattern_uvs
796 .extend_from_slice(&other.line_pattern_uvs);
797 self.indices.extend(other.indices.iter().map(|i| base + i));
798 self.circle_instances
799 .extend_from_slice(&other.circle_instances);
800 self.heatmap_points.extend_from_slice(&other.heatmap_points);
801 }
802
803 pub fn clear(&mut self) {
805 self.positions.clear();
806 self.colors.clear();
807 self.normals.clear();
808 self.line_distances.clear();
809 self.line_normals.clear();
810 self.indices.clear();
811 self.circle_instances.clear();
812 self.heatmap_points.clear();
813 self.fill_translate = [0.0, 0.0];
814 self.fill_opacity = 1.0;
815 self.fill_antialias = true;
816 self.fill_outline_color = [0.0, 0.0, 0.0, 0.0];
817 self.fill_pattern = None;
818 self.fill_pattern_uvs.clear();
819 self.line_pattern = None;
820 self.line_pattern_uvs.clear();
821 }
822}
823
824impl Default for VectorMeshData {
825 fn default() -> Self {
826 Self {
827 positions: Vec::new(),
828 colors: Vec::new(),
829 normals: Vec::new(),
830 indices: Vec::new(),
831 render_mode: VectorRenderMode::Generic,
832 line_distances: Vec::new(),
833 line_normals: Vec::new(),
834 line_cap_joins: Vec::new(),
835 line_params: [0.0; 4],
836 circle_instances: Vec::new(),
837 heatmap_points: Vec::new(),
838 heatmap_intensity: 0.0,
839 fill_translate: [0.0, 0.0],
840 fill_opacity: 1.0,
841 fill_antialias: true,
842 fill_outline_color: [0.0, 0.0, 0.0, 0.0],
843 fill_pattern: None,
844 fill_pattern_uvs: Vec::new(),
845 line_pattern: None,
846 line_pattern_uvs: Vec::new(),
847 }
848 }
849}
850
851pub struct VectorLayer {
860 id: LayerId,
861 name: String,
862 visible: bool,
863 opacity: f32,
864 pub query_layer_id: Option<String>,
866 pub query_source_id: Option<String>,
868 pub query_source_layer: Option<String>,
870 pub features: FeatureCollection,
872 pub feature_provenance: Vec<Option<FeatureProvenance>>,
874 pub style: VectorStyle,
876 data_generation: u64,
880}
881
882impl std::fmt::Debug for VectorLayer {
883 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
884 f.debug_struct("VectorLayer")
885 .field("id", &self.id)
886 .field("name", &self.name)
887 .field("visible", &self.visible)
888 .field("opacity", &self.opacity)
889 .field("feature_count", &self.features.len())
890 .field("style", &self.style)
891 .finish()
892 }
893}
894
895impl VectorLayer {
896 pub fn new(name: impl Into<String>, features: FeatureCollection, style: VectorStyle) -> Self {
898 Self {
899 id: LayerId::next(),
900 name: name.into(),
901 visible: true,
902 opacity: 1.0,
903 query_layer_id: None,
904 query_source_id: None,
905 query_source_layer: None,
906 feature_provenance: vec![None; features.len()],
907 features,
908 style,
909 data_generation: 0,
910 }
911 }
912
913 pub fn with_query_metadata(
915 mut self,
916 layer_id: impl Into<Option<String>>,
917 source_id: impl Into<Option<String>>,
918 ) -> Self {
919 self.query_layer_id = layer_id.into();
920 self.query_source_id = source_id.into();
921 self
922 }
923
924 pub fn with_source_layer(mut self, source_layer: Option<String>) -> Self {
926 self.query_source_layer = source_layer;
927 self
928 }
929
930 pub fn set_features_with_provenance(
932 &mut self,
933 features: FeatureCollection,
934 mut provenance: Vec<Option<FeatureProvenance>>,
935 ) {
936 if provenance.len() < features.len() {
937 provenance.resize(features.len(), None);
938 } else if provenance.len() > features.len() {
939 provenance.truncate(features.len());
940 }
941 self.features = features;
942 self.feature_provenance = provenance;
943 self.data_generation = self.data_generation.wrapping_add(1);
944 }
945
946 #[inline]
951 pub fn data_generation(&self) -> u64 {
952 self.data_generation
953 }
954
955 pub fn set_query_metadata(&mut self, layer_id: Option<String>, source_id: Option<String>) {
957 self.query_layer_id = layer_id;
958 self.query_source_id = source_id;
959 }
960
961 #[inline]
965 pub fn feature_count(&self) -> usize {
966 self.features.len()
967 }
968
969 #[inline]
971 pub fn total_coords(&self) -> usize {
972 self.features.total_coords()
973 }
974
975 pub fn drape_on_terrain(&mut self, terrain: &TerrainManager) {
984 if !terrain.enabled() {
985 return;
986 }
987 for feature in &mut self.features.features {
988 drape_geometry(&mut feature.geometry, terrain);
989 }
990 }
991
992 pub fn tessellate(&self, projection: CameraProjection) -> VectorMeshData {
1009 let mut mesh = VectorMeshData {
1010 render_mode: self.style.render_mode,
1011 ..VectorMeshData::default()
1012 };
1013
1014 let dd_width = self.style.is_width_data_driven();
1015 let dd_color = self.style.is_stroke_color_data_driven();
1016 let default_half_width = self.style.stroke_width as f64 * DEGREES_PER_PIXEL_APPROX;
1017
1018 if dd_width || dd_color {
1019 for feature in &self.features.features {
1021 let half_width = if dd_width {
1022 self.style.evaluate_width(feature) as f64 * DEGREES_PER_PIXEL_APPROX
1023 } else {
1024 default_half_width
1025 };
1026 if dd_color {
1027 let color = self.style.evaluate_stroke_color(feature);
1028 let mut feature_style = self.style.clone();
1029 feature_style.stroke_color = color;
1030 tessellate_geometry(
1031 &feature.geometry,
1032 &feature_style,
1033 projection,
1034 half_width,
1035 &mut mesh,
1036 );
1037 } else {
1038 tessellate_geometry(
1039 &feature.geometry,
1040 &self.style,
1041 projection,
1042 half_width,
1043 &mut mesh,
1044 );
1045 }
1046 }
1047 } else {
1048 for feature in &self.features.features {
1050 tessellate_geometry(
1051 &feature.geometry,
1052 &self.style,
1053 projection,
1054 default_half_width,
1055 &mut mesh,
1056 );
1057 }
1058 }
1059
1060 if self.style.render_mode == VectorRenderMode::Fill {
1063 mesh.fill_translate = self.style.fill_translate;
1064 mesh.fill_opacity = self.style.fill_opacity;
1065 mesh.fill_antialias = self.style.fill_antialias;
1066 mesh.fill_outline_color = self
1067 .style
1068 .fill_outline_color
1069 .unwrap_or(self.style.stroke_color);
1070 }
1071
1072 if self.style.render_mode == VectorRenderMode::Line {
1075 let (dash_len, gap_len) = match &self.style.dash_array {
1076 Some(arr) if arr.len() >= 2 => (arr[0], arr[1]),
1077 _ => (0.0, 0.0),
1078 };
1079 let cap_round = match self.style.line_cap {
1080 LineCap::Round => 1.0,
1081 _ => 0.0,
1082 };
1083 mesh.line_params = [dash_len, gap_len, cap_round, 0.0];
1084 }
1085
1086 mesh
1087 }
1088
1089 pub fn symbol_candidates(&self) -> Vec<SymbolCandidate> {
1091 self.symbol_candidates_for_features(&self.features, &self.feature_provenance)
1092 }
1093
1094 pub fn symbol_candidates_for_features(
1096 &self,
1097 features: &FeatureCollection,
1098 feature_provenance: &[Option<FeatureProvenance>],
1099 ) -> Vec<SymbolCandidate> {
1100 if self.style.render_mode != VectorRenderMode::Symbol {
1101 return Vec::new();
1102 }
1103
1104 let mut out = Vec::new();
1105 for (feature_index, feature) in features.features.iter().enumerate() {
1106 collect_symbol_candidates_from_geometry(
1107 self.id,
1108 self.query_layer_id.as_deref(),
1109 self.query_source_id.as_deref(),
1110 feature_provenance
1111 .get(feature_index)
1112 .and_then(|p| p.as_ref()),
1113 feature_index,
1114 0,
1115 feature,
1116 &feature.geometry,
1117 &self.style,
1118 &mut out,
1119 );
1120 }
1121 out
1122 }
1123}
1124
1125#[allow(clippy::too_many_arguments)]
1126fn collect_symbol_candidates_from_geometry(
1127 layer_id: LayerId,
1128 query_layer_id: Option<&str>,
1129 query_source_id: Option<&str>,
1130 provenance: Option<&FeatureProvenance>,
1131 feature_index: usize,
1132 point_index: usize,
1133 feature: &Feature,
1134 geometry: &Geometry,
1135 style: &VectorStyle,
1136 out: &mut Vec<SymbolCandidate>,
1137) -> usize {
1138 if style.symbol_placement == SymbolPlacement::Line {
1139 return collect_line_symbol_candidates_from_geometry(
1140 layer_id,
1141 query_layer_id,
1142 query_source_id,
1143 provenance,
1144 feature_index,
1145 point_index,
1146 feature,
1147 geometry,
1148 style,
1149 out,
1150 );
1151 }
1152
1153 match geometry {
1154 Geometry::Point(point) => {
1155 let candidates = symbol_candidates_for_point(
1156 layer_id,
1157 query_layer_id,
1158 query_source_id,
1159 provenance,
1160 feature_index,
1161 point_index,
1162 feature,
1163 point.coord,
1164 style,
1165 );
1166 out.extend(candidates);
1167 point_index + 1
1168 }
1169 Geometry::MultiPoint(points) => {
1170 let mut next = point_index;
1171 for point in &points.points {
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::Point(point.clone()),
1181 style,
1182 out,
1183 );
1184 }
1185 next
1186 }
1187 Geometry::GeometryCollection(geometries) => {
1188 let mut next = point_index;
1189 for geometry in geometries {
1190 next = collect_symbol_candidates_from_geometry(
1191 layer_id,
1192 query_layer_id,
1193 query_source_id,
1194 provenance,
1195 feature_index,
1196 next,
1197 feature,
1198 geometry,
1199 style,
1200 out,
1201 );
1202 }
1203 next
1204 }
1205 _ => point_index,
1206 }
1207}
1208
1209#[allow(clippy::too_many_arguments)]
1210fn collect_line_symbol_candidates_from_geometry(
1211 layer_id: LayerId,
1212 query_layer_id: Option<&str>,
1213 query_source_id: Option<&str>,
1214 provenance: Option<&FeatureProvenance>,
1215 feature_index: usize,
1216 point_index: usize,
1217 feature: &Feature,
1218 geometry: &Geometry,
1219 style: &VectorStyle,
1220 out: &mut Vec<SymbolCandidate>,
1221) -> usize {
1222 match geometry {
1223 Geometry::LineString(line) => {
1224 let mut next = point_index;
1225 let label_length = estimated_line_label_length_meters(feature, style);
1226 for (slot_index, anchor) in line_placement_anchors(line, style, label_length)
1227 .into_iter()
1228 .enumerate()
1229 {
1230 let candidates = symbol_candidates_at_anchor(
1231 layer_id,
1232 query_layer_id,
1233 query_source_id,
1234 provenance,
1235 feature_index,
1236 next,
1237 feature,
1238 anchor.coord,
1239 style,
1240 anchor.rotation_rad,
1241 Some(slot_index),
1242 );
1243 next += candidates.len();
1244 out.extend(candidates);
1245 }
1246 next
1247 }
1248 Geometry::MultiLineString(lines) => {
1249 let mut next = point_index;
1250 for line in &lines.lines {
1251 next = collect_line_symbol_candidates_from_geometry(
1252 layer_id,
1253 query_layer_id,
1254 query_source_id,
1255 provenance,
1256 feature_index,
1257 next,
1258 feature,
1259 &Geometry::LineString(line.clone()),
1260 style,
1261 out,
1262 );
1263 }
1264 next
1265 }
1266 Geometry::GeometryCollection(geometries) => {
1267 let mut next = point_index;
1268 for geometry in geometries {
1269 next = collect_line_symbol_candidates_from_geometry(
1270 layer_id,
1271 query_layer_id,
1272 query_source_id,
1273 provenance,
1274 feature_index,
1275 next,
1276 feature,
1277 geometry,
1278 style,
1279 out,
1280 );
1281 }
1282 next
1283 }
1284 _ => point_index,
1285 }
1286}
1287
1288#[allow(clippy::too_many_arguments)]
1289fn symbol_candidates_for_point(
1290 layer_id: LayerId,
1291 query_layer_id: Option<&str>,
1292 query_source_id: Option<&str>,
1293 provenance: Option<&FeatureProvenance>,
1294 feature_index: usize,
1295 point_index: usize,
1296 feature: &Feature,
1297 anchor: GeoCoord,
1298 style: &VectorStyle,
1299) -> Vec<SymbolCandidate> {
1300 symbol_candidates_at_anchor(
1301 layer_id,
1302 query_layer_id,
1303 query_source_id,
1304 provenance,
1305 feature_index,
1306 point_index,
1307 feature,
1308 anchor,
1309 style,
1310 0.0,
1311 None,
1312 )
1313}
1314
1315#[allow(clippy::too_many_arguments)]
1316fn symbol_candidates_at_anchor(
1317 layer_id: LayerId,
1318 query_layer_id: Option<&str>,
1319 query_source_id: Option<&str>,
1320 provenance: Option<&FeatureProvenance>,
1321 feature_index: usize,
1322 point_index: usize,
1323 feature: &Feature,
1324 anchor: GeoCoord,
1325 style: &VectorStyle,
1326 rotation_rad: f32,
1327 line_slot_index: Option<usize>,
1328) -> Vec<SymbolCandidate> {
1329 let feature_id = feature_id_for_feature(feature, feature_index);
1330 let text = style
1331 .symbol_text_field
1332 .as_deref()
1333 .and_then(|field| feature.property(field))
1334 .and_then(symbol_text_from_property)
1335 .map(|text| transform_symbol_text(text, style.symbol_text_transform));
1336 let icon_image = style.symbol_icon_image.clone();
1337
1338 if text.is_none() && icon_image.is_none() {
1339 return Vec::new();
1340 }
1341
1342 fn transform_symbol_text(text: String, transform: SymbolTextTransform) -> String {
1349 match transform {
1350 SymbolTextTransform::None => text,
1351 SymbolTextTransform::Uppercase => text.to_uppercase(),
1352 SymbolTextTransform::Lowercase => text.to_lowercase(),
1353 }
1354 }
1355
1356 let cross_tile_id = symbol_cross_tile_id(
1357 query_layer_id,
1358 query_source_id,
1359 &feature_id,
1360 text.as_deref(),
1361 icon_image.as_deref(),
1362 anchor,
1363 style.symbol_placement,
1364 line_slot_index,
1365 style,
1366 );
1367
1368 let base_id = format!("{}:{feature_index}:{point_index}", layer_id.as_u64());
1369 let text_present = text.is_some();
1370 let icon_present = icon_image.is_some();
1371 let text_optional = style.symbol_text_optional;
1372 let icon_optional = style.symbol_icon_optional;
1373
1374 let mut variants = Vec::new();
1375 let base_candidate = make_symbol_candidate(
1376 &base_id,
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 text.clone(),
1386 icon_image.clone(),
1387 style,
1388 cross_tile_id.clone(),
1389 rotation_rad,
1390 );
1391 variants.push(base_candidate);
1392
1393 if text_present && icon_present {
1398 if text_optional {
1399 variants.push(make_symbol_candidate(
1400 &format!("{base_id}:icon-only"),
1401 &base_id,
1402 query_layer_id,
1403 query_source_id,
1404 provenance,
1405 &feature_id,
1406 feature_index,
1407 style.symbol_placement,
1408 anchor,
1409 None,
1410 icon_image.clone(),
1411 style,
1412 cross_tile_id.clone(),
1413 rotation_rad,
1414 ));
1415 }
1416 if icon_optional {
1417 variants.push(make_symbol_candidate(
1418 &format!("{base_id}:text-only"),
1419 &base_id,
1420 query_layer_id,
1421 query_source_id,
1422 provenance,
1423 &feature_id,
1424 feature_index,
1425 style.symbol_placement,
1426 anchor,
1427 text,
1428 None,
1429 style,
1430 cross_tile_id,
1431 rotation_rad,
1432 ));
1433 }
1434 }
1435
1436 variants
1437}
1438
1439#[allow(clippy::too_many_arguments)]
1440fn make_symbol_candidate(
1441 id: &str,
1442 placement_group_id: &str,
1443 query_layer_id: Option<&str>,
1444 query_source_id: Option<&str>,
1445 provenance: Option<&FeatureProvenance>,
1446 feature_id: &str,
1447 feature_index: usize,
1448 placement: SymbolPlacement,
1449 anchor: GeoCoord,
1450 text: Option<String>,
1451 icon_image: Option<String>,
1452 style: &VectorStyle,
1453 cross_tile_id: String,
1454 rotation_rad: f32,
1455) -> SymbolCandidate {
1456 let has_text = text.is_some();
1457 let has_icon = icon_image.is_some();
1458
1459 SymbolCandidate {
1460 id: id.to_owned(),
1461 layer_id: query_layer_id.map(ToOwned::to_owned),
1462 source_id: query_source_id.map(ToOwned::to_owned),
1463 source_layer: provenance.and_then(|p| p.source_layer.clone()),
1464 source_tile: provenance.and_then(|p| p.source_tile),
1465 feature_id: feature_id.to_owned(),
1466 feature_index,
1467 placement_group_id: placement_group_id.to_owned(),
1468 placement,
1469 anchor,
1470 text,
1471 icon_image,
1472 font_stack: style.symbol_font_stack.clone(),
1473 cross_tile_id,
1474 rotation_rad,
1475 size_px: style.symbol_size,
1476 padding_px: style.symbol_padding,
1477 allow_overlap: effective_symbol_overlap(style, has_text, has_icon),
1478 ignore_placement: effective_symbol_ignore_placement(style, has_text, has_icon),
1479 sort_key: style.symbol_sort_key,
1480 radial_offset: style.symbol_text_radial_offset,
1481 variable_anchor_offsets: style.symbol_variable_anchor_offsets.clone(),
1482 text_max_width: style.symbol_text_max_width,
1483 text_line_height: style.symbol_text_line_height,
1484 text_letter_spacing: style.symbol_text_letter_spacing,
1485 icon_text_fit: style.symbol_icon_text_fit,
1486 icon_text_fit_padding: style.symbol_icon_text_fit_padding,
1487 anchors: if style.symbol_variable_anchor_offsets.is_some() {
1488 style
1489 .symbol_variable_anchor_offsets
1490 .as_ref()
1491 .map(|offsets| offsets.iter().map(|(anchor, _)| *anchor).collect())
1492 .unwrap_or_default()
1493 } else {
1494 style.symbol_anchors.clone()
1495 },
1496 writing_mode: style.symbol_writing_mode,
1497 offset_px: style.symbol_offset,
1498 fill_color: style.fill_color,
1499 halo_color: style.symbol_halo_color,
1500 }
1501}
1502
1503fn effective_symbol_ignore_placement(style: &VectorStyle, has_text: bool, has_icon: bool) -> bool {
1512 match (has_text, has_icon) {
1513 (true, true) => style.symbol_text_ignore_placement && style.symbol_icon_ignore_placement,
1514 (true, false) => style.symbol_text_ignore_placement,
1515 (false, true) => style.symbol_icon_ignore_placement,
1516 (false, false) => false,
1517 }
1518}
1519
1520fn effective_symbol_overlap(style: &VectorStyle, has_text: bool, has_icon: bool) -> bool {
1529 match (has_text, has_icon) {
1530 (true, true) => style.symbol_text_allow_overlap && style.symbol_icon_allow_overlap,
1531 (true, false) => style.symbol_text_allow_overlap,
1532 (false, true) => style.symbol_icon_allow_overlap,
1533 (false, false) => style.symbol_allow_overlap,
1534 }
1535}
1536
1537#[allow(clippy::too_many_arguments)]
1549fn symbol_cross_tile_id(
1550 query_layer_id: Option<&str>,
1551 query_source_id: Option<&str>,
1552 feature_id: &str,
1553 text: Option<&str>,
1554 icon_image: Option<&str>,
1555 anchor: GeoCoord,
1556 placement: SymbolPlacement,
1557 line_slot_index: Option<usize>,
1558 style: &VectorStyle,
1559) -> String {
1560 match placement {
1561 SymbolPlacement::Point => format!(
1562 "{}|{}|{:.6}|{:.6}",
1563 text.unwrap_or(""),
1564 icon_image.unwrap_or(""),
1565 anchor.lat,
1566 anchor.lon,
1567 ),
1568 SymbolPlacement::Line => {
1569 let slot = line_slot_index.unwrap_or(0);
1570 let world = CameraProjection::WebMercator.project(&anchor);
1571 let coarse_bucket = ((style.symbol_spacing.max(style.symbol_size).max(1.0) as f64)
1578 * METERS_PER_PIXEL_APPROX
1579 * 2.0)
1580 .max(1.0);
1581 let bucket_x = (world.position.x / coarse_bucket).round() as i64;
1582 let bucket_y = (world.position.y / coarse_bucket).round() as i64;
1583 format!(
1584 "line|{}|{}|{}|{}|{}|{}|{}|{}",
1585 query_source_id.unwrap_or(""),
1586 query_layer_id.unwrap_or(""),
1587 feature_id,
1588 slot,
1589 bucket_x,
1590 bucket_y,
1591 text.unwrap_or(""),
1592 icon_image.unwrap_or(""),
1593 )
1594 }
1595 }
1596}
1597
1598fn line_placement_anchors(
1607 line: &crate::geometry::LineString,
1608 style: &VectorStyle,
1609 label_length: f64,
1610) -> Vec<LinePlacementAnchor> {
1611 if line.coords.len() < 2 {
1612 return Vec::new();
1613 }
1614
1615 let projected: Vec<_> = line
1616 .coords
1617 .iter()
1618 .map(|coord| CameraProjection::WebMercator.project(coord))
1619 .collect();
1620 let total_length: f64 = projected
1621 .windows(2)
1622 .map(|segment| {
1623 let a = segment[0].position;
1624 let b = segment[1].position;
1625 let dx = b.x - a.x;
1626 let dy = b.y - a.y;
1627 let dz = b.z - a.z;
1628 (dx * dx + dy * dy + dz * dz).sqrt()
1629 })
1630 .sum();
1631 if total_length <= f64::EPSILON {
1632 return line
1633 .coords
1634 .first()
1635 .copied()
1636 .map(|coord| LinePlacementAnchor {
1637 coord,
1638 rotation_rad: 0.0,
1639 distance: total_length * 0.5,
1640 })
1641 .into_iter()
1642 .collect();
1643 }
1644
1645 let spacing =
1650 (style.symbol_spacing.max(style.symbol_size).max(1.0) as f64) * METERS_PER_PIXEL_APPROX;
1651 if spacing <= f64::EPSILON || total_length <= spacing {
1652 return interpolate_line_anchor_at_distance(
1653 line,
1654 &projected,
1655 total_length * 0.5,
1656 style.symbol_keep_upright,
1657 )
1658 .filter(|anchor| {
1659 line_anchor_passes_max_angle(&projected, anchor.distance, label_length, style)
1660 })
1661 .into_iter()
1662 .collect();
1663 }
1664
1665 let mut anchors = Vec::new();
1666 let mut target = spacing * 0.5;
1667 while target < total_length {
1668 if let Some(anchor) =
1669 interpolate_line_anchor_at_distance(line, &projected, target, style.symbol_keep_upright)
1670 {
1671 if line_anchor_passes_max_angle(&projected, anchor.distance, label_length, style) {
1672 anchors.push(anchor);
1673 }
1674 }
1675 target += spacing;
1676 }
1677
1678 if anchors.is_empty() {
1679 interpolate_line_anchor_at_distance(
1680 line,
1681 &projected,
1682 total_length * 0.5,
1683 style.symbol_keep_upright,
1684 )
1685 .filter(|anchor| {
1686 line_anchor_passes_max_angle(&projected, anchor.distance, label_length, style)
1687 })
1688 .into_iter()
1689 .collect()
1690 } else {
1691 anchors
1692 }
1693}
1694
1695fn estimated_line_label_length_meters(feature: &Feature, style: &VectorStyle) -> f64 {
1702 let text = style
1703 .symbol_text_field
1704 .as_deref()
1705 .and_then(|field| feature.property(field))
1706 .and_then(symbol_text_from_property);
1707 let icon = style.symbol_icon_image.as_deref();
1708 let size_px = style.symbol_size.max(1.0) as f64;
1709 let text_width_px = text
1710 .as_deref()
1711 .map(|value| value.chars().count() as f64 * size_px * 0.6)
1712 .unwrap_or(0.0);
1713 let icon_width_px = if icon.is_some() { size_px * 1.2 } else { 0.0 };
1714 (text_width_px.max(size_px) + icon_width_px + style.symbol_padding.max(0.0) as f64 * 2.0)
1715 * METERS_PER_PIXEL_APPROX
1716}
1717
1718fn line_anchor_passes_max_angle(
1725 projected: &[WorldCoord],
1726 anchor_distance: f64,
1727 label_length: f64,
1728 style: &VectorStyle,
1729) -> bool {
1730 if projected.len() < 3 {
1731 return true;
1732 }
1733 let half_label = label_length * 0.5;
1734 if anchor_distance - half_label < 0.0
1735 || anchor_distance + half_label > line_total_length(projected)
1736 {
1737 return false;
1738 }
1739
1740 let max_angle = style.symbol_max_angle.max(0.0) as f64 * std::f64::consts::PI / 180.0;
1741 if max_angle >= std::f64::consts::PI {
1742 return true;
1743 }
1744
1745 let start = anchor_distance - half_label;
1746 let end = anchor_distance + half_label;
1747 let mut distance = 0.0;
1748 let mut accumulated_turn = 0.0;
1749
1750 for index in 1..projected.len() - 1 {
1751 let prev = projected[index - 1].position;
1752 let current = projected[index].position;
1753 let next = projected[index + 1].position;
1754 let segment_dx = current.x - prev.x;
1755 let segment_dy = current.y - prev.y;
1756 let segment_dz = current.z - prev.z;
1757 distance +=
1758 (segment_dx * segment_dx + segment_dy * segment_dy + segment_dz * segment_dz).sqrt();
1759 if distance < start || distance > end {
1760 continue;
1761 }
1762
1763 let prev_angle = (current.y - prev.y).atan2(current.x - prev.x);
1764 let next_angle = (next.y - current.y).atan2(next.x - current.x);
1765 accumulated_turn += normalize_angle_delta(next_angle - prev_angle).abs();
1766 if accumulated_turn > max_angle {
1767 return false;
1768 }
1769 }
1770
1771 true
1772}
1773
1774fn normalize_angle_delta(angle: f64) -> f64 {
1775 ((angle + std::f64::consts::PI * 3.0) % (std::f64::consts::PI * 2.0)) - std::f64::consts::PI
1776}
1777
1778fn line_total_length(projected: &[WorldCoord]) -> f64 {
1779 projected
1780 .windows(2)
1781 .map(|segment| {
1782 let a = segment[0].position;
1783 let b = segment[1].position;
1784 let dx = b.x - a.x;
1785 let dy = b.y - a.y;
1786 let dz = b.z - a.z;
1787 (dx * dx + dy * dy + dz * dz).sqrt()
1788 })
1789 .sum()
1790}
1791
1792fn interpolate_line_anchor_at_distance(
1793 line: &crate::geometry::LineString,
1794 projected: &[WorldCoord],
1795 target: f64,
1796 keep_upright: bool,
1797) -> Option<LinePlacementAnchor> {
1798 let mut traversed = 0.0;
1799 for (coords, segment) in line.coords.windows(2).zip(projected.windows(2)) {
1800 let a = segment[0].position;
1801 let b = segment[1].position;
1802 let dx = b.x - a.x;
1803 let dy = b.y - a.y;
1804 let dz = b.z - a.z;
1805 let segment_length = (dx * dx + dy * dy + dz * dz).sqrt();
1806 if segment_length <= f64::EPSILON {
1807 continue;
1808 }
1809 if traversed + segment_length >= target {
1810 let t = ((target - traversed) / segment_length).clamp(0.0, 1.0);
1811 let start = coords[0];
1812 let end = coords[1];
1813 let coord = GeoCoord::new(
1814 start.lat + (end.lat - start.lat) * t,
1815 start.lon + (end.lon - start.lon) * t,
1816 start.alt + (end.alt - start.alt) * t,
1817 );
1818 let rotation_rad =
1821 normalize_line_label_rotation((b.y - a.y).atan2(b.x - a.x) as f32, keep_upright);
1822 return Some(LinePlacementAnchor {
1823 coord,
1824 rotation_rad,
1825 distance: target,
1826 });
1827 }
1828 traversed += segment_length;
1829 }
1830
1831 line.coords
1832 .last()
1833 .copied()
1834 .map(|coord| LinePlacementAnchor {
1835 coord,
1836 rotation_rad: 0.0,
1837 distance: target,
1838 })
1839}
1840
1841fn normalize_line_label_rotation(rotation_rad: f32, keep_upright: bool) -> f32 {
1848 if !keep_upright {
1849 return rotation_rad;
1850 }
1851
1852 let mut normalized = rotation_rad;
1853 while normalized > std::f32::consts::PI {
1854 normalized -= std::f32::consts::TAU;
1855 }
1856 while normalized < -std::f32::consts::PI {
1857 normalized += std::f32::consts::TAU;
1858 }
1859 if normalized > std::f32::consts::FRAC_PI_2 {
1860 normalized -= std::f32::consts::PI;
1861 } else if normalized < -std::f32::consts::FRAC_PI_2 {
1862 normalized += std::f32::consts::PI;
1863 }
1864 normalized
1865}
1866
1867fn symbol_text_from_property(value: &crate::geometry::PropertyValue) -> Option<String> {
1868 match value {
1869 crate::geometry::PropertyValue::Null => None,
1870 crate::geometry::PropertyValue::Bool(value) => Some(value.to_string()),
1871 crate::geometry::PropertyValue::Number(value) => Some(value.to_string()),
1872 crate::geometry::PropertyValue::String(value) => Some(value.clone()),
1873 }
1874}
1875
1876fn tessellate_geometry(
1885 geometry: &Geometry,
1886 style: &VectorStyle,
1887 projection: CameraProjection,
1888 half_width: f64,
1889 mesh: &mut VectorMeshData,
1890) {
1891 match style.render_mode {
1892 VectorRenderMode::Generic => {
1893 tessellate_generic_geometry(geometry, style, projection, half_width, mesh)
1894 }
1895 VectorRenderMode::Fill => tessellate_fill_geometry(geometry, style, projection, mesh),
1896 VectorRenderMode::Line => {
1897 tessellate_line_geometry(geometry, style, projection, half_width, mesh)
1898 }
1899 VectorRenderMode::Circle => tessellate_circle_geometry(geometry, style, projection, mesh),
1900 VectorRenderMode::Heatmap => tessellate_heatmap_geometry(geometry, style, projection, mesh),
1901 VectorRenderMode::FillExtrusion => {
1902 tessellate_fill_extrusion_geometry(geometry, style, projection, mesh)
1903 }
1904 VectorRenderMode::Symbol => tessellate_symbol_geometry(geometry, style, projection, mesh),
1905 }
1906}
1907
1908fn tessellate_generic_geometry(
1909 geometry: &Geometry,
1910 style: &VectorStyle,
1911 projection: CameraProjection,
1912 half_width: f64,
1913 mesh: &mut VectorMeshData,
1914) {
1915 match geometry {
1916 Geometry::Point(p) => append_square_marker(
1917 mesh,
1918 &p.coord,
1919 projection,
1920 half_width * METERS_PER_DEGREE,
1921 style.fill_color,
1922 ),
1923 Geometry::LineString(ls) => append_stroked_line(
1924 mesh,
1925 &ls.coords,
1926 projection,
1927 half_width,
1928 style.stroke_color,
1929 style,
1930 ),
1931 Geometry::Polygon(poly) => {
1932 append_polygon_fill(mesh, &poly.exterior, projection, style.fill_color, None)
1933 }
1934 Geometry::MultiPoint(mp) => {
1935 for p in &mp.points {
1936 tessellate_generic_geometry(
1937 &Geometry::Point(p.clone()),
1938 style,
1939 projection,
1940 half_width,
1941 mesh,
1942 );
1943 }
1944 }
1945 Geometry::MultiLineString(mls) => {
1946 for ls in &mls.lines {
1947 tessellate_generic_geometry(
1948 &Geometry::LineString(ls.clone()),
1949 style,
1950 projection,
1951 half_width,
1952 mesh,
1953 );
1954 }
1955 }
1956 Geometry::MultiPolygon(mpoly) => {
1957 for poly in &mpoly.polygons {
1958 tessellate_generic_geometry(
1959 &Geometry::Polygon(poly.clone()),
1960 style,
1961 projection,
1962 half_width,
1963 mesh,
1964 );
1965 }
1966 }
1967 Geometry::GeometryCollection(geoms) => {
1968 for g in geoms {
1969 tessellate_generic_geometry(g, style, projection, half_width, mesh);
1970 }
1971 }
1972 }
1973}
1974
1975fn tessellate_fill_geometry(
1976 geometry: &Geometry,
1977 style: &VectorStyle,
1978 projection: CameraProjection,
1979 mesh: &mut VectorMeshData,
1980) {
1981 if style.fill_pattern.is_some() && mesh.fill_pattern.is_none() {
1983 mesh.fill_pattern = style.fill_pattern.clone();
1984 }
1985
1986 match geometry {
1987 Geometry::Polygon(poly) => {
1988 append_polygon_fill(
1989 mesh,
1990 &poly.exterior,
1991 projection,
1992 style.fill_color,
1993 style.fill_pattern.as_deref(),
1994 );
1995 if style.stroke_width > 0.0 {
1996 append_stroked_line(
1997 mesh,
1998 &poly.exterior,
1999 projection,
2000 style.stroke_width as f64 * DEGREES_PER_PIXEL_APPROX,
2001 style.stroke_color,
2002 style,
2003 );
2004 for hole in &poly.interiors {
2005 append_stroked_line(
2006 mesh,
2007 hole,
2008 projection,
2009 style.stroke_width as f64 * DEGREES_PER_PIXEL_APPROX,
2010 style.stroke_color,
2011 style,
2012 );
2013 }
2014 }
2015 }
2016 Geometry::MultiPolygon(mpoly) => {
2017 for poly in &mpoly.polygons {
2018 tessellate_fill_geometry(&Geometry::Polygon(poly.clone()), style, projection, mesh);
2019 }
2020 }
2021 Geometry::GeometryCollection(geoms) => {
2022 for g in geoms {
2023 tessellate_fill_geometry(g, style, projection, mesh);
2024 }
2025 }
2026 _ => {}
2027 }
2028}
2029
2030fn tessellate_line_geometry(
2031 geometry: &Geometry,
2032 style: &VectorStyle,
2033 projection: CameraProjection,
2034 half_width: f64,
2035 mesh: &mut VectorMeshData,
2036) {
2037 if style.line_pattern.is_some() && mesh.line_pattern.is_none() {
2039 mesh.line_pattern = style.line_pattern.clone();
2040 }
2041
2042 match geometry {
2043 Geometry::LineString(ls) => append_stroked_line(
2044 mesh,
2045 &ls.coords,
2046 projection,
2047 half_width,
2048 style.stroke_color,
2049 style,
2050 ),
2051 Geometry::Polygon(poly) => {
2052 append_stroked_line(
2053 mesh,
2054 &poly.exterior,
2055 projection,
2056 half_width,
2057 style.stroke_color,
2058 style,
2059 );
2060 for hole in &poly.interiors {
2061 append_stroked_line(
2062 mesh,
2063 hole,
2064 projection,
2065 half_width,
2066 style.stroke_color,
2067 style,
2068 );
2069 }
2070 }
2071 Geometry::MultiLineString(mls) => {
2072 for ls in &mls.lines {
2073 tessellate_line_geometry(
2074 &Geometry::LineString(ls.clone()),
2075 style,
2076 projection,
2077 half_width,
2078 mesh,
2079 );
2080 }
2081 }
2082 Geometry::MultiPolygon(mpoly) => {
2083 for poly in &mpoly.polygons {
2084 tessellate_line_geometry(
2085 &Geometry::Polygon(poly.clone()),
2086 style,
2087 projection,
2088 half_width,
2089 mesh,
2090 );
2091 }
2092 }
2093 Geometry::GeometryCollection(geoms) => {
2094 for g in geoms {
2095 tessellate_line_geometry(g, style, projection, half_width, mesh);
2096 }
2097 }
2098 _ => {}
2099 }
2100}
2101
2102fn tessellate_circle_geometry(
2103 geometry: &Geometry,
2104 style: &VectorStyle,
2105 projection: CameraProjection,
2106 mesh: &mut VectorMeshData,
2107) {
2108 match geometry {
2109 Geometry::Point(p) => {
2110 let radius = style.point_radius as f64 * DEGREES_PER_PIXEL_APPROX * METERS_PER_DEGREE;
2111 let stroke_w =
2112 style.stroke_width.max(0.0) as f64 * DEGREES_PER_PIXEL_APPROX * METERS_PER_DEGREE;
2113 append_circle(
2114 mesh,
2115 &p.coord,
2116 projection,
2117 radius,
2118 style.fill_color,
2119 Some((style.stroke_color, stroke_w)),
2120 );
2121
2122 let w = projection.project(&p.coord);
2126 mesh.circle_instances.push(CircleInstanceData {
2127 center: [w.position.x, w.position.y, w.position.z],
2128 radius: radius as f32,
2129 color: style.fill_color,
2130 stroke_color: style.stroke_color,
2131 stroke_width: stroke_w as f32,
2132 blur: 0.0,
2133 });
2134 }
2135 Geometry::MultiPoint(mp) => {
2136 for p in &mp.points {
2137 tessellate_circle_geometry(&Geometry::Point(p.clone()), style, projection, mesh);
2138 }
2139 }
2140 Geometry::GeometryCollection(geoms) => {
2141 for g in geoms {
2142 tessellate_circle_geometry(g, style, projection, mesh);
2143 }
2144 }
2145 _ => {}
2146 }
2147}
2148
2149fn tessellate_heatmap_geometry(
2150 geometry: &Geometry,
2151 style: &VectorStyle,
2152 projection: CameraProjection,
2153 mesh: &mut VectorMeshData,
2154) {
2155 match geometry {
2156 Geometry::Point(p) => {
2157 let radius =
2158 style.heatmap_radius.max(0.0) as f64 * DEGREES_PER_PIXEL_APPROX * METERS_PER_DEGREE;
2159 append_heat_blob(
2160 mesh,
2161 &p.coord,
2162 projection,
2163 radius,
2164 style.fill_color,
2165 style.heatmap_intensity.max(0.0),
2166 );
2167
2168 let w = projection.project(&p.coord);
2171 let weight = style.heatmap_intensity.max(0.0) as f64;
2172 mesh.heatmap_points
2173 .push([w.position.x, w.position.y, weight, radius]);
2174 }
2175 Geometry::MultiPoint(mp) => {
2176 for p in &mp.points {
2177 tessellate_heatmap_geometry(&Geometry::Point(p.clone()), style, projection, mesh);
2178 }
2179 }
2180 Geometry::GeometryCollection(geoms) => {
2181 for g in geoms {
2182 tessellate_heatmap_geometry(g, style, projection, mesh);
2183 }
2184 }
2185 _ => {}
2186 }
2187}
2188
2189fn tessellate_fill_extrusion_geometry(
2190 geometry: &Geometry,
2191 style: &VectorStyle,
2192 projection: CameraProjection,
2193 mesh: &mut VectorMeshData,
2194) {
2195 match geometry {
2196 Geometry::Polygon(poly) => append_extruded_polygon(mesh, &poly.exterior, projection, style),
2197 Geometry::MultiPolygon(mpoly) => {
2198 for poly in &mpoly.polygons {
2199 append_extruded_polygon(mesh, &poly.exterior, projection, style);
2200 }
2201 }
2202 Geometry::GeometryCollection(geoms) => {
2203 for g in geoms {
2204 tessellate_fill_extrusion_geometry(g, style, projection, mesh);
2205 }
2206 }
2207 _ => {}
2208 }
2209}
2210
2211fn tessellate_symbol_geometry(
2212 geometry: &Geometry,
2213 style: &VectorStyle,
2214 projection: CameraProjection,
2215 mesh: &mut VectorMeshData,
2216) {
2217 match geometry {
2218 Geometry::Point(p) => {
2219 let size =
2220 style.symbol_size.max(0.0) as f64 * DEGREES_PER_PIXEL_APPROX * METERS_PER_DEGREE;
2221 append_square_marker(
2222 mesh,
2223 &p.coord,
2224 projection,
2225 size * 1.35,
2226 style.symbol_halo_color,
2227 );
2228 append_square_marker(mesh, &p.coord, projection, size, style.fill_color);
2229 }
2230 Geometry::MultiPoint(mp) => {
2231 for p in &mp.points {
2232 tessellate_symbol_geometry(&Geometry::Point(p.clone()), style, projection, mesh);
2233 }
2234 }
2235 Geometry::GeometryCollection(geoms) => {
2236 for g in geoms {
2237 tessellate_symbol_geometry(g, style, projection, mesh);
2238 }
2239 }
2240 _ => {}
2241 }
2242}
2243
2244fn append_polygon_fill(
2245 mesh: &mut VectorMeshData,
2246 coords: &[GeoCoord],
2247 projection: CameraProjection,
2248 color: [f32; 4],
2249 pattern: Option<&PatternImage>,
2250) {
2251 let ring = normalized_ring(coords);
2252 if ring.len() < 3 {
2253 return;
2254 }
2255 let indices = tessellator::triangulate_polygon(&ring);
2256 let base = mesh.positions.len() as u32;
2257 for coord in &ring {
2258 let w = projection.project(coord);
2259 mesh.positions
2260 .push([w.position.x, w.position.y, w.position.z]);
2261 mesh.colors.push(color);
2262
2263 if let Some(pat) = pattern {
2267 let u = w.position.x as f32 / pat.width.max(1) as f32;
2268 let v = w.position.y as f32 / pat.height.max(1) as f32;
2269 mesh.fill_pattern_uvs.push([u, v]);
2270 }
2271 }
2272 for idx in indices {
2273 mesh.indices.push(base + idx);
2274 }
2275}
2276
2277fn append_stroked_line(
2278 mesh: &mut VectorMeshData,
2279 coords: &[GeoCoord],
2280 projection: CameraProjection,
2281 half_width: f64,
2282 color: [f32; 4],
2283 style: &VectorStyle,
2284) {
2285 let result = tessellator::stroke_line_styled(
2286 coords,
2287 half_width,
2288 style.line_cap,
2289 style.line_join,
2290 style.miter_limit,
2291 );
2292 if result.positions.is_empty() {
2293 return;
2294 }
2295
2296 let gradient_max_dist = if style.line_gradient.is_some() {
2298 result
2299 .distances
2300 .iter()
2301 .cloned()
2302 .fold(0.0_f64, f64::max)
2303 .max(f64::EPSILON)
2304 } else {
2305 1.0
2306 };
2307
2308 let has_pattern = style.line_pattern.is_some();
2313 let pat_width = style
2314 .line_pattern
2315 .as_ref()
2316 .map_or(1.0_f32, |p| p.width.max(1) as f32);
2317 let mut body_side = false; let base = mesh.positions.len() as u32;
2320 for (i, pos) in result.positions.iter().enumerate() {
2321 let coord = GeoCoord::from_lat_lon(pos[1], pos[0]);
2322 let w = projection.project(&coord);
2323 mesh.positions
2324 .push([w.position.x, w.position.y, w.position.z]);
2325
2326 let vertex_color = if let Some(ref ramp) = style.line_gradient {
2329 let t = (result.distances[i] / gradient_max_dist) as f32;
2330 ramp.evaluate(t)
2331 } else {
2332 color
2333 };
2334 mesh.colors.push(vertex_color);
2335
2336 mesh.line_normals
2337 .push([result.normals[i][0] as f32, result.normals[i][1] as f32]);
2338 let dist_meters = (result.distances[i] * METERS_PER_DEGREE) as f32;
2340 mesh.line_distances.push(dist_meters);
2341 mesh.line_cap_joins.push(result.cap_join[i]);
2342
2343 if has_pattern {
2347 let u = dist_meters / pat_width;
2348 let v = if result.cap_join[i] > 0.5 {
2349 0.5
2351 } else {
2352 let v = if body_side { 1.0 } else { 0.0 };
2354 body_side = !body_side;
2355 v
2356 };
2357 mesh.line_pattern_uvs.push([u, v]);
2358 }
2359 }
2360 for idx in &result.indices {
2361 mesh.indices.push(base + idx);
2362 }
2363}
2364
2365fn append_square_marker(
2366 mesh: &mut VectorMeshData,
2367 coord: &GeoCoord,
2368 projection: CameraProjection,
2369 half_size: f64,
2370 color: [f32; 4],
2371) {
2372 let w = projection.project(coord);
2373 let base = mesh.positions.len() as u32;
2374 mesh.positions.push([
2375 w.position.x - half_size,
2376 w.position.y - half_size,
2377 w.position.z,
2378 ]);
2379 mesh.positions.push([
2380 w.position.x + half_size,
2381 w.position.y - half_size,
2382 w.position.z,
2383 ]);
2384 mesh.positions.push([
2385 w.position.x + half_size,
2386 w.position.y + half_size,
2387 w.position.z,
2388 ]);
2389 mesh.positions.push([
2390 w.position.x - half_size,
2391 w.position.y + half_size,
2392 w.position.z,
2393 ]);
2394 for _ in 0..4 {
2395 mesh.colors.push(color);
2396 }
2397 mesh.indices
2398 .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
2399}
2400
2401fn append_circle(
2402 mesh: &mut VectorMeshData,
2403 coord: &GeoCoord,
2404 projection: CameraProjection,
2405 radius: f64,
2406 fill_color: [f32; 4],
2407 outline: Option<([f32; 4], f64)>,
2408) {
2409 append_radial_fan(mesh, coord, projection, radius, fill_color, fill_color);
2410 if let Some((outline_color, outline_width)) = outline {
2411 if outline_width > 0.0 {
2412 let outer = radius + outline_width;
2413 append_ring(mesh, coord, projection, radius, outer, outline_color);
2414 }
2415 }
2416}
2417
2418fn append_heat_blob(
2419 mesh: &mut VectorMeshData,
2420 coord: &GeoCoord,
2421 projection: CameraProjection,
2422 radius: f64,
2423 color: [f32; 4],
2424 intensity: f32,
2425) {
2426 let mut center_color = color;
2427 center_color[3] = (center_color[3] * intensity).clamp(0.0, 1.0);
2428 let mut edge_color = color;
2429 edge_color[3] = 0.0;
2430 append_radial_fan(mesh, coord, projection, radius, center_color, edge_color);
2431}
2432
2433fn append_radial_fan(
2434 mesh: &mut VectorMeshData,
2435 coord: &GeoCoord,
2436 projection: CameraProjection,
2437 radius: f64,
2438 center_color: [f32; 4],
2439 edge_color: [f32; 4],
2440) {
2441 let center = projection.project(coord);
2442 let base = mesh.positions.len() as u32;
2443 mesh.positions
2444 .push([center.position.x, center.position.y, center.position.z]);
2445 mesh.colors.push(center_color);
2446 for i in 0..=DEFAULT_CIRCLE_SEGMENTS {
2447 let t = i as f64 / DEFAULT_CIRCLE_SEGMENTS as f64;
2448 let angle = std::f64::consts::TAU * t;
2449 mesh.positions.push([
2450 center.position.x + radius * angle.cos(),
2451 center.position.y + radius * angle.sin(),
2452 center.position.z,
2453 ]);
2454 mesh.colors.push(edge_color);
2455 }
2456 for i in 1..=DEFAULT_CIRCLE_SEGMENTS as u32 {
2457 mesh.indices
2458 .extend_from_slice(&[base, base + i, base + i + 1]);
2459 }
2460}
2461
2462fn append_ring(
2463 mesh: &mut VectorMeshData,
2464 coord: &GeoCoord,
2465 projection: CameraProjection,
2466 inner_radius: f64,
2467 outer_radius: f64,
2468 color: [f32; 4],
2469) {
2470 if outer_radius <= inner_radius {
2471 return;
2472 }
2473 let center = projection.project(coord);
2474 let base = mesh.positions.len() as u32;
2475 for i in 0..=DEFAULT_CIRCLE_SEGMENTS {
2476 let t = i as f64 / DEFAULT_CIRCLE_SEGMENTS as f64;
2477 let angle = std::f64::consts::TAU * t;
2478 let (sin, cos) = angle.sin_cos();
2479 mesh.positions.push([
2480 center.position.x + inner_radius * cos,
2481 center.position.y + inner_radius * sin,
2482 center.position.z,
2483 ]);
2484 mesh.colors.push(color);
2485 mesh.positions.push([
2486 center.position.x + outer_radius * cos,
2487 center.position.y + outer_radius * sin,
2488 center.position.z,
2489 ]);
2490 mesh.colors.push(color);
2491 }
2492 for i in 0..DEFAULT_CIRCLE_SEGMENTS as u32 {
2493 let a = base + i * 2;
2494 mesh.indices
2495 .extend_from_slice(&[a, a + 1, a + 2, a + 1, a + 3, a + 2]);
2496 }
2497}
2498
2499fn append_extruded_polygon(
2500 mesh: &mut VectorMeshData,
2501 coords: &[GeoCoord],
2502 projection: CameraProjection,
2503 style: &VectorStyle,
2504) {
2505 let ring = normalized_ring(coords);
2506 if ring.len() < 3 {
2507 return;
2508 }
2509
2510 let top_base = mesh.positions.len() as u32;
2512 for coord in &ring {
2513 let w = projection.project(coord);
2514 mesh.positions.push([
2515 w.position.x,
2516 w.position.y,
2517 coord.alt + style.extrusion_base as f64 + style.extrusion_height as f64,
2518 ]);
2519 mesh.colors.push(style.fill_color);
2520 mesh.normals.push([0.0, 0.0, 1.0]); }
2522 for idx in tessellator::triangulate_polygon(&ring) {
2523 mesh.indices.push(top_base + idx);
2524 }
2525
2526 let side_color = [
2528 style.fill_color[0] * 0.75,
2529 style.fill_color[1] * 0.75,
2530 style.fill_color[2] * 0.75,
2531 style.fill_color[3],
2532 ];
2533
2534 for i in 0..ring.len() {
2535 let a = &ring[i];
2536 let b = &ring[(i + 1) % ring.len()];
2537 let wa = projection.project(a);
2538 let wb = projection.project(b);
2539 let base_z_a = a.alt + style.extrusion_base as f64;
2540 let base_z_b = b.alt + style.extrusion_base as f64;
2541 let top_z_a = base_z_a + style.extrusion_height as f64;
2542 let top_z_b = base_z_b + style.extrusion_height as f64;
2543
2544 let dx = (wb.position.x - wa.position.x) as f32;
2547 let dy = (wb.position.y - wa.position.y) as f32;
2548 let len = (dx * dx + dy * dy).sqrt().max(1e-12);
2549 let normal = [dy / len, -dx / len, 0.0];
2550
2551 let base = mesh.positions.len() as u32;
2552 mesh.positions
2553 .push([wa.position.x, wa.position.y, base_z_a]);
2554 mesh.positions
2555 .push([wb.position.x, wb.position.y, base_z_b]);
2556 mesh.positions.push([wb.position.x, wb.position.y, top_z_b]);
2557 mesh.positions.push([wa.position.x, wa.position.y, top_z_a]);
2558 for _ in 0..4 {
2559 mesh.colors.push(side_color);
2560 mesh.normals.push(normal);
2561 }
2562 mesh.indices
2563 .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
2564 }
2565}
2566
2567fn normalized_ring(coords: &[GeoCoord]) -> Vec<GeoCoord> {
2568 if coords.len() > 1 {
2569 let first = coords.first().expect("ring first");
2570 let last = coords.last().expect("ring last");
2571 if (first.lat - last.lat).abs() < 1e-12
2572 && (first.lon - last.lon).abs() < 1e-12
2573 && (first.alt - last.alt).abs() < 1e-6
2574 {
2575 return coords[..coords.len() - 1].to_vec();
2576 }
2577 }
2578 coords.to_vec()
2579}
2580
2581fn drape_geometry(geometry: &mut Geometry, terrain: &TerrainManager) {
2587 match geometry {
2588 Geometry::Point(p) => {
2589 if let Some(elev) = terrain.elevation_at(&p.coord) {
2590 p.coord.alt = elev;
2591 }
2592 }
2593 Geometry::LineString(ls) => {
2594 drape_coords(&mut ls.coords, terrain);
2595 }
2596 Geometry::Polygon(poly) => {
2597 drape_coords(&mut poly.exterior, terrain);
2598 for hole in &mut poly.interiors {
2599 drape_coords(hole, terrain);
2600 }
2601 }
2602 Geometry::MultiPoint(mp) => {
2603 for p in &mut mp.points {
2604 if let Some(elev) = terrain.elevation_at(&p.coord) {
2605 p.coord.alt = elev;
2606 }
2607 }
2608 }
2609 Geometry::MultiLineString(mls) => {
2610 for ls in &mut mls.lines {
2611 drape_coords(&mut ls.coords, terrain);
2612 }
2613 }
2614 Geometry::MultiPolygon(mpoly) => {
2615 for poly in &mut mpoly.polygons {
2616 drape_coords(&mut poly.exterior, terrain);
2617 for hole in &mut poly.interiors {
2618 drape_coords(hole, terrain);
2619 }
2620 }
2621 }
2622 Geometry::GeometryCollection(geoms) => {
2623 for g in geoms {
2624 drape_geometry(g, terrain);
2625 }
2626 }
2627 }
2628}
2629
2630fn drape_coords(coords: &mut [GeoCoord], terrain: &TerrainManager) {
2632 for coord in coords.iter_mut() {
2633 if let Some(elev) = terrain.elevation_at(coord) {
2634 coord.alt = elev;
2635 }
2636 }
2637}
2638
2639impl Layer for VectorLayer {
2644 fn id(&self) -> LayerId {
2645 self.id
2646 }
2647
2648 fn kind(&self) -> crate::layer::LayerKind {
2649 crate::layer::LayerKind::Vector
2650 }
2651
2652 fn name(&self) -> &str {
2653 &self.name
2654 }
2655
2656 fn visible(&self) -> bool {
2657 self.visible
2658 }
2659
2660 fn set_visible(&mut self, visible: bool) {
2661 self.visible = visible;
2662 }
2663
2664 fn opacity(&self) -> f32 {
2665 self.opacity
2666 }
2667
2668 fn set_opacity(&mut self, opacity: f32) {
2669 self.opacity = opacity.clamp(0.0, 1.0);
2670 }
2671
2672 fn as_any(&self) -> &dyn Any {
2673 self
2674 }
2675
2676 fn as_any_mut(&mut self) -> &mut dyn Any {
2677 self
2678 }
2679}
2680
2681#[cfg(test)]
2686mod tests {
2687 use super::*;
2688 use crate::camera_projection::CameraProjection;
2689 use crate::geometry::{
2690 Feature, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon,
2691 };
2692 use crate::layer::Layer;
2693 use crate::terrain::{FlatElevationSource, TerrainConfig, TerrainManager};
2694 use rustial_math::{WebMercator, WorldBounds, WorldCoord};
2695 use std::collections::HashMap;
2696
2697 fn make_layer(geometry: Geometry) -> VectorLayer {
2702 let features = FeatureCollection {
2703 features: vec![Feature {
2704 geometry,
2705 properties: HashMap::new(),
2706 }],
2707 };
2708 VectorLayer::new("test", features, VectorStyle::default())
2709 }
2710
2711 fn square_polygon() -> Polygon {
2712 Polygon {
2713 exterior: vec![
2714 GeoCoord::from_lat_lon(0.0, 0.0),
2715 GeoCoord::from_lat_lon(0.0, 1.0),
2716 GeoCoord::from_lat_lon(1.0, 1.0),
2717 GeoCoord::from_lat_lon(1.0, 0.0),
2718 ],
2719 interiors: vec![],
2720 }
2721 }
2722
2723 fn two_point_line() -> LineString {
2724 LineString {
2725 coords: vec![
2726 GeoCoord::from_lat_lon(0.0, 0.0),
2727 GeoCoord::from_lat_lon(0.0, 1.0),
2728 ],
2729 }
2730 }
2731
2732 fn origin_point() -> Point {
2733 Point {
2734 coord: GeoCoord::from_lat_lon(0.0, 0.0),
2735 }
2736 }
2737
2738 fn flat_terrain_manager() -> TerrainManager {
2739 let config = TerrainConfig {
2740 enabled: true,
2741 mesh_resolution: 4,
2742 source: Box::new(FlatElevationSource::new(4, 4)),
2743 ..TerrainConfig::default()
2744 };
2745 let mut mgr = TerrainManager::new(config, 100);
2746 let extent = WebMercator::max_extent();
2747 let bounds = WorldBounds::new(
2748 WorldCoord::new(-extent, -extent, 0.0),
2749 WorldCoord::new(extent, extent, 0.0),
2750 );
2751 mgr.update(
2753 &bounds,
2754 0,
2755 (0.0, 0.0),
2756 CameraProjection::WebMercator,
2757 10_000_000.0,
2758 0.0,
2759 );
2760 mgr.update(
2761 &bounds,
2762 0,
2763 (0.0, 0.0),
2764 CameraProjection::WebMercator,
2765 10_000_000.0,
2766 0.0,
2767 );
2768 mgr
2769 }
2770
2771 #[test]
2776 fn new_layer_defaults() {
2777 let layer = make_layer(Geometry::Point(origin_point()));
2778 assert_eq!(layer.name(), "test");
2779 assert!(layer.visible());
2780 assert!((layer.opacity() - 1.0).abs() < f32::EPSILON);
2781 assert_eq!(layer.feature_count(), 1);
2782 assert_eq!(layer.total_coords(), 1);
2783 }
2784
2785 #[test]
2786 fn layer_trait_visibility() {
2787 let mut layer = make_layer(Geometry::Point(origin_point()));
2788 layer.set_visible(false);
2789 assert!(!layer.visible());
2790 layer.set_visible(true);
2791 assert!(layer.visible());
2792 }
2793
2794 #[test]
2795 fn layer_trait_opacity_clamped() {
2796 let mut layer = make_layer(Geometry::Point(origin_point()));
2797 layer.set_opacity(1.5);
2798 assert!((layer.opacity() - 1.0).abs() < f32::EPSILON);
2799 layer.set_opacity(-0.5);
2800 assert!((layer.opacity() - 0.0).abs() < f32::EPSILON);
2801 }
2802
2803 #[test]
2804 fn debug_impl() {
2805 let layer = make_layer(Geometry::Point(origin_point()));
2806 let dbg = format!("{layer:?}");
2807 assert!(dbg.contains("VectorLayer"));
2808 assert!(dbg.contains("test"));
2809 }
2810
2811 #[test]
2816 fn tessellate_polygon() {
2817 let layer = make_layer(Geometry::Polygon(square_polygon()));
2818 let mesh = layer.tessellate(CameraProjection::WebMercator);
2819 assert_eq!(mesh.vertex_count(), 4);
2820 assert_eq!(mesh.index_count(), 6); assert_eq!(mesh.triangle_count(), 2);
2822 assert_eq!(mesh.colors.len(), 4);
2823 assert!(!mesh.is_empty());
2824 }
2825
2826 #[test]
2831 fn tessellate_linestring() {
2832 let layer = make_layer(Geometry::LineString(two_point_line()));
2833 let mesh = layer.tessellate(CameraProjection::WebMercator);
2834 assert_eq!(mesh.vertex_count(), 4); assert_eq!(mesh.index_count(), 6);
2836 }
2837
2838 #[test]
2843 fn tessellate_point() {
2844 let layer = make_layer(Geometry::Point(origin_point()));
2845 let mesh = layer.tessellate(CameraProjection::WebMercator);
2846 assert_eq!(mesh.vertex_count(), 4); assert_eq!(mesh.index_count(), 6); assert_eq!(mesh.colors[0], VectorStyle::default().fill_color);
2850 }
2851
2852 #[test]
2857 fn tessellate_multi_point() {
2858 let mp = Geometry::MultiPoint(MultiPoint {
2859 points: vec![origin_point(), origin_point()],
2860 });
2861 let layer = make_layer(mp);
2862 let mesh = layer.tessellate(CameraProjection::WebMercator);
2863 assert_eq!(mesh.vertex_count(), 8);
2865 assert_eq!(mesh.index_count(), 12);
2866 }
2867
2868 #[test]
2869 fn tessellate_multi_linestring() {
2870 let mls = Geometry::MultiLineString(MultiLineString {
2871 lines: vec![two_point_line(), two_point_line()],
2872 });
2873 let layer = make_layer(mls);
2874 let mesh = layer.tessellate(CameraProjection::WebMercator);
2875 assert_eq!(mesh.vertex_count(), 8); assert_eq!(mesh.index_count(), 12);
2877 }
2878
2879 #[test]
2880 fn tessellate_multi_polygon() {
2881 let mpoly = Geometry::MultiPolygon(MultiPolygon {
2882 polygons: vec![square_polygon(), square_polygon()],
2883 });
2884 let layer = make_layer(mpoly);
2885 let mesh = layer.tessellate(CameraProjection::WebMercator);
2886 assert_eq!(mesh.vertex_count(), 8); assert_eq!(mesh.index_count(), 12);
2888 }
2889
2890 #[test]
2891 fn tessellate_geometry_collection() {
2892 let gc = Geometry::GeometryCollection(vec![
2893 Geometry::Point(origin_point()),
2894 Geometry::Polygon(square_polygon()),
2895 ]);
2896 let layer = make_layer(gc);
2897 let mesh = layer.tessellate(CameraProjection::WebMercator);
2898 assert_eq!(mesh.vertex_count(), 8);
2900 assert_eq!(mesh.index_count(), 12);
2901 }
2902
2903 #[test]
2908 fn tessellate_empty_collection() {
2909 let features = FeatureCollection { features: vec![] };
2910 let layer = VectorLayer::new("empty", features, VectorStyle::default());
2911 let mesh = layer.tessellate(CameraProjection::WebMercator);
2912 assert!(mesh.is_empty());
2913 assert_eq!(mesh.vertex_count(), 0);
2914 }
2915
2916 #[test]
2921 fn mesh_data_merge() {
2922 let mut a = VectorMeshData {
2923 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
2924 colors: vec![[1.0, 0.0, 0.0, 1.0], [1.0, 0.0, 0.0, 1.0]],
2925 indices: vec![0, 1, 0],
2926 ..Default::default()
2927 };
2928 let b = VectorMeshData {
2929 positions: vec![[2.0, 0.0, 0.0], [3.0, 0.0, 0.0]],
2930 colors: vec![[0.0, 1.0, 0.0, 1.0], [0.0, 1.0, 0.0, 1.0]],
2931 indices: vec![0, 1, 0],
2932 ..Default::default()
2933 };
2934 a.merge(&b);
2935 assert_eq!(a.vertex_count(), 4);
2936 assert_eq!(a.index_count(), 6);
2937 assert_eq!(a.indices, vec![0, 1, 0, 2, 3, 2]);
2939 }
2940
2941 #[test]
2942 fn mesh_data_clear() {
2943 let mut mesh = VectorMeshData {
2944 positions: vec![[0.0, 0.0, 0.0]],
2945 colors: vec![[1.0, 0.0, 0.0, 1.0]],
2946 indices: vec![0],
2947 ..Default::default()
2948 };
2949 mesh.clear();
2950 assert!(mesh.is_empty());
2951 assert_eq!(mesh.vertex_count(), 0);
2952 }
2953
2954 #[test]
2959 fn drape_sets_altitude() {
2960 let mgr = flat_terrain_manager();
2961
2962 let features = FeatureCollection {
2963 features: vec![Feature {
2964 geometry: Geometry::Point(Point {
2965 coord: GeoCoord::new(10.0, 20.0, 999.0),
2966 }),
2967 properties: HashMap::new(),
2968 }],
2969 };
2970
2971 let mut layer = VectorLayer::new("test", features, VectorStyle::default());
2972 layer.drape_on_terrain(&mgr);
2973
2974 match &layer.features.features[0].geometry {
2976 Geometry::Point(p) => assert!((p.coord.alt - 0.0).abs() < 1e-3),
2977 _ => panic!("expected Point"),
2978 }
2979 }
2980
2981 #[test]
2982 fn drape_skipped_when_terrain_disabled() {
2983 let config = TerrainConfig::default(); let mgr = TerrainManager::new(config, 100);
2985
2986 let features = FeatureCollection {
2987 features: vec![Feature {
2988 geometry: Geometry::Point(Point {
2989 coord: GeoCoord::new(10.0, 20.0, 999.0),
2990 }),
2991 properties: HashMap::new(),
2992 }],
2993 };
2994
2995 let mut layer = VectorLayer::new("test", features, VectorStyle::default());
2996 layer.drape_on_terrain(&mgr);
2997
2998 match &layer.features.features[0].geometry {
3000 Geometry::Point(p) => assert!((p.coord.alt - 999.0).abs() < 1e-3),
3001 _ => panic!("expected Point"),
3002 }
3003 }
3004
3005 #[test]
3006 fn drape_linestring_coords() {
3007 let mgr = flat_terrain_manager();
3008
3009 let features = FeatureCollection {
3010 features: vec![Feature {
3011 geometry: Geometry::LineString(LineString {
3012 coords: vec![
3013 GeoCoord::new(10.0, 20.0, 500.0),
3014 GeoCoord::new(11.0, 21.0, 600.0),
3015 ],
3016 }),
3017 properties: HashMap::new(),
3018 }],
3019 };
3020
3021 let mut layer = VectorLayer::new("test", features, VectorStyle::default());
3022 layer.drape_on_terrain(&mgr);
3023
3024 match &layer.features.features[0].geometry {
3025 Geometry::LineString(ls) => {
3026 for coord in &ls.coords {
3027 assert!(
3028 coord.alt.abs() < 1e-3,
3029 "expected flat terrain, got alt={}",
3030 coord.alt
3031 );
3032 }
3033 }
3034 _ => panic!("expected LineString"),
3035 }
3036 }
3037
3038 #[test]
3039 fn tessellate_circle_mode() {
3040 let style = VectorStyle {
3041 render_mode: VectorRenderMode::Circle,
3042 ..VectorStyle::default()
3043 };
3044 let layer = make_layer(Geometry::Point(origin_point()));
3045 let layer = VectorLayer::new("circle", layer.features, style);
3046 let mesh = layer.tessellate(CameraProjection::WebMercator);
3047 assert!(mesh.vertex_count() > 8);
3048 assert!(mesh.index_count() >= DEFAULT_CIRCLE_SEGMENTS * 3);
3049 }
3050
3051 #[test]
3052 fn tessellate_heatmap_mode_has_faded_edges() {
3053 let mut style = VectorStyle::heatmap([1.0, 0.0, 0.0, 0.5], 24.0, 1.0);
3054 style.render_mode = VectorRenderMode::Heatmap;
3055 let layer = make_layer(Geometry::Point(origin_point()));
3056 let layer = VectorLayer::new("heatmap", layer.features, style);
3057 let mesh = layer.tessellate(CameraProjection::WebMercator);
3058 assert_eq!(mesh.colors.first().map(|c| c[3]), Some(0.5));
3059 assert_eq!(mesh.colors.last().map(|c| c[3]), Some(0.0));
3060 }
3061
3062 #[test]
3063 fn tessellate_fill_extrusion_mode_produces_vertical_geometry() {
3064 let style = VectorStyle::fill_extrusion([0.5, 0.5, 0.8, 1.0], 0.0, 50.0);
3065 let layer = make_layer(Geometry::Polygon(square_polygon()));
3066 let layer = VectorLayer::new("extrusion", layer.features, style);
3067 let mesh = layer.tessellate(CameraProjection::WebMercator);
3068 let min_z = mesh
3069 .positions
3070 .iter()
3071 .map(|p| p[2])
3072 .fold(f64::INFINITY, f64::min);
3073 let max_z = mesh
3074 .positions
3075 .iter()
3076 .map(|p| p[2])
3077 .fold(f64::NEG_INFINITY, f64::max);
3078 assert!(max_z > min_z);
3079 }
3080
3081 #[test]
3082 fn fill_extrusion_tessellation_produces_normals() {
3083 let style = VectorStyle::fill_extrusion([0.5, 0.5, 0.8, 1.0], 0.0, 50.0);
3084 let layer = make_layer(Geometry::Polygon(square_polygon()));
3085 let layer = VectorLayer::new("extrusion", layer.features, style);
3086 let mesh = layer.tessellate(CameraProjection::WebMercator);
3087
3088 assert!(mesh.has_normals());
3089 assert_eq!(mesh.normals.len(), mesh.positions.len());
3090 assert_eq!(mesh.render_mode, VectorRenderMode::FillExtrusion);
3091 }
3092
3093 #[test]
3094 fn fill_extrusion_top_normals_point_up() {
3095 let style = VectorStyle::fill_extrusion([1.0, 0.0, 0.0, 1.0], 0.0, 100.0);
3096 let layer = make_layer(Geometry::Polygon(square_polygon()));
3097 let layer = VectorLayer::new("extrusion", layer.features, style);
3098 let mesh = layer.tessellate(CameraProjection::WebMercator);
3099
3100 let ring_len = 4; for i in 0..ring_len {
3104 let n = mesh.normals[i];
3105 assert!(
3106 (n[0]).abs() < 1e-6,
3107 "top normal x should be 0, got {}",
3108 n[0]
3109 );
3110 assert!(
3111 (n[1]).abs() < 1e-6,
3112 "top normal y should be 0, got {}",
3113 n[1]
3114 );
3115 assert!(
3116 (n[2] - 1.0).abs() < 1e-6,
3117 "top normal z should be 1, got {}",
3118 n[2]
3119 );
3120 }
3121 }
3122
3123 #[test]
3124 fn fill_extrusion_side_normals_are_horizontal() {
3125 let style = VectorStyle::fill_extrusion([1.0, 0.0, 0.0, 1.0], 0.0, 100.0);
3126 let layer = make_layer(Geometry::Polygon(square_polygon()));
3127 let layer = VectorLayer::new("extrusion", layer.features, style);
3128 let mesh = layer.tessellate(CameraProjection::WebMercator);
3129
3130 let ring_len = 4;
3132 for i in ring_len..mesh.normals.len() {
3133 let n = mesh.normals[i];
3134 assert!(
3136 (n[2]).abs() < 1e-6,
3137 "side normal z should be 0, got {} at vertex {}",
3138 n[2],
3139 i
3140 );
3141 let len = (n[0] * n[0] + n[1] * n[1]).sqrt();
3143 assert!(
3144 (len - 1.0).abs() < 0.01,
3145 "side normal length should be ~1, got {} at vertex {}",
3146 len,
3147 i
3148 );
3149 }
3150 }
3151
3152 #[test]
3153 fn flat_fill_tessellation_has_no_normals() {
3154 let style = VectorStyle {
3155 render_mode: VectorRenderMode::Fill,
3156 fill_color: [0.0, 1.0, 0.0, 1.0],
3157 ..VectorStyle::default()
3158 };
3159 let layer = make_layer(Geometry::Polygon(square_polygon()));
3160 let layer = VectorLayer::new("fill", layer.features, style);
3161 let mesh = layer.tessellate(CameraProjection::WebMercator);
3162
3163 assert!(!mesh.has_normals());
3164 assert!(mesh.normals.is_empty());
3165 assert_eq!(mesh.render_mode, VectorRenderMode::Fill);
3166 }
3167
3168 #[test]
3169 fn symbol_candidates_point_placement_skips_lines() {
3170 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3171 style.symbol_text_field = Some("name".to_owned());
3172 let mut properties = HashMap::new();
3173 properties.insert(
3174 "name".to_owned(),
3175 crate::geometry::PropertyValue::String("Road".to_owned()),
3176 );
3177 let layer = VectorLayer::new(
3178 "symbol",
3179 FeatureCollection {
3180 features: vec![Feature {
3181 geometry: Geometry::LineString(two_point_line()),
3182 properties,
3183 }],
3184 },
3185 style,
3186 );
3187
3188 assert!(layer.symbol_candidates().is_empty());
3189 }
3190
3191 #[test]
3192 fn symbol_candidates_text_only_use_text_overlap_flag() {
3193 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3194 style.symbol_text_field = Some("name".to_owned());
3195 style.symbol_text_allow_overlap = true;
3196 style.symbol_icon_allow_overlap = false;
3197
3198 let mut properties = HashMap::new();
3199 properties.insert(
3200 "name".to_owned(),
3201 crate::geometry::PropertyValue::String("Label".to_owned()),
3202 );
3203 let layer = VectorLayer::new(
3204 "symbol",
3205 FeatureCollection {
3206 features: vec![Feature {
3207 geometry: Geometry::Point(origin_point()),
3208 properties,
3209 }],
3210 },
3211 style,
3212 );
3213
3214 let candidates = layer.symbol_candidates();
3215 assert_eq!(candidates.len(), 1);
3216 assert!(candidates[0].allow_overlap);
3217 }
3218
3219 #[test]
3220 fn symbol_candidates_use_fixed_text_anchor_when_variable_anchors_absent() {
3221 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3222 style.symbol_text_field = Some("name".to_owned());
3223 style.symbol_text_anchor = SymbolAnchor::TopRight;
3224 style.symbol_anchors = vec![SymbolAnchor::TopRight];
3225
3226 let mut properties = HashMap::new();
3227 properties.insert(
3228 "name".to_owned(),
3229 crate::geometry::PropertyValue::String("Label".to_owned()),
3230 );
3231 let layer = VectorLayer::new(
3232 "symbol",
3233 FeatureCollection {
3234 features: vec![Feature {
3235 geometry: Geometry::Point(origin_point()),
3236 properties,
3237 }],
3238 },
3239 style,
3240 );
3241
3242 let candidates = layer.symbol_candidates();
3243 assert_eq!(candidates.len(), 1);
3244 assert_eq!(candidates[0].anchors, vec![SymbolAnchor::TopRight]);
3245 }
3246
3247 #[test]
3248 fn symbol_candidates_propagate_text_radial_offset() {
3249 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3250 style.symbol_text_field = Some("name".to_owned());
3251 style.symbol_text_anchor = SymbolAnchor::Top;
3252 style.symbol_anchors = vec![SymbolAnchor::Top];
3253 style.symbol_text_radial_offset = Some(2.0);
3254
3255 let mut properties = HashMap::new();
3256 properties.insert(
3257 "name".to_owned(),
3258 crate::geometry::PropertyValue::String("Label".to_owned()),
3259 );
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(), 1);
3273 assert_eq!(candidates[0].radial_offset, Some(2.0));
3274 }
3275
3276 #[test]
3277 fn symbol_candidates_propagate_text_max_width() {
3278 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3279 style.symbol_text_field = Some("name".to_owned());
3280 style.symbol_text_max_width = Some(6.0);
3281
3282 let mut properties = HashMap::new();
3283 properties.insert(
3284 "name".to_owned(),
3285 crate::geometry::PropertyValue::String("Long label".to_owned()),
3286 );
3287 let layer = VectorLayer::new(
3288 "symbol",
3289 FeatureCollection {
3290 features: vec![Feature {
3291 geometry: Geometry::Point(origin_point()),
3292 properties,
3293 }],
3294 },
3295 style,
3296 );
3297
3298 let candidates = layer.symbol_candidates();
3299 assert_eq!(candidates.len(), 1);
3300 assert_eq!(candidates[0].text_max_width, Some(6.0));
3301 }
3302
3303 #[test]
3304 fn symbol_candidates_propagate_text_line_height() {
3305 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3306 style.symbol_text_field = Some("name".to_owned());
3307 style.symbol_text_line_height = Some(1.5);
3308
3309 let mut properties = HashMap::new();
3310 properties.insert(
3311 "name".to_owned(),
3312 crate::geometry::PropertyValue::String("Long label".to_owned()),
3313 );
3314 let layer = VectorLayer::new(
3315 "symbol",
3316 FeatureCollection {
3317 features: vec![Feature {
3318 geometry: Geometry::Point(origin_point()),
3319 properties,
3320 }],
3321 },
3322 style,
3323 );
3324
3325 let candidates = layer.symbol_candidates();
3326 assert_eq!(candidates.len(), 1);
3327 assert_eq!(candidates[0].text_line_height, Some(1.5));
3328 }
3329
3330 #[test]
3331 fn symbol_candidates_propagate_text_letter_spacing() {
3332 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3333 style.symbol_text_field = Some("name".to_owned());
3334 style.symbol_text_letter_spacing = Some(0.25);
3335
3336 let mut properties = HashMap::new();
3337 properties.insert(
3338 "name".to_owned(),
3339 crate::geometry::PropertyValue::String("Long label".to_owned()),
3340 );
3341 let layer = VectorLayer::new(
3342 "symbol",
3343 FeatureCollection {
3344 features: vec![Feature {
3345 geometry: Geometry::Point(origin_point()),
3346 properties,
3347 }],
3348 },
3349 style,
3350 );
3351
3352 let candidates = layer.symbol_candidates();
3353 assert_eq!(candidates.len(), 1);
3354 assert_eq!(candidates[0].text_letter_spacing, Some(0.25));
3355 }
3356
3357 #[test]
3358 fn symbol_candidates_apply_uppercase_text_transform() {
3359 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3360 style.symbol_text_field = Some("name".to_owned());
3361 style.symbol_text_transform = SymbolTextTransform::Uppercase;
3362
3363 let mut properties = HashMap::new();
3364 properties.insert(
3365 "name".to_owned(),
3366 crate::geometry::PropertyValue::String("Main Street".to_owned()),
3367 );
3368 let layer = VectorLayer::new(
3369 "symbol",
3370 FeatureCollection {
3371 features: vec![Feature {
3372 geometry: Geometry::Point(origin_point()),
3373 properties,
3374 }],
3375 },
3376 style,
3377 );
3378
3379 let candidates = layer.symbol_candidates();
3380 assert_eq!(candidates.len(), 1);
3381 assert_eq!(candidates[0].text.as_deref(), Some("MAIN STREET"));
3382 }
3383
3384 #[test]
3385 fn symbol_candidates_apply_lowercase_text_transform() {
3386 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3387 style.symbol_text_field = Some("name".to_owned());
3388 style.symbol_text_transform = SymbolTextTransform::Lowercase;
3389
3390 let mut properties = HashMap::new();
3391 properties.insert(
3392 "name".to_owned(),
3393 crate::geometry::PropertyValue::String("Main Street".to_owned()),
3394 );
3395 let layer = VectorLayer::new(
3396 "symbol",
3397 FeatureCollection {
3398 features: vec![Feature {
3399 geometry: Geometry::Point(origin_point()),
3400 properties,
3401 }],
3402 },
3403 style,
3404 );
3405
3406 let candidates = layer.symbol_candidates();
3407 assert_eq!(candidates.len(), 1);
3408 assert_eq!(candidates[0].text.as_deref(), Some("main street"));
3409 }
3410
3411 #[test]
3412 fn symbol_candidates_keep_variable_anchor_priority_over_fixed_anchor() {
3413 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3414 style.symbol_text_field = Some("name".to_owned());
3415 style.symbol_text_anchor = SymbolAnchor::BottomLeft;
3416 style.symbol_anchors = vec![SymbolAnchor::Center, SymbolAnchor::Top];
3417
3418 let mut properties = HashMap::new();
3419 properties.insert(
3420 "name".to_owned(),
3421 crate::geometry::PropertyValue::String("Label".to_owned()),
3422 );
3423 let layer = VectorLayer::new(
3424 "symbol",
3425 FeatureCollection {
3426 features: vec![Feature {
3427 geometry: Geometry::Point(origin_point()),
3428 properties,
3429 }],
3430 },
3431 style,
3432 );
3433
3434 let candidates = layer.symbol_candidates();
3435 assert_eq!(candidates.len(), 1);
3436 assert_eq!(
3437 candidates[0].anchors,
3438 vec![SymbolAnchor::Center, SymbolAnchor::Top]
3439 );
3440 }
3441
3442 #[test]
3443 fn symbol_candidates_use_variable_anchor_offset_order_when_present() {
3444 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3445 style.symbol_text_field = Some("name".to_owned());
3446 style.symbol_text_anchor = SymbolAnchor::BottomLeft;
3447 style.symbol_anchors = vec![SymbolAnchor::Center];
3448 style.symbol_variable_anchor_offsets = Some(vec![
3449 (SymbolAnchor::Top, [1.0, 2.0]),
3450 (SymbolAnchor::Right, [3.0, 4.0]),
3451 ]);
3452
3453 let mut properties = HashMap::new();
3454 properties.insert(
3455 "name".to_owned(),
3456 crate::geometry::PropertyValue::String("Label".to_owned()),
3457 );
3458 let layer = VectorLayer::new(
3459 "symbol",
3460 FeatureCollection {
3461 features: vec![Feature {
3462 geometry: Geometry::Point(origin_point()),
3463 properties,
3464 }],
3465 },
3466 style,
3467 );
3468
3469 let candidates = layer.symbol_candidates();
3470 assert_eq!(candidates.len(), 1);
3471 assert_eq!(
3472 candidates[0].anchors,
3473 vec![SymbolAnchor::Top, SymbolAnchor::Right]
3474 );
3475 assert_eq!(
3476 candidates[0].variable_anchor_offsets,
3477 Some(vec![
3478 (SymbolAnchor::Top, [1.0, 2.0]),
3479 (SymbolAnchor::Right, [3.0, 4.0]),
3480 ])
3481 );
3482 }
3483
3484 #[test]
3485 fn symbol_candidates_text_and_icon_require_both_overlap_flags() {
3486 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3487 style.symbol_text_field = Some("name".to_owned());
3488 style.symbol_icon_image = Some("marker".to_owned());
3489 style.symbol_text_allow_overlap = true;
3490 style.symbol_icon_allow_overlap = false;
3491
3492 let mut properties = HashMap::new();
3493 properties.insert(
3494 "name".to_owned(),
3495 crate::geometry::PropertyValue::String("Label".to_owned()),
3496 );
3497 let layer = VectorLayer::new(
3498 "symbol",
3499 FeatureCollection {
3500 features: vec![Feature {
3501 geometry: Geometry::Point(origin_point()),
3502 properties,
3503 }],
3504 },
3505 style,
3506 );
3507
3508 let candidates = layer.symbol_candidates();
3509 assert_eq!(candidates.len(), 1);
3510 assert!(!candidates[0].allow_overlap);
3511 }
3512
3513 #[test]
3514 fn symbol_candidates_text_only_use_text_ignore_placement_flag() {
3515 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3516 style.symbol_text_field = Some("name".to_owned());
3517 style.symbol_text_ignore_placement = true;
3518 style.symbol_icon_ignore_placement = false;
3519
3520 let mut properties = HashMap::new();
3521 properties.insert(
3522 "name".to_owned(),
3523 crate::geometry::PropertyValue::String("Label".to_owned()),
3524 );
3525 let layer = VectorLayer::new(
3526 "symbol",
3527 FeatureCollection {
3528 features: vec![Feature {
3529 geometry: Geometry::Point(origin_point()),
3530 properties,
3531 }],
3532 },
3533 style,
3534 );
3535
3536 let candidates = layer.symbol_candidates();
3537 assert_eq!(candidates.len(), 1);
3538 assert!(candidates[0].ignore_placement);
3539 }
3540
3541 #[test]
3542 fn symbol_candidates_text_and_icon_require_both_ignore_flags() {
3543 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3544 style.symbol_text_field = Some("name".to_owned());
3545 style.symbol_icon_image = Some("marker".to_owned());
3546 style.symbol_text_ignore_placement = true;
3547 style.symbol_icon_ignore_placement = false;
3548
3549 let mut properties = HashMap::new();
3550 properties.insert(
3551 "name".to_owned(),
3552 crate::geometry::PropertyValue::String("Label".to_owned()),
3553 );
3554 let layer = VectorLayer::new(
3555 "symbol",
3556 FeatureCollection {
3557 features: vec![Feature {
3558 geometry: Geometry::Point(origin_point()),
3559 properties,
3560 }],
3561 },
3562 style,
3563 );
3564
3565 let candidates = layer.symbol_candidates();
3566 assert_eq!(candidates.len(), 1);
3567 assert!(!candidates[0].ignore_placement);
3568 }
3569
3570 #[test]
3571 fn symbol_candidates_emit_icon_fallback_when_text_is_optional() {
3572 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3573 style.symbol_text_field = Some("name".to_owned());
3574 style.symbol_icon_image = Some("marker".to_owned());
3575 style.symbol_text_optional = true;
3576
3577 let mut properties = HashMap::new();
3578 properties.insert(
3579 "name".to_owned(),
3580 crate::geometry::PropertyValue::String("Label".to_owned()),
3581 );
3582 let layer = VectorLayer::new(
3583 "symbol",
3584 FeatureCollection {
3585 features: vec![Feature {
3586 geometry: Geometry::Point(origin_point()),
3587 properties,
3588 }],
3589 },
3590 style,
3591 );
3592
3593 let candidates = layer.symbol_candidates();
3594 assert_eq!(candidates.len(), 2);
3595 assert_eq!(candidates[0].text.as_deref(), Some("Label"));
3596 assert_eq!(candidates[0].icon_image.as_deref(), Some("marker"));
3597 assert!(candidates[1].text.is_none());
3598 assert_eq!(candidates[1].icon_image.as_deref(), Some("marker"));
3599 assert_eq!(candidates[0].cross_tile_id, candidates[1].cross_tile_id);
3600 assert_eq!(
3601 candidates[0].placement_group_id,
3602 candidates[1].placement_group_id
3603 );
3604 }
3605
3606 #[test]
3607 fn symbol_candidates_emit_text_fallback_when_icon_is_optional() {
3608 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3609 style.symbol_text_field = Some("name".to_owned());
3610 style.symbol_icon_image = Some("marker".to_owned());
3611 style.symbol_icon_optional = true;
3612
3613 let mut properties = HashMap::new();
3614 properties.insert(
3615 "name".to_owned(),
3616 crate::geometry::PropertyValue::String("Label".to_owned()),
3617 );
3618 let layer = VectorLayer::new(
3619 "symbol",
3620 FeatureCollection {
3621 features: vec![Feature {
3622 geometry: Geometry::Point(origin_point()),
3623 properties,
3624 }],
3625 },
3626 style,
3627 );
3628
3629 let candidates = layer.symbol_candidates();
3630 assert_eq!(candidates.len(), 2);
3631 assert_eq!(candidates[0].text.as_deref(), Some("Label"));
3632 assert_eq!(candidates[0].icon_image.as_deref(), Some("marker"));
3633 assert_eq!(candidates[1].text.as_deref(), Some("Label"));
3634 assert!(candidates[1].icon_image.is_none());
3635 assert_eq!(candidates[0].cross_tile_id, candidates[1].cross_tile_id);
3636 assert_eq!(
3637 candidates[0].placement_group_id,
3638 candidates[1].placement_group_id
3639 );
3640 }
3641
3642 #[test]
3643 fn symbol_candidates_line_placement_repeats_anchors_with_spacing() {
3644 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3645 style.symbol_text_field = Some("name".to_owned());
3646 style.symbol_placement = SymbolPlacement::Line;
3647 style.symbol_spacing = 1000.0;
3648
3649 let start = GeoCoord::from_lat_lon(0.0, 0.0);
3650 let end = GeoCoord::from_lat_lon(0.0, 0.05);
3651 let mut properties = HashMap::new();
3652 properties.insert(
3653 "name".to_owned(),
3654 crate::geometry::PropertyValue::String("Road".to_owned()),
3655 );
3656 let layer = VectorLayer::new(
3657 "symbol",
3658 FeatureCollection {
3659 features: vec![Feature {
3660 geometry: Geometry::LineString(LineString {
3661 coords: vec![start, end],
3662 }),
3663 properties,
3664 }],
3665 },
3666 style,
3667 );
3668
3669 let candidates = layer.symbol_candidates();
3670 assert!(
3671 candidates.len() > 1,
3672 "longer lines should produce repeated anchors"
3673 );
3674 assert!(candidates
3675 .iter()
3676 .all(|candidate| (candidate.anchor.lat - 0.0).abs() < 1e-9));
3677 assert!(candidates
3678 .windows(2)
3679 .all(|pair| pair[0].anchor.lon < pair[1].anchor.lon));
3680 assert!(candidates
3681 .iter()
3682 .all(|candidate| candidate.rotation_rad.abs() < 1e-6));
3683 assert_eq!(candidates[0].text.as_deref(), Some("Road"));
3684 }
3685
3686 #[test]
3687 fn symbol_candidates_line_placement_rotates_vertical_lines() {
3688 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3689 style.symbol_text_field = Some("name".to_owned());
3690 style.symbol_placement = SymbolPlacement::Line;
3691
3692 let mut properties = HashMap::new();
3693 properties.insert(
3694 "name".to_owned(),
3695 crate::geometry::PropertyValue::String("North".to_owned()),
3696 );
3697 let layer = VectorLayer::new(
3698 "symbol",
3699 FeatureCollection {
3700 features: vec![Feature {
3701 geometry: Geometry::LineString(LineString {
3702 coords: vec![
3703 GeoCoord::from_lat_lon(0.0, 0.0),
3704 GeoCoord::from_lat_lon(1.0, 0.0),
3705 ],
3706 }),
3707 properties,
3708 }],
3709 },
3710 style,
3711 );
3712
3713 let candidates = layer.symbol_candidates();
3714 assert!(!candidates.is_empty());
3715 assert!(candidates.iter().all(|candidate| {
3716 (candidate.rotation_rad.abs() - std::f32::consts::FRAC_PI_2).abs() < 0.05
3717 }));
3718 }
3719
3720 #[test]
3721 fn symbol_candidates_line_placement_keeps_reversed_lines_upright() {
3722 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3723 style.symbol_text_field = Some("name".to_owned());
3724 style.symbol_placement = SymbolPlacement::Line;
3725 style.symbol_keep_upright = true;
3726
3727 let mut properties = HashMap::new();
3728 properties.insert(
3729 "name".to_owned(),
3730 crate::geometry::PropertyValue::String("West".to_owned()),
3731 );
3732 let layer = VectorLayer::new(
3733 "symbol",
3734 FeatureCollection {
3735 features: vec![Feature {
3736 geometry: Geometry::LineString(LineString {
3737 coords: vec![
3738 GeoCoord::from_lat_lon(0.0, 1.0),
3739 GeoCoord::from_lat_lon(0.0, 0.0),
3740 ],
3741 }),
3742 properties,
3743 }],
3744 },
3745 style,
3746 );
3747
3748 let candidates = layer.symbol_candidates();
3749 assert!(!candidates.is_empty());
3750 assert!(candidates
3751 .iter()
3752 .all(|candidate| candidate.rotation_rad.abs() < 0.05));
3753 }
3754
3755 #[test]
3756 fn symbol_candidates_line_placement_can_disable_keep_upright() {
3757 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3758 style.symbol_text_field = Some("name".to_owned());
3759 style.symbol_placement = SymbolPlacement::Line;
3760 style.symbol_keep_upright = false;
3761
3762 let mut properties = HashMap::new();
3763 properties.insert(
3764 "name".to_owned(),
3765 crate::geometry::PropertyValue::String("West".to_owned()),
3766 );
3767 let layer = VectorLayer::new(
3768 "symbol",
3769 FeatureCollection {
3770 features: vec![Feature {
3771 geometry: Geometry::LineString(LineString {
3772 coords: vec![
3773 GeoCoord::from_lat_lon(0.0, 1.0),
3774 GeoCoord::from_lat_lon(0.0, 0.0),
3775 ],
3776 }),
3777 properties,
3778 }],
3779 },
3780 style,
3781 );
3782
3783 let candidates = layer.symbol_candidates();
3784 assert!(!candidates.is_empty());
3785 assert!(candidates.iter().all(|candidate| {
3786 (candidate.rotation_rad.abs() - std::f32::consts::PI).abs() < 0.05
3787 }));
3788 }
3789
3790 #[test]
3791 fn symbol_candidates_line_placement_cross_tile_id_stays_stable_across_small_anchor_shifts() {
3792 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3793 style.symbol_text_field = Some("name".to_owned());
3794 style.symbol_placement = SymbolPlacement::Line;
3795 style.symbol_spacing = 1000.0;
3796
3797 let mut properties = HashMap::new();
3798 properties.insert(
3799 "id".to_owned(),
3800 crate::geometry::PropertyValue::String("road-1".to_owned()),
3801 );
3802 properties.insert(
3803 "name".to_owned(),
3804 crate::geometry::PropertyValue::String("Main".to_owned()),
3805 );
3806
3807 let base = VectorLayer::new(
3808 "symbol",
3809 FeatureCollection {
3810 features: vec![Feature {
3811 geometry: Geometry::LineString(LineString {
3812 coords: vec![
3813 GeoCoord::from_lat_lon(0.0, 0.0),
3814 GeoCoord::from_lat_lon(0.0, 0.05),
3815 ],
3816 }),
3817 properties: properties.clone(),
3818 }],
3819 },
3820 style.clone(),
3821 );
3822 let shifted = VectorLayer::new(
3823 "symbol",
3824 FeatureCollection {
3825 features: vec![Feature {
3826 geometry: Geometry::LineString(LineString {
3827 coords: vec![
3828 GeoCoord::from_lat_lon(0.0, 0.0),
3829 GeoCoord::from_lat_lon(0.0002, 0.0502),
3830 ],
3831 }),
3832 properties,
3833 }],
3834 },
3835 style,
3836 );
3837
3838 let base_ids = base
3839 .symbol_candidates()
3840 .into_iter()
3841 .map(|candidate| candidate.cross_tile_id)
3842 .collect::<Vec<_>>();
3843 let shifted_ids = shifted
3844 .symbol_candidates()
3845 .into_iter()
3846 .map(|candidate| candidate.cross_tile_id)
3847 .collect::<Vec<_>>();
3848
3849 assert_eq!(base_ids, shifted_ids);
3850 }
3851
3852 #[test]
3853 fn symbol_candidates_line_placement_cross_tile_id_changes_for_shifted_line_windows() {
3854 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3855 style.symbol_text_field = Some("name".to_owned());
3856 style.symbol_placement = SymbolPlacement::Line;
3857 style.symbol_spacing = 1000.0;
3858
3859 let mut properties = HashMap::new();
3860 properties.insert(
3861 "id".to_owned(),
3862 crate::geometry::PropertyValue::String("road-2".to_owned()),
3863 );
3864 properties.insert(
3865 "name".to_owned(),
3866 crate::geometry::PropertyValue::String("Main".to_owned()),
3867 );
3868
3869 let base = VectorLayer::new(
3870 "symbol",
3871 FeatureCollection {
3872 features: vec![Feature {
3873 geometry: Geometry::LineString(LineString {
3874 coords: vec![
3875 GeoCoord::from_lat_lon(0.0, 0.0),
3876 GeoCoord::from_lat_lon(0.0, 0.05),
3877 ],
3878 }),
3879 properties: properties.clone(),
3880 }],
3881 },
3882 style.clone(),
3883 );
3884 let shifted_window = VectorLayer::new(
3885 "symbol",
3886 FeatureCollection {
3887 features: vec![Feature {
3888 geometry: Geometry::LineString(LineString {
3889 coords: vec![
3890 GeoCoord::from_lat_lon(0.0, 0.01),
3891 GeoCoord::from_lat_lon(0.0, 0.06),
3892 ],
3893 }),
3894 properties,
3895 }],
3896 },
3897 style,
3898 );
3899
3900 let base_ids = base
3901 .symbol_candidates()
3902 .into_iter()
3903 .map(|candidate| candidate.cross_tile_id)
3904 .collect::<Vec<_>>();
3905 let shifted_ids = shifted_window
3906 .symbol_candidates()
3907 .into_iter()
3908 .map(|candidate| candidate.cross_tile_id)
3909 .collect::<Vec<_>>();
3910
3911 assert_ne!(base_ids.first(), shifted_ids.first());
3912 assert!(base_ids.iter().any(|id| shifted_ids.contains(id)));
3913 }
3914
3915 #[test]
3916 fn symbol_candidates_line_placement_filters_sharp_turns_with_max_angle() {
3917 let mut style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3918 style.symbol_text_field = Some("name".to_owned());
3919 style.symbol_placement = SymbolPlacement::Line;
3920 style.symbol_spacing = 10_000.0;
3921 style.symbol_max_angle = 10.0;
3922
3923 let mut properties = HashMap::new();
3924 properties.insert(
3925 "name".to_owned(),
3926 crate::geometry::PropertyValue::String("Turn".to_owned()),
3927 );
3928 let layer = VectorLayer::new(
3929 "symbol",
3930 FeatureCollection {
3931 features: vec![Feature {
3932 geometry: Geometry::LineString(LineString {
3933 coords: vec![
3934 GeoCoord::from_lat_lon(0.0, 0.0),
3935 GeoCoord::from_lat_lon(0.0, 0.03),
3936 GeoCoord::from_lat_lon(0.03, 0.03),
3937 ],
3938 }),
3939 properties,
3940 }],
3941 },
3942 style,
3943 );
3944
3945 assert!(layer.symbol_candidates().is_empty());
3946 }
3947
3948 #[test]
3949 fn tessellate_symbol_mode_stacks_halo_and_fill() {
3950 let style = VectorStyle::symbol([0.1, 0.2, 0.9, 1.0], [1.0, 1.0, 1.0, 1.0], 8.0);
3951 let layer = make_layer(Geometry::Point(origin_point()));
3952 let layer = VectorLayer::new("symbol", layer.features, style);
3953 let mesh = layer.tessellate(CameraProjection::WebMercator);
3954 assert_eq!(mesh.vertex_count(), 8);
3955 assert_eq!(mesh.index_count(), 12);
3956 }
3957
3958 #[test]
3959 fn tessellate_equirectangular_changes_xy_positions() {
3960 let layer = make_layer(Geometry::Polygon(square_polygon()));
3961 let merc = layer.tessellate(CameraProjection::WebMercator);
3962 let eq = layer.tessellate(CameraProjection::Equirectangular);
3963
3964 assert_eq!(merc.positions.len(), eq.positions.len());
3965 assert!(merc
3966 .positions
3967 .iter()
3968 .zip(eq.positions.iter())
3969 .any(|(a, b)| (a[0] - b[0]).abs() > 1.0 || (a[1] - b[1]).abs() > 1.0));
3970 }
3971
3972 #[test]
3977 fn tessellate_line_mode_populates_normals_and_distances() {
3978 let style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 4.0);
3979 let layer = VectorLayer::new(
3980 "line",
3981 make_layer(Geometry::LineString(two_point_line())).features,
3982 style,
3983 );
3984 let mesh = layer.tessellate(CameraProjection::WebMercator);
3985 assert!(!mesh.is_empty());
3986 assert_eq!(
3987 mesh.line_normals.len(),
3988 mesh.positions.len(),
3989 "line_normals must have one entry per vertex"
3990 );
3991 assert_eq!(
3992 mesh.line_distances.len(),
3993 mesh.positions.len(),
3994 "line_distances must have one entry per vertex"
3995 );
3996 assert!(
3998 mesh.line_distances.iter().any(|&d| d > 0.0),
3999 "at least one distance should be positive"
4000 );
4001 }
4002
4003 #[test]
4004 fn tessellate_line_mode_propagates_dash_params() {
4005 let style = VectorStyle::line_styled(
4006 [1.0, 0.0, 0.0, 1.0],
4007 4.0,
4008 LineCap::Round,
4009 LineJoin::Bevel,
4010 2.0,
4011 Some(vec![10.0, 5.0]),
4012 );
4013 let layer = VectorLayer::new(
4014 "dashed",
4015 make_layer(Geometry::LineString(two_point_line())).features,
4016 style,
4017 );
4018 let mesh = layer.tessellate(CameraProjection::WebMercator);
4019 assert_eq!(mesh.line_params[0], 10.0, "dash_length");
4020 assert_eq!(mesh.line_params[1], 5.0, "gap_length");
4021 assert_eq!(mesh.line_params[2], 1.0, "cap_round flag");
4022 }
4023
4024 #[test]
4025 fn tessellate_line_mode_round_join_adds_vertices() {
4026 let line = crate::geometry::LineString {
4027 coords: vec![
4028 GeoCoord::from_lat_lon(0.0, 0.0),
4029 GeoCoord::from_lat_lon(0.0, 1.0),
4030 GeoCoord::from_lat_lon(1.0, 1.0),
4031 ],
4032 };
4033 let miter_style = VectorStyle::line_styled(
4034 [1.0, 0.0, 0.0, 1.0],
4035 4.0,
4036 LineCap::Butt,
4037 LineJoin::Miter,
4038 10.0,
4039 None,
4040 );
4041 let round_style = VectorStyle::line_styled(
4042 [1.0, 0.0, 0.0, 1.0],
4043 4.0,
4044 LineCap::Butt,
4045 LineJoin::Round,
4046 2.0,
4047 None,
4048 );
4049 let miter_layer = VectorLayer::new(
4050 "m",
4051 make_layer(Geometry::LineString(line.clone())).features,
4052 miter_style,
4053 );
4054 let round_layer = VectorLayer::new(
4055 "r",
4056 make_layer(Geometry::LineString(line)).features,
4057 round_style,
4058 );
4059 let miter_mesh = miter_layer.tessellate(CameraProjection::WebMercator);
4060 let round_mesh = round_layer.tessellate(CameraProjection::WebMercator);
4061 assert!(
4062 round_mesh.vertex_count() > miter_mesh.vertex_count(),
4063 "round join should produce more vertices than miter: {} vs {}",
4064 round_mesh.vertex_count(),
4065 miter_mesh.vertex_count(),
4066 );
4067 }
4068
4069 fn two_feature_dd_width_layer() -> VectorLayer {
4077 use crate::geometry::PropertyValue;
4078
4079 let narrow_feature = Feature {
4080 geometry: Geometry::LineString(two_point_line()),
4081 properties: {
4082 let mut p = HashMap::new();
4083 p.insert("width".into(), PropertyValue::Number(2.0));
4084 p
4085 },
4086 };
4087 let wide_feature = Feature {
4088 geometry: Geometry::LineString(two_point_line()),
4089 properties: {
4090 let mut p = HashMap::new();
4091 p.insert("width".into(), PropertyValue::Number(10.0));
4092 p
4093 },
4094 };
4095 let features = FeatureCollection {
4096 features: vec![narrow_feature, wide_feature],
4097 };
4098
4099 let mut style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 2.0);
4100 style.width_expr = Some(Expression::GetProperty {
4101 key: "width".into(),
4102 fallback: 2.0,
4103 });
4104 style.eval_zoom = 10.0;
4105
4106 VectorLayer::new("dd_width", features, style)
4107 }
4108
4109 #[test]
4110 fn data_driven_width_produces_different_ribbon_widths() {
4111 let layer = two_feature_dd_width_layer();
4112 let mesh = layer.tessellate(CameraProjection::WebMercator);
4113
4114 assert!(!mesh.is_empty());
4116
4117 let narrow_style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 2.0);
4119 let wide_style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 10.0);
4120
4121 let narrow_layer = VectorLayer::new(
4122 "narrow",
4123 FeatureCollection {
4124 features: vec![layer.features.features[0].clone()],
4125 },
4126 narrow_style,
4127 );
4128 let wide_layer = VectorLayer::new(
4129 "wide",
4130 FeatureCollection {
4131 features: vec![layer.features.features[1].clone()],
4132 },
4133 wide_style,
4134 );
4135
4136 let narrow_mesh = narrow_layer.tessellate(CameraProjection::WebMercator);
4137 let wide_mesh = wide_layer.tessellate(CameraProjection::WebMercator);
4138
4139 let narrow_span = position_y_span(&narrow_mesh);
4141 let wide_span = position_y_span(&wide_mesh);
4142 assert!(
4143 wide_span > narrow_span,
4144 "wide ribbon span ({wide_span}) must exceed narrow ribbon span ({narrow_span})",
4145 );
4146
4147 let combined_span = position_y_span(&mesh);
4150 assert!(
4151 combined_span >= wide_span * 0.99,
4152 "combined mesh span ({combined_span}) should be >= wide span ({wide_span})",
4153 );
4154 }
4155
4156 #[test]
4157 fn data_driven_color_assigns_per_feature_colors() {
4158 use crate::geometry::PropertyValue;
4159
4160 let red_feature = Feature {
4161 geometry: Geometry::LineString(two_point_line()),
4162 properties: {
4163 let mut p = HashMap::new();
4164 p.insert("kind".into(), PropertyValue::String("highway".into()));
4165 p
4166 },
4167 };
4168 let blue_feature = Feature {
4169 geometry: Geometry::LineString(two_point_line()),
4170 properties: {
4171 let mut p = HashMap::new();
4172 p.insert("kind".into(), PropertyValue::String("local".into()));
4173 p
4174 },
4175 };
4176 let features = FeatureCollection {
4177 features: vec![red_feature, blue_feature],
4178 };
4179
4180 let mut style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 4.0);
4181 style.stroke_color_expr = Some(Expression::Match {
4182 input: Box::new(crate::expression::StringExpression::GetProperty {
4183 key: "kind".into(),
4184 fallback: String::new(),
4185 }),
4186 cases: vec![
4187 ("highway".into(), [1.0, 0.0, 0.0, 1.0]),
4188 ("local".into(), [0.0, 0.0, 1.0, 1.0]),
4189 ],
4190 fallback: [0.5, 0.5, 0.5, 1.0],
4191 });
4192 style.eval_zoom = 10.0;
4193
4194 let layer = VectorLayer::new("dd_color", features, style);
4195 let mesh = layer.tessellate(CameraProjection::WebMercator);
4196
4197 assert!(!mesh.is_empty());
4198
4199 let has_red = mesh
4201 .colors
4202 .iter()
4203 .any(|c| c[0] > 0.9 && c[1] < 0.1 && c[2] < 0.1);
4204 let has_blue = mesh
4205 .colors
4206 .iter()
4207 .any(|c| c[0] < 0.1 && c[1] < 0.1 && c[2] > 0.9);
4208 assert!(
4209 has_red,
4210 "mesh should contain red vertices from highway feature"
4211 );
4212 assert!(
4213 has_blue,
4214 "mesh should contain blue vertices from local feature"
4215 );
4216 }
4217
4218 #[test]
4219 fn non_data_driven_width_expr_uses_uniform_value() {
4220 let features = FeatureCollection {
4223 features: vec![
4224 Feature {
4225 geometry: Geometry::LineString(two_point_line()),
4226 properties: HashMap::new(),
4227 },
4228 Feature {
4229 geometry: Geometry::LineString(two_point_line()),
4230 properties: HashMap::new(),
4231 },
4232 ],
4233 };
4234 let mut style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 4.0);
4235 style.width_expr = Some(Expression::Constant(4.0));
4236 style.eval_zoom = 10.0;
4237
4238 let layer = VectorLayer::new("uniform", features, style);
4239 let mesh = layer.tessellate(CameraProjection::WebMercator);
4240 assert!(!mesh.is_empty());
4241
4242 let first_color = mesh.colors[0];
4244 assert!(mesh.colors.iter().all(|c| *c == first_color));
4245 }
4246
4247 #[test]
4248 fn data_driven_width_fingerprint_changes_with_zoom() {
4249 let mut style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 4.0);
4250 style.width_expr = Some(Expression::GetProperty {
4251 key: "width".into(),
4252 fallback: 4.0,
4253 });
4254
4255 style.eval_zoom = 5.0;
4256 let fp1 = style.tessellation_fingerprint();
4257
4258 style.eval_zoom = 10.0;
4259 let fp2 = style.tessellation_fingerprint();
4260
4261 assert_ne!(fp1, fp2, "data-driven fingerprint should differ by zoom");
4262 }
4263
4264 fn position_y_span(mesh: &VectorMeshData) -> f64 {
4266 let ys: Vec<f64> = mesh.positions.iter().map(|p| p[1]).collect();
4267 let min = ys.iter().cloned().fold(f64::INFINITY, f64::min);
4268 let max = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
4269 max - min
4270 }
4271
4272 fn blue_to_red_ramp() -> crate::visualization::ColorRamp {
4277 use crate::visualization::{ColorRamp, ColorStop};
4278 ColorRamp::new(vec![
4279 ColorStop {
4280 value: 0.0,
4281 color: [0.0, 0.0, 1.0, 1.0],
4282 },
4283 ColorStop {
4284 value: 1.0,
4285 color: [1.0, 0.0, 0.0, 1.0],
4286 },
4287 ])
4288 }
4289
4290 #[test]
4291 fn line_gradient_overrides_vertex_colors() {
4292 let ramp = blue_to_red_ramp();
4293 let style = VectorStyle::line_gradient(4.0, ramp);
4294 let layer = VectorLayer::new(
4295 "grad",
4296 FeatureCollection {
4297 features: vec![Feature {
4298 geometry: Geometry::LineString(LineString {
4299 coords: vec![
4300 GeoCoord::from_lat_lon(0.0, 0.0),
4301 GeoCoord::from_lat_lon(0.0, 1.0),
4302 ],
4303 }),
4304 properties: HashMap::new(),
4305 }],
4306 },
4307 style,
4308 );
4309 let mesh = layer.tessellate(CameraProjection::WebMercator);
4310 assert!(!mesh.colors.is_empty());
4311 let has_blue = mesh.colors.iter().any(|c| c[2] > 0.8 && c[0] < 0.2);
4314 let has_red = mesh.colors.iter().any(|c| c[0] > 0.8 && c[2] < 0.2);
4315 assert!(has_blue, "expected some blue-ish vertices near start");
4316 assert!(has_red, "expected some red-ish vertices near end");
4317 }
4318
4319 #[test]
4320 fn line_gradient_without_ramp_uses_solid_color() {
4321 let solid = [0.5, 0.5, 0.5, 1.0];
4322 let style = VectorStyle::line(solid, 4.0);
4323 let layer = VectorLayer::new(
4324 "solid",
4325 FeatureCollection {
4326 features: vec![Feature {
4327 geometry: Geometry::LineString(LineString {
4328 coords: vec![
4329 GeoCoord::from_lat_lon(0.0, 0.0),
4330 GeoCoord::from_lat_lon(0.0, 1.0),
4331 ],
4332 }),
4333 properties: HashMap::new(),
4334 }],
4335 },
4336 style,
4337 );
4338 let mesh = layer.tessellate(CameraProjection::WebMercator);
4339 assert!(!mesh.colors.is_empty());
4340 for c in &mesh.colors {
4341 assert_eq!(*c, solid, "all vertices should share the solid colour");
4342 }
4343 }
4344
4345 #[test]
4346 fn line_gradient_midpoint_is_interpolated() {
4347 use crate::visualization::{ColorRamp, ColorStop};
4348 let ramp = ColorRamp::new(vec![
4349 ColorStop {
4350 value: 0.0,
4351 color: [0.0, 0.0, 0.0, 1.0],
4352 },
4353 ColorStop {
4354 value: 1.0,
4355 color: [1.0, 1.0, 1.0, 1.0],
4356 },
4357 ]);
4358 let style = VectorStyle::line_gradient(2.0, ramp);
4359 let layer = VectorLayer::new(
4360 "mid",
4361 FeatureCollection {
4362 features: vec![Feature {
4363 geometry: Geometry::LineString(LineString {
4364 coords: vec![
4365 GeoCoord::from_lat_lon(0.0, 0.0),
4366 GeoCoord::from_lat_lon(0.0, 0.5),
4367 GeoCoord::from_lat_lon(0.0, 1.0),
4368 ],
4369 }),
4370 properties: HashMap::new(),
4371 }],
4372 },
4373 style,
4374 );
4375 let mesh = layer.tessellate(CameraProjection::WebMercator);
4376 let has_mid = mesh.colors.iter().any(|c| c[0] > 0.3 && c[0] < 0.7);
4379 assert!(
4380 has_mid,
4381 "expected midpoint vertices with interpolated colour"
4382 );
4383 }
4384
4385 #[test]
4386 fn line_gradient_style_roundtrips_through_line_style_layer() {
4387 use crate::style::{line_style_with_state, LineStyleLayer, StyleEvalContextFull};
4388 let ramp = blue_to_red_ramp();
4389 let mut layer = LineStyleLayer::new("grad", "src");
4390 layer.line_gradient = Some(ramp.clone());
4391 let state = HashMap::new();
4392 let ctx = StyleEvalContextFull::new(5.0, &state);
4393 let vs = line_style_with_state(&layer, &ctx);
4394 assert!(vs.line_gradient.is_some());
4395 }
4396
4397 fn checkerboard_2x2() -> Arc<PatternImage> {
4401 #[rustfmt::skip]
4403 let data = vec![
4404 0, 0, 0, 255, 255, 255, 255, 255,
4405 255, 255, 255, 255, 0, 0, 0, 255,
4406 ];
4407 Arc::new(PatternImage::new(2, 2, data))
4408 }
4409
4410 #[test]
4411 fn pattern_image_validates_data_length() {
4412 let img = PatternImage::new(2, 2, vec![0u8; 16]);
4414 assert_eq!(img.width, 2);
4415 assert_eq!(img.height, 2);
4416 }
4417
4418 #[test]
4419 #[should_panic(expected = "RGBA8 data length")]
4420 fn pattern_image_rejects_wrong_data_length() {
4421 let _img = PatternImage::new(2, 2, vec![0u8; 10]);
4422 }
4423
4424 #[test]
4425 fn fill_pattern_generates_uvs() {
4426 let pattern = checkerboard_2x2();
4427 let style = VectorStyle::fill_pattern(pattern);
4428
4429 let geom = Geometry::Polygon(square_polygon());
4430 let features = FeatureCollection {
4431 features: vec![Feature {
4432 geometry: geom,
4433 properties: HashMap::new(),
4434 }],
4435 };
4436 let layer = VectorLayer::new("pat", features, style);
4437 let mesh = layer.tessellate(CameraProjection::WebMercator);
4438
4439 assert!(mesh.fill_pattern.is_some());
4441 assert!(
4442 !mesh.fill_pattern_uvs.is_empty(),
4443 "expected fill_pattern_uvs to be non-empty"
4444 );
4445 assert!(
4449 mesh.fill_pattern_uvs.len() <= mesh.positions.len(),
4450 "fill_pattern_uvs should not exceed positions count"
4451 );
4452 }
4453
4454 #[test]
4455 fn solid_fill_has_no_pattern_uvs() {
4456 let style = VectorStyle::fill([0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 0.0, 1.0], 1.0);
4457
4458 let geom = Geometry::Polygon(square_polygon());
4459 let features = FeatureCollection {
4460 features: vec![Feature {
4461 geometry: geom,
4462 properties: HashMap::new(),
4463 }],
4464 };
4465 let layer = VectorLayer::new("solid", features, style);
4466 let mesh = layer.tessellate(CameraProjection::WebMercator);
4467
4468 assert!(mesh.fill_pattern.is_none());
4469 assert!(
4470 mesh.fill_pattern_uvs.is_empty(),
4471 "solid fills should not generate pattern UVs"
4472 );
4473 }
4474
4475 #[test]
4476 fn fill_pattern_style_roundtrips_through_fill_style_layer() {
4477 use crate::style::{fill_style_with_state, FillStyleLayer, StyleEvalContextFull};
4478 let pattern = checkerboard_2x2();
4479 let mut layer = FillStyleLayer::new("fp", "src");
4480 layer.fill_pattern = Some(pattern.clone());
4481 let state = HashMap::new();
4482 let ctx = StyleEvalContextFull::new(5.0, &state);
4483 let vs = fill_style_with_state(&layer, &ctx);
4484 assert!(vs.fill_pattern.is_some());
4485 assert_eq!(vs.fill_pattern.as_ref().unwrap().width, 2);
4486 }
4487
4488 fn line_feature() -> FeatureCollection {
4493 FeatureCollection {
4494 features: vec![Feature {
4495 geometry: Geometry::LineString(LineString {
4496 coords: vec![
4497 GeoCoord::from_lat_lon(0.0, 0.0),
4498 GeoCoord::from_lat_lon(0.0, 1.0),
4499 ],
4500 }),
4501 properties: HashMap::new(),
4502 }],
4503 }
4504 }
4505
4506 #[test]
4507 fn line_pattern_generates_uvs() {
4508 let pattern = checkerboard_2x2();
4509 let style = VectorStyle::line_pattern(4.0, pattern);
4510 let layer = VectorLayer::new("lp", line_feature(), style);
4511 let mesh = layer.tessellate(CameraProjection::WebMercator);
4512
4513 assert!(
4514 mesh.line_pattern.is_some(),
4515 "pattern image should be carried to mesh"
4516 );
4517 assert!(
4518 !mesh.line_pattern_uvs.is_empty(),
4519 "expected line_pattern_uvs to be non-empty"
4520 );
4521 assert_eq!(
4522 mesh.line_pattern_uvs.len(),
4523 mesh.positions.len(),
4524 "each position should have a corresponding pattern UV"
4525 );
4526 }
4527
4528 #[test]
4529 fn line_pattern_uvs_have_correct_v_range() {
4530 let pattern = checkerboard_2x2();
4531 let style = VectorStyle::line_pattern(4.0, pattern);
4532 let layer = VectorLayer::new("lp", line_feature(), style);
4533 let mesh = layer.tessellate(CameraProjection::WebMercator);
4534
4535 let has_left = mesh
4538 .line_pattern_uvs
4539 .iter()
4540 .any(|uv| (uv[1] - 0.0).abs() < 0.01);
4541 let has_right = mesh
4542 .line_pattern_uvs
4543 .iter()
4544 .any(|uv| (uv[1] - 1.0).abs() < 0.01);
4545 assert!(has_left, "expected some V=0.0 vertices (left edge)");
4546 assert!(has_right, "expected some V=1.0 vertices (right edge)");
4547
4548 for uv in &mesh.line_pattern_uvs {
4550 assert!(
4551 uv[1] >= -0.01 && uv[1] <= 1.01,
4552 "V coordinate {:.3} outside [0, 1]",
4553 uv[1]
4554 );
4555 }
4556 }
4557
4558 #[test]
4559 fn solid_line_has_no_pattern_uvs() {
4560 let style = VectorStyle::line([1.0, 0.0, 0.0, 1.0], 4.0);
4561 let layer = VectorLayer::new("solid", line_feature(), style);
4562 let mesh = layer.tessellate(CameraProjection::WebMercator);
4563
4564 assert!(mesh.line_pattern.is_none());
4565 assert!(
4566 mesh.line_pattern_uvs.is_empty(),
4567 "solid lines should not generate pattern UVs"
4568 );
4569 }
4570
4571 #[test]
4572 fn line_pattern_style_constructor() {
4573 let pattern = checkerboard_2x2();
4574 let style = VectorStyle::line_pattern(6.0, pattern.clone());
4575 assert_eq!(style.render_mode, VectorRenderMode::Line);
4576 assert_eq!(style.stroke_width, 6.0);
4577 assert!(style.line_pattern.is_some());
4578 assert_eq!(style.line_pattern.as_ref().unwrap().width, 2);
4579 }
4580
4581 #[test]
4582 fn line_pattern_style_roundtrips_through_line_style_layer() {
4583 use crate::style::{line_style_with_state, LineStyleLayer, StyleEvalContextFull};
4584 let pattern = checkerboard_2x2();
4585 let mut layer = LineStyleLayer::new("lp", "src");
4586 layer.line_pattern = Some(pattern.clone());
4587 let state = HashMap::new();
4588 let ctx = StyleEvalContextFull::new(5.0, &state);
4589 let vs = line_style_with_state(&layer, &ctx);
4590 assert!(vs.line_pattern.is_some());
4591 assert_eq!(vs.line_pattern.as_ref().unwrap().width, 2);
4592 }
4593
4594 #[test]
4595 fn line_pattern_u_increases_along_line() {
4596 let pattern = checkerboard_2x2();
4597 let style = VectorStyle::line_pattern(4.0, pattern);
4598
4599 let features = FeatureCollection {
4601 features: vec![Feature {
4602 geometry: Geometry::LineString(LineString {
4603 coords: vec![
4604 GeoCoord::from_lat_lon(0.0, 0.0),
4605 GeoCoord::from_lat_lon(0.0, 5.0),
4606 ],
4607 }),
4608 properties: HashMap::new(),
4609 }],
4610 };
4611 let layer = VectorLayer::new("lp", features, style);
4612 let mesh = layer.tessellate(CameraProjection::WebMercator);
4613
4614 let body_us: Vec<f32> = mesh
4616 .line_pattern_uvs
4617 .iter()
4618 .filter(|uv| (uv[1] - 0.0).abs() < 0.01 || (uv[1] - 1.0).abs() < 0.01)
4619 .map(|uv| uv[0])
4620 .collect();
4621 assert!(
4622 !body_us.is_empty(),
4623 "should have body vertices with pattern UVs"
4624 );
4625 let max_u = body_us.iter().cloned().fold(0.0_f32, f32::max);
4627 assert!(max_u > 0.0, "max U along line should be > 0, got {max_u}");
4628 }
4629
4630 #[test]
4631 fn heatmap_tessellation_populates_heatmap_points() {
4632 let gc = |lat: f64, lon: f64| rustial_math::GeoCoord::from_lat_lon(lat, lon);
4633 let features = FeatureCollection {
4634 features: vec![Feature {
4635 geometry: Geometry::Point(Point {
4636 coord: gc(0.0, 0.0),
4637 }),
4638 properties: HashMap::new(),
4639 }],
4640 };
4641 let style = VectorStyle::heatmap([1.0, 0.0, 0.0, 1.0], 20.0, 1.0);
4642 let layer = VectorLayer::new("hm", features, style);
4643 let mesh = layer.tessellate(CameraProjection::WebMercator);
4644 assert!(
4645 !mesh.heatmap_points.is_empty(),
4646 "heatmap tessellation should populate heatmap_points"
4647 );
4648 let pt = &mesh.heatmap_points[0];
4649 assert!(pt[3] > 0.0, "heatmap radius should be positive: {}", pt[3]);
4650 }
4651
4652 #[test]
4653 fn circle_tessellation_populates_circle_instances() {
4654 let gc = |lat: f64, lon: f64| rustial_math::GeoCoord::from_lat_lon(lat, lon);
4655 let features = FeatureCollection {
4656 features: vec![Feature {
4657 geometry: Geometry::Point(Point {
4658 coord: gc(0.0, 0.0),
4659 }),
4660 properties: HashMap::new(),
4661 }],
4662 };
4663 let style = VectorStyle::circle([0.0, 1.0, 0.0, 1.0], 10.0, [0.0, 0.0, 0.0, 1.0], 2.0);
4664 let layer = VectorLayer::new("cc", features, style);
4665 let mesh = layer.tessellate(CameraProjection::WebMercator);
4666 assert!(
4667 !mesh.circle_instances.is_empty(),
4668 "circle tessellation should populate circle_instances"
4669 );
4670 assert!(
4671 mesh.circle_instances[0].radius > 0.0,
4672 "circle radius should be positive"
4673 );
4674 }
4675}