1pub(crate) mod fill;
45mod labels;
46mod marking;
47mod ticks;
48
49use std::collections::HashMap;
50
51use serde::{Deserialize, Serialize};
52
53use crate::model::{
54 AxisMode, Catalogue, CutSource, EnvelopeItem, FORMAT, FillChoice, Inputs, Interval, Item, LabelMode,
55 Mark, NEUTRAL_COLOUR, Outcome, Paint, Plan, Section, SectionKey, Side, Target, TraceItem, TraceRole,
56 UnstratifiedStyle, View, WellColouring, WellItem, Window,
57};
58use fill::Span;
59
60#[derive(Clone, Debug, PartialEq)]
66pub struct Layout {
67 pub target: Target,
69 pub title: String,
71 pub size: [f64; 2],
73 pub background: String,
75 pub frame: Rect,
77 pub window: Window,
80 pub whole: Window,
84 pub scale: [f64; 2],
86 pub exaggeration: Option<f64>,
89 pub exaggeration_text: String,
91 pub origin_text: String,
96 pub origin_x: f64,
99 pub x_axis: Axis,
101 pub y_axis: Axis,
103 pub layers: Vec<Layer>,
105 pub warnings: Vec<String>,
108 pub folded_labels: usize,
110 pub note_lines: usize,
114 pub metadata: Metadata,
116 pub left_shift: f64,
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
123pub struct Rect {
124 pub x: f64,
126 pub y: f64,
128 pub w: f64,
130 pub h: f64,
132}
133
134#[derive(Clone, Debug, PartialEq)]
136pub struct Axis {
137 pub title: String,
139 pub mode: AxisMode,
141 pub ticks: Vec<Tick>,
143}
144
145#[derive(Clone, Debug, PartialEq)]
147pub struct Tick {
148 pub value: f64,
150 pub at: f64,
152 pub label: String,
154 pub sublabel: Option<String>,
156}
157
158#[derive(Clone, Debug, PartialEq)]
160pub struct Layer {
161 pub name: &'static str,
163 pub clipped: bool,
165 pub objects: Vec<LaidObject>,
167}
168
169#[derive(Clone, Debug, PartialEq)]
172pub enum Attr {
173 Key(String),
175 Section(String),
177 FillOf(String),
179 LabelOf(String),
181 Decor(String),
183 UnitEntry(String, String, Vec<String>),
187 Marker {
190 name: String,
192 key: Option<String>,
194 },
195}
196
197#[derive(Clone, Debug, PartialEq)]
199pub struct LaidObject {
200 pub attr: Attr,
202 pub kind: String,
204 pub title: String,
206 pub highlighted: bool,
208 pub at: Option<[f64; 2]>,
210 pub shapes: Vec<Shape>,
212 pub intervals: Vec<LaidInterval>,
214}
215
216#[derive(Clone, Debug, PartialEq)]
218pub struct LaidInterval {
219 pub from: f64,
221 pub to: f64,
223 pub unit: String,
225 pub unit_uri: Option<String>,
228 pub colour: String,
230 pub width: f64,
232 pub points: Vec<[f64; 2]>,
234 pub tick: Option<[[f64; 2]; 2]>,
237}
238
239#[derive(Clone, Copy, Debug, PartialEq, Eq)]
241pub enum Anchor {
242 Start,
244 Middle,
246 End,
248}
249
250#[derive(Clone, Debug, PartialEq)]
252pub enum Shape {
253 Polyline {
255 points: Vec<[f64; 2]>,
257 stroke: String,
259 width: f64,
261 dash: bool,
263 },
264 Dashed {
267 points: Vec<[f64; 2]>,
269 stroke: String,
271 width: f64,
273 dash: f64,
275 gap: f64,
277 },
278 Polygon {
280 points: Vec<[f64; 2]>,
282 fill: String,
284 opacity: f64,
286 stroke: Option<String>,
288 dash: bool,
290 },
291 Circle {
293 c: [f64; 2],
295 r: f64,
297 fill: String,
299 },
300 Symbol {
304 at: [f64; 2],
306 kind: String,
308 size: f64,
310 fill: String,
312 stroke: String,
314 },
315 Text {
317 at: [f64; 2],
319 text: String,
321 anchor: Anchor,
323 size: f64,
325 fill: String,
327 },
328}
329
330#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
334#[serde(rename_all = "camelCase")]
335pub struct Metadata {
336 pub format: u32,
338 pub version: String,
340 pub inputs: Inputs,
342 pub view: View,
344}
345
346impl Layout {
347 pub fn written_warnings(&self) -> Vec<String> {
351 let room = self.note_lines;
352 if self.warnings.len() <= room {
353 return self.warnings.clone();
354 }
355 if room == 0 {
356 return Vec::new();
357 }
358 let mut out: Vec<String> = self.warnings[..room - 1].to_vec();
359 let hidden = self.warnings.len() - (room - 1);
360 out.push(if out.is_empty() {
361 format!("{hidden} notes - not shown at this size")
362 } else {
363 format!("+ {hidden} more notes - not shown at this size")
364 });
365 out
366 }
367
368 pub fn keys(&self) -> Vec<&str> {
371 self.layers
372 .iter()
373 .flat_map(|l| l.objects.iter())
374 .filter_map(|o| {
375 if let Attr::Key(k) = &o.attr {
376 Some(k.as_str())
377 } else {
378 None
379 }
380 })
381 .collect()
382 }
383
384 pub fn layer(&self, name: &str) -> Option<&Layer> {
386 self.layers.iter().find(|l| l.name == name)
387 }
388}
389
390const MARGIN_LEFT: f64 = 64.0;
394const MARGIN_RIGHT: f64 = 20.0;
395const MARGIN_TOP: f64 = 72.0;
396const MARGIN_BOTTOM: f64 = 46.0;
397const LINE_PX: f64 = 14.0;
398const PLAN_TOP: f64 = 36.0;
400const PLOT_SHARE: f64 = 0.5;
404const PLOT_MIN_PX: f64 = 40.0;
406const FONT_PX: f64 = 11.0;
407const SMALL_PX: f64 = 10.0;
408const FILL_OPACITY: f64 = 0.35;
409
410const LEFT_COLOUR: &str = "#1f77b4";
411const RIGHT_COLOUR: &str = "#d95f02";
412const INK: &str = "#222222";
413const MARK_COLOUR: &str = "#7b1fa2";
414
415pub fn layout(catalogue: &Catalogue, view: &View) -> Layout {
421 match view.target {
422 Target::Section { key } => section(catalogue, view, key),
423 Target::Plan => plan(catalogue, view),
424 }
425}
426
427fn metadata(catalogue: &Catalogue, view: &View) -> Metadata {
429 Metadata {
430 format: FORMAT,
431 version: env!("CARGO_PKG_VERSION").to_string(),
432 inputs: catalogue.inputs.clone(),
433 view: view.clone(),
434 }
435}
436
437fn unit_of(catalogue: &Catalogue) -> String {
438 catalogue
439 .inputs
440 .system
441 .as_ref()
442 .map(|s| s.unit.clone())
443 .unwrap_or_default()
444}
445
446fn common_warnings(catalogue: &Catalogue) -> Vec<String> {
448 let mut out = Vec::new();
449 let refused = catalogue
450 .objects
451 .iter()
452 .filter(|r| matches!(r.outcome, Outcome::Refused { .. }))
453 .count();
454 if refused > 0 {
455 out.push(format!("{refused} objects refused - see the objects list"));
456 }
457 let set_aside = catalogue
458 .plan
459 .wells
460 .iter()
461 .filter(|w| w.rejected.is_some())
462 .count();
463 if set_aside > 0 {
464 out.push(format!("{set_aside} wells set aside - projected nowhere"));
465 }
466 out
467}
468
469#[derive(Clone, Copy)]
471struct Map {
472 window: Window,
473 frame: Rect,
474 sx: f64,
475 sy: f64,
476}
477
478impl Map {
479 fn px(&self, p: [f64; 2]) -> [f64; 2] {
480 [
481 self.frame.x + (p[0] - self.window.x[0]) * self.sx,
482 self.frame.y + self.frame.h - (p[1] - self.window.y[0]) * self.sy,
483 ]
484 }
485 fn points(&self, pts: &[[f64; 2]]) -> Vec<[f64; 2]> {
486 dedupe(
487 pts.iter()
488 .filter(|p| p[0].is_finite() && p[1].is_finite())
489 .map(|&p| self.px(p))
490 .collect(),
491 )
492 }
493}
494
495fn dedupe(points: Vec<[f64; 2]>) -> Vec<[f64; 2]> {
499 let mut out: Vec<[f64; 2]> = Vec::with_capacity(points.len());
500 for p in points {
501 if out
502 .last()
503 .is_none_or(|q| (q[0] - p[0]).abs() >= 0.05 || (q[1] - p[1]).abs() >= 0.05)
504 {
505 out.push(p);
506 }
507 }
508 out
509}
510
511#[derive(Default)]
513struct Extent {
514 x: Option<[f64; 2]>,
515 y: Option<[f64; 2]>,
516}
517
518impl Extent {
519 fn add(&mut self, p: [f64; 2]) {
520 if !(p[0].is_finite() && p[1].is_finite()) {
521 return;
522 }
523 let grow = |r: &mut Option<[f64; 2]>, v: f64| {
524 *r = Some(match *r {
525 Some([a, b]) => [a.min(v), b.max(v)],
526 None => [v, v],
527 })
528 };
529 grow(&mut self.x, p[0]);
530 grow(&mut self.y, p[1]);
531 }
532 fn add_all(&mut self, pts: &[[f64; 2]]) {
533 pts.iter().for_each(|&p| self.add(p));
534 }
535}
536
537fn padded(range: Option<[f64; 2]>, margin: f64) -> [f64; 2] {
539 let [a, b] = range.unwrap_or([0.0, 1.0]);
540 let span = b - a;
541 if span <= f64::EPSILON * a.abs().max(1.0) {
542 return [a - 0.5, b + 0.5];
543 }
544 [a - span * margin, b + span * margin]
545}
546
547fn frame_of(view: &View, lines: usize, right: f64, left: f64, top: f64) -> Rect {
551 let [w, h] = view.size_px;
552 let bottom = MARGIN_BOTTOM + LINE_PX * lines as f64;
553 Rect {
554 x: MARGIN_LEFT + left,
555 y: top,
556 w: (w - MARGIN_LEFT - left - MARGIN_RIGHT - right).max(10.0),
557 h: (h - top - bottom).max(10.0),
558 }
559}
560
561fn budget(
567 h: f64,
568 wanted_levels: usize,
569 wanted_lines: usize,
570 top_for: impl Fn(usize) -> f64,
571) -> (f64, usize, usize) {
572 let floor = (h * PLOT_SHARE).max(PLOT_MIN_PX);
573 let (mut levels, mut lines) = (wanted_levels, wanted_lines);
574 loop {
575 let plot = h - top_for(levels) - MARGIN_BOTTOM - LINE_PX * lines as f64;
576 if plot >= floor {
577 break;
578 }
579 if lines > 1 {
580 lines -= 1;
581 } else if levels > 1 {
582 levels -= 1;
583 } else {
584 break;
585 }
586 }
587 (top_for(levels), levels, lines)
588}
589
590fn is_hidden(view: &View, key: &str) -> bool {
591 view.hidden.iter().any(|h| h == key)
592}
593
594fn width_of(view: &View, key: &str, default: f64) -> f64 {
597 view.widths
598 .iter()
599 .find(|(k, _)| k == key)
600 .map(|(_, w)| *w)
601 .filter(|w| valid_width(*w))
602 .unwrap_or(default)
603}
604
605fn valid_width(w: f64) -> bool {
606 w.is_finite() && w > 0.0 && w <= crate::model::MAX_WIDTH_PX
607}
608
609fn width_warnings(view: &View) -> Vec<String> {
612 view.widths
613 .iter()
614 .filter(|(_, w)| !valid_width(*w))
615 .map(|(k, w)| {
616 let uri = k.split('\0').next_back().unwrap_or(k);
617 format!("line width {w} refused for {uri} - it must be above 0 and at most {} px; the default is drawn", crate::model::MAX_WIDTH_PX)
618 })
619 .collect()
620}
621
622pub(crate) fn colour_of(view: &View, key: &str) -> Option<String> {
623 view.colours
624 .iter()
625 .find(|(k, _)| k == key)
626 .map(|(_, c)| c.clone())
627}
628
629fn key_text(key: SectionKey) -> String {
633 match key {
634 SectionKey::Long => "long".to_string(),
635 SectionKey::Cross { index } => format!("cross-{index}"),
636 }
637}
638
639fn find_section(catalogue: &Catalogue, key: SectionKey) -> Option<&Section> {
640 match key {
641 SectionKey::Long => Some(&catalogue.long),
642 SectionKey::Cross { .. } => catalogue.crosses.iter().find(|s| s.key == key),
643 }
644}
645
646fn span_extent(span: &[[f64; 2]]) -> f64 {
648 match (span.first(), span.last()) {
649 (Some(a), Some(b)) => (b[0] - a[0]).abs(),
650 _ => 0.0,
651 }
652}
653
654pub(crate) fn fallback_colour(uuid: &str, role: TraceRole) -> String {
659 if uuid.is_empty() {
660 default_trace_colour(role).to_string()
661 } else {
662 energyml_domain::color::derived_colour(uuid)
663 }
664}
665
666fn envelope_colour(view: &View, e: &EnvelopeItem) -> String {
668 colour_of(view, &e.object.key())
669 .or_else(|| e.colour.clone())
670 .unwrap_or_else(|| fallback_colour(&e.object.uuid, TraceRole::Other))
671}
672
673pub(crate) fn trace_colour(view: &View, t: &TraceItem) -> String {
678 colour_of(view, &t.object.key())
679 .or_else(|| t.colour.clone())
680 .unwrap_or_else(|| fallback_colour(&t.object.uuid, t.role))
681}
682
683fn default_trace_colour(role: TraceRole) -> &'static str {
684 match role {
685 TraceRole::Horizon => "#2e7d32",
686 TraceRole::Fault => "#c62828",
687 TraceRole::Other => NEUTRAL_COLOUR,
688 }
689}
690
691struct KeptTrace<'a> {
694 item: &'a TraceItem,
695 key: String,
696 colour: String,
697 spans: Vec<Span>,
698}
699
700fn section(catalogue: &Catalogue, view: &View, key: SectionKey) -> Layout {
701 let mut warnings = common_warnings(catalogue);
702 warnings.extend(width_warnings(view));
703 let unit = unit_of(catalogue);
704 let title = match key {
705 SectionKey::Long => "Long section".to_string(),
706 SectionKey::Cross { index } => format!("Cross section {index}"),
707 };
708 let sec = find_section(catalogue, key);
709 if sec.is_none() {
710 warnings.push(format!("{title}: not in the catalogue - nothing to draw"));
711 }
712 let empty = Section {
713 key,
714 trace: Vec::new(),
715 length: 0.0,
716 marks: Vec::new(),
717 band: Vec::new(),
718 items: Vec::new(),
719 z_range: None,
720 };
721 let sec = sec.unwrap_or(&empty);
722
723 let portion = view.portion.map(|[a, b]| if a <= b { [a, b] } else { [b, a] });
725 let (plo, phi) = portion.map_or((f64::NEG_INFINITY, f64::INFINITY), |[a, b]| (a, b));
726 let in_portion = |s: f64| s >= plo && s <= phi;
727
728 let mut wells: Vec<&WellItem> = Vec::new();
730 let mut traces: Vec<KeptTrace> = Vec::new();
731 #[allow(clippy::type_complexity)]
733 let mut envelopes: Vec<(
734 &EnvelopeItem,
735 Vec<Span>,
736 Vec<Span>,
737 Vec<Vec<[f64; 2]>>,
738 Vec<Vec<[f64; 2]>>,
739 )> = Vec::new();
740 let mut time_drawn = false;
741 for item in &sec.items {
742 match item {
743 Item::Well(w) => {
744 let k = w.object.key();
745 if !is_hidden(view, &k) && in_portion(w.s) {
746 wells.push(w);
747 }
748 }
749 Item::Trace(t) => {
750 let k = t.object.key();
751 if is_hidden(view, &k) {
752 continue;
753 }
754 let spans: Vec<Span> = t
755 .spans
756 .iter()
757 .filter_map(|s| fill::clip_span(s, plo, phi))
758 .collect();
759 if spans.is_empty() {
760 continue;
761 }
762 time_drawn |= t.time;
763 let colour = trace_colour(view, t);
767 traces.push(KeptTrace {
768 item: t,
769 key: k,
770 colour,
771 spans,
772 });
773 }
774 Item::Envelope(e) => {
775 let k = e.object.key();
776 if is_hidden(view, &k) {
777 continue;
778 }
779 let top: Vec<Span> = e
780 .top
781 .iter()
782 .filter_map(|s| fill::clip_span(s, plo, phi))
783 .collect();
784 let bottom: Vec<Span> = e
785 .bottom
786 .iter()
787 .filter_map(|s| fill::clip_span(s, plo, phi))
788 .collect();
789 let fills: Vec<_> = e
790 .fills
791 .iter()
792 .map(|p| fill::clip_polygon(p, plo, phi))
793 .filter(|p| !p.is_empty())
794 .collect();
795 let faults: Vec<_> = e
796 .faults
797 .iter()
798 .filter_map(|s| fill::clip_span(s, plo, phi))
799 .collect();
800 if top.is_empty() && bottom.is_empty() && fills.is_empty() && faults.is_empty() {
801 continue;
802 }
803 time_drawn |= e.time;
804 envelopes.push((e, top, bottom, fills, faults));
805 }
806 }
807 }
808 if time_drawn {
809 warnings.push(match catalogue.inputs.time_factor {
810 Some(f) => {
811 format!("surfaces in time are drawn on the depth axis at {f} depth units per time unit")
812 }
813 None => "surfaces in time are drawn on the depth axis without a time scale".to_string(),
814 });
815 }
816 for (code, one, many) in [
818 (crate::erosion::CODE, "erosion suspected", "erosions suspected"),
819 ("crossingMismatch", "crossing mismatch", "crossing mismatches"),
820 ] {
821 let count = catalogue
822 .notes
823 .iter()
824 .filter(|n| n.code == code && n.sections.contains(&key))
825 .count();
826 if count > 0 {
827 warnings.push(format!(
828 "{count} {} - see the notes",
829 if count == 1 { one } else { many }
830 ));
831 }
832 }
833
834 let whole = {
837 let mut e = Extent::default();
838 e.add([portion.map_or(0.0, |p| p[0]), 0.0]);
839 e.add([portion.map_or(sec.length, |p| p[1]), 0.0]);
840 let mut z = Extent::default();
841 for w in &wells {
842 z.add(w.head);
843 z.add_all(&w.path);
844 }
845 for t in &traces {
846 t.spans.iter().for_each(|s| z.add_all(s));
847 }
848 for (_, top, bottom, fills, faults) in &envelopes {
849 top.iter().chain(bottom).chain(faults).for_each(|s| z.add_all(s));
850 fills.iter().for_each(|s| z.add_all(s));
851 }
852 Window {
853 x: padded(e.x, 0.02),
854 y: padded(z.y, 0.05),
855 }
856 };
857 let window = view.window.unwrap_or(whole);
858 wells.retain(|w| {
860 view.window.is_none()
861 || w.path
862 .iter()
863 .chain(std::iter::once(&w.head))
864 .any(|p| p[0] >= window.x[0] && p[0] <= window.x[1])
865 });
866
867 let mut mode = view.axis;
869 if mode == AxisMode::Geodesic {
870 warnings.push(
871 "geodesic axis not available: the horizontal axis shows the projected distance \
872 (a scale factor along the trace from energyml-proj is needed)"
873 .to_string(),
874 );
875 mode = AxisMode::Projected;
876 }
877 for w in &wells {
880 if let Some((paint, _)) = paint_of(view, w)
881 && let Err(why) = crate::paint::check(w.path.len(), paint, &view.joints)
882 {
883 warnings.push(paint_refusal(w, &why));
884 }
885 }
886 let strata = if view.strata_legend {
890 strata_units(&wells, view)
891 } else {
892 Vec::new()
893 };
894 if view.strata_legend && strata.is_empty() {
895 warnings.push(strata_refusal(&wells, view));
896 }
897 let unstratified = effective_style(view, &mut warnings);
898 if unstratified == UnstratifiedStyle::Hidden && view.well_colouring == WellColouring::Strata {
899 let lost = wells
900 .iter()
901 .filter(|w| !painted(view, w) && has_unstratified(w))
902 .count();
903 if lost > 0 {
904 warnings.push(format!(
905 "{lost} well(s) drawn only where a stratigraphic unit is known - the portions with no strata information are not drawn"
906 ));
907 }
908 }
909 let no_strata = (view.strata_legend
912 && !strata.is_empty()
913 && unstratified != UnstratifiedStyle::Hidden
914 && !unstratified.is_default()
915 && wells.iter().any(|w| !painted(view, w) && has_unstratified(w)))
916 .then(|| unstratified.clone());
917 let marked = marking::resolve(view, &wells, &traces, &mut warnings);
920 let strata_w = strata_width(&strata, no_strata.is_some(), view);
921 let left_shift = if strata_w > 0.0 {
922 strata_w + STRATA_GAP_PX
923 } else {
924 0.0
925 };
926 let labelled = view.show.names || view.show.distances;
927 let reserved = if view.show.notes {
930 warnings.len() + usize::from(labelled && !wells.is_empty())
931 } else {
932 0
933 };
934 let coded = (view.labels == LabelMode::Codes).then(|| codes(catalogue));
937 let mut entries: Vec<LegendEntry> = Vec::new();
938 if let Some(coded) = &coded {
939 for w in &wells {
940 if let Some(code) = coded.get(&w.object.key()) {
941 entries.push(LegendEntry {
942 code: code.clone(),
943 title: well_name(&w.object),
944 colour: NEUTRAL_COLOUR.into(),
945 round: true,
946 });
947 }
948 }
949 for t in &traces {
950 if let Some(code) = coded.get(&t.key) {
951 entries.push(LegendEntry {
952 code: code.clone(),
953 title: t.item.object.title.clone(),
954 colour: t.colour.clone(),
955 round: false,
956 });
957 }
958 }
959 for (e, ..) in &envelopes {
960 if let Some(code) = coded.get(&e.object.key()) {
961 entries.push(LegendEntry {
962 code: code.clone(),
963 title: e.object.title.clone(),
964 colour: envelope_colour(view, e),
965 round: false,
966 });
967 }
968 }
969 }
970 let legend_w = legend_width(&entries);
971 let markers_w = marked.width();
972 let right_w = legend_w.max(markers_w);
973 let label_lines = usize::from(view.show.names) + usize::from(view.show.distances);
977 let banded = label_lines > 0 && !wells.is_empty();
978 let line_h = (label_lines as f64 - 1.0) * LINE_PX + FONT_PX + 3.0;
979 let top_for = |levels: usize| {
980 if banded {
981 TITLE_BAND_PX + levels as f64 * (line_h + 3.0) + 8.0
982 } else {
983 MARGIN_TOP
984 }
985 };
986 let (top, _, note_lines) = budget(view.size_px[1], HEAD_BAND_LEVELS, reserved, top_for);
987 let frame = frame_of(
988 view,
989 note_lines,
990 if right_w > 0.0 { right_w + 12.0 } else { 0.0 },
991 left_shift,
992 top,
993 );
994 let sx = frame.w / (window.x[1] - window.x[0]);
995 let sy = frame.h / (window.y[1] - window.y[0]);
996 let map = Map {
997 window,
998 frame,
999 sx,
1000 sy,
1001 };
1002 let exaggeration = sy / sx;
1003 let exaggeration_text = format!("vertical exaggeration ×{}", round_sig(exaggeration));
1004 let (origin_text, origin_x) = origin_line(
1005 view.show.origin,
1006 sec.trace.first().copied(),
1007 catalogue.inputs.system.as_ref(),
1008 &title,
1009 &exaggeration_text,
1010 &frame,
1011 &mut warnings,
1012 );
1013
1014 let mut layers = Vec::new();
1015
1016 let mut env_fills = Vec::new();
1021 let mut env_lines = Vec::new();
1022 for (e, top, bottom, fills, faults) in &envelopes {
1023 let key = e.object.key();
1024 let colour = envelope_colour(view, e);
1025 let polygons: Vec<Shape> = fills
1026 .iter()
1027 .map(|poly| Shape::Polygon {
1028 points: map.points(poly),
1029 fill: colour.clone(),
1030 opacity: FILL_OPACITY,
1031 stroke: None,
1032 dash: false,
1033 })
1034 .collect();
1035 if !polygons.is_empty() {
1036 env_fills.push(LaidObject {
1037 attr: Attr::FillOf(key.clone()),
1038 kind: "fill".into(),
1039 title: e.object.title.clone(),
1040 highlighted: false,
1041 at: None,
1042 shapes: polygons,
1043 intervals: Vec::new(),
1044 });
1045 }
1046 let lines: Vec<Shape> = top
1047 .iter()
1048 .chain(bottom)
1049 .chain(faults)
1050 .map(|line| Shape::Polyline {
1051 points: map.points(line),
1052 stroke: colour.clone(),
1053 width: width_of(view, &key, 1.0),
1054 dash: false,
1055 })
1056 .collect();
1057 env_lines.push(LaidObject {
1058 highlighted: view.highlight.contains(&key),
1059 attr: Attr::Key(key),
1060 kind: "envelope".into(),
1061 title: e.object.title.clone(),
1062 at: None,
1063 shapes: lines,
1064 intervals: Vec::new(),
1065 });
1066 }
1067 layers.push(Layer {
1068 name: "envelopes",
1069 clipped: true,
1070 objects: env_fills,
1071 });
1072
1073 let lines: Vec<fill::Line<'_>> = traces
1075 .iter()
1076 .map(|t| fill::Line {
1077 key: &t.key,
1078 item: t.item,
1079 spans: &t.spans,
1080 })
1081 .collect();
1082 let mut fill_objects = Vec::new();
1083 for (t, mine) in traces.iter().zip(&lines) {
1084 let Some(FillChoice { up, down, .. }) = view.fills.iter().find(|f| f.key == t.key) else {
1085 continue;
1086 };
1087 let mut shapes = Vec::new();
1088 for (wanted, is_up) in [(*up, true), (*down, false)] {
1089 if !wanted {
1090 continue;
1091 }
1092 for poly in fill::polygons(mine, &lines, is_up) {
1093 shapes.push(Shape::Polygon {
1094 points: map.points(&poly),
1095 fill: t.colour.clone(),
1096 opacity: FILL_OPACITY,
1097 stroke: None,
1098 dash: false,
1099 });
1100 }
1101 }
1102 if !shapes.is_empty() {
1103 fill_objects.push(LaidObject {
1104 attr: Attr::FillOf(t.key.clone()),
1105 kind: "fill".into(),
1106 title: t.item.object.title.clone(),
1107 highlighted: false,
1108 at: None,
1109 shapes,
1110 intervals: Vec::new(),
1111 });
1112 }
1113 }
1114 layers.push(Layer {
1115 name: "fills",
1116 clipped: true,
1117 objects: fill_objects,
1118 });
1119 layers.push(Layer {
1120 name: "outlines",
1121 clipped: true,
1122 objects: env_lines,
1123 });
1124
1125 let erosion_objects = erosion_marks(sec, &traces, [plo, phi], &map);
1128 if !erosion_objects.is_empty() {
1129 layers.push(Layer {
1130 name: "erosions",
1131 clipped: true,
1132 objects: erosion_objects,
1133 });
1134 }
1135
1136 let mut trace_objects: Vec<LaidObject> = Vec::new();
1138 let mut names: Vec<LineName> = Vec::new();
1139 for t in &traces {
1140 let highlighted = view.highlight.contains(&t.key);
1141 let line_width = width_of(view, &t.key, 1.5) + if highlighted { 1.5 } else { 0.0 };
1142 let paint = view
1145 .paints
1146 .iter()
1147 .find(|(k, _)| *k == t.key)
1148 .map(|(_, paint)| paint);
1149 let sources = cut_sources(t.item);
1150 let mut paint_refusal: Option<String> = None;
1151 let mut shapes: Vec<Shape> = Vec::new();
1152 for s in &t.spans {
1153 if let (Some(paint), Some(sources)) = (paint, sources)
1154 && let Some(from) = source_of_span(t.item, sources, s)
1155 {
1156 let px: Vec<[f64; 2]> = s.iter().map(|&p| map.px(p)).collect();
1157 if px.iter().all(|p| p[0].is_finite() && p[1].is_finite()) {
1158 match crate::paint::surfaces::trace_pieces(&px, from, paint) {
1159 Ok(pieces) => {
1160 shapes.extend(pieces.into_iter().map(|piece| Shape::Polyline {
1161 stroke: crate::paint::over(&piece.colour, piece.opacity, &view.background),
1162 points: piece.points,
1163 width: line_width,
1164 dash: false,
1165 }));
1166 continue;
1167 }
1168 Err(why) => {
1169 paint_refusal.get_or_insert(why);
1170 }
1171 }
1172 }
1173 }
1174 shapes.push(Shape::Polyline {
1175 points: map.points(s),
1176 stroke: t.colour.clone(),
1177 width: line_width,
1178 dash: false,
1179 });
1180 }
1181 if let Some(why) = paint_refusal {
1182 warnings.push(format!(
1183 "scene colours refused for {} - {why}; the surface is drawn in its own colour",
1184 t.item.object.title
1185 ));
1186 }
1187 if view.show.surface_names
1192 && let Some(longest) = t
1193 .spans
1194 .iter()
1195 .max_by(|a, b| span_extent(a).total_cmp(&span_extent(b)))
1196 && let Some(&end) = longest.last()
1197 && let Some(&end) = map.points(&[end]).first()
1198 {
1199 names.push(LineName {
1200 owner: trace_objects.len(),
1201 end,
1202 text: shown_name(coded.as_ref(), &t.item.object),
1203 colour: t.colour.clone(),
1204 });
1205 }
1206 trace_objects.push(LaidObject {
1207 attr: Attr::Key(t.key.clone()),
1208 kind: match t.item.role {
1209 TraceRole::Horizon => "horizon",
1210 TraceRole::Fault => "fault",
1211 TraceRole::Other => "surface",
1212 }
1213 .into(),
1214 title: t.item.object.title.clone(),
1215 highlighted,
1216 at: None,
1217 shapes,
1218 intervals: Vec::new(),
1219 });
1220 }
1221 let mut mark_shapes = Vec::new();
1223 let mut mark_placer = labels::Placer::new(labels::Rect2 {
1224 x0: frame.x,
1225 y0: frame.y,
1226 x1: frame.x + frame.w,
1227 y1: frame.y + frame.h,
1228 });
1229 let mut marks_unlabelled = 0usize;
1230 for mark in &sec.marks {
1231 let (s, kind, text, show) = match *mark {
1232 Mark::Vertex { s, index } => (s, "vertex", format!("V{index}"), view.show.vertices),
1233 Mark::Crossing { s, other } => {
1234 let text = match other {
1235 SectionKey::Long => "× long".to_string(),
1236 SectionKey::Cross { index } => format!("× cross {index}"),
1237 };
1238 (s, "crossing", text, view.show.crossings)
1239 }
1240 };
1241 if !show || !in_portion(s) || s < window.x[0] || s > window.x[1] {
1242 continue;
1243 }
1244 let x = map.px([s, 0.0])[0];
1245 let (y0, y1) = if kind == "vertex" {
1246 (frame.y + frame.h - 8.0, frame.y + frame.h)
1247 } else {
1248 (frame.y, frame.y + frame.h)
1249 };
1250 mark_shapes.push(Shape::Polyline {
1251 points: vec![[x, y0], [x, y1]],
1252 stroke: MARK_COLOUR.into(),
1253 width: 1.0,
1254 dash: kind == "crossing",
1255 });
1256 let base_y = if kind == "vertex" {
1265 frame.y + frame.h - 10.0
1266 } else {
1267 frame.y + SMALL_PX + 2.0
1268 };
1269 let step = if kind == "vertex" {
1270 -MARK_LEVEL_PX
1271 } else {
1272 MARK_LEVEL_PX
1273 };
1274 let mut forms = vec![text.clone()];
1275 let short = short_mark_text(&text);
1276 if short != text {
1277 forms.push(short);
1278 }
1279 let mut candidates: Vec<(String, labels::Rect2, [f64; 2], Anchor)> = Vec::new();
1280 for level in 0..MARK_LEVELS {
1281 let y = base_y + step * level as f64;
1282 for form in &forms {
1283 let w = labels::width_px(form, SMALL_PX);
1284 for (text_x, anchor, x0) in [
1285 (x + 3.0, Anchor::Start, x + 3.0),
1286 (x - 3.0, Anchor::End, x - 3.0 - w),
1287 ] {
1288 let rect = labels::Rect2 {
1290 x0,
1291 y0: y - SMALL_PX,
1292 x1: x0 + w,
1293 y1: y + 2.0,
1294 };
1295 candidates.push((form.clone(), rect, [text_x, y], anchor));
1296 }
1297 }
1298 }
1299 let rects: Vec<labels::Rect2> = candidates.iter().map(|c| c.1).collect();
1300 match mark_placer.place(&rects) {
1301 Some(at) => {
1302 let (text, _, at, anchor) = candidates.swap_remove(at);
1303 mark_shapes.push(Shape::Text {
1304 at,
1305 text,
1306 anchor,
1307 size: SMALL_PX,
1308 fill: MARK_COLOUR.into(),
1309 });
1310 }
1311 None => marks_unlabelled += 1,
1312 }
1313 }
1314 if marks_unlabelled > 0 {
1315 warnings.push(format!(
1316 "{marks_unlabelled} section marks unlabelled - they did not fit"
1317 ));
1318 }
1319 let marks_group = if mark_shapes.is_empty() {
1320 Vec::new()
1321 } else {
1322 vec![LaidObject {
1323 attr: Attr::Decor("marks".into()),
1324 kind: "marks".into(),
1325 title: String::new(),
1326 highlighted: false,
1327 at: None,
1328 shapes: mark_shapes,
1329 intervals: Vec::new(),
1330 }]
1331 };
1332 let mark_texts = text_rects(marks_group.iter().flat_map(|o| o.shapes.iter()));
1334 let bounds = labels::Rect2 {
1335 x0: frame.x,
1336 y0: frame.y,
1337 x1: frame.x + frame.w,
1338 y1: frame.y + frame.h,
1339 };
1340 let (placed, names_folded) = line_names(names, bounds, &mark_texts);
1341 for (owner, text) in placed {
1342 trace_objects[owner].shapes.push(text);
1343 }
1344 if names_folded > 0 {
1345 warnings.push(format!("{names_folded} surface names folded - they did not fit"));
1346 }
1347 let contours = marked.contours(&map);
1349 if !contours.is_empty() {
1350 layers.push(Layer {
1351 name: "marker-contours",
1352 clipped: true,
1353 objects: contours,
1354 });
1355 }
1356 layers.push(Layer {
1357 name: "traces",
1358 clipped: true,
1359 objects: trace_objects,
1360 });
1361
1362 let mut well_objects = Vec::new();
1364 for w in &wells {
1365 well_objects.push(laid_well(w, view, &unstratified, &map, &unit, &mut warnings));
1366 }
1367 let drawn_units: std::collections::HashSet<(String, String)> = well_objects
1370 .iter()
1371 .flat_map(|o| o.intervals.iter())
1372 .map(|iv| (iv.unit.clone(), iv.colour.clone()))
1373 .collect();
1374 layers.push(Layer {
1375 name: "wells",
1376 clipped: true,
1377 objects: well_objects,
1378 });
1379
1380 let symbols = marked.symbols(&map);
1381 if !symbols.is_empty() {
1382 layers.push(Layer {
1383 name: "markers",
1384 clipped: true,
1385 objects: symbols,
1386 });
1387 }
1388
1389 layers.push(Layer {
1390 name: "marks",
1391 clipped: true,
1392 objects: marks_group,
1393 });
1394
1395 let (label_objects, folded) = well_labels(&wells, view, &map, &unit, coded.as_ref(), &mark_texts);
1397 layers.push(Layer {
1398 name: "labels",
1399 clipped: false,
1400 objects: label_objects,
1401 });
1402 let legend_box = legend(
1403 entries,
1404 frame.x + frame.w + 12.0,
1405 legend_w,
1406 frame.y,
1407 frame.y + frame.h,
1408 );
1409 let codes_bottom = legend_box.as_ref().and_then(marking::bottom_of);
1413 layers.push(Layer {
1414 name: "legend",
1415 clipped: false,
1416 objects: legend_box.into_iter().collect(),
1417 });
1418 if !marked.is_empty() {
1419 let top = codes_bottom.map_or(frame.y, |b| b + 8.0);
1420 let objects = marked.legend(frame.x + frame.w + 12.0, markers_w, top, frame.y + frame.h);
1421 layers.push(Layer {
1422 name: MARKERS_LAYER,
1423 clipped: false,
1424 objects,
1425 });
1426 }
1427 if folded > 0 {
1428 warnings.push(format!("{folded} well labels folded - they did not fit"));
1429 }
1430 if view.strata_legend {
1431 let shown: Vec<StrataEntry> = strata
1432 .into_iter()
1433 .filter(|e| drawn_units.contains(&(e.unit.clone(), e.colour.clone())))
1434 .collect();
1435 let objects = strata_legend(
1436 &shown,
1437 view,
1438 no_strata.as_ref(),
1439 STRATA_LEFT_PX,
1440 strata_w,
1441 frame.y,
1442 frame.y + frame.h,
1443 );
1444 layers.push(Layer {
1445 name: STRATA_LAYER,
1446 clipped: false,
1447 objects,
1448 });
1449 }
1450
1451 let x_axis = x_axis(sec, &map, mode, &unit);
1453 let y_axis = y_axis(&map, &unit, "z");
1454
1455 Layout {
1456 target: view.target,
1457 title,
1458 size: view.size_px,
1459 background: view.background.clone(),
1460 frame,
1461 window,
1462 whole,
1463 scale: [sx, sy],
1464 exaggeration: Some(exaggeration),
1465 exaggeration_text,
1466 origin_text,
1467 origin_x,
1468 x_axis,
1469 y_axis,
1470 layers,
1471 warnings,
1472 folded_labels: folded,
1473 note_lines,
1474 metadata: metadata(catalogue, view),
1475 left_shift,
1476 }
1477}
1478
1479const EROSION_COLOUR: &str = "#d81b60";
1481
1482fn erosion_marks(
1487 sec: &Section,
1488 traces: &[KeptTrace<'_>],
1489 [plo, phi]: [f64; 2],
1490 map: &Map,
1491) -> Vec<LaidObject> {
1492 let mut out = Vec::new();
1493 for portion in crate::erosion::of_section(sec) {
1494 let (Some(older), Some(younger)) = (
1495 traces.iter().find(|t| t.item.object == portion.older),
1496 traces.iter().find(|t| t.item.object == portion.younger),
1497 ) else {
1498 continue;
1499 };
1500 let (lo, hi) = (portion.s0.max(plo), portion.s1.min(phi));
1501 if hi <= lo {
1502 continue;
1503 }
1504 let part = |t: &KeptTrace<'_>| t.item.spans.iter().find_map(|s| fill::clip_span(s, lo, hi));
1505 let (Some(upper), Some(lower)) = (part(older), part(younger)) else {
1506 continue;
1507 };
1508 let mut ring = upper.clone();
1509 ring.extend(lower.iter().rev().copied());
1510 out.push(LaidObject {
1511 attr: Attr::Decor("erosion".into()),
1512 kind: "erosion".into(),
1513 title: format!("{} / {}", portion.older.title, portion.younger.title),
1514 highlighted: false,
1515 at: None,
1516 shapes: vec![
1517 Shape::Polygon {
1518 points: map.points(&ring),
1519 fill: EROSION_COLOUR.into(),
1520 opacity: FILL_OPACITY,
1521 stroke: None,
1522 dash: false,
1523 },
1524 Shape::Polyline {
1525 points: map.points(&upper),
1526 stroke: EROSION_COLOUR.into(),
1527 width: 3.0,
1528 dash: true,
1529 },
1530 ],
1531 intervals: Vec::new(),
1532 });
1533 }
1534 out
1535}
1536
1537const MARK_LEVELS: usize = 3;
1539const MARK_LEVEL_PX: f64 = SMALL_PX + 2.0 + labels::GAP_PX;
1542
1543fn short_mark_text(text: &str) -> String {
1546 match text.strip_prefix("× cross ") {
1547 Some(n) => format!("×{n}"),
1548 None if text == "× long" => "×L".to_string(),
1549 None => text.to_string(),
1550 }
1551}
1552
1553const CHEVRON_EVERY_PX: f64 = 100.0;
1555const CHEVRON_CLEAR_PX: f64 = 6.0;
1557const CHEVRON_ALONG_PX: f64 = 3.0;
1559const CHEVRON_ACROSS_PX: f64 = 3.5;
1560
1561fn direction_marks(px: &[[f64; 2]], colour: &str) -> Vec<Shape> {
1568 let mut out = Vec::new();
1569 let Some(first) = px.first() else { return out };
1570 for pair in px.windows(2) {
1571 let (a, b) = (pair[0], pair[1]);
1572 let len = (b[0] - a[0]).hypot(b[1] - a[1]);
1573 if len < 2.0 * CHEVRON_CLEAR_PX {
1574 continue;
1575 }
1576 let (ux, uy) = ((b[0] - a[0]) / len, (b[1] - a[1]) / len);
1577 let (nx, ny) = (-uy, ux);
1578 let n = ((len / CHEVRON_EVERY_PX).floor() as usize).max(1);
1579 for i in 0..n {
1580 let t = (i as f64 + 0.5) / n as f64 * len;
1581 let c = [a[0] + ux * t, a[1] + uy * t];
1582 let tip = [c[0] + ux * CHEVRON_ALONG_PX, c[1] + uy * CHEVRON_ALONG_PX];
1583 let back = [c[0] - ux * CHEVRON_ALONG_PX, c[1] - uy * CHEVRON_ALONG_PX];
1584 let left = [back[0] + nx * CHEVRON_ACROSS_PX, back[1] + ny * CHEVRON_ACROSS_PX];
1585 let right = [back[0] - nx * CHEVRON_ACROSS_PX, back[1] - ny * CHEVRON_ACROSS_PX];
1586 out.push(Shape::Polyline {
1587 points: vec![left, tip, right],
1588 stroke: colour.to_string(),
1589 width: 1.2,
1590 dash: false,
1591 });
1592 }
1593 }
1594 if !out.is_empty() {
1595 out.push(Shape::Circle {
1596 c: *first,
1597 r: 2.5,
1598 fill: colour.to_string(),
1599 });
1600 }
1601 out
1602}
1603
1604const TITLE_PX: f64 = 14.0;
1606const TITLE_GAP_PX: f64 = 16.0;
1607
1608fn origin_line(
1617 on: bool,
1618 origin: Option<[f64; 2]>,
1619 system: Option<&crate::model::System>,
1620 title: &str,
1621 right_text: &str,
1622 frame: &Rect,
1623 warnings: &mut Vec<String>,
1624) -> (String, f64) {
1625 let (true, Some([x, y])) = (on, origin) else {
1626 return (String::new(), 0.0);
1627 };
1628 if !x.is_finite() || !y.is_finite() {
1629 return (String::new(), 0.0);
1630 }
1631 let code = system.map(|s| format!("EPSG:{}", s.epsg));
1632 let whole = match &code {
1633 Some(c) => format!("origin (s = 0): X {x:.1} · Y {y:.1} - {c}"),
1634 None => format!("origin (s = 0): X {x:.1} · Y {y:.1}"),
1635 };
1636 let left = frame.x + labels::width_px(title, TITLE_PX) * 1.1 + TITLE_GAP_PX;
1637 let right = frame.x + frame.w - labels::width_px(right_text, FONT_PX) - TITLE_GAP_PX;
1638 let room = right - left;
1639 let put = |text: String| {
1640 let w = labels::width_px(&text, FONT_PX);
1641 let x = (frame.x + frame.w / 2.0 - w / 2.0).clamp(left, (right - w).max(left));
1642 (text, x)
1643 };
1644 if labels::width_px(&whole, FONT_PX) <= room {
1645 return put(whole);
1646 }
1647 if let Some(c) = code {
1648 let short = format!("origin: {c}");
1649 if labels::width_px(&short, FONT_PX) <= room {
1650 warnings.push(
1651 "origin position shortened to its EPSG code - the title band is too narrow".to_string(),
1652 );
1653 return put(short);
1654 }
1655 }
1656 warnings.push("origin position not written - the title band is too narrow".to_string());
1657 (String::new(), 0.0)
1658}
1659
1660fn round_sig(v: f64) -> String {
1662 if !v.is_finite() || v <= 0.0 {
1663 return "?".to_string();
1664 }
1665 let decimals = (2 - v.log10().floor() as i64).clamp(0, 6) as usize;
1666 let text = format!("{v:.decimals$}");
1667 if text.contains('.') {
1668 text.trim_end_matches('0').trim_end_matches('.').to_string()
1669 } else {
1670 text
1671 }
1672}
1673
1674fn x_axis(sec: &Section, map: &Map, mode: AxisMode, unit: &str) -> Axis {
1675 let (step, values) = ticks::values(map.window.x[0], map.window.x[1], map.frame.w / 90.0);
1676 let suffix = if unit.is_empty() {
1677 String::new()
1678 } else {
1679 format!(" ({unit})")
1680 };
1681 let ticks = values
1682 .into_iter()
1683 .map(|v| {
1684 let sublabel = (mode == AxisMode::Coordinates && sec.trace.len() >= 2).then(|| {
1685 let [x, y] = energyml_geometry::fence::point_at(&sec.trace, v);
1686 format!("{x:.0}, {y:.0}")
1687 });
1688 Tick {
1689 value: v,
1690 at: map.px([v, 0.0])[0],
1691 label: ticks::label(v, step),
1692 sublabel,
1693 }
1694 })
1695 .collect();
1696 Axis {
1697 title: match mode {
1698 AxisMode::Coordinates => format!("distance along the trace{suffix} - true coordinates (x, y)"),
1699 _ => format!("distance along the trace{suffix}"),
1700 },
1701 mode,
1702 ticks,
1703 }
1704}
1705
1706fn y_axis(map: &Map, unit: &str, name: &str) -> Axis {
1707 let (step, values) = ticks::values(map.window.y[0], map.window.y[1], map.frame.h / 40.0);
1708 let suffix = if unit.is_empty() {
1709 String::new()
1710 } else {
1711 format!(" ({unit})")
1712 };
1713 Axis {
1714 title: format!("{name}{suffix}"),
1715 mode: AxisMode::Projected,
1716 ticks: values
1717 .into_iter()
1718 .map(|v| Tick {
1719 value: v,
1720 at: map.px([0.0, v])[1],
1721 label: ticks::label(v, step),
1722 sublabel: None,
1723 })
1724 .collect(),
1725 }
1726}
1727
1728fn well_colour(w: &WellItem, view: &View) -> String {
1730 let side = match w.side {
1731 Side::Left => LEFT_COLOUR,
1732 Side::Right => RIGHT_COLOUR,
1733 };
1734 match view.well_colouring {
1735 WellColouring::Side => side.to_string(),
1736 WellColouring::Scene => {
1739 colour_of(view, &w.object.key()).unwrap_or_else(|| NEUTRAL_COLOUR.to_string())
1740 }
1741 WellColouring::Strata => {
1744 colour_of(view, &w.object.key()).unwrap_or_else(|| NEUTRAL_COLOUR.to_string())
1745 }
1746 }
1747}
1748
1749fn cut_sources(item: &TraceItem) -> Option<&[Vec<CutSource>]> {
1756 (!item.sources.is_empty()).then_some(item.sources.as_slice())
1757}
1758
1759fn source_of_span<'a>(
1763 item: &TraceItem,
1764 sources: &'a [Vec<CutSource>],
1765 span: &[[f64; 2]],
1766) -> Option<&'a [CutSource]> {
1767 item.spans
1768 .iter()
1769 .zip(sources)
1770 .find(|(own, from)| own.as_slice() == span && from.len() == span.len())
1771 .map(|(_, from)| from.as_slice())
1772}
1773
1774fn paint_of<'a>(view: &'a View, w: &WellItem) -> Option<(&'a Paint, String)> {
1781 if view.well_colouring != WellColouring::Scene {
1787 return None;
1788 }
1789 std::iter::once(w.object.key())
1790 .chain(w.merged.iter().map(|o| o.key()))
1791 .find_map(|key| {
1792 view.paints
1793 .iter()
1794 .find(|(k, _)| *k == key)
1795 .map(|(_, paint)| (paint, key))
1796 })
1797}
1798
1799fn paint_refusal(w: &WellItem, why: &str) -> String {
1800 format!(
1801 "scene colours refused for {} - {why}; the well is drawn in its own colour",
1802 well_name(&w.object)
1803 )
1804}
1805
1806fn md_at_px(path_px: &[[f64; 2]], md: &[f64], p: [f64; 2]) -> f64 {
1809 let mut best = (f64::INFINITY, md.first().copied().unwrap_or(0.0));
1810 for (i, pair) in path_px.windows(2).enumerate() {
1811 let (a, b) = (pair[0], pair[1]);
1812 let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
1813 let len2 = dx * dx + dy * dy;
1814 let t = if len2 > 0.0 {
1815 (((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2).clamp(0.0, 1.0)
1816 } else {
1817 0.0
1818 };
1819 let (ex, ey) = (a[0] + dx * t - p[0], a[1] + dy * t - p[1]);
1820 let d2 = ex * ex + ey * ey;
1821 if d2 < best.0 {
1822 best = (d2, md[i] + (md[i + 1] - md[i]) * t);
1823 }
1824 }
1825 best.1
1826}
1827
1828fn laid_well(
1829 w: &WellItem,
1830 view: &View,
1831 unstratified: &UnstratifiedStyle,
1832 map: &Map,
1833 unit: &str,
1834 warnings: &mut Vec<String>,
1835) -> LaidObject {
1836 let key = w.object.key();
1837 let colour = well_colour(w, view);
1838 let highlighted = view.highlight.contains(&key);
1839 let width = width_of(view, &key, view.well_width_px.max(1.0));
1840 let path = map.points(&w.path);
1841 let mut shapes = Vec::new();
1842 if highlighted {
1843 shapes.push(Shape::Polyline {
1844 points: path.clone(),
1845 stroke: "#ffb300".into(),
1846 width: width + 5.0,
1847 dash: false,
1848 });
1849 }
1850
1851 let painted =
1856 paint_of(view, w).filter(|(paint, _)| crate::paint::check(w.path.len(), paint, &view.joints).is_ok());
1857 if let Some((paint, _)) = painted {
1858 let path_px: Vec<[f64; 2]> = w.path.iter().map(|&p| map.px(p)).collect();
1860 match crate::paint::well_pieces(&path_px, paint, &view.joints) {
1861 Ok(pieces) => {
1862 shapes.push(Shape::Circle {
1863 c: map.px(w.head),
1864 r: (width / 2.0 + 1.5).max(2.5),
1865 fill: colour.clone(),
1866 });
1867 let md = w.md.as_ref().filter(|md| md.len() == w.path.len());
1868 let intervals = pieces
1871 .into_iter()
1872 .map(|piece| {
1873 let (from, to) = match (md, piece.points.first(), piece.points.last()) {
1874 (Some(md), Some(&a), Some(&b)) => {
1875 (md_at_px(&path_px, md, a), md_at_px(&path_px, md, b))
1876 }
1877 _ => (0.0, 0.0),
1878 };
1879 let stroke = crate::paint::over(&piece.colour, piece.opacity, &view.background);
1880 LaidInterval {
1881 from,
1882 to,
1883 unit: String::new(),
1884 unit_uri: None,
1885 colour: stroke,
1886 width,
1887 points: piece.points,
1888 tick: None,
1889 }
1890 })
1891 .collect();
1892 return LaidObject {
1893 attr: Attr::Key(key),
1894 kind: "well".into(),
1895 title: w.object.title.clone(),
1896 highlighted,
1897 at: Some(w.head),
1898 shapes,
1899 intervals,
1900 };
1901 }
1902 Err(why) => warnings.push(paint_refusal(w, &why)),
1903 }
1904 }
1905
1906 if view.well_colouring != WellColouring::Strata || unstratified.is_default() {
1911 shapes.push(Shape::Polyline {
1913 points: path,
1914 stroke: colour.clone(),
1915 width,
1916 dash: false,
1917 });
1918 } else {
1919 shapes.extend(unstratified_shapes(w, map, unstratified, width));
1920 }
1921 shapes.push(Shape::Circle {
1922 c: map.px(w.head),
1923 r: (width / 2.0 + 1.5).max(2.5),
1924 fill: colour.clone(),
1925 });
1926
1927 let mut intervals = Vec::new();
1928 let mut last_to: Option<f64> = None;
1929 if let Some(md) = w.md.as_ref().filter(|md| md.len() == w.path.len()) {
1930 for iv in &w.intervals {
1931 let pts = sub_path(&w.path, md, iv.from, iv.to);
1932 let points = map.points(&pts);
1933 if points.len() < 2 {
1934 continue;
1935 }
1936 let drawn = interval_colour(iv, &w.object.store, &colour, view);
1938 let tick = if view.strata_ticks && view.well_colouring == WellColouring::Strata {
1939 let adjacent = last_to.is_some_and(|before| (before - iv.from).abs() < 1e-9);
1940 if adjacent {
1941 tick_across(&points, width)
1942 } else {
1943 None
1944 }
1945 } else {
1946 None
1947 };
1948 last_to = Some(iv.to);
1949 intervals.push(LaidInterval {
1950 from: iv.from,
1951 to: iv.to,
1952 unit: iv.unit.clone(),
1953 unit_uri: iv.unit_uri.clone(),
1954 colour: drawn,
1955 width,
1956 points,
1957 tick,
1958 });
1959 }
1960 }
1961 let _ = unit;
1962 LaidObject {
1963 attr: Attr::Key(key),
1964 kind: "well".into(),
1965 title: w.object.title.clone(),
1966 highlighted,
1967 at: Some(w.head),
1968 shapes,
1969 intervals,
1970 }
1971}
1972
1973fn effective_style(view: &View, warnings: &mut Vec<String>) -> UnstratifiedStyle {
1976 let style = view.unstratified_style();
1977 match style.refusal() {
1978 None => style,
1979 Some(why) => {
1980 warnings.push(format!(
1981 "unstratified style not drawn: {why}; the default is drawn"
1982 ));
1983 UnstratifiedStyle::default()
1984 }
1985 }
1986}
1987
1988pub const HATCH_ID: &str = "esv-unstratified-hatch";
1991
1992pub fn hatch_ref() -> String {
1994 format!("url(#{HATCH_ID})")
1995}
1996
1997pub fn unstratified_spans(w: &WellItem) -> Option<Vec<(f64, f64)>> {
2002 let md =
2003 w.md.as_ref()
2004 .filter(|md| md.len() == w.path.len() && md.len() > 1)?;
2005 let (lo, hi) = (md[0], md[md.len() - 1]);
2006 let mut covered: Vec<(f64, f64)> = w
2007 .intervals
2008 .iter()
2009 .map(|iv| (iv.from.max(lo), iv.to.min(hi)))
2010 .filter(|(a, b)| b > a)
2011 .collect();
2012 covered.sort_by(|a, b| a.0.total_cmp(&b.0));
2013 let mut out = Vec::new();
2014 let mut reached = lo;
2015 for (a, b) in covered {
2016 if a > reached + 1e-9 {
2017 out.push((reached, a));
2018 }
2019 reached = reached.max(b);
2020 }
2021 if reached < hi - 1e-9 {
2022 out.push((reached, hi));
2023 }
2024 Some(out)
2025}
2026
2027fn unstratified_shapes(w: &WellItem, map: &Map, style: &UnstratifiedStyle, width: f64) -> Vec<Shape> {
2031 let pieces: Vec<Vec<[f64; 2]>> = match (unstratified_spans(w), w.md.as_ref()) {
2032 (Some(spans), Some(md)) => spans
2033 .into_iter()
2034 .map(|(a, b)| map.points(&sub_path(&w.path, md, a, b)))
2035 .collect(),
2036 _ => vec![map.points(&w.path)],
2037 };
2038 pieces
2039 .into_iter()
2040 .filter(|points| points.len() >= 2)
2041 .filter_map(|points| match style {
2042 UnstratifiedStyle::Hidden => None,
2043 UnstratifiedStyle::Solid { colour } => Some(Shape::Polyline {
2044 points,
2045 stroke: colour.clone(),
2046 width,
2047 dash: false,
2048 }),
2049 UnstratifiedStyle::Dashed { colour, dash, gap } => Some(Shape::Dashed {
2050 points,
2051 stroke: colour.clone(),
2052 width,
2053 dash: *dash,
2054 gap: *gap,
2055 }),
2056 UnstratifiedStyle::Hatched { .. } => Some(Shape::Polyline {
2057 points,
2058 stroke: hatch_ref(),
2059 width,
2060 dash: false,
2061 }),
2062 })
2063 .collect()
2064}
2065
2066const TICK_OVERHANG_PX: f64 = 2.0;
2068
2069fn tick_across(points: &[[f64; 2]], width: f64) -> Option<[[f64; 2]; 2]> {
2073 let (a, b) = (points.first()?, points.get(1)?);
2074 let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
2075 let length = dx.hypot(dy);
2076 if !length.is_finite() || length <= 0.0 {
2078 return None;
2079 }
2080 let half = width / 2.0 + TICK_OVERHANG_PX;
2081 let (nx, ny) = (-dy / length * half, dx / length * half);
2082 Some([[a[0] - nx, a[1] - ny], [a[0] + nx, a[1] + ny]])
2083}
2084
2085fn own_colour(iv: &Interval, store: &str, view: &View) -> Option<String> {
2090 let told = iv.unit_uri.as_deref().and_then(|uri| {
2091 let key = crate::model::object_key(store, uri);
2092 view.unit_colours
2093 .iter()
2094 .find(|(k, _)| *k == key)
2095 .map(|(_, colour)| colour.clone())
2096 });
2097 told.or_else(|| iv.colour.clone())
2098}
2099
2100fn interval_colour(iv: &Interval, store: &str, well_colour: &str, view: &View) -> String {
2104 match view.well_colouring {
2105 WellColouring::Strata => own_colour(iv, store, view).unwrap_or_else(|| well_colour.to_string()),
2106 _ => well_colour.to_string(),
2107 }
2108}
2109
2110fn sub_path(path: &[[f64; 2]], md: &[f64], from: f64, to: f64) -> Vec<[f64; 2]> {
2112 let mut out: Vec<[f64; 2]> = Vec::new();
2113 for i in 0..path.len().saturating_sub(1) {
2114 let (m0, m1) = (md[i], md[i + 1]);
2115 let (lo, hi) = (from.max(m0), to.min(m1));
2116 if hi < lo || m1 <= m0 {
2117 continue;
2118 }
2119 let at = |m: f64| {
2120 let t = (m - m0) / (m1 - m0);
2121 [
2122 path[i][0] + (path[i + 1][0] - path[i][0]) * t,
2123 path[i][1] + (path[i + 1][1] - path[i][1]) * t,
2124 ]
2125 };
2126 let a = at(lo);
2127 if out.last() != Some(&a) {
2128 out.push(a);
2129 }
2130 out.push(at(hi));
2131 }
2132 out
2133}
2134
2135fn well_name(object: &crate::model::ObjectId) -> String {
2137 if object.title.is_empty() {
2138 object.uuid.chars().take(8).collect()
2139 } else {
2140 object.title.clone()
2141 }
2142}
2143
2144fn side_letter(side: Side) -> &'static str {
2145 match side {
2146 Side::Left => "L",
2147 Side::Right => "R",
2148 }
2149}
2150
2151fn well_labels(
2154 wells: &[&WellItem],
2155 view: &View,
2156 map: &Map,
2157 unit: &str,
2158 coded: Option<&HashMap<String, String>>,
2159 obstacles: &[labels::Rect2],
2160) -> (Vec<LaidObject>, usize) {
2161 if !(view.show.names || view.show.distances) {
2162 return (Vec::new(), 0);
2163 }
2164 let mut cands = Vec::new();
2165 for w in wells {
2166 let head = map.px(w.head);
2167 if head[0] < map.frame.x || head[0] > map.frame.x + map.frame.w {
2168 continue;
2169 }
2170 let mut lines = Vec::new();
2171 if view.show.names {
2172 lines.push(labels::fit(
2173 &shown_name(coded, &w.object),
2174 FONT_PX,
2175 labels::MAX_WIDTH_PX,
2176 ));
2177 }
2178 if view.show.distances {
2179 let space = if unit.is_empty() { "" } else { " " };
2180 lines.push(format!("{:.0}{space}{unit} {}", w.d, side_letter(w.side)));
2181 }
2182 let floor = if head[1] < map.frame.y + map.frame.h * 0.2 {
2185 map.frame.y - 2.0
2186 } else {
2187 head[1] - 4.0
2188 };
2189 cands.push(HeadLabel {
2190 key: w.object.key(),
2191 title: w.object.title.clone(),
2192 head,
2193 lines,
2194 floor,
2195 });
2196 }
2197 let bounds = labels::Rect2 {
2199 x0: 2.0,
2200 y0: TITLE_BAND_PX,
2201 x1: map.frame.x + map.frame.w,
2202 y1: map.frame.y + map.frame.h,
2203 };
2204 head_labels(cands, bounds, obstacles)
2205}
2206
2207fn plan(catalogue: &Catalogue, view: &View) -> Layout {
2211 let p: &Plan = &catalogue.plan;
2212 let mut warnings = common_warnings(catalogue);
2213 warnings.extend(width_warnings(view));
2214 let unit = unit_of(catalogue);
2215 if view.axis != AxisMode::Projected {
2216 warnings.push("the plan is drawn in local plan coordinates whatever the axis mode".to_string());
2217 }
2218 let wells: Vec<_> = p
2219 .wells
2220 .iter()
2221 .filter(|w| !is_hidden(view, &w.object.key()))
2222 .collect();
2223
2224 let mut e = Extent::default();
2225 for (_, t) in &p.sections {
2226 e.add_all(t);
2227 }
2228 if view.show.bands {
2229 for (_, b) in &p.bands {
2230 e.add_all(b);
2231 }
2232 }
2233 for w in &wells {
2234 if w.rejected.is_none() || view.show.rejected {
2235 e.add(w.head);
2236 }
2237 for (_, f) in &w.feet {
2238 e.add(*f);
2239 }
2240 }
2241 let given = view.window;
2242 let everything = Window {
2243 x: padded(e.x, 0.05),
2244 y: padded(e.y, 0.05),
2245 };
2246 let wanted = given.unwrap_or(everything);
2247 if view.strata_legend {
2248 warnings.push("strata legend not drawn: the plan shows no strata along its wells".to_string());
2249 }
2250 if !view.markers.is_empty() {
2251 warnings.push(format!(
2252 "{} marker(s) not drawn: markers are drawn in sections only",
2253 view.markers.len()
2254 ));
2255 }
2256 let labelled = view.show.names;
2257 let reserved = if view.show.notes {
2258 warnings.len() + usize::from(labelled && !wells.is_empty())
2259 } else {
2260 0
2261 };
2262 let coded = (view.labels == LabelMode::Codes).then(|| codes(catalogue));
2264 let mut entries: Vec<LegendEntry> = Vec::new();
2265 if let Some(coded) = &coded {
2266 for w in &wells {
2267 if w.rejected.is_some() && !view.show.rejected {
2268 continue;
2269 }
2270 if let Some(code) = coded.get(&w.object.key()) {
2271 entries.push(LegendEntry {
2272 code: code.clone(),
2273 title: well_name(&w.object),
2274 colour: NEUTRAL_COLOUR.into(),
2275 round: true,
2276 });
2277 }
2278 }
2279 }
2280 let legend_w = legend_width(&entries);
2281 let (_, _, note_lines) = budget(view.size_px[1], 1, reserved, |_| PLAN_TOP);
2282 let frame = frame_of(
2283 view,
2284 note_lines,
2285 if legend_w > 0.0 { legend_w + 12.0 } else { 0.0 },
2286 0.0,
2287 PLAN_TOP,
2288 );
2289 let equal = |w: Window| {
2291 let scale = (frame.w / (w.x[1] - w.x[0])).min(frame.h / (w.y[1] - w.y[0]));
2292 let (cx, cy) = ((w.x[0] + w.x[1]) / 2.0, (w.y[0] + w.y[1]) / 2.0);
2293 (
2294 scale,
2295 Window {
2296 x: [cx - frame.w / scale / 2.0, cx + frame.w / scale / 2.0],
2297 y: [cy - frame.h / scale / 2.0, cy + frame.h / scale / 2.0],
2298 },
2299 )
2300 };
2301 let (scale, window) = equal(wanted);
2302 let whole = if given.is_some() {
2303 equal(everything).1
2304 } else {
2305 window
2306 };
2307 let map = Map {
2308 window,
2309 frame,
2310 sx: scale,
2311 sy: scale,
2312 };
2313
2314 let mut layers = Vec::new();
2315 let mut section_objects = Vec::new();
2316 if view.show.bands {
2317 for (k, band) in &p.bands {
2318 section_objects.push(LaidObject {
2319 attr: Attr::Section(key_text(*k)),
2320 kind: "band".into(),
2321 title: String::new(),
2322 highlighted: false,
2323 at: None,
2324 shapes: vec![Shape::Polygon {
2325 points: map.points(band),
2326 fill: "#90a4ae".into(),
2327 opacity: 0.15,
2328 stroke: Some("#90a4ae".into()),
2329 dash: true,
2330 }],
2331 intervals: Vec::new(),
2332 });
2333 }
2334 }
2335 layers.push(Layer {
2336 name: "fills",
2337 clipped: true,
2338 objects: section_objects,
2339 });
2340
2341 let mut lines = Vec::new();
2342 let mut directions = Vec::new();
2343 for (k, t) in &p.sections {
2344 if view.show.direction {
2345 let colour = if *k == SectionKey::Long { INK } else { MARK_COLOUR };
2346 let shapes = direction_marks(&map.points(t), colour);
2347 if !shapes.is_empty() {
2348 directions.push(LaidObject {
2349 attr: Attr::Decor(format!("direction-{}", key_text(*k))),
2350 kind: "direction".into(),
2351 title: String::new(),
2352 highlighted: false,
2353 at: None,
2354 shapes,
2355 intervals: Vec::new(),
2356 });
2357 }
2358 }
2359 lines.push(LaidObject {
2360 attr: Attr::Section(key_text(*k)),
2361 kind: "section".into(),
2362 title: String::new(),
2363 highlighted: false,
2364 at: None,
2365 shapes: vec![Shape::Polyline {
2366 points: map.points(t),
2367 stroke: if *k == SectionKey::Long { INK } else { MARK_COLOUR }.into(),
2368 width: if *k == SectionKey::Long { 2.0 } else { 1.2 },
2369 dash: false,
2370 }],
2371 intervals: Vec::new(),
2372 });
2373 }
2374 lines.extend(directions);
2375 layers.push(Layer {
2376 name: "traces",
2377 clipped: true,
2378 objects: lines,
2379 });
2380
2381 let mut well_objects = Vec::new();
2382 for w in &wells {
2383 let key = w.object.key();
2384 let out = w.rejected.is_some();
2385 if out && !view.show.rejected {
2386 continue;
2387 }
2388 let colour = if out {
2389 "#9e9e9e".to_string()
2390 } else {
2391 colour_of(view, &key).unwrap_or_else(|| NEUTRAL_COLOUR.into())
2392 };
2393 let head = map.px(w.head);
2394 let mut shapes = Vec::new();
2395 for (_, foot) in &w.feet {
2396 shapes.push(Shape::Polyline {
2397 points: vec![head, map.px(*foot)],
2398 stroke: colour.clone(),
2399 width: width_of(view, &key, 1.0),
2400 dash: true,
2401 });
2402 }
2403 shapes.push(Shape::Circle {
2404 c: head,
2405 r: 3.5,
2406 fill: colour,
2407 });
2408 well_objects.push(LaidObject {
2409 highlighted: view.highlight.contains(&key),
2410 attr: Attr::Key(key),
2411 kind: if out { "well-rejected" } else { "well" }.into(),
2412 title: w.object.title.clone(),
2413 at: Some(w.head),
2414 shapes,
2415 intervals: Vec::new(),
2416 });
2417 }
2418 layers.push(Layer {
2419 name: "wells",
2420 clipped: true,
2421 objects: well_objects,
2422 });
2423 layers.push(Layer {
2424 name: "marks",
2425 clipped: true,
2426 objects: Vec::new(),
2427 });
2428
2429 let mut label_objects = Vec::new();
2431 let mut folded = 0;
2432 if view.show.names {
2433 let mut cands = Vec::new();
2434 for w in &wells {
2435 if w.rejected.is_some() && !view.show.rejected {
2436 continue;
2437 }
2438 let head = map.px(w.head);
2439 if head[0] < frame.x
2440 || head[0] > frame.x + frame.w
2441 || head[1] < frame.y
2442 || head[1] > frame.y + frame.h
2443 {
2444 continue;
2445 }
2446 cands.push(HeadLabel {
2447 key: w.object.key(),
2448 title: w.object.title.clone(),
2449 head,
2450 lines: vec![labels::fit(
2451 &shown_name(coded.as_ref(), &w.object),
2452 FONT_PX,
2453 labels::MAX_WIDTH_PX,
2454 )],
2455 floor: head[1] - 4.0,
2456 });
2457 }
2458 let heads: Vec<labels::Rect2> = cands
2460 .iter()
2461 .map(|c| labels::Rect2 {
2462 x0: c.head[0] - 4.0,
2463 y0: c.head[1] - 4.0,
2464 x1: c.head[0] + 4.0,
2465 y1: c.head[1] + 4.0,
2466 })
2467 .collect();
2468 let bounds = labels::Rect2 {
2469 x0: frame.x,
2470 y0: frame.y,
2471 x1: frame.x + frame.w,
2472 y1: frame.y + frame.h,
2473 };
2474 (label_objects, folded) = head_labels(cands, bounds, &heads);
2475 }
2476 layers.push(Layer {
2477 name: "labels",
2478 clipped: false,
2479 objects: label_objects,
2480 });
2481 let legend_box = legend(
2482 entries,
2483 frame.x + frame.w + 12.0,
2484 legend_w,
2485 frame.y,
2486 frame.y + frame.h,
2487 );
2488 layers.push(Layer {
2489 name: "legend",
2490 clipped: false,
2491 objects: legend_box.into_iter().collect(),
2492 });
2493 if folded > 0 {
2494 warnings.push(format!("{folded} well labels folded - they did not fit"));
2495 }
2496
2497 let (xs, xv) = ticks::values(window.x[0], window.x[1], frame.w / 90.0);
2498 let x_axis = Axis {
2499 title: format!(
2500 "x, local{}",
2501 if unit.is_empty() {
2502 String::new()
2503 } else {
2504 format!(" ({unit})")
2505 }
2506 ),
2507 mode: AxisMode::Projected,
2508 ticks: xv
2509 .into_iter()
2510 .map(|v| Tick {
2511 value: v,
2512 at: map.px([v, 0.0])[0],
2513 label: ticks::label(v, xs),
2514 sublabel: None,
2515 })
2516 .collect(),
2517 };
2518 let y_axis = y_axis(&map, &unit, "y, local");
2519
2520 let (origin_text, origin_x) = origin_line(
2521 view.show.origin,
2522 Some(catalogue.plan.origin),
2523 catalogue
2524 .inputs
2525 .system
2526 .as_ref()
2527 .or(catalogue.plan.system.as_ref()),
2528 "Plan",
2529 "scale 1:1 on both axes",
2530 &frame,
2531 &mut warnings,
2532 );
2533
2534 Layout {
2535 target: view.target,
2536 title: "Plan".to_string(),
2537 size: view.size_px,
2538 background: view.background.clone(),
2539 frame,
2540 window,
2541 whole,
2542 scale: [scale, scale],
2543 exaggeration: None,
2544 exaggeration_text: "scale 1:1 on both axes".to_string(),
2545 origin_text,
2546 origin_x,
2547 x_axis,
2548 y_axis,
2549 layers,
2550 warnings,
2551 folded_labels: folded,
2552 note_lines,
2553 metadata: metadata(catalogue, view),
2554 left_shift: 0.0,
2555 }
2556}
2557
2558const HEAD_LEVELS: usize = 5;
2563const HEAD_BAND_LEVELS: usize = 3;
2565const TITLE_BAND_PX: f64 = 22.0;
2567const NAME_SHIFTS: usize = 6;
2569const LEGEND_MIN_PX: f64 = 120.0;
2571const LEGEND_MAX_PX: f64 = 280.0;
2572
2573fn codes(catalogue: &Catalogue) -> HashMap<String, String> {
2580 let sections: Vec<&Section> = std::iter::once(&catalogue.long)
2581 .chain(&catalogue.crosses)
2582 .collect();
2583 let mut wells: Vec<(String, usize, f64)> = Vec::new();
2584 let mut surfaces: Vec<(String, char, f64)> = Vec::new();
2586 let mut seen = std::collections::HashSet::new();
2587 for (rank, sec) in sections.iter().enumerate() {
2588 for item in &sec.items {
2589 match item {
2590 Item::Well(w) => {
2591 if seen.insert(w.object.key()) {
2592 wells.push((w.object.key(), rank, w.s));
2593 }
2594 }
2595 Item::Trace(t) => {
2596 if seen.insert(t.object.key()) {
2597 let (family, value) = match t.role {
2598 TraceRole::Fault => ('F', mean_of(&t.spans, 0)),
2599 TraceRole::Horizon => ('H', -mean_of(&t.spans, 1)),
2600 TraceRole::Other => ('S', -mean_of(&t.spans, 1)),
2601 };
2602 surfaces.push((t.object.key(), family, rank as f64 * 1e12 + value));
2603 }
2604 }
2605 Item::Envelope(e) => {
2606 if seen.insert(e.object.key()) {
2607 surfaces.push((e.object.key(), 'G', rank as f64 * 1e12 - mean_of(&e.top, 1)));
2608 }
2609 }
2610 }
2611 }
2612 }
2613 for w in &catalogue.plan.wells {
2614 let key = w.object.key();
2615 if seen.insert(key.clone()) {
2616 wells.push((key, usize::MAX, 0.0));
2617 }
2618 }
2619 wells.sort_by(|a, b| a.1.cmp(&b.1).then(a.2.total_cmp(&b.2)));
2620 let mut out: HashMap<String, String> = HashMap::new();
2621 for (n, (key, _, _)) in wells.into_iter().enumerate() {
2622 out.insert(key, format!("W{n}"));
2623 }
2624 for family in ['H', 'F', 'S', 'G'] {
2625 let mut these: Vec<&(String, char, f64)> = surfaces.iter().filter(|s| s.1 == family).collect();
2626 these.sort_by(|a, b| a.2.total_cmp(&b.2));
2627 for (n, (key, _, _)) in these.into_iter().enumerate() {
2628 out.insert(key.clone(), format!("{family}{n}"));
2629 }
2630 }
2631 out
2632}
2633
2634fn mean_of(spans: &[Vec<[f64; 2]>], axis: usize) -> f64 {
2636 let (sum, n) = spans
2637 .iter()
2638 .flatten()
2639 .fold((0.0, 0usize), |(sum, n), p| (sum + p[axis], n + 1));
2640 if n == 0 { 0.0 } else { sum / n as f64 }
2641}
2642
2643fn shown_name(codes: Option<&HashMap<String, String>>, object: &crate::model::ObjectId) -> String {
2646 codes
2647 .and_then(|c| c.get(&object.key()).cloned())
2648 .unwrap_or_else(|| well_name(object))
2649}
2650
2651struct HeadLabel {
2653 key: String,
2654 title: String,
2655 head: [f64; 2],
2656 lines: Vec<String>,
2657 floor: f64,
2659}
2660
2661fn head_labels(
2668 mut cands: Vec<HeadLabel>,
2669 bounds: labels::Rect2,
2670 obstacles: &[labels::Rect2],
2671) -> (Vec<LaidObject>, usize) {
2672 cands.sort_by(|a, b| a.head[0].total_cmp(&b.head[0]));
2673 let mut placer = labels::Placer::new(bounds);
2674 obstacles.iter().for_each(|r| placer.block(*r));
2675 let mut out = Vec::new();
2676 let mut folded = 0;
2677 for c in cands {
2678 let n = c.lines.len().max(1) as f64;
2679 let w = c
2680 .lines
2681 .iter()
2682 .map(|l| labels::width_px(l, FONT_PX))
2683 .fold(0.0, f64::max);
2684 let h = (n - 1.0) * LINE_PX + FONT_PX + 3.0;
2685 let mut spots: Vec<(labels::Rect2, f64, f64)> = Vec::new(); for dx in [0.0, w / 2.0 + 8.0, -(w / 2.0 + 8.0), w + 8.0, -(w + 8.0)] {
2691 for level in 0..HEAD_LEVELS {
2692 let baseline = c.head[1] - 7.0 - level as f64 * (h + 3.0);
2693 let cx = c.head[0] + dx;
2694 let rect = labels::Rect2 {
2695 x0: cx - w / 2.0,
2696 y0: baseline + 3.0 - h,
2697 x1: cx + w / 2.0,
2698 y1: baseline + 3.0,
2699 };
2700 if rect.y1 <= c.floor {
2703 spots.push((rect, cx, baseline));
2704 }
2705 }
2706 }
2707 let rects: Vec<labels::Rect2> = spots.iter().map(|s| s.0).collect();
2708 let Some(at) = placer.place(&rects) else {
2709 folded += 1;
2710 continue;
2711 };
2712 let (rect, cx, baseline) = spots[at];
2713 let mut shapes = Vec::new();
2714 if at > 0 {
2715 shapes.push(Shape::Polyline {
2717 points: vec![[cx, rect.y1], [c.head[0], c.head[1] - 4.0]],
2718 stroke: "#888888".into(),
2719 width: 0.7,
2720 dash: true,
2721 });
2722 }
2723 let lines = c.lines.len();
2724 for (j, line) in c.lines.iter().enumerate() {
2725 shapes.push(Shape::Text {
2726 at: [cx, baseline - (lines - 1 - j) as f64 * LINE_PX],
2727 text: line.clone(),
2728 anchor: Anchor::Middle,
2729 size: FONT_PX,
2730 fill: INK.into(),
2731 });
2732 }
2733 out.push(LaidObject {
2734 attr: Attr::LabelOf(c.key),
2735 kind: "label".into(),
2736 title: c.title,
2737 highlighted: false,
2738 at: None,
2739 shapes,
2740 intervals: Vec::new(),
2741 });
2742 }
2743 (out, folded)
2744}
2745
2746fn text_rects<'a>(shapes: impl Iterator<Item = &'a Shape>) -> Vec<labels::Rect2> {
2749 shapes
2750 .filter_map(|shape| match shape {
2751 Shape::Text {
2752 at,
2753 text,
2754 anchor,
2755 size,
2756 ..
2757 } => {
2758 let w = labels::width_px(text, *size);
2759 let x0 = match anchor {
2760 Anchor::Start => at[0],
2761 Anchor::Middle => at[0] - w / 2.0,
2762 Anchor::End => at[0] - w,
2763 };
2764 Some(labels::Rect2 {
2765 x0,
2766 y0: at[1] - size,
2767 x1: x0 + w,
2768 y1: at[1] + 2.0,
2769 })
2770 }
2771 _ => None,
2772 })
2773 .collect()
2774}
2775
2776struct LineName {
2778 owner: usize,
2780 end: [f64; 2],
2781 text: String,
2782 colour: String,
2783}
2784
2785fn line_names(
2791 mut names: Vec<LineName>,
2792 bounds: labels::Rect2,
2793 obstacles: &[labels::Rect2],
2794) -> (Vec<(usize, Shape)>, usize) {
2795 names.sort_by(|a, b| a.end[1].total_cmp(&b.end[1]));
2796 let mut placer = labels::Placer::new(bounds);
2797 obstacles.iter().for_each(|r| placer.block(*r));
2798 let mut out = Vec::new();
2799 let mut folded = 0;
2800 for n in names {
2801 let w = labels::width_px(&n.text, SMALL_PX);
2802 let mut spots: Vec<(labels::Rect2, [f64; 2])> = Vec::new(); for k in 0..NAME_SHIFTS {
2804 let right = n.end[0] - 3.0 - k as f64 * (w + 10.0);
2805 let above = n.end[1] - 3.0;
2806 let below = n.end[1] + SMALL_PX + 2.0;
2807 for baseline in [above, below] {
2808 let rect = labels::Rect2 {
2809 x0: right - w,
2810 y0: baseline - SMALL_PX,
2811 x1: right,
2812 y1: baseline + 2.0,
2813 };
2814 spots.push((rect, [right, baseline]));
2815 }
2816 }
2817 let rects: Vec<labels::Rect2> = spots.iter().map(|s| s.0).collect();
2818 match placer.place(&rects) {
2819 Some(at) => out.push((
2820 n.owner,
2821 Shape::Text {
2822 at: spots[at].1,
2823 text: n.text,
2824 anchor: Anchor::End,
2825 size: SMALL_PX,
2826 fill: n.colour,
2827 },
2828 )),
2829 None => folded += 1,
2830 }
2831 }
2832 (out, folded)
2833}
2834
2835pub(crate) const STRATA_LAYER: &str = "strata-legend";
2837pub(crate) const MARKERS_LAYER: &str = "markers-legend";
2839const STRATA_LEFT_PX: f64 = 4.0;
2841const STRATA_GAP_PX: f64 = 8.0;
2843
2844#[derive(Clone, Debug, PartialEq, Eq)]
2848struct StrataEntry {
2849 unit: String,
2850 colour: String,
2851 unit_uri: Option<String>,
2853 stores: Vec<String>,
2855}
2856
2857impl StrataEntry {
2858 fn same_line(&self, unit: &str, colour: &str, unit_uri: Option<&str>) -> bool {
2861 self.unit == unit && self.colour == colour && self.unit_uri.as_deref() == unit_uri
2862 }
2863
2864 fn same_unit(&self, other: &StrataEntry) -> bool {
2867 match (&self.unit_uri, &other.unit_uri) {
2868 (Some(a), Some(b)) => a == b,
2869 (None, None) => self.unit == other.unit,
2870 _ => false,
2871 }
2872 }
2873}
2874
2875fn strata_titles(entries: &[StrataEntry], view: &View) -> Vec<String> {
2880 let name = |store: &str| {
2881 view.store_names
2882 .iter()
2883 .find(|(id, _)| id == store)
2884 .map(|(_, name)| name.clone())
2885 .unwrap_or_else(|| store.to_string())
2886 };
2887 entries
2888 .iter()
2889 .enumerate()
2890 .map(|(i, e)| {
2891 let title = unit_title(&e.unit);
2892 let twice = entries
2893 .iter()
2894 .enumerate()
2895 .any(|(j, other)| j != i && other.same_unit(e));
2896 if twice && !e.stores.is_empty() {
2897 format!(
2898 "{title} - {}",
2899 e.stores.iter().map(|s| name(s)).collect::<Vec<_>>().join(", ")
2900 )
2901 } else {
2902 title
2903 }
2904 })
2905 .collect()
2906}
2907
2908fn painted(view: &View, w: &WellItem) -> bool {
2909 paint_of(view, w).is_some_and(|(paint, _)| crate::paint::check(w.path.len(), paint, &view.joints).is_ok())
2910}
2911
2912fn strata_units(wells: &[&WellItem], view: &View) -> Vec<StrataEntry> {
2920 if view.well_colouring != WellColouring::Strata {
2921 return Vec::new();
2922 }
2923 let mut nodes: Vec<StrataEntry> = Vec::new();
2924 let mut edges: Vec<(usize, usize)> = Vec::new();
2925 for w in wells {
2926 if painted(view, w) || w.md.as_ref().is_none_or(|md| md.len() != w.path.len()) {
2927 continue;
2928 }
2929 let mut intervals: Vec<&Interval> = w
2930 .intervals
2931 .iter()
2932 .filter(|iv| own_colour(iv, &w.object.store, view).is_some())
2933 .collect();
2934 intervals.sort_by(|a, b| a.from.total_cmp(&b.from));
2935 let mut previous: Option<usize> = None;
2936 for iv in intervals {
2937 let colour = interval_colour(iv, &w.object.store, "", view);
2938 let at = match nodes
2939 .iter()
2940 .position(|n| n.same_line(&iv.unit, &colour, iv.unit_uri.as_deref()))
2941 {
2942 Some(at) => {
2943 if !nodes[at].stores.contains(&w.object.store) {
2944 nodes[at].stores.push(w.object.store.clone());
2945 }
2946 at
2947 }
2948 None => {
2949 nodes.push(StrataEntry {
2950 unit: iv.unit.clone(),
2951 colour,
2952 unit_uri: iv.unit_uri.clone(),
2953 stores: vec![w.object.store.clone()],
2954 });
2955 nodes.len() - 1
2956 }
2957 };
2958 if let Some(before) = previous.filter(|&p| p != at) {
2959 edges.push((before, at));
2960 }
2961 previous = Some(at);
2962 }
2963 }
2964 let mut waiting: Vec<usize> = vec![0; nodes.len()];
2967 for &(_, to) in &edges {
2968 waiting[to] += 1;
2969 }
2970 let mut done = vec![false; nodes.len()];
2971 let mut order = Vec::with_capacity(nodes.len());
2972 while order.len() < nodes.len() {
2973 let next = (0..nodes.len())
2974 .find(|&i| !done[i] && waiting[i] == 0)
2975 .or_else(|| (0..nodes.len()).find(|&i| !done[i]));
2976 let Some(next) = next else { break };
2977 done[next] = true;
2978 order.push(nodes[next].clone());
2979 for &(from, to) in &edges {
2980 if from == next && waiting[to] > 0 {
2981 waiting[to] -= 1;
2982 }
2983 }
2984 }
2985 order
2986}
2987
2988fn has_unstratified(w: &WellItem) -> bool {
2991 unstratified_spans(w).is_none_or(|spans| !spans.is_empty())
2992}
2993
2994fn strata_refusal(wells: &[&WellItem], view: &View) -> String {
2996 if view.well_colouring != WellColouring::Strata {
2997 return "strata legend not drawn: the wells are not coloured by strata".to_string();
2998 }
2999 if wells.is_empty() {
3000 return "strata legend not drawn: no well is drawn".to_string();
3001 }
3002 let by_scene = wells.iter().filter(|w| painted(view, w)).count();
3003 if by_scene > 0 {
3004 format!(
3005 "strata legend not drawn: no unit with a colour of its own on the wells drawn \
3006 ({by_scene} well(s) are painted by the scene, whose colours carry no unit)"
3007 )
3008 } else {
3009 "strata legend not drawn: no well shows a stratigraphic unit with a colour of its own".to_string()
3010 }
3011}
3012
3013fn strata_width(entries: &[StrataEntry], no_strata: bool, view: &View) -> f64 {
3015 if entries.is_empty() {
3016 return 0.0;
3017 }
3018 let widest = strata_titles(entries, view)
3019 .iter()
3020 .map(|title| labels::width_px(title, SMALL_PX))
3021 .chain(no_strata.then(|| labels::width_px(NO_STRATA_TITLE, SMALL_PX)))
3022 .fold(labels::width_px("Stratigraphy", FONT_PX), f64::max);
3023 (widest + 36.0).clamp(LEGEND_MIN_PX, LEGEND_MAX_PX)
3024}
3025
3026const NO_STRATA_TITLE: &str = "No strata";
3029
3030fn unit_title(unit: &str) -> String {
3031 if unit.is_empty() {
3032 "(unnamed unit)".to_string()
3033 } else {
3034 unit.to_string()
3035 }
3036}
3037
3038fn strata_legend(
3043 entries: &[StrataEntry],
3044 view: &View,
3045 no_strata: Option<&UnstratifiedStyle>,
3046 x: f64,
3047 width: f64,
3048 top: f64,
3049 bottom: f64,
3050) -> Vec<LaidObject> {
3051 if entries.is_empty() {
3052 return Vec::new();
3053 }
3054 let room = (((bottom - top) - LINE_PX - 8.0) / LINE_PX).floor().max(1.0) as usize;
3055 let extra = usize::from(no_strata.is_some());
3056 let shown = if entries.len() + extra > room {
3057 room.saturating_sub(1 + extra)
3058 } else {
3059 entries.len()
3060 };
3061 let height = LINE_PX * (shown + 1 + extra + usize::from(shown < entries.len())) as f64 + 10.0;
3062 let decor = |name: &str, title: String, shapes: Vec<Shape>| LaidObject {
3063 attr: Attr::Decor(name.into()),
3064 kind: name.into(),
3065 title,
3066 highlighted: false,
3067 at: None,
3068 shapes,
3069 intervals: Vec::new(),
3070 };
3071 let mut frame = vec![
3072 Shape::Polygon {
3073 points: vec![
3074 [x, top],
3075 [x + width, top],
3076 [x + width, top + height],
3077 [x, top + height],
3078 ],
3079 fill: "#ffffff".into(),
3080 opacity: 0.92,
3081 stroke: Some("#999999".into()),
3082 dash: false,
3083 },
3084 Shape::Text {
3085 at: [x + 8.0, top + LINE_PX],
3086 text: "Stratigraphy".into(),
3087 anchor: Anchor::Start,
3088 size: FONT_PX,
3089 fill: INK.into(),
3090 },
3091 ];
3092 if shown < entries.len() {
3093 frame.push(Shape::Text {
3094 at: [x + 8.0, top + LINE_PX * ((shown + extra) as f64 + 2.0)],
3095 text: format!("+ {} more units", entries.len() - shown),
3096 anchor: Anchor::Start,
3097 size: SMALL_PX,
3098 fill: INK.into(),
3099 });
3100 }
3101 let mut out = vec![decor("strata-box", String::new(), frame)];
3102 let titles = strata_titles(entries, view);
3103 for (i, e) in entries.iter().take(shown).enumerate() {
3104 let y = top + LINE_PX * (i as f64 + 2.0);
3105 let title = titles[i].clone();
3106 let mut entry = decor(
3107 "strata-entry",
3108 e.unit.clone(),
3109 vec![
3110 Shape::Polygon {
3111 points: vec![
3112 [x + 8.0, y - 9.0],
3113 [x + 22.0, y - 9.0],
3114 [x + 22.0, y - 1.0],
3115 [x + 8.0, y - 1.0],
3116 ],
3117 fill: e.colour.clone(),
3118 opacity: 1.0,
3119 stroke: Some("#666666".into()),
3120 dash: false,
3121 },
3122 Shape::Text {
3123 at: [x + 28.0, y],
3124 text: labels::fit(&title, SMALL_PX, (width - 28.0 - 6.0).max(20.0)),
3125 anchor: Anchor::Start,
3126 size: SMALL_PX,
3127 fill: INK.into(),
3128 },
3129 ],
3130 );
3131 if let Some(uri) = &e.unit_uri {
3132 entry.attr = Attr::UnitEntry("strata-entry".into(), uri.clone(), e.stores.clone());
3133 }
3134 out.push(entry);
3135 }
3136 if let Some(style) = no_strata {
3137 let y = top + LINE_PX * (shown as f64 + 2.0);
3139 let swatch = [
3140 [x + 8.0, y - 9.0],
3141 [x + 22.0, y - 9.0],
3142 [x + 22.0, y - 1.0],
3143 [x + 8.0, y - 1.0],
3144 ];
3145 let boxed = |fill: String| Shape::Polygon {
3146 points: swatch.to_vec(),
3147 fill,
3148 opacity: 1.0,
3149 stroke: Some("#666666".into()),
3150 dash: false,
3151 };
3152 let mut shapes = match style {
3153 UnstratifiedStyle::Solid { colour } => vec![boxed(colour.clone())],
3154 UnstratifiedStyle::Dashed { colour, dash, gap } => vec![Shape::Dashed {
3155 points: vec![[x + 6.0, y - 5.0], [x + 24.0, y - 5.0]],
3156 stroke: colour.clone(),
3157 width: 3.0,
3158 dash: *dash,
3159 gap: *gap,
3160 }],
3161 UnstratifiedStyle::Hatched { .. } => vec![boxed(hatch_ref())],
3162 UnstratifiedStyle::Hidden => Vec::new(),
3163 };
3164 shapes.push(Shape::Text {
3165 at: [x + 28.0, y],
3166 text: labels::fit(NO_STRATA_TITLE, SMALL_PX, (width - 28.0 - 6.0).max(20.0)),
3167 anchor: Anchor::Start,
3168 size: SMALL_PX,
3169 fill: INK.into(),
3170 });
3171 out.push(decor("strata-none", String::new(), shapes));
3172 }
3173 out
3174}
3175
3176struct LegendEntry {
3178 code: String,
3179 title: String,
3180 colour: String,
3181 round: bool,
3182}
3183
3184fn legend_width(entries: &[LegendEntry]) -> f64 {
3186 if entries.is_empty() {
3187 return 0.0;
3188 }
3189 let widest = entries
3190 .iter()
3191 .map(|e| labels::width_px(&e.code, SMALL_PX) + 6.0 + labels::width_px(&e.title, SMALL_PX))
3192 .fold(0.0, f64::max);
3193 (widest + 30.0).clamp(LEGEND_MIN_PX, LEGEND_MAX_PX)
3194}
3195
3196fn legend(mut entries: Vec<LegendEntry>, x: f64, width: f64, top: f64, bottom: f64) -> Option<LaidObject> {
3200 if entries.is_empty() {
3201 return None;
3202 }
3203 let family = |c: &str| {
3204 ["W", "H", "F", "S", "G"]
3205 .iter()
3206 .position(|f| c.starts_with(f))
3207 .unwrap_or(9)
3208 };
3209 let number = |c: &str| c[1..].parse::<usize>().unwrap_or(0);
3210 entries.sort_by(|a, b| {
3211 family(&a.code)
3212 .cmp(&family(&b.code))
3213 .then(number(&a.code).cmp(&number(&b.code)))
3214 });
3215 let room = (((bottom - top) - LINE_PX - 8.0) / LINE_PX).floor().max(1.0) as usize;
3216 let shown = if entries.len() > room {
3217 room.saturating_sub(1)
3218 } else {
3219 entries.len()
3220 };
3221 let height = LINE_PX * (shown + 1 + usize::from(shown < entries.len())) as f64 + 10.0;
3222 let mut shapes = vec![
3223 Shape::Polygon {
3224 points: vec![
3225 [x, top],
3226 [x + width, top],
3227 [x + width, top + height],
3228 [x, top + height],
3229 ],
3230 fill: "#ffffff".into(),
3231 opacity: 0.92,
3232 stroke: Some("#999999".into()),
3233 dash: false,
3234 },
3235 Shape::Text {
3236 at: [x + 8.0, top + LINE_PX],
3237 text: "Legend".into(),
3238 anchor: Anchor::Start,
3239 size: FONT_PX,
3240 fill: INK.into(),
3241 },
3242 ];
3243 let code_w = entries
3244 .iter()
3245 .map(|e| labels::width_px(&e.code, SMALL_PX))
3246 .fold(0.0, f64::max);
3247 for (i, e) in entries.iter().take(shown).enumerate() {
3248 let y = top + LINE_PX * (i as f64 + 2.0);
3249 if e.round {
3250 shapes.push(Shape::Circle {
3251 c: [x + 12.0, y - 4.0],
3252 r: 3.5,
3253 fill: e.colour.clone(),
3254 });
3255 } else {
3256 shapes.push(Shape::Polyline {
3257 points: vec![[x + 6.0, y - 4.0], [x + 18.0, y - 4.0]],
3258 stroke: e.colour.clone(),
3259 width: 3.0,
3260 dash: false,
3261 });
3262 }
3263 shapes.push(Shape::Text {
3264 at: [x + 24.0, y],
3265 text: e.code.clone(),
3266 anchor: Anchor::Start,
3267 size: SMALL_PX,
3268 fill: INK.into(),
3269 });
3270 let room = width - 24.0 - code_w - 12.0;
3271 shapes.push(Shape::Text {
3272 at: [x + 24.0 + code_w + 6.0, y],
3273 text: labels::fit(&e.title, SMALL_PX, room.max(20.0)),
3274 anchor: Anchor::Start,
3275 size: SMALL_PX,
3276 fill: INK.into(),
3277 });
3278 }
3279 if shown < entries.len() {
3280 shapes.push(Shape::Text {
3281 at: [x + 8.0, top + LINE_PX * (shown as f64 + 2.0)],
3282 text: format!("+ {} more - in the objects list", entries.len() - shown),
3283 anchor: Anchor::Start,
3284 size: SMALL_PX,
3285 fill: INK.into(),
3286 });
3287 }
3288 Some(LaidObject {
3289 attr: Attr::Decor("legend".into()),
3290 kind: "legend".into(),
3291 title: "Legend".into(),
3292 highlighted: false,
3293 at: None,
3294 shapes,
3295 intervals: Vec::new(),
3296 })
3297}