source: git/src/gfxcore.cc @ 9fcc81a

Last change on this file since 9fcc81a was 9fcc81a, checked in by Olly Betts <olly@…>, 3 months ago

Revert "Attempt to work around macOS 10.14 problems"

The we were working around bug is fixed in git ready for wxWidgets 3.0.5
and there's a backported fix in the homebrew wxmac 3.0.4-2 package. We
direct Mac users to use homebrew, so we should no longer need this
workaround.

Closes #101, reported by floho.

This reverts commit 7750c72ceb62e675e3f8be0fdd02a1d42d9b1712.

  • Property mode set to 100644
File size: 119.1 KB
RevLine 
[5809313]1//
[156dc16]2//  gfxcore.cc
[5809313]3//
[33b2094]4//  Core drawing code for Aven.
[5809313]5//
[b72f4b5]6//  Copyright (C) 2000-2003,2005,2006 Mark R. Shinwell
[522e0bd]7//  Copyright (C) 2001-2003,2004,2005,2006,2007,2010,2011,2012,2014,2015,2016,2017,2018 Olly Betts
[887c26e]8//  Copyright (C) 2005 Martin Green
[5809313]9//
10//  This program is free software; you can redistribute it and/or modify
11//  it under the terms of the GNU General Public License as published by
12//  the Free Software Foundation; either version 2 of the License, or
13//  (at your option) any later version.
14//
15//  This program is distributed in the hope that it will be useful,
16//  but WITHOUT ANY WARRANTY; without even the implied warranty of
17//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18//  GNU General Public License for more details.
19//
20//  You should have received a copy of the GNU General Public License
21//  along with this program; if not, write to the Free Software
[5940815]22//  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
[5809313]23//
24
[cbfa50d]25#ifdef HAVE_CONFIG_H
26#include <config.h>
27#endif
28
[892a40c]29#include <assert.h>
[7aa15c0]30#include <float.h>
31
[7a89dc2]32#include "aven.h"
[672459c]33#include "aventreectrl.h"
[1ee204e]34#include "date.h"
[112f80c]35#include "filename.h"
[5809313]36#include "gfxcore.h"
[68fb07a]37#include "hash.h"
[137bf99]38#include "mainfrm.h"
[93c3f97]39#include "message.h"
[7a89dc2]40#include "useful.h"
[4283d6f]41#include "printing.h"
[5876fcb]42#include "guicontrol.h"
[6a4cdcb6]43#include "moviemaker.h"
[8000d8f]44
45#include <wx/confbase.h>
[be98901]46#include <wx/wfstream.h>
[8000d8f]47#include <wx/image.h>
[be98901]48#include <wx/zipstrm.h>
[5809313]49
[8719711]50#define ACCEPT_USE_OF_DEPRECATED_PROJ_API_H 1
[22b0a8f]51#include <proj_api.h>
52
[9df33bc]53const unsigned long DEFAULT_HGT_DIM = 3601;
54const unsigned long DEFAULT_HGT_SIZE = sqrd(DEFAULT_HGT_DIM) * 2;
55
[3d00693]56// Values for m_SwitchingTo
57#define PLAN 1
58#define ELEVATION 2
[3ddd351]59#define NORTH 3
60#define EAST 4
61#define SOUTH 5
62#define WEST 6
[3d00693]63
[c61aa79]64// Any error value higher than this is clamped to this.
65#define MAX_ERROR 12.0
66
[af50685]67// Any length greater than pow(10, LOG_LEN_MAX) will be clamped to this.
68const Double LOG_LEN_MAX = 1.5;
69
[86cdcf2]70// How many bins per letter height to use when working out non-overlapping
71// labels.
72const unsigned int QUANTISE_FACTOR = 2;
73
[0e69efe]74#include "avenpal.h"
75
[6606406]76static const int INDICATOR_BOX_SIZE = 60;
[c300a04]77static const int INDICATOR_GAP = 2;
[6606406]78static const int INDICATOR_MARGIN = 5;
79static const int INDICATOR_OFFSET_X = 15;
[c300a04]80static const int INDICATOR_OFFSET_Y = 15;
[fe665c4]81static const int INDICATOR_RADIUS = INDICATOR_BOX_SIZE / 2 - INDICATOR_MARGIN;
[62da267]82static const int KEY_OFFSET_X = 10;
83static const int KEY_OFFSET_Y = 10;
84static const int KEY_EXTRA_LEFT_MARGIN = 2;
85static const int KEY_BLOCK_WIDTH = 20;
86static const int KEY_BLOCK_HEIGHT = 16;
[b56df45]87static const int TICK_LENGTH = 4;
[42adb19]88static const int SCALE_BAR_OFFSET_X = 15;
89static const int SCALE_BAR_OFFSET_Y = 12;
90static const int SCALE_BAR_HEIGHT = 12;
[aa048c3]91
92static const gla_colour TEXT_COLOUR = col_GREEN;
93static const gla_colour HERE_COLOUR = col_WHITE;
94static const gla_colour NAME_COLOUR = col_GREEN;
[dd6af8b]95static const gla_colour SEL_COLOUR = col_WHITE;
[522e0bd]96// Used with colour by date for legs without date information and with colour
97// by error for legs not in a loop.
98static const gla_colour NODATA_COLOUR = col_LIGHT_GREY_2;
[33b2094]99
[0b0520c]100// Number of entries across and down the hit-test grid:
[39e460c9]101#define HITTEST_SIZE 20
102
[395c3f8]103// How close the pointer needs to be to a station to be considered:
104#define MEASURE_THRESHOLD 7
105
[dde4fe7]106// vector for lighting angle
107static const Vector3 light(.577, .577, .577);
108
[9071cf5]109BEGIN_EVENT_TABLE(GfxCore, GLACanvas)
[5809313]110    EVT_PAINT(GfxCore::OnPaint)
111    EVT_LEFT_DOWN(GfxCore::OnLButtonDown)
112    EVT_LEFT_UP(GfxCore::OnLButtonUp)
113    EVT_MIDDLE_DOWN(GfxCore::OnMButtonDown)
114    EVT_MIDDLE_UP(GfxCore::OnMButtonUp)
115    EVT_RIGHT_DOWN(GfxCore::OnRButtonDown)
116    EVT_RIGHT_UP(GfxCore::OnRButtonUp)
[34d8d1a]117    EVT_MOUSEWHEEL(GfxCore::OnMouseWheel)
[5809313]118    EVT_MOTION(GfxCore::OnMouseMove)
[887c26e]119    EVT_LEAVE_WINDOW(GfxCore::OnLeaveWindow)
[5809313]120    EVT_SIZE(GfxCore::OnSize)
[a8e9fde]121    EVT_IDLE(GfxCore::OnIdle)
[4b1fc48]122    EVT_CHAR(GfxCore::OnKeyPress)
[5809313]123END_EVENT_TABLE()
124
[5876fcb]125GfxCore::GfxCore(MainFrm* parent, wxWindow* parent_win, GUIControl* control) :
[88707e0b]126    GLACanvas(parent_win, 100),
127    m_Scale(0.0),
[39bd2ef]128    initial_scale(1.0),
[88707e0b]129    m_ScaleBarWidth(0),
130    m_Control(control),
131    m_LabelGrid(NULL),
132    m_Parent(parent),
133    m_DoneFirstShow(false),
134    m_TiltAngle(0.0),
135    m_PanAngle(0.0),
136    m_Rotating(false),
137    m_RotationStep(0.0),
138    m_SwitchingTo(0),
139    m_Crosses(false),
140    m_Legs(true),
[cca3cee]141    m_Splays(SHOW_FADED),
142    m_Dupes(SHOW_DASHED),
[88707e0b]143    m_Names(false),
144    m_Scalebar(true),
[97ea48d]145    m_ColourKey(true),
[88707e0b]146    m_OverlappingNames(false),
147    m_Compass(true),
148    m_Clino(true),
149    m_Tubes(false),
150    m_ColourBy(COLOUR_BY_DEPTH),
151    m_HaveData(false),
[22b0a8f]152    m_HaveTerrain(true),
[88707e0b]153    m_MouseOutsideCompass(false),
154    m_MouseOutsideElev(false),
155    m_Surface(false),
156    m_Entrances(false),
157    m_FixedPts(false),
158    m_ExportedPts(false),
159    m_Grid(false),
160    m_BoundingBox(false),
[4938bcd]161    m_Terrain(false),
[88707e0b]162    m_Degrees(false),
163    m_Metric(false),
[d171c0c]164    m_Percent(false),
[7171240]165    m_HitTestDebug(false),
[11169cb]166    m_RenderStats(false),
[7171240]167    m_PointGrid(NULL),
[88707e0b]168    m_HitTestGridValid(false),
[381ae6e]169    m_here(NULL),
170    m_there(NULL),
[88707e0b]171    presentation_mode(0),
172    pres_reverse(false),
173    pres_speed(0.0),
[75d4a2b]174    movie(NULL),
[d96c95c]175    current_cursor(GfxCore::CURSOR_DEFAULT),
[6388423]176    sqrd_measure_threshold(sqrd(MEASURE_THRESHOLD)),
[9df33bc]177    dem(NULL),
[11169cb]178    last_time(0),
[6388423]179    n_tris(0)
[5809313]180{
[da6c802]181    AddQuad = &GfxCore::AddQuadrilateralDepth;
182    AddPoly = &GfxCore::AddPolylineDepth;
[5627cbb]183    wxConfigBase::Get()->Read(wxT("metric"), &m_Metric, true);
184    wxConfigBase::Get()->Read(wxT("degrees"), &m_Degrees, true);
[82277dd]185    wxConfigBase::Get()->Read(wxT("percent"), &m_Percent, false);
[5809313]186
[97ea48d]187    for (int pen = 0; pen < NUM_COLOUR_BANDS + 1; ++pen) {
[4a1cede]188        m_Pens[pen].SetColour(REDS[pen] / 255.0,
[0e69efe]189                              GREENS[pen] / 255.0,
190                              BLUES[pen] / 255.0);
191    }
[5455bb2]192
193    timer.Start();
[5809313]194}
195
196GfxCore::~GfxCore()
197{
198    TryToFreeArrays();
[156dc16]199
[39e460c9]200    delete[] m_PointGrid;
[5809313]201}
202
203void GfxCore::TryToFreeArrays()
204{
205    // Free up any memory allocated for arrays.
[81f1266]206    delete[] m_LabelGrid;
207    m_LabelGrid = NULL;
[5809313]208}
209
210//
211//  Initialisation methods
212//
213
[0c6bf5e8]214void GfxCore::Initialise(bool same_file)
[5809313]215{
216    // Initialise the view from the parent holding the survey data.
217
218    TryToFreeArrays();
219
[33b2094]220    m_DoneFirstShow = false;
221
[dfe4454c]222    m_HitTestGridValid = false;
[381ae6e]223    m_here = NULL;
224    m_there = NULL;
[dfe4454c]225
[d35144d]226    m_MouseOutsideCompass = m_MouseOutsideElev = false;
227
[0c6bf5e8]228    if (!same_file) {
229        // Apply default parameters unless reloading the same file.
230        DefaultParameters();
231    }
[9eb58d0]232
233    m_HaveData = true;
[936a197]234
[92cf7a8]235    // Clear any cached OpenGL lists which depend on the data.
236    InvalidateList(LIST_SCALE_BAR);
237    InvalidateList(LIST_DEPTH_KEY);
238    InvalidateList(LIST_DATE_KEY);
239    InvalidateList(LIST_ERROR_KEY);
[cc9e2c65]240    InvalidateList(LIST_GRADIENT_KEY);
[af50685]241    InvalidateList(LIST_LENGTH_KEY);
[92cf7a8]242    InvalidateList(LIST_UNDERGROUND_LEGS);
243    InvalidateList(LIST_TUBES);
244    InvalidateList(LIST_SURFACE_LEGS);
245    InvalidateList(LIST_BLOBS);
246    InvalidateList(LIST_CROSSES);
247    InvalidateList(LIST_GRID);
248    InvalidateList(LIST_SHADOW);
[22b0a8f]249    InvalidateList(LIST_TERRAIN);
[92cf7a8]250
[39bd2ef]251    // Set diameter of the viewing volume.
[7ab01e7]252    auto ext = m_Parent->GetExtent();
253    double cave_diameter = sqrt(sqrd(ext.GetX()) +
254                                sqrd(ext.GetY()) +
255                                sqrd(ext.GetZ()));
[39bd2ef]256
257    // Allow for terrain.
258    double diameter = max(1000.0 * 2, cave_diameter * 2);
259
260    if (!same_file) {
261        SetVolumeDiameter(diameter);
262
263        // Set initial scale based on the size of the cave.
264        initial_scale = diameter / cave_diameter;
265        SetScale(initial_scale);
266    } else {
[8b0a077]267        // Adjust the position when restricting the view to a subsurvey (or
268        // expanding the view to show the whole survey).
269        AddTranslation(m_Parent->GetOffset() - offsets);
270
[39bd2ef]271        // Try to keep the same scale, allowing for the
272        // cave having grown (or shrunk).
273        double rescale = GetVolumeDiameter() / diameter;
274        SetVolumeDiameter(diameter);
[8b0a077]275        SetScale(GetScale() / rescale); // ?
[39bd2ef]276        initial_scale = initial_scale * rescale;
277    }
278
[8b0a077]279    offsets = m_Parent->GetOffset();
280
[936a197]281    ForceRefresh();
[9eb58d0]282}
283
284void GfxCore::FirstShow()
285{
286    GLACanvas::FirstShow();
287
[8bd480e]288    const unsigned int quantise(GetFontSize() / QUANTISE_FACTOR);
[86cdcf2]289    list<LabelInfo*>::iterator pos = m_Parent->GetLabelsNC();
290    while (pos != m_Parent->GetLabelsNCEnd()) {
291        LabelInfo* label = *pos++;
292        // Calculate and set the label width for use when plotting
293        // none-overlapping labels.
294        int ext_x;
295        GLACanvas::GetTextExtent(label->GetText(), &ext_x, NULL);
[5a24583]296        label->set_width(unsigned(ext_x) / quantise + 1);
[86cdcf2]297    }
298
[5809313]299    m_DoneFirstShow = true;
300}
301
302//
303//  Recalculating methods
304//
305
[cd6ea75]306void GfxCore::SetScale(Double scale)
[5047c53]307{
[8b0d57f]308    if (scale < 0.05) {
309        scale = 0.05;
[70acad9]310    } else if (scale > GetVolumeDiameter()) {
311        scale = GetVolumeDiameter();
[5047c53]312    }
313
[5b7164d]314    m_Scale = scale;
[00a68e0]315    m_HitTestGridValid = false;
[381ae6e]316    if (m_here && m_here == &temp_here) SetHere();
[33b2094]317
318    GLACanvas::SetScale(scale);
[d9b3270]319}
320
[f433fda]321bool GfxCore::HasUndergroundLegs() const
322{
323    return m_Parent->HasUndergroundLegs();
324}
325
[5fe7292]326bool GfxCore::HasSplays() const
327{
328    return m_Parent->HasSplays();
329}
330
[e5c5f3c]331bool GfxCore::HasDupes() const
332{
333    return m_Parent->HasDupes();
334}
335
[f433fda]336bool GfxCore::HasSurfaceLegs() const
337{
338    return m_Parent->HasSurfaceLegs();
339}
340
[50e8979]341bool GfxCore::HasTubes() const
342{
343    return m_Parent->HasTubes();
344}
345
[d9b3270]346void GfxCore::UpdateBlobs()
347{
[d2fcc9b]348    InvalidateList(LIST_BLOBS);
[33b2094]349}
350
[cd6ea75]351//
[5876fcb]352//  Event handlers
[cd6ea75]353//
[5047c53]354
[41f7a27]355void GfxCore::OnLeaveWindow(wxMouseEvent&) {
[887c26e]356    SetHere();
357    ClearCoords();
358}
359
[5876fcb]360void GfxCore::OnIdle(wxIdleEvent& event)
361{
362    // Handle an idle event.
[5455bb2]363    if (Animating()) {
364        Animate();
365        // If still animating, we want more idle events.
366        if (Animating())
367            event.RequestMore();
[11169cb]368    } else {
369        // If we're idle, don't show a bogus FPS next time we render.
370        last_time = 0;
[b72f4b5]371    }
[5809313]372}
373
[b4fe9fb]374void GfxCore::OnPaint(wxPaintEvent&)
[5809313]375{
376    // Redraw the window.
[b462168]377
378    // Get a graphics context.
[1b12b82]379    wxPaintDC dc(this);
[5809313]380
[815eab2]381    if (m_HaveData) {
[01d91fd]382        // Make sure we're initialised.
383        bool first_time = !m_DoneFirstShow;
384        if (first_time) {
385            FirstShow();
386        }
387
[58dfdd21]388        StartDrawing();
389
390        // Clear the background.
391        Clear();
392
[429465a]393        // Set up model transformation matrix.
394        SetDataTransform();
395
396        if (m_Legs || m_Tubes) {
397            if (m_Tubes) {
[d67450e]398                EnableSmoothPolygons(true); // FIXME: allow false for wireframe view
[d2fcc9b]399                DrawList(LIST_TUBES);
[429465a]400                DisableSmoothPolygons();
401            }
[e4d4079]402
[50e8979]403            // Draw the underground legs.  Do this last so that anti-aliasing
[e4d4079]404            // works over polygons.
405            SetColour(col_GREEN);
[d2fcc9b]406            DrawList(LIST_UNDERGROUND_LEGS);
[429465a]407        }
[33b2094]408
[429465a]409        if (m_Surface) {
410            // Draw the surface legs.
[d2fcc9b]411            DrawList(LIST_SURFACE_LEGS);
[429465a]412        }
[bbc22ca]413
[f4c5932]414        if (m_BoundingBox) {
415            DrawShadowedBoundingBox();
416        }
[429465a]417        if (m_Grid) {
418            // Draw the grid.
[37d7084]419            DrawList(LIST_GRID);
[429465a]420        }
[b13aee4]421
[6a4c11b7]422        DrawList(LIST_BLOBS);
423
424        if (m_Crosses) {
425            DrawList(LIST_CROSSES);
426        }
427
[d347a2c]428        if (m_Terrain) {
[036c777]429            // Disable texturing while drawing terrain.
430            bool texturing = GetTextured();
431            if (texturing) GLACanvas::ToggleTextured();
432
[6a4c11b7]433            // This is needed if blobs and/or crosses are drawn using lines -
434            // otherwise the terrain doesn't appear when they are enabled.
435            SetDataTransform();
436
[11fe902]437            // We don't want to be able to see the terrain through itself, so
438            // do a "Z-prepass" - plot the terrain once only updating the
439            // Z-buffer, then again with Z-clipping only plotting where the
440            // depth matches the value in the Z-buffer.
441            DrawListZPrepass(LIST_TERRAIN);
[036c777]442
443            if (texturing) GLACanvas::ToggleTextured();
[d347a2c]444        }
445
[429465a]446        SetIndicatorTransform();
447
[6adffadf]448        // Draw station names.
449        if (m_Names /*&& !m_Control->MouseDown() && !Animating()*/) {
450            SetColour(NAME_COLOUR);
451
452            if (m_OverlappingNames) {
453                SimpleDrawNames();
454            } else {
455                NattyDrawNames();
456            }
457        }
458
[0642381]459        if (!highlighted_survey.empty()) {
460            HighlightSurvey();
461        }
462
[5e0b3a13]463        if (m_HitTestDebug) {
464            // Show the hit test grid bucket sizes...
465            SetColour(m_HitTestGridValid ? col_LIGHT_GREY : col_DARK_GREY);
[7171240]466            if (m_PointGrid) {
467                for (int i = 0; i != HITTEST_SIZE; ++i) {
468                    int x = (GetXSize() + 1) * i / HITTEST_SIZE + 2;
469                    for (int j = 0; j != HITTEST_SIZE; ++j) {
470                        int square = i + j * HITTEST_SIZE;
[a49a023]471                        unsigned long bucket_size = m_PointGrid[square].size();
[7171240]472                        if (bucket_size) {
473                            int y = (GetYSize() + 1) * (HITTEST_SIZE - 1 - j) / HITTEST_SIZE;
[7b9b700]474                            DrawIndicatorText(x, y, wxString::Format(wxT("%lu"), bucket_size));
[7171240]475                        }
[5e0b3a13]476                    }
477                }
478            }
479
480            EnableDashedLines();
481            BeginLines();
482            for (int i = 0; i != HITTEST_SIZE; ++i) {
483                int x = (GetXSize() + 1) * i / HITTEST_SIZE;
484                PlaceIndicatorVertex(x, 0);
485                PlaceIndicatorVertex(x, GetYSize());
486            }
487            for (int j = 0; j != HITTEST_SIZE; ++j) {
488                int y = (GetYSize() + 1) * (HITTEST_SIZE - 1 - j) / HITTEST_SIZE;
489                PlaceIndicatorVertex(0, y);
490                PlaceIndicatorVertex(GetXSize(), y);
491            }
492            EndLines();
493            DisableDashedLines();
494        }
495
[11169cb]496        long now = timer.Time();
497        if (m_RenderStats) {
498            // Show stats about rendering.
499            SetColour(col_TURQUOISE);
500            int y = GetYSize() - GetFontSize();
501            if (last_time != 0.0) {
502                // timer.Time() measure in milliseconds.
503                double fps = 1000.0 / (now - last_time);
504                DrawIndicatorText(1, y, wxString::Format(wxT("FPS:% 5.1f"), fps));
505            }
506            y -= GetFontSize();
507            DrawIndicatorText(1, y, wxString::Format(wxT("▲:%lu"), (unsigned long)n_tris));
508        }
509        last_time = now;
510
[c091027]511        // Draw indicators.
512        //
513        // There's no advantage in generating an OpenGL list for the
514        // indicators since they change with almost every redraw (and
515        // sometimes several times between redraws).  This way we avoid
516        // the need to track when to update the indicator OpenGL list,
517        // and also avoid indicator update bugs when we don't quite get this
518        // right...
519        DrawIndicators();
520
[dd6af8b]521        if (zoombox.active()) {
522            SetColour(SEL_COLOUR);
523            EnableDashedLines();
524            BeginPolyline();
525            glaCoord Y = GetYSize();
526            PlaceIndicatorVertex(zoombox.x1, Y - zoombox.y1);
527            PlaceIndicatorVertex(zoombox.x1, Y - zoombox.y2);
528            PlaceIndicatorVertex(zoombox.x2, Y - zoombox.y2);
529            PlaceIndicatorVertex(zoombox.x2, Y - zoombox.y1);
530            PlaceIndicatorVertex(zoombox.x1, Y - zoombox.y1);
531            EndPolyline();
532            DisableDashedLines();
533        } else if (MeasuringLineActive()) {
[429465a]534            // Draw "here" and "there".
[f6d8375]535            double hx, hy;
[aa048c3]536            SetColour(HERE_COLOUR);
[381ae6e]537            if (m_here) {
[f6d8375]538                double dummy;
[381ae6e]539                Transform(*m_here, &hx, &hy, &dummy);
540                if (m_here != &temp_here) DrawRing(hx, hy);
[429465a]541            }
[381ae6e]542            if (m_there) {
[f6d8375]543                double tx, ty;
544                double dummy;
[381ae6e]545                Transform(*m_there, &tx, &ty, &dummy);
546                if (m_here) {
[429465a]547                    BeginLines();
548                    PlaceIndicatorVertex(hx, hy);
549                    PlaceIndicatorVertex(tx, ty);
[42d23c5]550                    EndLines();
[429465a]551                }
[e633bb1]552                BeginBlobs();
[81aea4e]553                DrawBlob(tx, ty);
[e633bb1]554                EndBlobs();
[429465a]555            }
556        }
[f433fda]557
[58dfdd21]558        FinishDrawing();
[5997ffd]559    } else {
560        dc.SetBackground(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWFRAME));
561        dc.Clear();
[58dfdd21]562    }
[5809313]563}
564
[f4c5932]565void GfxCore::DrawBoundingBox()
566{
[d67450e]567    const Vector3 v = 0.5 * m_Parent->GetExtent();
[f4c5932]568
569    SetColour(col_BLUE);
570    EnableDashedLines();
571    BeginPolyline();
[d67450e]572    PlaceVertex(-v.GetX(), -v.GetY(), v.GetZ());
573    PlaceVertex(-v.GetX(), v.GetY(), v.GetZ());
574    PlaceVertex(v.GetX(), v.GetY(), v.GetZ());
575    PlaceVertex(v.GetX(), -v.GetY(), v.GetZ());
576    PlaceVertex(-v.GetX(), -v.GetY(), v.GetZ());
[f4c5932]577    EndPolyline();
578    BeginPolyline();
[d67450e]579    PlaceVertex(-v.GetX(), -v.GetY(), -v.GetZ());
580    PlaceVertex(-v.GetX(), v.GetY(), -v.GetZ());
581    PlaceVertex(v.GetX(), v.GetY(), -v.GetZ());
582    PlaceVertex(v.GetX(), -v.GetY(), -v.GetZ());
583    PlaceVertex(-v.GetX(), -v.GetY(), -v.GetZ());
[f4c5932]584    EndPolyline();
585    BeginLines();
[d67450e]586    PlaceVertex(-v.GetX(), -v.GetY(), v.GetZ());
587    PlaceVertex(-v.GetX(), -v.GetY(), -v.GetZ());
588    PlaceVertex(-v.GetX(), v.GetY(), v.GetZ());
589    PlaceVertex(-v.GetX(), v.GetY(), -v.GetZ());
590    PlaceVertex(v.GetX(), v.GetY(), v.GetZ());
591    PlaceVertex(v.GetX(), v.GetY(), -v.GetZ());
592    PlaceVertex(v.GetX(), -v.GetY(), v.GetZ());
593    PlaceVertex(v.GetX(), -v.GetY(), -v.GetZ());
[f4c5932]594    EndLines();
595    DisableDashedLines();
596}
597
598void GfxCore::DrawShadowedBoundingBox()
599{
[d67450e]600    const Vector3 v = 0.5 * m_Parent->GetExtent();
[f4c5932]601
[f7dae86]602    DrawBoundingBox();
603
[f9ca87c]604    PolygonOffset(true);
[f4c5932]605    SetColour(col_DARK_GREY);
606    BeginQuadrilaterals();
[d67450e]607    PlaceVertex(-v.GetX(), -v.GetY(), -v.GetZ());
608    PlaceVertex(-v.GetX(), v.GetY(), -v.GetZ());
609    PlaceVertex(v.GetX(), v.GetY(), -v.GetZ());
610    PlaceVertex(v.GetX(), -v.GetY(), -v.GetZ());
[f4c5932]611    EndQuadrilaterals();
[f9ca87c]612    PolygonOffset(false);
[f4c5932]613
[d2fcc9b]614    DrawList(LIST_SHADOW);
[f4c5932]615}
616
[c1cf79d]617void GfxCore::DrawGrid()
618{
619    // Draw the grid.
[156dc16]620    SetColour(col_RED);
[c1cf79d]621
622    // Calculate the extent of the survey, in metres across the screen plane.
[b81eee2]623    Double m_across_screen = SurveyUnitsAcrossViewport();
[c1cf79d]624    // Calculate the length of the scale bar in metres.
625    //--move this elsewhere
[c6d95d8]626    Double size_snap = pow(10.0, floor(log10(0.75 * m_across_screen)));
627    Double t = m_across_screen * 0.75 / size_snap;
[c1cf79d]628    if (t >= 5.0) {
[429465a]629        size_snap *= 5.0;
[c1cf79d]630    }
631    else if (t >= 2.0) {
[429465a]632        size_snap *= 2.0;
[c1cf79d]633    }
634
[d67450e]635    Double grid_size = size_snap * 0.1;
[c6d95d8]636    Double edge = grid_size * 2.0;
[7ab01e7]637    auto ext = m_Parent->GetExtent();
638    Double grid_z = -ext.GetZ() * 0.5 - grid_size;
639    Double left = -ext.GetX() * 0.5 - edge;
640    Double right = ext.GetX() * 0.5 + edge;
641    Double bottom = -ext.GetY() * 0.5 - edge;
642    Double top = ext.GetY() * 0.5 + edge;
[c1cf79d]643    int count_x = (int) ceil((right - left) / grid_size);
644    int count_y = (int) ceil((top - bottom) / grid_size);
[c6d95d8]645    Double actual_right = left + count_x*grid_size;
646    Double actual_top = bottom + count_y*grid_size;
[c1cf79d]647
[b81eee2]648    BeginLines();
649
[c1cf79d]650    for (int xc = 0; xc <= count_x; xc++) {
[429465a]651        Double x = left + xc*grid_size;
[f433fda]652
[b81eee2]653        PlaceVertex(x, bottom, grid_z);
654        PlaceVertex(x, actual_top, grid_z);
[c1cf79d]655    }
656
657    for (int yc = 0; yc <= count_y; yc++) {
[429465a]658        Double y = bottom + yc*grid_size;
[b81eee2]659        PlaceVertex(left, y, grid_z);
660        PlaceVertex(actual_right, y, grid_z);
[c1cf79d]661    }
[b81eee2]662
663    EndLines();
[c1cf79d]664}
665
[1eeb55a]666int GfxCore::GetClinoOffset() const
[c300a04]667{
[2a26b45]668    int result = INDICATOR_OFFSET_X;
669    if (m_Compass) {
670        result += 6 + GetCompassWidth() + INDICATOR_GAP;
671    }
672    return result;
[c300a04]673}
674
[fe665c4]675void GfxCore::DrawTick(int angle_cw)
[6606406]676{
[fe665c4]677    const Double theta = rad(angle_cw);
678    const wxCoord length1 = INDICATOR_RADIUS;
679    const wxCoord length0 = length1 + TICK_LENGTH;
680    wxCoord x0 = wxCoord(length0 * sin(theta));
681    wxCoord y0 = wxCoord(length0 * cos(theta));
682    wxCoord x1 = wxCoord(length1 * sin(theta));
683    wxCoord y1 = wxCoord(length1 * cos(theta));
[6606406]684
[fe665c4]685    PlaceIndicatorVertex(x0, y0);
686    PlaceIndicatorVertex(x1, y1);
[6606406]687}
688
[d67450e]689void GfxCore::DrawArrow(gla_colour col1, gla_colour col2) {
690    Vector3 p1(0, INDICATOR_RADIUS, 0);
691    Vector3 p2(INDICATOR_RADIUS/2, INDICATOR_RADIUS*-.866025404, 0); // 150deg
692    Vector3 p3(-INDICATOR_RADIUS/2, INDICATOR_RADIUS*-.866025404, 0); // 210deg
693    Vector3 pc(0, 0, 0);
694
695    DrawTriangle(col_LIGHT_GREY, col1, p2, p1, pc);
696    DrawTriangle(col_LIGHT_GREY, col2, p3, p1, pc);
697}
698
[fe665c4]699void GfxCore::DrawCompass() {
700    // Ticks.
701    BeginLines();
702    for (int angle = 315; angle > 0; angle -= 45) {
703        DrawTick(angle);
704    }
705    SetColour(col_GREEN);
706    DrawTick(0);
707    EndLines();
708
709    // Compass background.
710    DrawCircle(col_LIGHT_GREY_2, col_GREY, 0, 0, INDICATOR_RADIUS);
[6606406]711
[fe665c4]712    // Compass arrow.
[d67450e]713    DrawArrow(col_INDICATOR_1, col_INDICATOR_2);
[6606406]714}
715
[fe665c4]716// Draw the non-rotating background to the clino.
717void GfxCore::DrawClinoBack() {
718    BeginLines();
719    for (int angle = 0; angle <= 180; angle += 90) {
720        DrawTick(angle);
721    }
722
723    SetColour(col_GREY);
724    PlaceIndicatorVertex(0, INDICATOR_RADIUS);
725    PlaceIndicatorVertex(0, -INDICATOR_RADIUS);
726    PlaceIndicatorVertex(0, 0);
727    PlaceIndicatorVertex(INDICATOR_RADIUS, 0);
[b56df45]728
[fe665c4]729    EndLines();
730}
731
732void GfxCore::DrawClino() {
733    // Ticks.
734    SetColour(col_GREEN);
[33b2094]735    BeginLines();
[fe665c4]736    DrawTick(0);
[33b2094]737    EndLines();
[fe665c4]738
739    // Clino background.
740    DrawSemicircle(col_LIGHT_GREY_2, col_GREY, 0, 0, INDICATOR_RADIUS, 0);
741
742    // Elevation arrow.
[d67450e]743    DrawArrow(col_INDICATOR_2, col_INDICATOR_1);
[b56df45]744}
745
[6606406]746void GfxCore::Draw2dIndicators()
747{
[76dd228]748    // Draw the compass and elevation indicators.
749
750    const int centre_y = INDICATOR_BOX_SIZE / 2 + INDICATOR_OFFSET_Y;
751
[fe665c4]752    const int comp_centre_x = GetCompassXPosition();
[6606406]753
[eef68f9]754    if (m_Compass && !m_Parent->IsExtendedElevation()) {
[fe665c4]755        // If the user is dragging the compass with the pointer outside the
756        // compass, we snap to 45 degree multiples, and the ticks go white.
[76dd228]757        SetColour(m_MouseOutsideCompass ? col_WHITE : col_LIGHT_GREY_2);
[fe665c4]758        DrawList2D(LIST_COMPASS, comp_centre_x, centre_y, -m_PanAngle);
[c300a04]759    }
[76dd228]760
[fe665c4]761    const int elev_centre_x = GetClinoXPosition();
[76dd228]762
[eef68f9]763    if (m_Clino) {
[fe665c4]764        // If the user is dragging the clino with the pointer outside the
765        // clino, we snap to 90 degree multiples, and the ticks go white.
766        SetColour(m_MouseOutsideElev ? col_WHITE : col_LIGHT_GREY_2);
767        DrawList2D(LIST_CLINO_BACK, elev_centre_x, centre_y, 0);
[7a57dc7]768        DrawList2D(LIST_CLINO, elev_centre_x, centre_y, 90 - m_TiltAngle);
[c300a04]769    }
[6606406]770
[aa048c3]771    SetColour(TEXT_COLOUR);
[6606406]772
[21958ec]773    static int triple_zero_width = 0;
774    static int height = 0;
775    if (!triple_zero_width) {
[5627cbb]776        GetTextExtent(wxT("000"), &triple_zero_width, &height);
[21958ec]777    }
778    const int y_off = INDICATOR_OFFSET_Y + INDICATOR_BOX_SIZE + height / 2;
[421b7d2]779
[eef68f9]780    if (m_Compass && !m_Parent->IsExtendedElevation()) {
[21958ec]781        wxString str;
782        int value;
[d171c0c]783        int brg_unit;
[429465a]784        if (m_Degrees) {
[21958ec]785            value = int(m_PanAngle);
[736f7df]786            /* TRANSLATORS: degree symbol - probably should be translated to
787             * itself. */
[d171c0c]788            brg_unit = /*°*/344;
[429465a]789        } else {
[21958ec]790            value = int(m_PanAngle * 200.0 / 180.0);
[736f7df]791            /* TRANSLATORS: symbol for grad (400 grad = 360 degrees = full
792             * circle). */
[85dcdcd]793            brg_unit = /*ᵍ*/345;
[f433fda]794        }
[5627cbb]795        str.Printf(wxT("%03d"), value);
[d171c0c]796        str += wmsg(brg_unit);
[21958ec]797        DrawIndicatorText(comp_centre_x - triple_zero_width / 2, y_off, str);
798
[1c507cf]799        // TRANSLATORS: Used in aven above the compass indicator at the lower
800        // right of the display, with a bearing below "Facing".  This indicates the
801        // direction the viewer is "facing" in.
802        //
803        // Try to keep this translation short - ideally at most 10 characters -
804        // as otherwise the compass and clino will be moved further apart to
805        // make room. */
[5627cbb]806        str = wmsg(/*Facing*/203);
[21958ec]807        int w;
[d92d282]808        GetTextExtent(str, &w, NULL);
[21958ec]809        DrawIndicatorText(comp_centre_x - w / 2, y_off + height, str);
[c300a04]810    }
811
[eef68f9]812    if (m_Clino) {
[c13d121f]813        if (m_TiltAngle == -90.0) {
814            // TRANSLATORS: Label used for "clino" in Aven when the view is
815            // from directly above.
[1c507cf]816            //
817            // Try to keep this translation short - ideally at most 10
818            // characters - as otherwise the compass and clino will be moved
819            // further apart to make room. */
[c13d121f]820            wxString str = wmsg(/*Plan*/432);
821            static int width = 0;
822            if (!width) {
823                GetTextExtent(str, &width, NULL);
[d171c0c]824            }
[c13d121f]825            int x = elev_centre_x - width / 2;
826            DrawIndicatorText(x, y_off + height / 2, str);
827        } else if (m_TiltAngle == 90.0) {
828            // TRANSLATORS: Label used for "clino" in Aven when the view is
829            // from directly below.
[1c507cf]830            //
831            // Try to keep this translation short - ideally at most 10
832            // characters - as otherwise the compass and clino will be moved
833            // further apart to make room. */
[c13d121f]834            wxString str = wmsg(/*Kiwi Plan*/433);
835            static int width = 0;
836            if (!width) {
837                GetTextExtent(str, &width, NULL);
[d171c0c]838            }
[c13d121f]839            int x = elev_centre_x - width / 2;
840            DrawIndicatorText(x, y_off + height / 2, str);
841        } else {
842            int angle;
843            wxString str;
844            int width;
845            int unit;
846            if (m_Percent) {
847                static int zero_width = 0;
848                if (!zero_width) {
849                    GetTextExtent(wxT("0"), &zero_width, NULL);
850                }
851                width = zero_width;
852                if (m_TiltAngle > 89.99) {
853                    angle = 1000000;
854                } else if (m_TiltAngle < -89.99) {
855                    angle = -1000000;
856                } else {
857                    angle = int(100 * tan(rad(m_TiltAngle)));
858                }
859                if (angle > 99999 || angle < -99999) {
860                    str = angle > 0 ? wxT("+") : wxT("-");
[0b8c321]861                    /* TRANSLATORS: infinity symbol - used for the percentage gradient on
[feefc6a]862                     * vertical angles. */
[c13d121f]863                    str += wmsg(/*∞*/431);
864                } else {
865                    str = angle ? wxString::Format(wxT("%+03d"), angle) : wxT("0");
866                }
[feefc6a]867                /* TRANSLATORS: symbol for percentage gradient (100% = 45
868                 * degrees = 50 grad). */
[c13d121f]869                unit = /*%*/96;
870            } else if (m_Degrees) {
871                static int zero_zero_width = 0;
872                if (!zero_zero_width) {
873                    GetTextExtent(wxT("00"), &zero_zero_width, NULL);
874                }
875                width = zero_zero_width;
876                angle = int(m_TiltAngle);
877                str = angle ? wxString::Format(wxT("%+03d"), angle) : wxT("00");
878                unit = /*°*/344;
[d171c0c]879            } else {
[c13d121f]880                width = triple_zero_width;
881                angle = int(m_TiltAngle * 200.0 / 180.0);
882                str = angle ? wxString::Format(wxT("%+04d"), angle) : wxT("000");
[85dcdcd]883                unit = /*ᵍ*/345;
[21958ec]884            }
885
[c13d121f]886            int sign_offset = 0;
887            if (unit == /*%*/96) {
888                // Right align % since the width changes so much.
889                GetTextExtent(str, &sign_offset, NULL);
890                sign_offset -= width;
891            } else if (angle < 0) {
892                // Adjust horizontal position so the left of the first digit is
893                // always in the same place.
894                static int minus_width = 0;
895                if (!minus_width) {
896                    GetTextExtent(wxT("-"), &minus_width, NULL);
897                }
898                sign_offset = minus_width;
899            } else if (angle > 0) {
900                // Adjust horizontal position so the left of the first digit is
901                // always in the same place.
902                static int plus_width = 0;
903                if (!plus_width) {
904                    GetTextExtent(wxT("+"), &plus_width, NULL);
905                }
906                sign_offset = plus_width;
[21958ec]907            }
[d171c0c]908
[c13d121f]909            str += wmsg(unit);
910            DrawIndicatorText(elev_centre_x - sign_offset - width / 2, y_off, str);
[21958ec]911
[1c507cf]912            // TRANSLATORS: Label used for "clino" in Aven when the view is
913            // neither from directly above nor from directly below.  It is
914            // also used in the dialog for editing a marked position in a
915            // presentation.
916            //
917            // Try to keep this translation short - ideally at most 10
918            // characters - as otherwise the compass and clino will be moved
919            // further apart to make room. */
[c13d121f]920            str = wmsg(/*Elevation*/118);
921            static int elevation_width = 0;
922            if (!elevation_width) {
923                GetTextExtent(str, &elevation_width, NULL);
924            }
925            int x = elev_centre_x - elevation_width / 2;
926            DrawIndicatorText(x, y_off + height, str);
[d171c0c]927        }
[c300a04]928    }
[5809313]929}
930
931void GfxCore::NattyDrawNames()
932{
[84cab34]933    // Draw station names, without overlapping.
[f433fda]934
[d92d282]935    const unsigned int quantise(GetFontSize() / QUANTISE_FACTOR);
[90430f2]936    const unsigned int quantised_x = GetXSize() / quantise;
937    const unsigned int quantised_y = GetYSize() / quantise;
[84cab34]938    const size_t buffer_size = quantised_x * quantised_y;
[f433fda]939
[69463a0]940    if (!m_LabelGrid) m_LabelGrid = new char[buffer_size];
[5809313]941
[33b2094]942    memset((void*) m_LabelGrid, 0, buffer_size);
[156dc16]943
[1a46879]944    const SurveyFilter* filter = m_Parent->GetTreeFilter();
[33b2094]945    list<LabelInfo*>::const_iterator label = m_Parent->GetLabels();
[36c3285]946    for ( ; label != m_Parent->GetLabelsEnd(); ++label) {
[a6401ff3]947        if (m_Splays == SHOW_HIDE && (*label)->IsSplayEnd())
948            continue;
949
[ece003f]950        if (!((m_Surface && (*label)->IsSurface()) ||
951              (m_Legs && (*label)->IsUnderground()) ||
952              (!(*label)->IsSurface() && !(*label)->IsUnderground()))) {
953            // if this station isn't to be displayed, skip to the next
954            // (last case is for stns with no legs attached)
955            continue;
956        }
[672459c]957        if (filter && !filter->CheckVisible((*label)->GetText()))
958            continue;
[ece003f]959
[f6d8375]960        double x, y, z;
[f433fda]961
[d67450e]962        Transform(**label, &x, &y, &z);
[36c3285]963        // Check if the label is behind us (in perspective view).
[d92d282]964        if (z <= 0.0 || z >= 1.0) continue;
[421b7d2]965
[1eeb55a]966        // Apply a small shift so that translating the view doesn't make which
967        // labels are displayed change as the resulting twinkling effect is
968        // distracting.
[f6d8375]969        double tx, ty, tz;
[d67450e]970        Transform(Vector3(), &tx, &ty, &tz);
[429465a]971        tx -= floor(tx / quantise) * quantise;
972        ty -= floor(ty / quantise) * quantise;
[5809313]973
[f12e8cc]974        tx = x - tx;
975        if (tx < 0) continue;
976
977        ty = y - ty;
978        if (ty < 0) continue;
[421b7d2]979
[f12e8cc]980        unsigned int iy = unsigned(ty) / quantise;
[d92d282]981        if (iy >= quantised_y) continue;
[5a24583]982        unsigned int width = (*label)->get_width();
[f12e8cc]983        unsigned int ix = unsigned(tx) / quantise;
984        if (ix + width >= quantised_x) continue;
[33b2094]985
[f12e8cc]986        char * test = m_LabelGrid + ix + iy * quantised_x;
[d92d282]987        if (memchr(test, 1, width)) continue;
[33b2094]988
[8bd480e]989        x += 3;
990        y -= GetFontSize() / 2;
[d92d282]991        DrawIndicatorText((int)x, (int)y, (*label)->GetText());
992
993        if (iy > QUANTISE_FACTOR) iy = QUANTISE_FACTOR;
994        test -= quantised_x * iy;
[f12e8cc]995        iy += 4;
996        while (--iy && test < m_LabelGrid + buffer_size) {
[d92d282]997            memset(test, 1, width);
998            test += quantised_x;
[429465a]999        }
[84cab34]1000    }
[5809313]1001}
1002
1003void GfxCore::SimpleDrawNames()
1004{
[1a46879]1005    const SurveyFilter* filter = m_Parent->GetTreeFilter();
[a8aedf4]1006    // Draw all station names, without worrying about overlaps
[d5de678]1007    list<LabelInfo*>::const_iterator label = m_Parent->GetLabels();
[01d91fd]1008    for ( ; label != m_Parent->GetLabelsEnd(); ++label) {
[a6401ff3]1009        if (m_Splays == SHOW_HIDE && (*label)->IsSplayEnd())
1010            continue;
1011
[ece003f]1012        if (!((m_Surface && (*label)->IsSurface()) ||
1013              (m_Legs && (*label)->IsUnderground()) ||
1014              (!(*label)->IsSurface() && !(*label)->IsUnderground()))) {
1015            // if this station isn't to be displayed, skip to the next
1016            // (last case is for stns with no legs attached)
1017            continue;
1018        }
[672459c]1019        if (filter && !filter->CheckVisible((*label)->GetText()))
1020            continue;
[ece003f]1021
[f6d8375]1022        double x, y, z;
[d67450e]1023        Transform(**label, &x, &y, &z);
[d92d282]1024
1025        // Check if the label is behind us (in perspective view).
1026        if (z <= 0) continue;
1027
[8bd480e]1028        x += 3;
1029        y -= GetFontSize() / 2;
[d92d282]1030        DrawIndicatorText((int)x, (int)y, (*label)->GetText());
[84cab34]1031    }
[5809313]1032}
1033
[825bdff]1034void GfxCore::DrawColourKey(int num_bands, const wxString & other, const wxString & units)
[5809313]1035{
[7cdb1c3]1036    int total_block_height =
[62da267]1037        KEY_BLOCK_HEIGHT * (num_bands == 1 ? num_bands : num_bands - 1);
1038    if (!other.empty()) total_block_height += KEY_BLOCK_HEIGHT * 2;
[825bdff]1039    if (!units.empty()) total_block_height += KEY_BLOCK_HEIGHT;
[5809313]1040
[3585243]1041    const int bottom = -total_block_height;
[a74b014]1042
[d4650b3]1043    int size = 0;
[7cdb1c3]1044    if (!other.empty()) GetTextExtent(other, &size, NULL);
[d4650b3]1045    int band;
[2043961]1046    for (band = 0; band < num_bands; ++band) {
[a74b014]1047        int x;
[7cdb1c3]1048        GetTextExtent(key_legends[band], &x, NULL);
[d4650b3]1049        if (x > size) size = x;
1050    }
1051
[3585243]1052    int left = -KEY_BLOCK_WIDTH - size;
[d4650b3]1053
[fcc3741]1054    key_lowerleft[m_ColourBy].x = left - KEY_EXTRA_LEFT_MARGIN;
1055    key_lowerleft[m_ColourBy].y = bottom;
[d4650b3]1056
[d43fa84]1057    int y = bottom;
[825bdff]1058    if (!units.empty()) y += KEY_BLOCK_HEIGHT;
[a74b014]1059
[7cdb1c3]1060    if (!other.empty()) {
[522e0bd]1061        DrawRectangle(NODATA_COLOUR, col_BLACK,
1062                      left, y,
1063                      KEY_BLOCK_WIDTH, KEY_BLOCK_HEIGHT);
[62da267]1064        y += KEY_BLOCK_HEIGHT * 2;
[a74b014]1065    }
1066
[2779338f]1067    int start = y;
[2043961]1068    if (num_bands == 1) {
1069        DrawShadedRectangle(GetPen(0), GetPen(0), left, y,
[62da267]1070                            KEY_BLOCK_WIDTH, KEY_BLOCK_HEIGHT);
[2779338f]1071        y += KEY_BLOCK_HEIGHT;
[2043961]1072    } else {
1073        for (band = 0; band < num_bands - 1; ++band) {
1074            DrawShadedRectangle(GetPen(band), GetPen(band + 1), left, y,
[62da267]1075                                KEY_BLOCK_WIDTH, KEY_BLOCK_HEIGHT);
1076            y += KEY_BLOCK_HEIGHT;
[2043961]1077        }
[d4650b3]1078    }
1079
[2779338f]1080    SetColour(col_BLACK);
1081    BeginPolyline();
1082    PlaceIndicatorVertex(left, y);
1083    PlaceIndicatorVertex(left + KEY_BLOCK_WIDTH, y);
1084    PlaceIndicatorVertex(left + KEY_BLOCK_WIDTH, start);
1085    PlaceIndicatorVertex(left, start);
1086    PlaceIndicatorVertex(left, y);
1087    EndPolyline();
1088
[825bdff]1089    SetColour(TEXT_COLOUR);
1090
1091    y = bottom;
1092    if (!units.empty()) {
1093        GetTextExtent(units, &size, NULL);
1094        DrawIndicatorText(left + (KEY_BLOCK_WIDTH - size) / 2, y, units);
1095        y += KEY_BLOCK_HEIGHT;
1096    }
1097    y -= GetFontSize() / 2;
[62da267]1098    left += KEY_BLOCK_WIDTH + 5;
[d4650b3]1099
[7cdb1c3]1100    if (!other.empty()) {
[62da267]1101        y += KEY_BLOCK_HEIGHT / 2;
[7cdb1c3]1102        DrawIndicatorText(left, y, other);
[62da267]1103        y += KEY_BLOCK_HEIGHT * 2 - KEY_BLOCK_HEIGHT / 2;
[a74b014]1104    }
1105
[2043961]1106    if (num_bands == 1) {
[62da267]1107        y += KEY_BLOCK_HEIGHT / 2;
[7cdb1c3]1108        DrawIndicatorText(left, y, key_legends[0]);
[2043961]1109    } else {
[62c5fc6]1110        for (band = 0; band < num_bands; ++band) {
[7cdb1c3]1111            DrawIndicatorText(left, y, key_legends[band]);
[62da267]1112            y += KEY_BLOCK_HEIGHT;
[2043961]1113        }
[d4650b3]1114    }
1115}
1116
[e2ea75a]1117void GfxCore::DrawDepthKey()
[c61aa79]1118{
[62c5fc6]1119    Double z_ext = m_Parent->GetDepthExtent();
[825bdff]1120    int num_bands = 1;
1121    int sf = 0;
1122    if (z_ext > 0.0) {
[62c5fc6]1123        num_bands = GetNumColourBands();
[825bdff]1124        Double z_range = z_ext;
1125        if (!m_Metric) z_range /= METRES_PER_FOOT;
1126        sf = max(0, 1 - (int)floor(log10(z_range)));
[62c5fc6]1127    }
1128
[825bdff]1129    Double z_min = m_Parent->GetDepthMin() + m_Parent->GetOffset().GetZ();
1130    for (int band = 0; band < num_bands; ++band) {
1131        Double z = z_min;
[62c5fc6]1132        if (band)
1133            z += z_ext * band / (num_bands - 1);
[825bdff]1134
1135        if (!m_Metric)
1136            z /= METRES_PER_FOOT;
1137
1138        key_legends[band].Printf(wxT("%.*f"), sf, z);
[c61aa79]1139    }
1140
[ccb83b7]1141    DrawColourKey(num_bands, wxString(), wmsg(m_Metric ? /*m*/424: /*ft*/428));
[7cdb1c3]1142}
[a74b014]1143
[e2ea75a]1144void GfxCore::DrawDateKey()
[7cdb1c3]1145{
1146    int num_bands;
[62c5fc6]1147    if (!HasDateInformation()) {
1148        num_bands = 0;
[7cdb1c3]1149    } else {
[62c5fc6]1150        int date_ext = m_Parent->GetDateExtent();
1151        if (date_ext == 0) {
1152            num_bands = 1;
1153        } else {
1154            num_bands = GetNumColourBands();
1155        }
1156        for (int band = 0; band < num_bands; ++band) {
1157            int y, m, d;
1158            int days = m_Parent->GetDateMin();
1159            if (band)
1160                days += date_ext * band / (num_bands - 1);
1161            ymd_from_days_since_1900(days, &y, &m, &d);
1162            key_legends[band].Printf(wxT("%04d-%02d-%02d"), y, m, d);
1163        }
[7cdb1c3]1164    }
[62c5fc6]1165
[7cdb1c3]1166    wxString other;
1167    if (!m_Parent->HasCompleteDateInfo()) {
[736f7df]1168        /* TRANSLATORS: Used in the "colour key" for "colour by date" if there
[0b8c321]1169         * are surveys without date information.  Try to keep this fairly short.
1170         */
[7cdb1c3]1171        other = wmsg(/*Undated*/221);
[c61aa79]1172    }
1173
[825bdff]1174    DrawColourKey(num_bands, other, wxString());
[7cdb1c3]1175}
[a74b014]1176
[e2ea75a]1177void GfxCore::DrawErrorKey()
[7cdb1c3]1178{
[62c5fc6]1179    int num_bands;
1180    if (HasErrorInformation()) {
1181        // Use fixed colours for each error factor so it's directly visually
1182        // comparable between surveys.
1183        num_bands = GetNumColourBands();
1184        for (int band = 0; band < num_bands; ++band) {
1185            double E = MAX_ERROR * band / (num_bands - 1);
1186            key_legends[band].Printf(wxT("%.2f"), E);
1187        }
1188    } else {
1189        num_bands = 0;
[c61aa79]1190    }
1191
[7cdb1c3]1192    // Always show the "Not in loop" legend for now (FIXME).
[736f7df]1193    /* TRANSLATORS: Used in the "colour key" for "colour by error" for surveys
[0b8c321]1194     * which aren’t part of a loop and so have no error information. Try to keep
1195     * this fairly short. */
[825bdff]1196    DrawColourKey(num_bands, wmsg(/*Not in loop*/290), wxString());
[ac537e9]1197}
1198
[cc9e2c65]1199void GfxCore::DrawGradientKey()
1200{
1201    int num_bands;
1202    // Use fixed colours for each gradient so it's directly visually comparable
1203    // between surveys.
1204    num_bands = GetNumColourBands();
[85dcdcd]1205    wxString units = wmsg(m_Degrees ? /*°*/344 : /*ᵍ*/345);
[cc9e2c65]1206    for (int band = 0; band < num_bands; ++band) {
1207        double gradient = double(band) / (num_bands - 1);
1208        if (m_Degrees) {
1209            gradient *= 90.0;
1210        } else {
1211            gradient *= 100.0;
1212        }
1213        key_legends[band].Printf(wxT("%.f%s"), gradient, units);
1214    }
1215
1216    DrawColourKey(num_bands, wxString(), wxString());
1217}
1218
[af50685]1219void GfxCore::DrawLengthKey()
1220{
1221    int num_bands;
1222    // Use fixed colours for each length so it's directly visually comparable
1223    // between surveys.
1224    num_bands = GetNumColourBands();
1225    for (int band = 0; band < num_bands; ++band) {
1226        double len = pow(10, LOG_LEN_MAX * band / (num_bands - 1));
1227        if (!m_Metric) {
1228            len /= METRES_PER_FOOT;
1229        }
[355809f]1230        key_legends[band].Printf(wxT("%.1f"), len);
[af50685]1231    }
1232
[355809f]1233    DrawColourKey(num_bands, wxString(), wmsg(m_Metric ? /*m*/424: /*ft*/428));
[af50685]1234}
1235
[9c37beb]1236void GfxCore::DrawScaleBar()
[5809313]1237{
[37bc1f5]1238    // Calculate how many metres of survey are currently displayed across the
1239    // screen.
[087bc72]1240    Double across_screen = SurveyUnitsAcrossViewport();
[156dc16]1241
[90430f2]1242    double f = double(GetClinoXPosition() - INDICATOR_BOX_SIZE / 2 - SCALE_BAR_OFFSET_X) / GetXSize();
[5f50488]1243    if (f > 0.75) {
1244        f = 0.75;
1245    } else if (f < 0.5) {
1246        // Stop it getting squeezed to nothing.
1247        // FIXME: In this case we should probably move the compass and clino up
1248        // to make room rather than letting stuff overlap.
1249        f = 0.5;
1250    }
1251
[087bc72]1252    // Convert to imperial measurements if required.
[7a89dc2]1253    Double multiplier = 1.0;
1254    if (!m_Metric) {
[429465a]1255        across_screen /= METRES_PER_FOOT;
1256        multiplier = METRES_PER_FOOT;
[5f50488]1257        if (across_screen >= 5280.0 / f) {
[429465a]1258            across_screen /= 5280.0;
1259            multiplier *= 5280.0;
1260        }
[7a89dc2]1261    }
[5757725]1262
[7a89dc2]1263    // Calculate the length of the scale bar.
[5f50488]1264    Double size_snap = pow(10.0, floor(log10(f * across_screen)));
1265    Double t = across_screen * f / size_snap;
[98860c5]1266    if (t >= 5.0) {
[429465a]1267        size_snap *= 5.0;
[7a89dc2]1268    } else if (t >= 2.0) {
[429465a]1269        size_snap *= 2.0;
[98860c5]1270    }
1271
[7a89dc2]1272    if (!m_Metric) size_snap *= multiplier;
1273
[84cab34]1274    // Actual size of the thing in pixels:
[90430f2]1275    int size = int((size_snap / SurveyUnitsAcrossViewport()) * GetXSize());
[e2c1671]1276    m_ScaleBarWidth = size;
[421b7d2]1277
[5809313]1278    // Draw it...
[e2c1671]1279    const int end_y = SCALE_BAR_OFFSET_Y + SCALE_BAR_HEIGHT;
[5809313]1280    int interval = size / 10;
1281
[aa048c3]1282    gla_colour col = col_WHITE;
[5809313]1283    for (int ix = 0; ix < 10; ix++) {
[e2c1671]1284        int x = SCALE_BAR_OFFSET_X + int(ix * ((Double) size / 10.0));
[421b7d2]1285
[e2c1671]1286        DrawRectangle(col, col, x, end_y, interval + 2, SCALE_BAR_HEIGHT);
[421b7d2]1287
[aa048c3]1288        col = (col == col_WHITE) ? col_GREY : col_WHITE;
[5809313]1289    }
1290
[84cab34]1291    // Add labels.
[825bdff]1292    wxString str;
[ccb83b7]1293    int units;
[825bdff]1294    if (m_Metric) {
[ccb83b7]1295        Double km = size_snap * 1e-3;
1296        if (km >= 1.0) {
1297            size_snap = km;
[736f7df]1298            /* TRANSLATORS: abbreviation for "kilometres" (unit of length),
1299             * used e.g.  "5km".
1300             *
1301             * If there should be a space between the number and this, include
1302             * one in the translation. */
[ccb83b7]1303            units = /*km*/423;
[825bdff]1304        } else if (size_snap >= 1.0) {
[736f7df]1305            /* TRANSLATORS: abbreviation for "metres" (unit of length), used
1306             * e.g. "10m".
[b49ac56]1307             *
[736f7df]1308             * If there should be a space between the number and this, include
1309             * one in the translation. */
[ccb83b7]1310            units = /*m*/424;
[825bdff]1311        } else {
[ccb83b7]1312            size_snap *= 1e2;
[736f7df]1313            /* TRANSLATORS: abbreviation for "centimetres" (unit of length),
1314             * used e.g.  "50cm".
[b49ac56]1315             *
[736f7df]1316             * If there should be a space between the number and this, include
1317             * one in the translation. */
[ccb83b7]1318            units = /*cm*/425;
[825bdff]1319        }
1320    } else {
1321        size_snap /= METRES_PER_FOOT;
1322        Double miles = size_snap / 5280.0;
[ccb83b7]1323        if (miles >= 1.0) {
1324            size_snap = miles;
1325            if (size_snap >= 2.0) {
[736f7df]1326                /* TRANSLATORS: abbreviation for "miles" (unit of length,
1327                 * plural), used e.g.  "2 miles".
[b49ac56]1328                 *
[736f7df]1329                 * If there should be a space between the number and this,
1330                 * include one in the translation. */
[ccb83b7]1331                units = /* miles*/426;
[825bdff]1332            } else {
[736f7df]1333                /* TRANSLATORS: abbreviation for "mile" (unit of length,
1334                 * singular), used e.g.  "1 mile".
[b49ac56]1335                 *
[736f7df]1336                 * If there should be a space between the number and this,
1337                 * include one in the translation. */
[ccb83b7]1338                units = /* mile*/427;
[825bdff]1339            }
[ccb83b7]1340        } else if (size_snap >= 1.0) {
[736f7df]1341            /* TRANSLATORS: abbreviation for "feet" (unit of length), used e.g.
1342             * as "10ft".
[b49ac56]1343             *
[736f7df]1344             * If there should be a space between the number and this, include
1345             * one in the translation. */
[ccb83b7]1346            units = /*ft*/428;
1347        } else {
1348            size_snap *= 12.0;
[736f7df]1349            /* TRANSLATORS: abbreviation for "inches" (unit of length), used
1350             * e.g. as "6in".
[b49ac56]1351             *
[736f7df]1352             * If there should be a space between the number and this, include
1353             * one in the translation. */
[ccb83b7]1354            units = /*in*/429;
[825bdff]1355        }
1356    }
[ccb83b7]1357    if (size_snap >= 1.0) {
1358        str.Printf(wxT("%.f%s"), size_snap, wmsg(units).c_str());
1359    } else {
1360        int sf = -(int)floor(log10(size_snap));
1361        str.Printf(wxT("%.*f%s"), sf, size_snap, wmsg(units).c_str());
1362    }
[84cab34]1363
[1eeb55a]1364    int text_width, text_height;
[56da40e]1365    GetTextExtent(str, &text_width, &text_height);
[8bd480e]1366    const int text_y = end_y - text_height + 1;
1367    SetColour(TEXT_COLOUR);
[5627cbb]1368    DrawIndicatorText(SCALE_BAR_OFFSET_X, text_y, wxT("0"));
[8bd480e]1369    DrawIndicatorText(SCALE_BAR_OFFSET_X + size - text_width, text_y, str);
[5809313]1370}
[56da40e]1371
[2072157]1372bool GfxCore::CheckHitTestGrid(const wxPoint& point, bool centre)
[2effbf1]1373{
[0874c07e]1374    if (Animating()) return false;
1375
[90430f2]1376    if (point.x < 0 || point.x >= GetXSize() ||
1377        point.y < 0 || point.y >= GetYSize()) {
[429465a]1378        return false;
[137e31b]1379    }
[421b7d2]1380
[156f645]1381    SetDataTransform();
[00a68e0]1382
[69463a0]1383    if (!m_HitTestGridValid) CreateHitTestGrid();
[fa42426]1384
[0b0520c]1385    int grid_x = point.x * HITTEST_SIZE / (GetXSize() + 1);
1386    int grid_y = point.y * HITTEST_SIZE / (GetYSize() + 1);
[137e31b]1387
[fa42426]1388    LabelInfo *best = NULL;
[d96c95c]1389    int dist_sqrd = sqrd_measure_threshold;
[2effbf1]1390    int square = grid_x + grid_y * HITTEST_SIZE;
[fa42426]1391    list<LabelInfo*>::iterator iter = m_PointGrid[square].begin();
[00a68e0]1392
[fa42426]1393    while (iter != m_PointGrid[square].end()) {
[429465a]1394        LabelInfo *pt = *iter++;
[fa42426]1395
[f6d8375]1396        double cx, cy, cz;
[00a68e0]1397
[d67450e]1398        Transform(*pt, &cx, &cy, &cz);
[00a68e0]1399
[90430f2]1400        cy = GetYSize() - cy;
[00a68e0]1401
[429465a]1402        int dx = point.x - int(cx);
1403        int ds = dx * dx;
1404        if (ds >= dist_sqrd) continue;
1405        int dy = point.y - int(cy);
[fa42426]1406
[429465a]1407        ds += dy * dy;
1408        if (ds >= dist_sqrd) continue;
[f433fda]1409
[429465a]1410        dist_sqrd = ds;
1411        best = pt;
[f433fda]1412
[429465a]1413        if (ds == 0) break;
[2effbf1]1414    }
[f433fda]1415
[fa42426]1416    if (best) {
[381ae6e]1417        m_Parent->ShowInfo(best, m_there);
[429465a]1418        if (centre) {
[e67ed1b]1419            // FIXME: allow Ctrl-Click to not set there or something?
[82c3731]1420            CentreOn(*best);
[90430f2]1421            WarpPointer(GetXSize() / 2, GetYSize() / 2);
[381ae6e]1422            SetThere(best);
[429465a]1423            m_Parent->SelectTreeItem(best);
1424        }
[e67ed1b]1425    } else {
1426        // Left-clicking not on a survey cancels the measuring line.
[0633bcc]1427        if (centre) {
1428            ClearTreeSelection();
1429        } else {
[381ae6e]1430            m_Parent->ShowInfo(best, m_there);
[f6d8375]1431            double x, y, z;
[90430f2]1432            ReverseTransform(point.x, GetYSize() - point.y, &x, &y, &z);
[381ae6e]1433            temp_here.assign(Vector3(x, y, z));
1434            SetHere(&temp_here);
[0633bcc]1435        }
[2effbf1]1436    }
[203d2a7]1437
1438    return best;
[2effbf1]1439}
1440
[5876fcb]1441void GfxCore::OnSize(wxSizeEvent& event)
[5809313]1442{
[5876fcb]1443    // Handle a change in window size.
1444    wxSize size = event.GetSize();
[5809313]1445
[78beaf1]1446    if (size.GetWidth() <= 0 || size.GetHeight() <= 0) {
[0580c6a]1447        // Before things are fully initialised, we sometimes get a bogus
1448        // resize message...
[6ef8bd73]1449        // FIXME have changes in MainFrm cured this?  It still happens with
[880b954]1450        // 1.0.32 and wxGTK 2.5.2 (load a file from the command line).
1451        // With 1.1.6 and wxGTK 2.4.2 we only get negative sizes if MainFrm
[78beaf1]1452        // is resized such that the GfxCore window isn't visible.
1453        //printf("OnSize(%d,%d)\n", size.GetWidth(), size.GetHeight());
[0580c6a]1454        return;
1455    }
[b72f4b5]1456
[9071cf5]1457    event.Skip();
[39e460c9]1458
[5876fcb]1459    if (m_DoneFirstShow) {
[81f1266]1460        TryToFreeArrays();
[6ebc0ce]1461
[69463a0]1462        m_HitTestGridValid = false;
[33b2094]1463
[d67450e]1464        ForceRefresh();
[5809313]1465    }
1466}
1467
[de7a879]1468void GfxCore::DefaultParameters()
[5809313]1469{
[33b2094]1470    // Set default viewing parameters.
[b462168]1471
[f433fda]1472    m_Surface = false;
1473    if (!m_Parent->HasUndergroundLegs()) {
1474        if (m_Parent->HasSurfaceLegs()) {
1475            // If there are surface legs, but no underground legs, turn
1476            // surface surveys on.
1477            m_Surface = true;
1478        } else {
1479            // If there are no legs (e.g. after loading a .pos file), turn
1480            // crosses on.
1481            m_Crosses = true;
1482        }
[b462168]1483    }
1484
[714daae]1485    m_PanAngle = 0.0;
[eef68f9]1486    if (m_Parent->IsExtendedElevation()) {
1487        m_TiltAngle = 0.0;
1488    } else {
[7a57dc7]1489        m_TiltAngle = -90.0;
[b462168]1490    }
[714daae]1491
[08253d9]1492    SetRotation(m_PanAngle, m_TiltAngle);
[d67450e]1493    SetTranslation(Vector3());
[33b2094]1494
[e577f89]1495    m_RotationStep = 30.0;
[5809313]1496    m_Rotating = false;
[3d00693]1497    m_SwitchingTo = 0;
[fe444b8]1498    m_Entrances = false;
1499    m_FixedPts = false;
1500    m_ExportedPts = false;
[c1cf79d]1501    m_Grid = false;
[f4c5932]1502    m_BoundingBox = false;
[33b2094]1503    m_Tubes = false;
[1eeb55a]1504    if (GetPerspective()) TogglePerspective();
[39bd2ef]1505
1506    // Set the initial scale.
1507    SetScale(initial_scale);
[de7a879]1508}
[5809313]1509
[de7a879]1510void GfxCore::Defaults()
1511{
1512    // Restore default scale, rotation and translation parameters.
1513    DefaultParameters();
[ba358fc]1514
1515    // Invalidate all the cached lists.
1516    GLACanvas::FirstShow();
1517
[fa42426]1518    ForceRefresh();
[5809313]1519}
[84cab34]1520
[5455bb2]1521void GfxCore::Animate()
[5809313]1522{
[2a3d328]1523    // Don't show pointer coordinates while animating.
[4b031c0]1524    // FIXME : only do this when we *START* animating!  Use a static copy
1525    // of the value of "Animating()" last time we were here to track this?
1526    // MainFrm now checks if we're trying to clear already cleared labels
1527    // and just returns, but it might be simpler to check here!
[2a3d328]1528    ClearCoords();
[381ae6e]1529    m_Parent->ShowInfo();
[5809313]1530
[5455bb2]1531    long t;
[75d4a2b]1532    if (movie) {
[aea4f8b]1533        ReadPixels(movie->GetWidth(), movie->GetHeight(), movie->GetBuffer());
[98fd937]1534        if (!movie->AddFrame()) {
1535            wxGetApp().ReportError(wxString(movie->get_error_string(), wxConvUTF8));
1536            delete movie;
1537            movie = NULL;
1538            presentation_mode = 0;
[5455bb2]1539            return;
[98fd937]1540        }
[5455bb2]1541        t = 1000 / 25; // 25 frames per second
[6a4cdcb6]1542    } else {
[5455bb2]1543        static long t_prev = 0;
1544        t = timer.Time();
1545        // Avoid redrawing twice in the same frame.
[ff1601e]1546        long delta_t = (t_prev == 0 ? 1000 / MAX_FRAMERATE : t - t_prev);
1547        if (delta_t < 1000 / MAX_FRAMERATE)
[6987d2a]1548            return;
[5455bb2]1549        t_prev = t;
[6987d2a]1550        if (presentation_mode == PLAYING && pres_speed != 0.0)
1551            t = delta_t;
[6a4cdcb6]1552    }
[5809313]1553
[128fac4]1554    if (presentation_mode == PLAYING && pres_speed != 0.0) {
[6987d2a]1555        // FIXME: It would probably be better to work relative to the time we
1556        // passed the last mark, but that's complicated by the speed
1557        // potentially changing (or even the direction of playback reversing)
1558        // at any point during playback.
1559        Double tick = t * 0.001 * fabs(pres_speed);
1560        while (tick >= next_mark_time) {
1561            tick -= next_mark_time;
[128fac4]1562            this_mark_total = 0;
[58dfdd21]1563            PresentationMark prev_mark = next_mark;
[e577f89]1564            if (prev_mark.angle < 0) prev_mark.angle += 360.0;
1565            else if (prev_mark.angle >= 360.0) prev_mark.angle -= 360.0;
[128fac4]1566            if (pres_reverse)
1567                next_mark = m_Parent->GetPresMark(MARK_PREV);
1568            else
1569                next_mark = m_Parent->GetPresMark(MARK_NEXT);
[1690fa9]1570            if (!next_mark.is_valid()) {
[128fac4]1571                SetView(prev_mark);
[1690fa9]1572                presentation_mode = 0;
[387babf]1573                if (movie && !movie->Close()) {
[98fd937]1574                    wxGetApp().ReportError(wxString(movie->get_error_string(), wxConvUTF8));
1575                }
[81f1266]1576                delete movie;
1577                movie = NULL;
[1690fa9]1578                break;
1579            }
[58dfdd21]1580
[128fac4]1581            double tmp = (pres_reverse ? prev_mark.time : next_mark.time);
1582            if (tmp > 0) {
1583                next_mark_time = tmp;
[58dfdd21]1584            } else {
[d67450e]1585                double d = (next_mark - prev_mark).magnitude();
[49ce5b0]1586                // FIXME: should ignore component of d which is unseen in
1587                // non-perspective mode?
[8674eea]1588                next_mark_time = sqrd(d / 30.0);
[49ce5b0]1589                double a = next_mark.angle - prev_mark.angle;
1590                if (a > 180.0) {
1591                    next_mark.angle -= 360.0;
1592                    a = 360.0 - a;
1593                } else if (a < -180.0) {
1594                    next_mark.angle += 360.0;
1595                    a += 360.0;
1596                } else {
1597                    a = fabs(a);
1598                }
1599                next_mark_time += sqrd(a / 60.0);
1600                double ta = fabs(next_mark.tilt_angle - prev_mark.tilt_angle);
1601                next_mark_time += sqrd(ta / 60.0);
1602                double s = fabs(log(next_mark.scale) - log(prev_mark.scale));
[8674eea]1603                next_mark_time += sqrd(s / 2.0);
[49ce5b0]1604                next_mark_time = sqrt(next_mark_time);
[8674eea]1605                // was: next_mark_time = max(max(d / 30, s / 2), max(a, ta) / 60);
[49ce5b0]1606                //printf("*** %.6f from (\nd: %.6f\ns: %.6f\na: %.6f\nt: %.6f )\n",
[8674eea]1607                //       next_mark_time, d/30.0, s/2.0, a/60.0, ta/60.0);
1608                if (tmp < 0) next_mark_time /= -tmp;
[58dfdd21]1609            }
[1690fa9]1610        }
1611
1612        if (presentation_mode) {
1613            // Advance position towards next_mark
[6987d2a]1614            double p = tick / next_mark_time;
[1690fa9]1615            double q = 1 - p;
1616            PresentationMark here = GetView();
[d877aa2]1617            if (next_mark.angle < 0) {
[e577f89]1618                if (here.angle >= next_mark.angle + 360.0)
1619                    here.angle -= 360.0;
1620            } else if (next_mark.angle >= 360.0) {
1621                if (here.angle <= next_mark.angle - 360.0)
1622                    here.angle += 360.0;
[d877aa2]1623            }
[8674eea]1624            here.assign(q * here + p * next_mark);
[1690fa9]1625            here.angle = q * here.angle + p * next_mark.angle;
[e577f89]1626            if (here.angle < 0) here.angle += 360.0;
1627            else if (here.angle >= 360.0) here.angle -= 360.0;
[1690fa9]1628            here.tilt_angle = q * here.tilt_angle + p * next_mark.tilt_angle;
[58dfdd21]1629            here.scale = exp(q * log(here.scale) + p * log(next_mark.scale));
[1690fa9]1630            SetView(here);
[6987d2a]1631            this_mark_total += tick;
1632            next_mark_time -= tick;
[1690fa9]1633        }
[6987d2a]1634
1635        ForceRefresh();
1636        return;
[1690fa9]1637    }
1638
[5876fcb]1639    // When rotating...
1640    if (m_Rotating) {
[5455bb2]1641        Double step = base_pan + (t - base_pan_time) * 1e-3 * m_RotationStep - m_PanAngle;
1642        TurnCave(step);
[5876fcb]1643    }
[5809313]1644
[5876fcb]1645    if (m_SwitchingTo == PLAN) {
[429465a]1646        // When switching to plan view...
[5455bb2]1647        Double step = base_tilt - (t - base_tilt_time) * 1e-3 * 90.0 - m_TiltAngle;
1648        TiltCave(step);
[7a57dc7]1649        if (m_TiltAngle == -90.0) {
[429465a]1650            m_SwitchingTo = 0;
1651        }
[1690fa9]1652    } else if (m_SwitchingTo == ELEVATION) {
[429465a]1653        // When switching to elevation view...
[5455bb2]1654        Double step;
1655        if (m_TiltAngle > 0.0) {
1656            step = base_tilt - (t - base_tilt_time) * 1e-3 * 90.0 - m_TiltAngle;
[7a57dc7]1657        } else {
[5455bb2]1658            step = base_tilt + (t - base_tilt_time) * 1e-3 * 90.0 - m_TiltAngle;
[429465a]1659        }
[5455bb2]1660        if (fabs(step) >= fabs(m_TiltAngle)) {
[3ddd351]1661            m_SwitchingTo = 0;
[5455bb2]1662            step = -m_TiltAngle;
1663        }
1664        TiltCave(step);
1665    } else if (m_SwitchingTo) {
1666        // Rotate the shortest way around to the destination angle.  If we're
1667        // 180 off, we favour turning anticlockwise, as auto-rotation does by
1668        // default.
1669        Double target = (m_SwitchingTo - NORTH) * 90;
1670        Double diff = target - m_PanAngle;
1671        diff = fmod(diff, 360);
1672        if (diff <= -180)
1673            diff += 360;
1674        else if (diff > 180)
1675            diff -= 360;
1676        if (m_RotationStep < 0 && diff == 180.0)
1677            diff = -180.0;
1678        Double step = base_pan - m_PanAngle;
1679        Double delta = (t - base_pan_time) * 1e-3 * fabs(m_RotationStep);
1680        if (diff > 0) {
1681            step += delta;
[3ddd351]1682        } else {
[5455bb2]1683            step -= delta;
[3ddd351]1684        }
[5455bb2]1685        step = fmod(step, 360);
1686        if (step <= -180)
1687            step += 360;
1688        else if (step > 180)
1689            step -= 360;
1690        if (fabs(step) >= fabs(diff)) {
1691            m_SwitchingTo = 0;
1692            step = diff;
1693        }
1694        TurnCave(step);
[5876fcb]1695    }
[5809313]1696
[5455bb2]1697    ForceRefresh();
[5809313]1698}
[84cab34]1699
[0580c6a]1700// How much to allow around the box - this is because of the ring shape
1701// at one end of the line.
1702static const int HIGHLIGHTED_PT_SIZE = 2; // FIXME: tie in to blob and ring size
1703#define MARGIN (HIGHLIGHTED_PT_SIZE * 2 + 1)
[381ae6e]1704void GfxCore::RefreshLine(const Point *a, const Point *b, const Point *c)
[7a89dc2]1705{
[0f86024]1706#ifdef __WXMSW__
1707    (void)a;
1708    (void)b;
1709    (void)c;
1710    // FIXME: We get odd redraw artifacts if we just update the line, and
1711    // redrawing the whole scene doesn't actually seem to be measurably
1712    // slower.  That may not be true with software rendering though...
1713    ForceRefresh();
1714#else
[06d367d]1715    // Best of all might be to copy the window contents before we draw the
1716    // line, then replace each time we redraw.
[796d7bf]1717
[5876fcb]1718    // Calculate the minimum rectangle which includes the old and new
1719    // measuring lines to minimise the redraw time
1720    int l = INT_MAX, r = INT_MIN, u = INT_MIN, d = INT_MAX;
[f6d8375]1721    double X, Y, Z;
[381ae6e]1722    if (a) {
1723        if (!Transform(*a, &X, &Y, &Z)) {
[796d7bf]1724            printf("oops\n");
1725        } else {
1726            int x = int(X);
[90430f2]1727            int y = GetYSize() - 1 - int(Y);
[1c448e1]1728            l = x;
1729            r = x;
1730            u = y;
1731            d = y;
[796d7bf]1732        }
[5876fcb]1733    }
[381ae6e]1734    if (b) {
1735        if (!Transform(*b, &X, &Y, &Z)) {
[796d7bf]1736            printf("oops\n");
1737        } else {
1738            int x = int(X);
[90430f2]1739            int y = GetYSize() - 1 - int(Y);
[1c448e1]1740            l = min(l, x);
1741            r = max(r, x);
1742            u = max(u, y);
1743            d = min(d, y);
[796d7bf]1744        }
[5876fcb]1745    }
[381ae6e]1746    if (c) {
1747        if (!Transform(*c, &X, &Y, &Z)) {
[796d7bf]1748            printf("oops\n");
1749        } else {
1750            int x = int(X);
[90430f2]1751            int y = GetYSize() - 1 - int(Y);
[1c448e1]1752            l = min(l, x);
1753            r = max(r, x);
1754            u = max(u, y);
1755            d = min(d, y);
[796d7bf]1756        }
[5876fcb]1757    }
[1c448e1]1758    l -= MARGIN;
1759    r += MARGIN;
1760    u += MARGIN;
1761    d -= MARGIN;
[0ca8fe0]1762    RefreshRect(wxRect(l, d, r - l, u - d), false);
[0f86024]1763#endif
[7a89dc2]1764}
1765
[0642381]1766void GfxCore::HighlightSurvey()
1767{
1768    SurveyFilter filter;
1769    filter.add(highlighted_survey);
1770    filter.SetSeparator(m_Parent->GetSeparator());
1771
1772    double x_min = HUGE_VAL, x_max = -HUGE_VAL;
1773    double y_min = HUGE_VAL, y_max = -HUGE_VAL;
1774    double xpy_min = HUGE_VAL, xpy_max = -HUGE_VAL;
1775    double xmy_min = HUGE_VAL, xmy_max = -HUGE_VAL;
1776    list<LabelInfo*>::const_iterator pos = m_Parent->GetLabels();
1777    double x_tot = 0, y_tot = 0;
1778    size_t c = 0;
1779    while (pos != m_Parent->GetLabelsEnd()) {
1780        const LabelInfo* label = *pos++;
1781        if (!filter.CheckVisible(label->GetText()))
1782            continue;
1783
1784        double x, y, z;
1785        Transform(*label, &x, &y, &z);
1786        if (x < x_min) x_min = x;
1787        if (x > x_max) x_max = x;
1788        if (y < y_min) y_min = y;
1789        if (y > y_max) y_max = y;
1790        double xpy = x + y;
1791        if (xpy < xpy_min) xpy_min = xpy;
1792        if (xpy > xpy_max) xpy_max = xpy;
1793        double xmy = x - y;
1794        if (xmy < xmy_min) xmy_min = xmy;
1795        if (xmy > xmy_max) xmy_max = xmy;
1796        x_tot += x;
1797        y_tot += y;
1798        ++c;
1799    }
1800    for (int f = 0; f != 8; ++f) {
1801        list<traverse>::const_iterator trav = m_Parent->traverses_begin(f, &filter);
1802        list<traverse>::const_iterator tend = m_Parent->traverses_end(f);
1803        while (trav != tend) {
1804            for (auto&& p : *trav) {
1805                double x, y, z;
1806                Transform(p, &x, &y, &z);
1807                if (x < x_min) x_min = x;
1808                if (x > x_max) x_max = x;
1809                if (y < y_min) y_min = y;
1810                if (y > y_max) y_max = y;
1811                double xpy = x + y;
1812                if (xpy < xpy_min) xpy_min = xpy;
1813                if (xpy > xpy_max) xpy_max = xpy;
1814                double xmy = x - y;
1815                if (xmy < xmy_min) xmy_min = xmy;
1816                if (xmy > xmy_max) xmy_max = xmy;
1817                x_tot += x;
1818                y_tot += y;
1819                ++c;
1820            }
1821            trav = m_Parent->traverses_next(f, &filter, trav);
1822        }
1823    }
1824
1825    if (c == 0) return;
1826
1827    // Minimum margin around survey.
1828    const double M = 4.0;
1829    // X/Y component when M measured diagonally.
1830    const double D = M * sqrt(2.0) / 2.0;
1831
1832    SetColour(col_WHITE);
1833    BeginPolyloop();
1834    PlaceIndicatorVertex(xmy_max + y_min, y_min - M);
1835    PlaceIndicatorVertex(xmy_max + y_min + D, y_min - D);
1836    PlaceIndicatorVertex(x_max + D, x_max - xmy_max - D);
1837    PlaceIndicatorVertex(x_max + M, x_max - xmy_max);
1838    PlaceIndicatorVertex(x_max + M, xpy_max - x_max);
1839    PlaceIndicatorVertex(x_max + D, xpy_max - x_max + D);
1840    PlaceIndicatorVertex(xpy_max - y_max + D, y_max + D);
1841    PlaceIndicatorVertex(xpy_max - y_max, y_max + M);
1842    PlaceIndicatorVertex(xmy_min + y_max, y_max + M);
1843    PlaceIndicatorVertex(xmy_min + y_max - D, y_max + D);
1844    PlaceIndicatorVertex(x_min - D, x_min - xmy_min + D);
1845    PlaceIndicatorVertex(x_min - M, x_min - xmy_min);
1846    PlaceIndicatorVertex(x_min - M, xpy_min - x_min);
1847    PlaceIndicatorVertex(x_min - D, xpy_min - x_min - D);
1848    PlaceIndicatorVertex(xpy_min - y_min - D, y_min - D);
1849    PlaceIndicatorVertex(xpy_min - y_min, y_min - M);
1850    EndPolyloop();
1851}
1852
1853void GfxCore::ZoomToSurvey(const wxString& survey) {
1854    SurveyFilter filter;
1855    filter.add(survey);
1856    filter.SetSeparator(m_Parent->GetSeparator());
1857
1858    Double xmin = DBL_MAX;
1859    Double xmax = -DBL_MAX;
1860    Double ymin = DBL_MAX;
1861    Double ymax = -DBL_MAX;
1862    Double zmin = DBL_MAX;
1863    Double zmax = -DBL_MAX;
1864
1865    list<LabelInfo*>::const_iterator pos = m_Parent->GetLabels();
1866    while (pos != m_Parent->GetLabelsEnd()) {
1867        LabelInfo* label = *pos++;
1868
1869        if (!filter.CheckVisible(label->GetText()))
1870            continue;
1871
1872        if (label->GetX() < xmin) xmin = label->GetX();
1873        if (label->GetX() > xmax) xmax = label->GetX();
1874        if (label->GetY() < ymin) ymin = label->GetY();
1875        if (label->GetY() > ymax) ymax = label->GetY();
1876        if (label->GetZ() < zmin) zmin = label->GetZ();
1877        if (label->GetZ() > zmax) zmax = label->GetZ();
1878    }
1879
1880    SetViewTo(xmin, xmax, ymin, ymax, zmin, zmax);
1881}
1882
[381ae6e]1883void GfxCore::SetHereFromTree(const LabelInfo * p)
1884{
1885    SetHere(p);
1886    m_Parent->ShowInfo(m_here, m_there);
[0642381]1887    SetHereSurvey(wxString());
[381ae6e]1888}
1889
1890void GfxCore::SetHere(const LabelInfo *p)
[c6d95d8]1891{
[5d18821]1892    if (p == m_here) return;
[6b061db]1893    bool line_active = MeasuringLineActive();
[381ae6e]1894    const LabelInfo * old = m_here;
[82c3731]1895    m_here = p;
[6b061db]1896    if (line_active || MeasuringLineActive())
1897        RefreshLine(old, m_there, m_here);
[156dc16]1898}
1899
[381ae6e]1900void GfxCore::SetThere(const LabelInfo * p)
[156dc16]1901{
[5d18821]1902    if (p == m_there) return;
[381ae6e]1903    const LabelInfo * old = m_there;
[82c3731]1904    m_there = p;
[0580c6a]1905    RefreshLine(m_here, old, m_there);
[156dc16]1906}
1907
[5876fcb]1908void GfxCore::CreateHitTestGrid()
[156dc16]1909{
[7171240]1910    if (!m_PointGrid) {
1911        // Initialise hit-test grid.
1912        m_PointGrid = new list<LabelInfo*>[HITTEST_SIZE * HITTEST_SIZE];
1913    } else {
1914        // Clear hit-test grid.
1915        for (int i = 0; i < HITTEST_SIZE * HITTEST_SIZE; i++) {
1916            m_PointGrid[i].clear();
1917        }
[5876fcb]1918    }
[156dc16]1919
[1a46879]1920    const SurveyFilter* filter = m_Parent->GetTreeFilter();
[5876fcb]1921    // Fill the grid.
1922    list<LabelInfo*>::const_iterator pos = m_Parent->GetLabels();
1923    list<LabelInfo*>::const_iterator end = m_Parent->GetLabelsEnd();
1924    while (pos != end) {
[429465a]1925        LabelInfo* label = *pos++;
[33b2094]1926
[a6401ff3]1927        if (m_Splays == SHOW_HIDE && label->IsSplayEnd())
1928            continue;
1929
[429465a]1930        if (!((m_Surface && label->IsSurface()) ||
[dc42b8e]1931              (m_Legs && label->IsUnderground()) ||
1932              (!label->IsSurface() && !label->IsUnderground()))) {
1933            // if this station isn't to be displayed, skip to the next
1934            // (last case is for stns with no legs attached)
[429465a]1935            continue;
1936        }
[a6401ff3]1937
[672459c]1938        if (filter && !filter->CheckVisible(label->GetText()))
1939            continue;
[33b2094]1940
[429465a]1941        // Calculate screen coordinates.
[f6d8375]1942        double cx, cy, cz;
[d67450e]1943        Transform(*label, &cx, &cy, &cz);
[90430f2]1944        if (cx < 0 || cx >= GetXSize()) continue;
1945        if (cy < 0 || cy >= GetYSize()) continue;
[33b2094]1946
[90430f2]1947        cy = GetYSize() - cy;
[00a68e0]1948
[429465a]1949        // On-screen, so add to hit-test grid...
[0b0520c]1950        int grid_x = int(cx * HITTEST_SIZE / (GetXSize() + 1));
1951        int grid_y = int(cy * HITTEST_SIZE / (GetYSize() + 1));
[33b2094]1952
[429465a]1953        m_PointGrid[grid_x + grid_y * HITTEST_SIZE].push_back(label);
[33b2094]1954    }
1955
[00a68e0]1956    m_HitTestGridValid = true;
[33b2094]1957}
[c6d95d8]1958
[2a02de2]1959//
[5876fcb]1960//  Methods for controlling the orientation of the survey
[2a02de2]1961//
1962
[5876fcb]1963void GfxCore::TurnCave(Double angle)
[156dc16]1964{
[5876fcb]1965    // Turn the cave around its z-axis by a given angle.
[156dc16]1966
[5876fcb]1967    m_PanAngle += angle;
[5a7597a]1968    // Wrap to range [0, 360):
1969    m_PanAngle = fmod(m_PanAngle, 360.0);
1970    if (m_PanAngle < 0.0) {
[e577f89]1971        m_PanAngle += 360.0;
[156dc16]1972    }
[33b2094]1973
[00a68e0]1974    m_HitTestGridValid = false;
[381ae6e]1975    if (m_here && m_here == &temp_here) SetHere();
[00a68e0]1976
[08253d9]1977    SetRotation(m_PanAngle, m_TiltAngle);
[156dc16]1978}
1979
[5876fcb]1980void GfxCore::TurnCaveTo(Double angle)
[d80805e]1981{
[5455bb2]1982    if (m_Rotating) {
1983        // If we're rotating, jump to the specified angle.
1984        TurnCave(angle - m_PanAngle);
1985        SetPanBase();
1986        return;
1987    }
1988
[3ddd351]1989    int new_switching_to = ((int)angle) / 90 + NORTH;
1990    if (new_switching_to == m_SwitchingTo) {
1991        // A second order to switch takes us there right away
1992        TurnCave(angle - m_PanAngle);
1993        m_SwitchingTo = 0;
1994        ForceRefresh();
1995    } else {
[5455bb2]1996        SetPanBase();
[3ddd351]1997        m_SwitchingTo = new_switching_to;
1998    }
[d80805e]1999}
2000
[5876fcb]2001void GfxCore::TiltCave(Double tilt_angle)
[156dc16]2002{
[5876fcb]2003    // Tilt the cave by a given angle.
[e577f89]2004    if (m_TiltAngle + tilt_angle > 90.0) {
[08253d9]2005        m_TiltAngle = 90.0;
[e577f89]2006    } else if (m_TiltAngle + tilt_angle < -90.0) {
[08253d9]2007        m_TiltAngle = -90.0;
2008    } else {
2009        m_TiltAngle += tilt_angle;
[d80805e]2010    }
2011
[00a68e0]2012    m_HitTestGridValid = false;
[381ae6e]2013    if (m_here && m_here == &temp_here) SetHere();
[00a68e0]2014
[08253d9]2015    SetRotation(m_PanAngle, m_TiltAngle);
[5ffa439]2016}
2017
[5876fcb]2018void GfxCore::TranslateCave(int dx, int dy)
[5ffa439]2019{
[33b2094]2020    AddTranslationScreenCoordinates(dx, dy);
[00a68e0]2021    m_HitTestGridValid = false;
2022
[381ae6e]2023    if (m_here && m_here == &temp_here) SetHere();
[c00c6713]2024
[33b2094]2025    ForceRefresh();
2026}
[5876fcb]2027
[33b2094]2028void GfxCore::DragFinished()
2029{
[76dd228]2030    m_MouseOutsideCompass = m_MouseOutsideElev = false;
[5876fcb]2031    ForceRefresh();
[2173dbd]2032}
2033
[d877aa2]2034void GfxCore::ClearCoords()
2035{
2036    m_Parent->ClearCoords();
2037}
2038
[5876fcb]2039void GfxCore::SetCoords(wxPoint point)
[fd6e0d5]2040{
[0874c07e]2041    // We can't work out 2D coordinates from a perspective view, and it
2042    // doesn't really make sense to show coordinates while we're animating.
2043    if (GetPerspective() || Animating()) return;
[0a811ab]2044
[5876fcb]2045    // Update the coordinate or altitude display, given the (x, y) position in
2046    // window coordinates.  The relevant display is updated depending on
2047    // whether we're in plan or elevation view.
2048
[f6d8375]2049    double cx, cy, cz;
[5876fcb]2050
[a2b3d62]2051    SetDataTransform();
[90430f2]2052    ReverseTransform(point.x, GetYSize() - 1 - point.y, &cx, &cy, &cz);
[5876fcb]2053
[0633bcc]2054    if (ShowingPlan()) {
[d67450e]2055        m_Parent->SetCoords(cx + m_Parent->GetOffset().GetX(),
[381ae6e]2056                            cy + m_Parent->GetOffset().GetY(),
2057                            m_there);
[0633bcc]2058    } else if (ShowingElevation()) {
[381ae6e]2059        m_Parent->SetAltitude(cz + m_Parent->GetOffset().GetZ(),
2060                              m_there);
[d479c15]2061    } else {
[429465a]2062        m_Parent->ClearCoords();
[a2b3d62]2063    }
[fd6e0d5]2064}
2065
[2a26b45]2066int GfxCore::GetCompassWidth() const
2067{
2068    static int result = 0;
2069    if (result == 0) {
2070        result = INDICATOR_BOX_SIZE;
2071        int width;
2072        const wxString & msg = wmsg(/*Facing*/203);
2073        GetTextExtent(msg, &width, NULL);
2074        if (width > result) result = width;
2075    }
2076    return result;
2077}
2078
2079int GfxCore::GetClinoWidth() const
2080{
2081    static int result = 0;
2082    if (result == 0) {
2083        result = INDICATOR_BOX_SIZE;
2084        int width;
2085        const wxString & msg1 = wmsg(/*Plan*/432);
2086        GetTextExtent(msg1, &width, NULL);
2087        if (width > result) result = width;
2088        const wxString & msg2 = wmsg(/*Kiwi Plan*/433);
2089        GetTextExtent(msg2, &width, NULL);
2090        if (width > result) result = width;
2091        const wxString & msg3 = wmsg(/*Elevation*/118);
2092        GetTextExtent(msg3, &width, NULL);
2093        if (width > result) result = width;
2094    }
2095    return result;
2096}
2097
[1eeb55a]2098int GfxCore::GetCompassXPosition() const
[1fd2edb]2099{
[f433fda]2100    // Return the x-coordinate of the centre of the compass in window
2101    // coordinates.
[2a26b45]2102    return GetXSize() - INDICATOR_OFFSET_X - GetCompassWidth() / 2;
[1fd2edb]2103}
2104
[1eeb55a]2105int GfxCore::GetClinoXPosition() const
[1fd2edb]2106{
[f433fda]2107    // Return the x-coordinate of the centre of the compass in window
2108    // coordinates.
[2a26b45]2109    return GetXSize() - GetClinoOffset() - GetClinoWidth() / 2;
[1fd2edb]2110}
2111
[1eeb55a]2112int GfxCore::GetIndicatorYPosition() const
[dfe4454c]2113{
[f433fda]2114    // Return the y-coordinate of the centre of the indicators in window
2115    // coordinates.
[90430f2]2116    return GetYSize() - INDICATOR_OFFSET_Y - INDICATOR_BOX_SIZE / 2;
[5876fcb]2117}
[fa42426]2118
[1eeb55a]2119int GfxCore::GetIndicatorRadius() const
[5876fcb]2120{
2121    // Return the radius of each indicator.
[1eeb55a]2122    return (INDICATOR_BOX_SIZE - INDICATOR_MARGIN * 2) / 2;
[5876fcb]2123}
[dfe4454c]2124
[14acdae]2125bool GfxCore::PointWithinCompass(wxPoint point) const
[5876fcb]2126{
[f433fda]2127    // Determine whether a point (in window coordinates) lies within the
2128    // compass.
[e2c1671]2129    if (!ShowingCompass()) return false;
2130
[33b2094]2131    glaCoord dx = point.x - GetCompassXPosition();
2132    glaCoord dy = point.y - GetIndicatorYPosition();
2133    glaCoord radius = GetIndicatorRadius();
[f433fda]2134
[5876fcb]2135    return (dx * dx + dy * dy <= radius * radius);
2136}
[fa42426]2137
[14acdae]2138bool GfxCore::PointWithinClino(wxPoint point) const
[5876fcb]2139{
2140    // Determine whether a point (in window coordinates) lies within the clino.
[e2c1671]2141    if (!ShowingClino()) return false;
2142
[33b2094]2143    glaCoord dx = point.x - GetClinoXPosition();
2144    glaCoord dy = point.y - GetIndicatorYPosition();
2145    glaCoord radius = GetIndicatorRadius();
[f433fda]2146
[5876fcb]2147    return (dx * dx + dy * dy <= radius * radius);
[dfe4454c]2148}
[8000d8f]2149
[14acdae]2150bool GfxCore::PointWithinScaleBar(wxPoint point) const
[5876fcb]2151{
[e2c1671]2152    // Determine whether a point (in window coordinates) lies within the scale
2153    // bar.
2154    if (!ShowingScaleBar()) return false;
[8000d8f]2155
[e2c1671]2156    return (point.x >= SCALE_BAR_OFFSET_X &&
2157            point.x <= SCALE_BAR_OFFSET_X + m_ScaleBarWidth &&
[90430f2]2158            point.y <= GetYSize() - SCALE_BAR_OFFSET_Y - SCALE_BAR_HEIGHT &&
2159            point.y >= GetYSize() - SCALE_BAR_OFFSET_Y - SCALE_BAR_HEIGHT*2);
[5876fcb]2160}
[8000d8f]2161
[d43fa84]2162bool GfxCore::PointWithinColourKey(wxPoint point) const
2163{
2164    // Determine whether a point (in window coordinates) lies within the key.
[62da267]2165    point.x -= GetXSize() - KEY_OFFSET_X;
2166    point.y = KEY_OFFSET_Y - point.y;
[fcc3741]2167    return (point.x >= key_lowerleft[m_ColourBy].x && point.x <= 0 &&
2168            point.y >= key_lowerleft[m_ColourBy].y && point.y <= 0);
[d43fa84]2169}
2170
[5876fcb]2171void GfxCore::SetCompassFromPoint(wxPoint point)
[8000d8f]2172{
[d877aa2]2173    // Given a point in window coordinates, set the heading of the survey.  If
2174    // the point is outside the compass, it snaps to 45 degree intervals;
2175    // otherwise it operates as normal.
[8000d8f]2176
[5876fcb]2177    wxCoord dx = point.x - GetCompassXPosition();
2178    wxCoord dy = point.y - GetIndicatorYPosition();
2179    wxCoord radius = GetIndicatorRadius();
[7aa15c0]2180
[0580c6a]2181    double angle = deg(atan2(double(dx), double(dy))) - 180.0;
[5876fcb]2182    if (dx * dx + dy * dy <= radius * radius) {
[3ddd351]2183        TurnCave(angle - m_PanAngle);
[429465a]2184        m_MouseOutsideCompass = false;
[e577f89]2185    } else {
[3ddd351]2186        TurnCave(int(angle / 45.0) * 45.0 - m_PanAngle);
[429465a]2187        m_MouseOutsideCompass = true;
[7aa15c0]2188    }
[8000d8f]2189
[5876fcb]2190    ForceRefresh();
2191}
2192
2193void GfxCore::SetClinoFromPoint(wxPoint point)
2194{
[d877aa2]2195    // Given a point in window coordinates, set the elevation of the survey.
2196    // If the point is outside the clino, it snaps to 90 degree intervals;
2197    // otherwise it operates as normal.
[8000d8f]2198
[33b2094]2199    glaCoord dx = point.x - GetClinoXPosition();
2200    glaCoord dy = point.y - GetIndicatorYPosition();
2201    glaCoord radius = GetIndicatorRadius();
[f433fda]2202
[5876fcb]2203    if (dx >= 0 && dx * dx + dy * dy <= radius * radius) {
[7a57dc7]2204        TiltCave(-deg(atan2(double(dy), double(dx))) - m_TiltAngle);
[429465a]2205        m_MouseOutsideElev = false;
[e577f89]2206    } else if (dy >= INDICATOR_MARGIN) {
[7a57dc7]2207        TiltCave(-90.0 - m_TiltAngle);
[429465a]2208        m_MouseOutsideElev = true;
[e577f89]2209    } else if (dy <= -INDICATOR_MARGIN) {
[7a57dc7]2210        TiltCave(90.0 - m_TiltAngle);
[429465a]2211        m_MouseOutsideElev = true;
[e577f89]2212    } else {
[429465a]2213        TiltCave(-m_TiltAngle);
2214        m_MouseOutsideElev = true;
[5876fcb]2215    }
[8000d8f]2216
[5876fcb]2217    ForceRefresh();
[8000d8f]2218}
2219
[5876fcb]2220void GfxCore::SetScaleBarFromOffset(wxCoord dx)
[8000d8f]2221{
[5876fcb]2222    // Set the scale of the survey, given an offset as to how much the mouse has
2223    // been dragged over the scalebar since the last scale change.
[8000d8f]2224
[5b7164d]2225    SetScale((m_ScaleBarWidth + dx) * m_Scale / m_ScaleBarWidth);
[5876fcb]2226    ForceRefresh();
2227}
[8000d8f]2228
[5876fcb]2229void GfxCore::RedrawIndicators()
2230{
2231    // Redraw the compass and clino indicators.
[8000d8f]2232
[2a26b45]2233    int total_width = GetCompassWidth() + INDICATOR_GAP + GetClinoWidth();
2234    RefreshRect(wxRect(GetXSize() - INDICATOR_OFFSET_X - total_width,
[e24b7fb]2235                       GetYSize() - INDICATOR_OFFSET_Y - INDICATOR_BOX_SIZE,
[2a26b45]2236                       total_width,
[e24b7fb]2237                       INDICATOR_BOX_SIZE), false);
[8000d8f]2238}
2239
[5876fcb]2240void GfxCore::StartRotation()
[8000d8f]2241{
[5876fcb]2242    // Start the survey rotating.
[f433fda]2243
[5455bb2]2244    if (m_SwitchingTo >= NORTH)
2245        m_SwitchingTo = 0;
[5876fcb]2246    m_Rotating = true;
[5455bb2]2247    SetPanBase();
[5876fcb]2248}
[8000d8f]2249
[5876fcb]2250void GfxCore::ToggleRotation()
2251{
2252    // Toggle the survey rotation on/off.
[f433fda]2253
[5876fcb]2254    if (m_Rotating) {
[2a3d328]2255        StopRotation();
2256    } else {
2257        StartRotation();
[5876fcb]2258    }
2259}
[8000d8f]2260
[5876fcb]2261void GfxCore::StopRotation()
2262{
2263    // Stop the survey rotating.
[8000d8f]2264
[5876fcb]2265    m_Rotating = false;
[33b2094]2266    ForceRefresh();
[5876fcb]2267}
[8000d8f]2268
[eef68f9]2269bool GfxCore::IsExtendedElevation() const
[5876fcb]2270{
[eef68f9]2271    return m_Parent->IsExtendedElevation();
[5876fcb]2272}
[8000d8f]2273
[5876fcb]2274void GfxCore::ReverseRotation()
2275{
2276    // Reverse the direction of rotation.
[8000d8f]2277
[5876fcb]2278    m_RotationStep = -m_RotationStep;
[5455bb2]2279    if (m_Rotating)
2280        SetPanBase();
[5876fcb]2281}
[8000d8f]2282
[5876fcb]2283void GfxCore::RotateSlower(bool accel)
2284{
2285    // Decrease the speed of rotation, optionally by an increased amount.
[5455bb2]2286    if (fabs(m_RotationStep) == 1.0)
2287        return;
[8000d8f]2288
[5455bb2]2289    m_RotationStep *= accel ? (1 / 1.44) : (1 / 1.2);
2290
2291    if (fabs(m_RotationStep) < 1.0) {
2292        m_RotationStep = (m_RotationStep > 0 ? 1.0 : -1.0);
[1690fa9]2293    }
[5455bb2]2294    if (m_Rotating)
2295        SetPanBase();
[8000d8f]2296}
2297
[5876fcb]2298void GfxCore::RotateFaster(bool accel)
2299{
2300    // Increase the speed of rotation, optionally by an increased amount.
[5455bb2]2301    if (fabs(m_RotationStep) == 180.0)
2302        return;
[5876fcb]2303
2304    m_RotationStep *= accel ? 1.44 : 1.2;
[5455bb2]2305    if (fabs(m_RotationStep) > 180.0) {
2306        m_RotationStep = (m_RotationStep > 0 ? 180.0 : -180.0);
[1690fa9]2307    }
[5455bb2]2308    if (m_Rotating)
2309        SetPanBase();
[5876fcb]2310}
[8000d8f]2311
[5876fcb]2312void GfxCore::SwitchToElevation()
[8000d8f]2313{
[5876fcb]2314    // Perform an animated switch to elevation view.
[8000d8f]2315
[5455bb2]2316    if (m_SwitchingTo != ELEVATION) {
2317        SetTiltBase();
2318        m_SwitchingTo = ELEVATION;
2319    } else {
2320        // A second order to switch takes us there right away
2321        TiltCave(-m_TiltAngle);
2322        m_SwitchingTo = 0;
2323        ForceRefresh();
[5876fcb]2324    }
[8000d8f]2325}
2326
[5876fcb]2327void GfxCore::SwitchToPlan()
[8000d8f]2328{
[5876fcb]2329    // Perform an animated switch to plan view.
[8000d8f]2330
[5455bb2]2331    if (m_SwitchingTo != PLAN) {
2332        SetTiltBase();
2333        m_SwitchingTo = PLAN;
2334    } else {
2335        // A second order to switch takes us there right away
2336        TiltCave(-90.0 - m_TiltAngle);
2337        m_SwitchingTo = 0;
2338        ForceRefresh();
[8000d8f]2339    }
[5876fcb]2340}
[8000d8f]2341
[d1628e8e]2342void GfxCore::SetViewTo(Double xmin, Double xmax, Double ymin, Double ymax, Double zmin, Double zmax)
2343{
2344
2345    SetTranslation(-Vector3((xmin + xmax) / 2, (ymin + ymax) / 2, (zmin + zmax) / 2));
[d0f5918]2346    Double scale = HUGE_VAL;
[d1628e8e]2347    const Vector3 ext = m_Parent->GetExtent();
2348    if (xmax > xmin) {
2349        Double s = ext.GetX() / (xmax - xmin);
2350        if (s < scale) scale = s;
2351    }
2352    if (ymax > ymin) {
2353        Double s = ext.GetY() / (ymax - ymin);
2354        if (s < scale) scale = s;
2355    }
2356    if (!ShowingPlan() && zmax > zmin) {
2357        Double s = ext.GetZ() / (zmax - zmin);
2358        if (s < scale) scale = s;
2359    }
[d0f5918]2360    if (scale != HUGE_VAL) SetScale(scale);
[d1628e8e]2361    ForceRefresh();
2362}
2363
[14acdae]2364bool GfxCore::CanRaiseViewpoint() const
[5876fcb]2365{
2366    // Determine if the survey can be viewed from a higher angle of elevation.
[f433fda]2367
[7a57dc7]2368    return GetPerspective() ? (m_TiltAngle < 90.0) : (m_TiltAngle > -90.0);
[8000d8f]2369}
2370
[14acdae]2371bool GfxCore::CanLowerViewpoint() const
[2effbf1]2372{
[5876fcb]2373    // Determine if the survey can be viewed from a lower angle of elevation.
[2effbf1]2374
[7a57dc7]2375    return GetPerspective() ? (m_TiltAngle > -90.0) : (m_TiltAngle < 90.0);
[5876fcb]2376}
[2effbf1]2377
[78c67a6]2378bool GfxCore::HasDepth() const
[bd21214]2379{
[78c67a6]2380    return m_Parent->GetDepthExtent() == 0.0;
[bd21214]2381}
2382
[843ee7b]2383bool GfxCore::HasErrorInformation() const
[d4650b3]2384{
[843ee7b]2385    return m_Parent->HasErrorInformation();
[d4650b3]2386}
2387
[843ee7b]2388bool GfxCore::HasDateInformation() const
[c61aa79]2389{
[843ee7b]2390    return m_Parent->GetDateMin() >= 0;
[c61aa79]2391}
2392
[14acdae]2393bool GfxCore::ShowingPlan() const
[5876fcb]2394{
2395    // Determine if the survey is in plan view.
[f433fda]2396
[7a57dc7]2397    return (m_TiltAngle == -90.0);
[2effbf1]2398}
2399
[14acdae]2400bool GfxCore::ShowingElevation() const
[8000d8f]2401{
[5876fcb]2402    // Determine if the survey is in elevation view.
[f433fda]2403
[5876fcb]2404    return (m_TiltAngle == 0.0);
[8000d8f]2405}
2406
[14acdae]2407bool GfxCore::ShowingMeasuringLine() const
[8000d8f]2408{
[0afefe6]2409    // Determine if the measuring line is being shown.  Only check if "there"
2410    // is valid, since that means the measuring line anchor is out.
[f433fda]2411
[381ae6e]2412    return m_there;
[8000d8f]2413}
2414
[eff44b9]2415void GfxCore::ToggleFlag(bool* flag, int update)
[5876fcb]2416{
2417    *flag = !*flag;
[6747314]2418    if (update == UPDATE_BLOBS) {
[eff44b9]2419        UpdateBlobs();
[ef1870d]2420    } else if (update == UPDATE_BLOBS_AND_CROSSES) {
2421        UpdateBlobs();
2422        InvalidateList(LIST_CROSSES);
[de43be7]2423        m_HitTestGridValid = false;
[6cd6bbe]2424    }
[eff44b9]2425    ForceRefresh();
[5876fcb]2426}
[93744a5]2427
[14acdae]2428int GfxCore::GetNumEntrances() const
[4b1fc48]2429{
[5876fcb]2430    return m_Parent->GetNumEntrances();
2431}
[bd7a61b]2432
[14acdae]2433int GfxCore::GetNumFixedPts() const
[5876fcb]2434{
2435    return m_Parent->GetNumFixedPts();
2436}
[7757a4ed]2437
[14acdae]2438int GfxCore::GetNumExportedPts() const
[5876fcb]2439{
2440    return m_Parent->GetNumExportedPts();
2441}
2442
[622460e]2443void GfxCore::ToggleTerrain()
2444{
[1e2c0fa]2445    if (!m_Terrain && !dem) {
2446        // OnOpenTerrain() calls us if a file is selected.
[622460e]2447        wxCommandEvent dummy;
2448        m_Parent->OnOpenTerrain(dummy);
[1e2c0fa]2449        return;
[622460e]2450    }
[1e2c0fa]2451    ToggleFlag(&m_Terrain);
[622460e]2452}
2453
[d96c95c]2454void GfxCore::ToggleFatFinger()
2455{
2456    if (sqrd_measure_threshold == sqrd(MEASURE_THRESHOLD)) {
2457        sqrd_measure_threshold = sqrd(5 * MEASURE_THRESHOLD);
[7b9b700]2458        wxMessageBox(wxT("Fat finger enabled"), wxT("Aven Debug"), wxOK | wxICON_INFORMATION);
[d96c95c]2459    } else {
2460        sqrd_measure_threshold = sqrd(MEASURE_THRESHOLD);
[7b9b700]2461        wxMessageBox(wxT("Fat finger disabled"), wxT("Aven Debug"), wxOK | wxICON_INFORMATION);
[d96c95c]2462    }
2463}
2464
[5876fcb]2465void GfxCore::ClearTreeSelection()
2466{
2467    m_Parent->ClearTreeSelection();
2468}
2469
[82c3731]2470void GfxCore::CentreOn(const Point &p)
[5876fcb]2471{
[d67450e]2472    SetTranslation(-p);
[00a68e0]2473    m_HitTestGridValid = false;
[f433fda]2474
[5876fcb]2475    ForceRefresh();
[4b1fc48]2476}
[5876fcb]2477
[33b2094]2478void GfxCore::ForceRefresh()
2479{
2480    Refresh(false);
2481}
2482
[d2fcc9b]2483void GfxCore::GenerateList(unsigned int l)
[33b2094]2484{
[9eb58d0]2485    assert(m_HaveData);
[3ddcad8]2486
[d2fcc9b]2487    switch (l) {
[fe665c4]2488        case LIST_COMPASS:
2489            DrawCompass();
2490            break;
2491        case LIST_CLINO:
2492            DrawClino();
2493            break;
2494        case LIST_CLINO_BACK:
2495            DrawClinoBack();
2496            break;
[9c37beb]2497        case LIST_SCALE_BAR:
2498            DrawScaleBar();
2499            break;
[e2ea75a]2500        case LIST_DEPTH_KEY:
2501            DrawDepthKey();
[252d759]2502            break;
[e2ea75a]2503        case LIST_DATE_KEY:
2504            DrawDateKey();
[d4650b3]2505            break;
[e2ea75a]2506        case LIST_ERROR_KEY:
2507            DrawErrorKey();
[c61aa79]2508            break;
[cc9e2c65]2509        case LIST_GRADIENT_KEY:
2510            DrawGradientKey();
2511            break;
[af50685]2512        case LIST_LENGTH_KEY:
2513            DrawLengthKey();
2514            break;
[d2fcc9b]2515        case LIST_UNDERGROUND_LEGS:
[190b1b9]2516            GenerateDisplayList(false);
[d2fcc9b]2517            break;
2518        case LIST_TUBES:
2519            GenerateDisplayListTubes();
2520            break;
2521        case LIST_SURFACE_LEGS:
[b96edeb]2522            GenerateDisplayList(true);
[d2fcc9b]2523            break;
[86fe6e4]2524        case LIST_BLOBS:
2525            GenerateBlobsDisplayList();
[37d7084]2526            break;
[86fe6e4]2527        case LIST_CROSSES: {
2528            BeginCrosses();
2529            SetColour(col_LIGHT_GREY);
[1a46879]2530            const SurveyFilter* filter = m_Parent->GetTreeFilter();
[86fe6e4]2531            list<LabelInfo*>::const_iterator pos = m_Parent->GetLabels();
2532            while (pos != m_Parent->GetLabelsEnd()) {
2533                const LabelInfo* label = *pos++;
2534
[a6401ff3]2535                if (m_Splays == SHOW_HIDE && label->IsSplayEnd())
2536                    continue;
2537
[86fe6e4]2538                if ((m_Surface && label->IsSurface()) ||
2539                    (m_Legs && label->IsUnderground()) ||
2540                    (!label->IsSurface() && !label->IsUnderground())) {
2541                    // Check if this station should be displayed
[672459c]2542                    // (last case above is for stns with no legs attached)
2543                    if (filter && !filter->CheckVisible(label->GetText()))
2544                        continue;
[86fe6e4]2545                    DrawCross(label->GetX(), label->GetY(), label->GetZ());
2546                }
2547            }
2548            EndCrosses();
2549            break;
2550        }
[37d7084]2551        case LIST_GRID:
2552            DrawGrid();
[d2fcc9b]2553            break;
[86fe6e4]2554        case LIST_SHADOW:
2555            GenerateDisplayListShadow();
[d2fcc9b]2556            break;
[6388423]2557        case LIST_TERRAIN:
2558            DrawTerrain();
[22b0a8f]2559            break;
[d2fcc9b]2560        default:
2561            assert(false);
2562            break;
2563    }
2564}
2565
[d67450e]2566void GfxCore::ToggleSmoothShading()
2567{
2568    GLACanvas::ToggleSmoothShading();
2569    InvalidateList(LIST_TUBES);
2570    ForceRefresh();
2571}
2572
[b96edeb]2573void GfxCore::GenerateDisplayList(bool surface)
[d2fcc9b]2574{
[b96edeb]2575    unsigned surf_or_not = surface ? img_FLAG_SURFACE : 0;
2576    // Generate the display list for the surface or underground legs.
2577    for (int f = 0; f != 8; ++f) {
2578        if ((f & img_FLAG_SURFACE) != surf_or_not) continue;
2579        const unsigned SHOW_DASHED_AND_FADED = unsigned(-1);
2580        unsigned style = SHOW_NORMAL;
2581        if ((f & img_FLAG_SPLAY) && m_Splays != SHOW_NORMAL) {
2582            style = m_Splays;
2583        } else if (f & img_FLAG_DUPLICATE) {
2584            style = m_Dupes;
2585        }
2586        if (f & img_FLAG_SURFACE) {
2587            if (style == SHOW_FADED) {
2588                style = SHOW_DASHED_AND_FADED;
2589            } else {
2590                style = SHOW_DASHED;
2591            }
2592        }
[8666fc7]2593
[b96edeb]2594        switch (style) {
2595            case SHOW_HIDE:
2596                continue;
2597            case SHOW_FADED:
2598                SetAlpha(0.4);
2599                break;
2600            case SHOW_DASHED:
2601                EnableDashedLines();
2602                break;
2603            case SHOW_DASHED_AND_FADED:
2604                SetAlpha(0.4);
2605                EnableDashedLines();
2606                break;
[ad661cc]2607        }
[8666fc7]2608
[b96edeb]2609        void (GfxCore::* add_poly)(const traverse&);
2610        if (surface) {
2611            if (m_ColourBy == COLOUR_BY_ERROR) {
2612                add_poly = &GfxCore::AddPolylineError;
2613            } else {
2614                add_poly = &GfxCore::AddPolyline;
2615            }
2616        } else {
2617            add_poly = AddPoly;
2618        }
2619
[1a46879]2620        const SurveyFilter* filter = m_Parent->GetTreeFilter();
2621        list<traverse>::const_iterator trav = m_Parent->traverses_begin(f, filter);
[b96edeb]2622        list<traverse>::const_iterator tend = m_Parent->traverses_end(f);
[e5c5f3c]2623        while (trav != tend) {
[1a46879]2624            (this->*add_poly)(*trav);
2625            trav = m_Parent->traverses_next(f, filter, trav);
[e5c5f3c]2626        }
2627
[b96edeb]2628        switch (style) {
2629            case SHOW_FADED:
2630                SetAlpha(1.0);
2631                break;
2632            case SHOW_DASHED:
2633                DisableDashedLines();
2634                break;
2635            case SHOW_DASHED_AND_FADED:
2636                DisableDashedLines();
2637                SetAlpha(1.0);
2638                break;
2639        }
[3ddcad8]2640    }
[33b2094]2641}
2642
[9eb58d0]2643void GfxCore::GenerateDisplayListTubes()
[33b2094]2644{
[9eb58d0]2645    // Generate the display list for the tubes.
[d7078b4]2646    list<vector<XSect>>::iterator trav = m_Parent->tubes_begin();
2647    list<vector<XSect>>::iterator tend = m_Parent->tubes_end();
[3ddcad8]2648    while (trav != tend) {
2649        SkinPassage(*trav);
2650        ++trav;
2651    }
[9eb58d0]2652}
[33b2094]2653
[37d7084]2654void GfxCore::GenerateDisplayListShadow()
[f4c5932]2655{
[1a46879]2656    const SurveyFilter* filter = m_Parent->GetTreeFilter();
[f4c5932]2657    SetColour(col_BLACK);
[b96edeb]2658    for (int f = 0; f != 8; ++f) {
2659        // Only include underground legs in the shadow.
2660        if ((f & img_FLAG_SURFACE) != 0) continue;
[1a46879]2661        list<traverse>::const_iterator trav = m_Parent->traverses_begin(f, filter);
[b96edeb]2662        list<traverse>::const_iterator tend = m_Parent->traverses_end(f);
2663        while (trav != tend) {
[1a46879]2664            AddPolylineShadow(*trav);
2665            trav = m_Parent->traverses_next(f, filter, trav);
[b96edeb]2666        }
[f4c5932]2667    }
2668}
2669
[112f80c]2670void
2671GfxCore::parse_hgt_filename(const wxString & lc_name)
2672{
[8562abc]2673    char * leaf = leaf_from_fnm(lc_name.utf8_str());
[112f80c]2674    const char * p = leaf;
2675    char * q;
2676    char dirn = *p++;
2677    o_y = strtoul(p, &q, 10);
2678    p = q;
2679    if (dirn == 's')
2680        o_y = -o_y;
2681    ++o_y;
2682    dirn = *p++;
2683    o_x = strtoul(p, &q, 10);
2684    if (dirn == 'w')
2685        o_x = -o_x;
2686    bigendian = true;
2687    nodata_value = -32768;
2688    osfree(leaf);
2689}
2690
2691size_t
2692GfxCore::parse_hdr(wxInputStream & is, unsigned long & skipbytes)
2693{
[9260793]2694    // ESRI docs say NBITS defaults to 8.
2695    unsigned long nbits = 8;
2696    // ESRI docs say NBANDS defaults to 1.
2697    unsigned long nbands = 1;
2698    unsigned long bandrowbytes = 0;
2699    unsigned long totalrowbytes = 0;
2700    // ESRI docs say ULXMAP defaults to 0.
2701    o_x = 0.0;
2702    // ESRI docs say ULYMAP defaults to NROWS - 1.
2703    o_y = HUGE_VAL;
2704    // ESRI docs say XDIM and YDIM default to 1.
2705    step_x = step_y = 1.0;
[112f80c]2706    while (!is.Eof()) {
2707        wxString line;
2708        int ch;
2709        while ((ch = is.GetC()) != wxEOF) {
2710            if (ch == '\n' || ch == '\r') break;
2711            line += wxChar(ch);
2712        }
[6388423]2713#define CHECK(X, COND) \
[1c55fb5]2714} else if (line.StartsWith(wxT(X " "))) { \
[6388423]2715size_t v = line.find_first_not_of(wxT(' '), sizeof(X)); \
2716if (v == line.npos || !(COND)) { \
[1c55fb5]2717err += wxT("Unexpected value for " X); \
[5314a0e]2718}
[112f80c]2719        wxString err;
2720        if (false) {
2721        // I = little-endian; M = big-endian
2722        CHECK("BYTEORDER", (bigendian = (line[v] == 'M')) || line[v] == 'I')
[9260793]2723        // ESRI docs say LAYOUT defaults to BIL if not specified.
[112f80c]2724        CHECK("LAYOUT", line.substr(v) == wxT("BIL"))
[1a381ae]2725        CHECK("NROWS", line.substr(v).ToCULong(&dem_height))
2726        CHECK("NCOLS", line.substr(v).ToCULong(&dem_width))
[9260793]2727        // ESRI docs say NBANDS defaults to 1 if not specified.
2728        CHECK("NBANDS", line.substr(v).ToCULong(&nbands) && nbands == 1)
[112f80c]2729        CHECK("NBITS", line.substr(v).ToCULong(&nbits) && nbits == 16)
[9260793]2730        CHECK("BANDROWBYTES", line.substr(v).ToCULong(&bandrowbytes))
2731        CHECK("TOTALROWBYTES", line.substr(v).ToCULong(&totalrowbytes))
[112f80c]2732        // PIXELTYPE is a GDAL extension, so may not be present.
2733        CHECK("PIXELTYPE", line.substr(v) == wxT("SIGNEDINT"))
2734        CHECK("ULXMAP", line.substr(v).ToCDouble(&o_x))
2735        CHECK("ULYMAP", line.substr(v).ToCDouble(&o_y))
2736        CHECK("XDIM", line.substr(v).ToCDouble(&step_x))
2737        CHECK("YDIM", line.substr(v).ToCDouble(&step_y))
2738        CHECK("NODATA", line.substr(v).ToCLong(&nodata_value))
2739        CHECK("SKIPBYTES", line.substr(v).ToCULong(&skipbytes))
2740        }
2741        if (!err.empty()) {
2742            wxMessageBox(err);
[6388423]2743        }
[c2fa50c]2744    }
[9260793]2745    if (o_y == HUGE_VAL) {
2746        o_y = dem_height - 1;
[0456466]2747    }
[9260793]2748    if (bandrowbytes != 0) {
2749        if (nbits * dem_width != bandrowbytes * 8) {
2750            wxMessageBox("BANDROWBYTES setting indicates unused bits after each band - not currently supported");
2751        }
2752    }
2753    if (totalrowbytes != 0) {
2754        // This is the ESRI default for BIL, for BIP it would be
2755        // nbands * bandrowbytes.
2756        if (nbands * nbits * dem_width != totalrowbytes * 8) {
2757            wxMessageBox("TOTALROWBYTES setting indicates unused bits after "
2758                         "each row - not currently supported");
2759        }
2760    }
2761    return ((nbits * dem_width + 7) / 8) * dem_height;
[112f80c]2762}
[c2fa50c]2763
[112f80c]2764bool
2765GfxCore::read_bil(wxInputStream & is, size_t size, unsigned long skipbytes)
2766{
2767    bool know_size = true;
2768    if (!size) {
2769        // If the stream doesn't know its size, GetSize() returns 0.
2770        size = is.GetSize();
2771        if (!size) {
2772            size = DEFAULT_HGT_SIZE;
2773            know_size = false;
2774        }
[c2fa50c]2775    }
2776    dem = new unsigned short[size / 2];
2777    if (skipbytes) {
[112f80c]2778        if (is.SeekI(skipbytes, wxFromStart) == ::wxInvalidOffset) {
[c2fa50c]2779            while (skipbytes) {
2780                unsigned long to_read = skipbytes;
2781                if (size < to_read) to_read = size;
[112f80c]2782                is.Read(reinterpret_cast<char *>(dem), to_read);
2783                size_t c = is.LastRead();
[c2fa50c]2784                if (c == 0) {
2785                    wxMessageBox(wxT("Failed to skip terrain data header"));
2786                    break;
[bfb3ab0]2787                }
[c2fa50c]2788                skipbytes -= c;
[bfb3ab0]2789            }
[5314a0e]2790        }
[c2fa50c]2791    }
[bfb3ab0]2792
[112f80c]2793    if (!is.ReadAll(dem, size)) {
2794        if (know_size) {
2795            // FIXME: On __WXMSW__ currently we fail to
2796            // read any data from files in zips.
2797            delete [] dem;
2798            dem = NULL;
2799            wxMessageBox(wxT("Failed to read terrain data"));
2800            return false;
[c2fa50c]2801        }
[112f80c]2802        size = is.LastRead();
[c2fa50c]2803    }
[112f80c]2804
2805    if (dem_width == 0 && dem_height == 0) {
2806        dem_width = dem_height = sqrt(size / 2);
2807        if (dem_width * dem_height * 2 != size) {
2808            delete [] dem;
2809            dem = NULL;
2810            wxMessageBox(wxT("HGT format data doesn't form a square"));
2811            return false;
2812        }
2813        step_x = step_y = 1.0 / dem_width;
2814    }
2815
2816    return true;
2817}
2818
2819bool GfxCore::LoadDEM(const wxString & file)
2820{
[7997096]2821    if (m_Parent->GetCSProj().empty()) {
[112f80c]2822        wxMessageBox(wxT("No coordinate system specified in survey data"));
2823        return false;
2824    }
2825
2826    delete [] dem;
2827    dem = NULL;
2828
2829    size_t size = 0;
2830    // Default is to not skip any bytes.
2831    unsigned long skipbytes = 0;
2832    // For .hgt files, default to using filesize to determine.
2833    dem_width = dem_height = 0;
2834    // ESRI say "The default byte order is the same as that of the host machine
2835    // executing the software", but that's stupid so we default to
2836    // little-endian.
2837    bigendian = false;
2838
[522fb4c]2839    wxFileInputStream fs(file);
2840    if (!fs.IsOk()) {
[112f80c]2841        wxMessageBox(wxT("Failed to open DEM file"));
2842        return false;
[6388423]2843    }
[c2fa50c]2844
[112f80c]2845    const wxString & lc_file = file.Lower();
2846    if (lc_file.EndsWith(wxT(".hgt"))) {
2847        parse_hgt_filename(lc_file);
2848        read_bil(fs, size, skipbytes);
2849    } else if (lc_file.EndsWith(wxT(".bil"))) {
2850        wxString hdr_file = file;
2851        hdr_file.replace(file.size() - 4, 4, wxT(".hdr"));
[522fb4c]2852        wxFileInputStream hdr_is(hdr_file);
2853        if (!hdr_is.IsOk()) {
[112f80c]2854            wxMessageBox(wxT("Failed to open HDR file '") + hdr_file + wxT("'"));
2855            return false;
2856        }
2857        size = parse_hdr(hdr_is, skipbytes);
2858        read_bil(fs, size, skipbytes);
2859    } else if (lc_file.EndsWith(wxT(".zip"))) {
2860        wxZipEntry * ze_data = NULL;
2861        wxZipInputStream zs(fs);
2862        wxZipEntry * ze;
2863        while ((ze = zs.GetNextEntry()) != NULL) {
2864            if (!ze->IsDir()) {
2865                const wxString & lc_name = ze->GetName().Lower();
2866                if (!ze_data && lc_name.EndsWith(wxT(".hgt"))) {
2867                    // SRTM .hgt files are raw binary data, with the filename
2868                    // encoding the coordinates.
2869                    parse_hgt_filename(lc_name);
2870                    read_bil(zs, size, skipbytes);
2871                    delete ze;
2872                    break;
2873                }
2874
2875                if (!ze_data && lc_name.EndsWith(wxT(".bil"))) {
2876                    if (size) {
2877                        read_bil(zs, size, skipbytes);
2878                        break;
2879                    }
2880                    ze_data = ze;
2881                    continue;
2882                }
2883
2884                if (lc_name.EndsWith(wxT(".hdr"))) {
2885                    size = parse_hdr(zs, skipbytes);
2886                    if (ze_data) {
2887                        if (!zs.OpenEntry(*ze_data)) {
2888                            wxMessageBox(wxT("Couldn't read DEM data from .zip file"));
2889                            break;
2890                        }
2891                        read_bil(zs, size, skipbytes);
2892                    }
2893                } else if (lc_name.EndsWith(wxT(".prj"))) {
2894                    //FIXME: check this matches the datum string we use
2895                    //Projection    GEOGRAPHIC
2896                    //Datum         WGS84
2897                    //Zunits        METERS
2898                    //Units         DD
2899                    //Spheroid      WGS84
2900                    //Xshift        0.0000000000
2901                    //Yshift        0.0000000000
2902                    //Parameters
2903                }
2904            }
2905            delete ze;
2906        }
2907        delete ze_data;
2908    }
2909
2910    if (!dem) {
2911        return false;
2912    }
[622460e]2913
2914    InvalidateList(LIST_TERRAIN);
[74869a7]2915    ForceRefresh();
[5314a0e]2916    return true;
2917}
2918
2919void GfxCore::DrawTerrainTriangle(const Vector3 & a, const Vector3 & b, const Vector3 & c)
2920{
2921    Vector3 n = (b - a) * (c - a);
2922    n.normalise();
2923    Double factor = dot(n, light) * .95 + .05;
2924    SetColour(col_WHITE, factor);
2925    PlaceVertex(a);
2926    PlaceVertex(b);
2927    PlaceVertex(c);
2928    ++n_tris;
2929}
2930
[7685ae3]2931// Like wxBusyCursor, but you can cancel it early.
2932class AvenBusyCursor {
2933    bool active;
2934
2935  public:
2936    AvenBusyCursor() : active(true) {
2937        wxBeginBusyCursor();
2938    }
2939
2940    void stop() {
2941        if (active) {
2942            active = false;
2943            wxEndBusyCursor();
2944        }
2945    }
2946
2947    ~AvenBusyCursor() {
2948        stop();
2949    }
2950};
2951
[5314a0e]2952void GfxCore::DrawTerrain()
2953{
[622460e]2954    if (!dem) return;
2955
[7685ae3]2956    AvenBusyCursor hourglass;
[c26455e]2957
[6388423]2958    // Draw terrain to twice the extent, or at least 1km.
2959    double r_sqrd = sqrd(max(m_Parent->GetExtent().magnitude(), 1000.0));
2960#define WGS84_DATUM_STRING "+proj=longlat +ellps=WGS84 +datum=WGS84"
2961    static projPJ pj_in = pj_init_plus(WGS84_DATUM_STRING);
2962    if (!pj_in) {
[9df33bc]2963        ToggleTerrain();
[7685ae3]2964        delete [] dem;
2965        dem = NULL;
2966        hourglass.stop();
[9df33bc]2967        error(/*Failed to initialise input coordinate system “%s”*/287, WGS84_DATUM_STRING);
2968        return;
[6388423]2969    }
[7997096]2970    static projPJ pj_out = pj_init_plus(m_Parent->GetCSProj().c_str());
[6388423]2971    if (!pj_out) {
[9df33bc]2972        ToggleTerrain();
[7685ae3]2973        delete [] dem;
2974        dem = NULL;
2975        hourglass.stop();
[7997096]2976        error(/*Failed to initialise output coordinate system “%s”*/288, (const char *)m_Parent->GetCSProj().c_str());
[9df33bc]2977        return;
[6388423]2978    }
2979    n_tris = 0;
2980    SetAlpha(0.3);
2981    BeginTriangles();
2982    const Vector3 & off = m_Parent->GetOffset();
[5314a0e]2983    vector<Vector3> prevcol(dem_height + 1);
2984    for (size_t x = 0; x < dem_width; ++x) {
[6388423]2985        double X_ = (o_x + x * step_x) * DEG_TO_RAD;
2986        Vector3 prev;
[5314a0e]2987        for (size_t y = 0; y < dem_height; ++y) {
[9df33bc]2988            unsigned short elev = dem[x + y * dem_width];
[194503c]2989#ifdef WORDS_BIGENDIAN
[9df33bc]2990            const bool MACHINE_BIGENDIAN = true;
2991#else
2992            const bool MACHINE_BIGENDIAN = false;
2993#endif
2994            if (bigendian != MACHINE_BIGENDIAN) {
2995#if defined __GNUC__ && (__GNUC__ * 100 + __GNUC_MINOR__ >= 408)
2996                elev = __builtin_bswap16(elev);
2997#else
2998                elev = (elev >> 8) | (elev << 8);
[194503c]2999#endif
[9df33bc]3000            }
[194503c]3001            double Z = (short)elev;
[6388423]3002            Vector3 pt;
3003            if (Z == nodata_value) {
3004                pt = Vector3(DBL_MAX, DBL_MAX, DBL_MAX);
3005            } else {
3006                double X = X_;
3007                double Y = (o_y - y * step_y) * DEG_TO_RAD;
3008                pj_transform(pj_in, pj_out, 1, 1, &X, &Y, &Z);
3009                pt = Vector3(X, Y, Z) - off;
3010                double dist_2 = sqrd(pt.GetX()) + sqrd(pt.GetY());
3011                if (dist_2 > r_sqrd) {
3012                    pt = Vector3(DBL_MAX, DBL_MAX, DBL_MAX);
3013                }
3014            }
3015            if (x > 0 && y > 0) {
3016                const Vector3 & a = prevcol[y - 1];
3017                const Vector3 & b = prevcol[y];
3018                // If all points are valid, split the quadrilateral into
3019                // triangles along the shorter 3D diagonal, which typically
3020                // looks better:
3021                //
3022                //               ----->
3023                //     prev---a    x     prev---a
3024                //   |   |P  /|            |\  S|
3025                // y |   |  / |    or      | \  |
3026                //   V   | /  |            |  \ |
3027                //       |/  Q|            |R  \|
3028                //       b----pt           b----pt
3029                //
3030                //       FORWARD           BACKWARD
3031                enum { NONE = 0, P = 1, Q = 2, R = 4, S = 8, ALL = P|Q|R|S };
3032                int valid =
3033                    ((prev.GetZ() != DBL_MAX)) |
3034                    ((a.GetZ() != DBL_MAX) << 1) |
3035                    ((b.GetZ() != DBL_MAX) << 2) |
3036                    ((pt.GetZ() != DBL_MAX) << 3);
3037                static const int tris_map[16] = {
3038                    NONE, // nothing valid
3039                    NONE, // prev
3040                    NONE, // a
3041                    NONE, // a, prev
3042                    NONE, // b
3043                    NONE, // b, prev
3044                    NONE, // b, a
3045                    P, // b, a, prev
3046                    NONE, // pt
3047                    NONE, // pt, prev
3048                    NONE, // pt, a
3049                    S, // pt, a, prev
3050                    NONE, // pt, b
3051                    R, // pt, b, prev
3052                    Q, // pt, b, a
3053                    ALL, // pt, b, a, prev
3054                };
3055                int tris = tris_map[valid];
3056                if (tris == ALL) {
3057                    // All points valid.
3058                    if ((a - b).magnitude() < (prev - pt).magnitude()) {
3059                        tris = P | Q;
3060                    } else {
3061                        tris = R | S;
3062                    }
3063                }
3064                if (tris & P)
3065                    DrawTerrainTriangle(a, prev, b);
3066                if (tris & Q)
3067                    DrawTerrainTriangle(a, b, pt);
3068                if (tris & R)
3069                    DrawTerrainTriangle(pt, prev, b);
3070                if (tris & S)
3071                    DrawTerrainTriangle(a, prev, pt);
3072            }
3073            prev = prevcol[y];
3074            prevcol[y].assign(pt);
3075        }
3076    }
3077    EndTriangles();
3078    SetAlpha(1.0);
[7685ae3]3079    if (n_tris == 0) {
3080        ToggleTerrain();
3081        delete [] dem;
3082        dem = NULL;
3083        hourglass.stop();
3084        /* TRANSLATORS: Aven shows a circle of terrain covering the area
3085         * of the survey plus a bit, but the terrain data file didn't
3086         * contain any data inside that circle.
3087         */
3088        error(/*No terrain data near area of survey*/161);
3089    }
[6388423]3090}
3091
[d2fcc9b]3092// Plot blobs.
[d9b3270]3093void GfxCore::GenerateBlobsDisplayList()
3094{
[e633bb1]3095    if (!(m_Entrances || m_FixedPts || m_ExportedPts ||
3096          m_Parent->GetNumHighlightedPts()))
3097        return;
[429465a]3098
[e633bb1]3099    // Plot blobs.
[1a46879]3100    const SurveyFilter* filter = m_Parent->GetTreeFilter();
[e633bb1]3101    gla_colour prev_col = col_BLACK; // not a colour used for blobs
3102    list<LabelInfo*>::const_iterator pos = m_Parent->GetLabels();
3103    BeginBlobs();
3104    while (pos != m_Parent->GetLabelsEnd()) {
[429465a]3105        const LabelInfo* label = *pos++;
3106
3107        // When more than one flag is set on a point:
3108        // search results take priority over entrance highlighting
3109        // which takes priority over fixed point
3110        // highlighting, which in turn takes priority over exported
3111        // point highlighting.
3112
[a6401ff3]3113        if (m_Splays == SHOW_HIDE && label->IsSplayEnd())
3114            continue;
3115
[429465a]3116        if (!((m_Surface && label->IsSurface()) ||
3117              (m_Legs && label->IsUnderground()) ||
3118              (!label->IsSurface() && !label->IsUnderground()))) {
3119            // if this station isn't to be displayed, skip to the next
3120            // (last case is for stns with no legs attached)
3121            continue;
3122        }
[672459c]3123        if (filter && !filter->CheckVisible(label->GetText()))
3124            continue;
[429465a]3125
[e633bb1]3126        gla_colour col;
3127
[429465a]3128        if (label->IsHighLighted()) {
3129            col = col_YELLOW;
3130        } else if (m_Entrances && label->IsEntrance()) {
3131            col = col_GREEN;
3132        } else if (m_FixedPts && label->IsFixedPt()) {
3133            col = col_RED;
3134        } else if (m_ExportedPts && label->IsExportedPt()) {
3135            col = col_TURQUOISE;
3136        } else {
3137            continue;
3138        }
3139
[e633bb1]3140        // Stations are sorted by blob type, so colour changes are infrequent.
3141        if (col != prev_col) {
[aa048c3]3142            SetColour(col);
[e633bb1]3143            prev_col = col;
[429465a]3144        }
[e633bb1]3145        DrawBlob(label->GetX(), label->GetY(), label->GetZ());
[d9b3270]3146    }
[e633bb1]3147    EndBlobs();
[33b2094]3148}
3149
[6747314]3150void GfxCore::DrawIndicators()
[33b2094]3151{
[97ea48d]3152    // Draw colour key.
3153    if (m_ColourKey) {
[47c62d04]3154        drawing_list key_list = LIST_LIMIT_;
3155        switch (m_ColourBy) {
3156            case COLOUR_BY_DEPTH:
3157                key_list = LIST_DEPTH_KEY; break;
3158            case COLOUR_BY_DATE:
3159                key_list = LIST_DATE_KEY; break;
3160            case COLOUR_BY_ERROR:
3161                key_list = LIST_ERROR_KEY; break;
[cc9e2c65]3162            case COLOUR_BY_GRADIENT:
3163                key_list = LIST_GRADIENT_KEY; break;
[47c62d04]3164            case COLOUR_BY_LENGTH:
3165                key_list = LIST_LENGTH_KEY; break;
[68fb07a]3166#if 0 // FIXME Key for survey colours?
3167            case COLOUR_BY_SURVEY:
3168                key_list = LIST_SURVEY_KEY; break;
3169#endif
[47c62d04]3170        }
3171        if (key_list != LIST_LIMIT_) {
3172            DrawList2D(key_list, GetXSize() - KEY_OFFSET_X,
[af50685]3173                       GetYSize() - KEY_OFFSET_Y, 0);
[1b164a0]3174        }
[33b2094]3175    }
[56da40e]3176
[203d2a7]3177    // Draw compass or elevation/heading indicators.
[eef68f9]3178    if (m_Compass || m_Clino) {
3179        if (!m_Parent->IsExtendedElevation()) Draw2dIndicators();
[203d2a7]3180    }
[f433fda]3181
[56da40e]3182    // Draw scalebar.
[4a0e0623]3183    if (m_Scalebar && !GetPerspective()) {
[9c37beb]3184        DrawList2D(LIST_SCALE_BAR, 0, 0, 0);
[56da40e]3185    }
[33b2094]3186}
3187
[f336ab9]3188void GfxCore::PlaceVertexWithColour(const Vector3 & v,
3189                                    glaTexCoord tex_x, glaTexCoord tex_y,
[b839829]3190                                    Double factor)
[f383708]3191{
[d1ce9bd]3192    SetColour(col_WHITE, factor);
[b839829]3193    PlaceVertex(v, tex_x, tex_y);
[da6c802]3194}
[f433fda]3195
[b839829]3196void GfxCore::SetDepthColour(Double z, Double factor) {
[da6c802]3197    // Set the drawing colour based on the altitude.
[78c67a6]3198    Double z_ext = m_Parent->GetDepthExtent();
[f383708]3199
[b839829]3200    z -= m_Parent->GetDepthMin();
[f383708]3201    // points arising from tubes may be slightly outside the limits...
[78c67a6]3202    if (z < 0) z = 0;
3203    if (z > z_ext) z = z_ext;
[a6f081c]3204
[2a9d2fa]3205    if (z == 0) {
3206        SetColour(GetPen(0), factor);
3207        return;
3208    }
3209
3210    assert(z_ext > 0.0);
[78c67a6]3211    Double how_far = z / z_ext;
[f383708]3212    assert(how_far >= 0.0);
3213    assert(how_far <= 1.0);
3214
[97ea48d]3215    int band = int(floor(how_far * (GetNumColourBands() - 1)));
[0e69efe]3216    GLAPen pen1 = GetPen(band);
[97ea48d]3217    if (band < GetNumColourBands() - 1) {
[d4650b3]3218        const GLAPen& pen2 = GetPen(band + 1);
[f433fda]3219
[97ea48d]3220        Double interval = z_ext / (GetNumColourBands() - 1);
[78c67a6]3221        Double into_band = z / interval - band;
[f433fda]3222
[d4650b3]3223//      printf("%g z_offset=%g interval=%g band=%d\n", into_band,
3224//             z_offset, interval, band);
3225        // FIXME: why do we need to clamp here?  Is it because the walls can
3226        // extend further up/down than the centre-line?
3227        if (into_band < 0.0) into_band = 0.0;
3228        if (into_band > 1.0) into_band = 1.0;
3229        assert(into_band >= 0.0);
3230        assert(into_band <= 1.0);
[f433fda]3231
[d4650b3]3232        pen1.Interpolate(pen2, into_band);
3233    }
[aa048c3]3234    SetColour(pen1, factor);
[b839829]3235}
[f383708]3236
[b839829]3237void GfxCore::PlaceVertexWithDepthColour(const Vector3 &v, Double factor)
3238{
3239    SetDepthColour(v.GetZ(), factor);
[d67450e]3240    PlaceVertex(v);
[f383708]3241}
3242
[b839829]3243void GfxCore::PlaceVertexWithDepthColour(const Vector3 &v,
[f336ab9]3244                                         glaTexCoord tex_x, glaTexCoord tex_y,
[b839829]3245                                         Double factor)
3246{
3247    SetDepthColour(v.GetZ(), factor);
3248    PlaceVertex(v, tex_x, tex_y);
3249}
3250
[82f584f]3251void GfxCore::SplitLineAcrossBands(int band, int band2,
[4a0e6b35]3252                                   const Vector3 &p, const Vector3 &q,
[82f584f]3253                                   Double factor)
[b5d64e6]3254{
[4a0e6b35]3255    const int step = (band < band2) ? 1 : -1;
[b5d64e6]3256    for (int i = band; i != band2; i += step) {
[4a0e6b35]3257        const Double z = GetDepthBoundaryBetweenBands(i, i + step);
3258
3259        // Find the intersection point of the line p -> q
3260        // with the plane parallel to the xy-plane with z-axis intersection z.
[d67450e]3261        assert(q.GetZ() - p.GetZ() != 0.0);
[4a0e6b35]3262
[d67450e]3263        const Double t = (z - p.GetZ()) / (q.GetZ() - p.GetZ());
[4a0e6b35]3264//      assert(0.0 <= t && t <= 1.0);           FIXME: rounding problems!
3265
[d67450e]3266        const Double x = p.GetX() + t * (q.GetX() - p.GetX());
3267        const Double y = p.GetY() + t * (q.GetY() - p.GetY());
[4a0e6b35]3268
[d67450e]3269        PlaceVertexWithDepthColour(Vector3(x, y, z), factor);
[b5d64e6]3270    }
3271}
3272
[d7078b4]3273void GfxCore::SplitPolyAcrossBands(vector<vector<Split>>& splits,
[f2d6d32]3274                                   int band, int band2,
[ba828d4]3275                                   const Vector3 &p, const Vector3 &q,
3276                                   glaTexCoord ptx, glaTexCoord pty,
3277                                   glaTexCoord w, glaTexCoord h)
[f2d6d32]3278{
3279    const int step = (band < band2) ? 1 : -1;
3280    for (int i = band; i != band2; i += step) {
3281        const Double z = GetDepthBoundaryBetweenBands(i, i + step);
3282
3283        // Find the intersection point of the line p -> q
3284        // with the plane parallel to the xy-plane with z-axis intersection z.
3285        assert(q.GetZ() - p.GetZ() != 0.0);
3286
3287        const Double t = (z - p.GetZ()) / (q.GetZ() - p.GetZ());
3288//      assert(0.0 <= t && t <= 1.0);           FIXME: rounding problems!
3289
3290        const Double x = p.GetX() + t * (q.GetX() - p.GetX());
3291        const Double y = p.GetY() + t * (q.GetY() - p.GetY());
[ba828d4]3292        glaTexCoord tx = ptx, ty = pty;
3293        if (w) tx += t * w;
3294        if (h) ty += t * h;
[f2d6d32]3295
[ba828d4]3296        splits[i].push_back(Split(Vector3(x, y, z), tx, ty));
3297        splits[i + step].push_back(Split(Vector3(x, y, z), tx, ty));
[f2d6d32]3298    }
3299}
3300
[14acdae]3301int GfxCore::GetDepthColour(Double z) const
[b5d64e6]3302{
[82f584f]3303    // Return the (0-based) depth colour band index for a z-coordinate.
[78c67a6]3304    Double z_ext = m_Parent->GetDepthExtent();
3305    z -= m_Parent->GetDepthMin();
[2ba3882]3306    // We seem to get rounding differences causing z to sometimes be slightly
[0a2aab8]3307    // less than GetDepthMin() here, and it can certainly be true for passage
3308    // tubes, so just clamp the value to 0.
[2ba3882]3309    if (z <= 0) return 0;
[6027220]3310    // We seem to get rounding differences causing z to sometimes exceed z_ext
[2c1c52e]3311    // by a small amount here (see: https://trac.survex.com/ticket/26) and it
[0a2aab8]3312    // can certainly be true for passage tubes, so just clamp the value.
3313    if (z >= z_ext) return GetNumColourBands() - 1;
[97ea48d]3314    return int(z / z_ext * (GetNumColourBands() - 1));
[b5d64e6]3315}
3316
[14acdae]3317Double GfxCore::GetDepthBoundaryBetweenBands(int a, int b) const
[b5d64e6]3318{
[82f584f]3319    // Return the z-coordinate of the depth colour boundary between
3320    // two adjacent depth colour bands (specified by 0-based indices).
3321
3322    assert((a == b - 1) || (a == b + 1));
[97ea48d]3323    if (GetNumColourBands() == 1) return 0;
[82f584f]3324
3325    int band = (a > b) ? a : b; // boundary N lies on the bottom of band N.
[78c67a6]3326    Double z_ext = m_Parent->GetDepthExtent();
[2a9d2fa]3327    return (z_ext * band / (GetNumColourBands() - 1)) + m_Parent->GetDepthMin();
[b5d64e6]3328}
3329
[c61aa79]3330void GfxCore::AddPolyline(const traverse & centreline)
[da6c802]3331{
3332    BeginPolyline();
[d1ce9bd]3333    SetColour(col_WHITE);
[d4650b3]3334    vector<PointInfo>::const_iterator i = centreline.begin();
[d67450e]3335    PlaceVertex(*i);
[da6c802]3336    ++i;
3337    while (i != centreline.end()) {
[d67450e]3338        PlaceVertex(*i);
[da6c802]3339        ++i;
3340    }
3341    EndPolyline();
3342}
[2a3d328]3343
[c61aa79]3344void GfxCore::AddPolylineShadow(const traverse & centreline)
[f4c5932]3345{
3346    BeginPolyline();
[7ab01e7]3347    const double z = -0.5 * m_Parent->GetExtent().GetZ();
[d4650b3]3348    vector<PointInfo>::const_iterator i = centreline.begin();
[78c67a6]3349    PlaceVertex(i->GetX(), i->GetY(), z);
[f4c5932]3350    ++i;
3351    while (i != centreline.end()) {
[78c67a6]3352        PlaceVertex(i->GetX(), i->GetY(), z);
[f4c5932]3353        ++i;
3354    }
3355    EndPolyline();
3356}
3357
[c61aa79]3358void GfxCore::AddPolylineDepth(const traverse & centreline)
[da6c802]3359{
3360    BeginPolyline();
[d4650b3]3361    vector<PointInfo>::const_iterator i, prev_i;
[da6c802]3362    i = centreline.begin();
[ee7af72]3363    int band0 = GetDepthColour(i->GetZ());
[d67450e]3364    PlaceVertexWithDepthColour(*i);
[da6c802]3365    prev_i = i;
3366    ++i;
3367    while (i != centreline.end()) {
[ee7af72]3368        int band = GetDepthColour(i->GetZ());
[da6c802]3369        if (band != band0) {
[d67450e]3370            SplitLineAcrossBands(band0, band, *prev_i, *i);
[da6c802]3371            band0 = band;
3372        }
[d67450e]3373        PlaceVertexWithDepthColour(*i);
[da6c802]3374        prev_i = i;
3375        ++i;
3376    }
3377    EndPolyline();
3378}
3379
[f433fda]3380void GfxCore::AddQuadrilateral(const Vector3 &a, const Vector3 &b,
[14acdae]3381                               const Vector3 &c, const Vector3 &d)
[da6c802]3382{
3383    Vector3 normal = (a - c) * (d - b);
3384    normal.normalise();
3385    Double factor = dot(normal, light) * .3 + .7;
[ba828d4]3386    glaTexCoord w(((b - a).magnitude() + (d - c).magnitude()) * .5);
3387    glaTexCoord h(((b - c).magnitude() + (d - a).magnitude()) * .5);
[9b57c71b]3388    // FIXME: should plot triangles instead to avoid rendering glitches.
[da6c802]3389    BeginQuadrilaterals();
[b839829]3390    PlaceVertexWithColour(a, 0, 0, factor);
3391    PlaceVertexWithColour(b, w, 0, factor);
3392    PlaceVertexWithColour(c, w, h, factor);
3393    PlaceVertexWithColour(d, 0, h, factor);
[da6c802]3394    EndQuadrilaterals();
3395}
3396
3397void GfxCore::AddQuadrilateralDepth(const Vector3 &a, const Vector3 &b,
3398                                    const Vector3 &c, const Vector3 &d)
[2b02270]3399{
3400    Vector3 normal = (a - c) * (d - b);
3401    normal.normalise();
3402    Double factor = dot(normal, light) * .3 + .7;
3403    int a_band, b_band, c_band, d_band;
[d67450e]3404    a_band = GetDepthColour(a.GetZ());
[97ea48d]3405    a_band = min(max(a_band, 0), GetNumColourBands());
[d67450e]3406    b_band = GetDepthColour(b.GetZ());
[97ea48d]3407    b_band = min(max(b_band, 0), GetNumColourBands());
[d67450e]3408    c_band = GetDepthColour(c.GetZ());
[97ea48d]3409    c_band = min(max(c_band, 0), GetNumColourBands());
[d67450e]3410    d_band = GetDepthColour(d.GetZ());
[97ea48d]3411    d_band = min(max(d_band, 0), GetNumColourBands());
[ba828d4]3412    glaTexCoord w(((b - a).magnitude() + (d - c).magnitude()) * .5);
3413    glaTexCoord h(((b - c).magnitude() + (d - a).magnitude()) * .5);
[f2d6d32]3414    int min_band = min(min(a_band, b_band), min(c_band, d_band));
3415    int max_band = max(max(a_band, b_band), max(c_band, d_band));
3416    if (min_band == max_band) {
3417        // Simple case - the polygon is entirely within one band.
3418        BeginPolygon();
3419////    PlaceNormal(normal);
3420        PlaceVertexWithDepthColour(a, 0, 0, factor);
3421        PlaceVertexWithDepthColour(b, w, 0, factor);
3422        PlaceVertexWithDepthColour(c, w, h, factor);
3423        PlaceVertexWithDepthColour(d, 0, h, factor);
3424        EndPolygon();
3425    } else {
3426        // We need to make a separate polygon for each depth band...
[d7078b4]3427        vector<vector<Split>> splits;
[f2d6d32]3428        splits.resize(max_band + 1);
[ba828d4]3429        splits[a_band].push_back(Split(a, 0, 0));
[f2d6d32]3430        if (a_band != b_band) {
[ba828d4]3431            SplitPolyAcrossBands(splits, a_band, b_band, a, b, 0, 0, w, 0);
[f2d6d32]3432        }
[ba828d4]3433        splits[b_band].push_back(Split(b, w, 0));
[f2d6d32]3434        if (b_band != c_band) {
[ba828d4]3435            SplitPolyAcrossBands(splits, b_band, c_band, b, c, w, 0, 0, h);
[f2d6d32]3436        }
[ba828d4]3437        splits[c_band].push_back(Split(c, w, h));
[f2d6d32]3438        if (c_band != d_band) {
[ba828d4]3439            SplitPolyAcrossBands(splits, c_band, d_band, c, d, w, h, -w, 0);
[f2d6d32]3440        }
[ba828d4]3441        splits[d_band].push_back(Split(d, 0, h));
[f2d6d32]3442        if (d_band != a_band) {
[ba828d4]3443            SplitPolyAcrossBands(splits, d_band, a_band, d, a, 0, h, 0, -h);
[f2d6d32]3444        }
3445        for (int band = min_band; band <= max_band; ++band) {
3446            BeginPolygon();
[ba828d4]3447            for (auto&& item : splits[band]) {
3448                PlaceVertexWithDepthColour(item.vec, item.tx, item.ty, factor);
[f2d6d32]3449            }
3450            EndPolygon();
3451        }
[2b02270]3452    }
3453}
3454
[1ee204e]3455void GfxCore::SetColourFromDate(int date, Double factor)
[d4650b3]3456{
3457    // Set the drawing colour based on a date.
3458
[1ee204e]3459    if (date == -1) {
[2043961]3460        // Undated.
[522e0bd]3461        SetColour(NODATA_COLOUR, factor);
[d4650b3]3462        return;
3463    }
3464
[1ee204e]3465    int date_offset = date - m_Parent->GetDateMin();
[2043961]3466    if (date_offset == 0) {
3467        // Earliest date - handle as a special case for the single date case.
3468        SetColour(GetPen(0), factor);
3469        return;
3470    }
[d4650b3]3471