source: git/src/gfxcore.cc @ 7ca37f4

RELEASE/1.2debug-cidebug-ci-sanitisersstereowalls-data
Last change on this file since 7ca37f4 was 80346ea, checked in by Olly Betts <olly@…>, 7 years ago

Fix custom cursors under GTK3

It seems the wxCursor constructor which we currently use for wxGTK
isn't implemented correctly for GTK3, so switch to the approach we
use for other platforms for GTK3, but leave wxGTK2 on the current
approach to avoid unnecessary risk of breakage.

  • Property mode set to 100644
File size: 112.3 KB
Line 
1//
2//  gfxcore.cc
3//
4//  Core drawing code for Aven.
5//
6//  Copyright (C) 2000-2003,2005,2006 Mark R. Shinwell
7//  Copyright (C) 2001-2003,2004,2005,2006,2007,2010,2011,2012,2014,2015,2016,2017 Olly Betts
8//  Copyright (C) 2005 Martin Green
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
22//  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
23//
24
25#ifdef HAVE_CONFIG_H
26#include <config.h>
27#endif
28
29#include <assert.h>
30#include <float.h>
31
32#include "aven.h"
33#include "date.h"
34#include "filename.h"
35#include "gfxcore.h"
36#include "mainfrm.h"
37#include "message.h"
38#include "useful.h"
39#include "printing.h"
40#include "guicontrol.h"
41#include "moviemaker.h"
42
43#include <wx/confbase.h>
44#include <wx/wfstream.h>
45#include <wx/image.h>
46#include <wx/zipstrm.h>
47
48#include <proj_api.h>
49
50const unsigned long DEFAULT_HGT_DIM = 3601;
51const unsigned long DEFAULT_HGT_SIZE = sqrd(DEFAULT_HGT_DIM) * 2;
52
53// Values for m_SwitchingTo
54#define PLAN 1
55#define ELEVATION 2
56#define NORTH 3
57#define EAST 4
58#define SOUTH 5
59#define WEST 6
60
61// Any error value higher than this is clamped to this.
62#define MAX_ERROR 12.0
63
64// Any length greater than pow(10, LOG_LEN_MAX) will be clamped to this.
65const Double LOG_LEN_MAX = 1.5;
66
67// How many bins per letter height to use when working out non-overlapping
68// labels.
69const unsigned int QUANTISE_FACTOR = 2;
70
71#include "avenpal.h"
72
73static const int INDICATOR_BOX_SIZE = 60;
74static const int INDICATOR_GAP = 2;
75static const int INDICATOR_MARGIN = 5;
76static const int INDICATOR_OFFSET_X = 15;
77static const int INDICATOR_OFFSET_Y = 15;
78static const int INDICATOR_RADIUS = INDICATOR_BOX_SIZE / 2 - INDICATOR_MARGIN;
79static const int KEY_OFFSET_X = 10;
80static const int KEY_OFFSET_Y = 10;
81static const int KEY_EXTRA_LEFT_MARGIN = 2;
82static const int KEY_BLOCK_WIDTH = 20;
83static const int KEY_BLOCK_HEIGHT = 16;
84static const int TICK_LENGTH = 4;
85static const int SCALE_BAR_OFFSET_X = 15;
86static const int SCALE_BAR_OFFSET_Y = 12;
87static const int SCALE_BAR_HEIGHT = 12;
88
89static const gla_colour TEXT_COLOUR = col_GREEN;
90static const gla_colour HERE_COLOUR = col_WHITE;
91static const gla_colour NAME_COLOUR = col_GREEN;
92static const gla_colour SEL_COLOUR = col_WHITE;
93
94// Number of entries across and down the hit-test grid:
95#define HITTEST_SIZE 20
96
97// How close the pointer needs to be to a station to be considered:
98#define MEASURE_THRESHOLD 7
99
100// vector for lighting angle
101static const Vector3 light(.577, .577, .577);
102
103BEGIN_EVENT_TABLE(GfxCore, GLACanvas)
104    EVT_PAINT(GfxCore::OnPaint)
105    EVT_LEFT_DOWN(GfxCore::OnLButtonDown)
106    EVT_LEFT_UP(GfxCore::OnLButtonUp)
107    EVT_MIDDLE_DOWN(GfxCore::OnMButtonDown)
108    EVT_MIDDLE_UP(GfxCore::OnMButtonUp)
109    EVT_RIGHT_DOWN(GfxCore::OnRButtonDown)
110    EVT_RIGHT_UP(GfxCore::OnRButtonUp)
111    EVT_MOUSEWHEEL(GfxCore::OnMouseWheel)
112    EVT_MOTION(GfxCore::OnMouseMove)
113    EVT_LEAVE_WINDOW(GfxCore::OnLeaveWindow)
114    EVT_SIZE(GfxCore::OnSize)
115    EVT_IDLE(GfxCore::OnIdle)
116    EVT_CHAR(GfxCore::OnKeyPress)
117END_EVENT_TABLE()
118
119GfxCore::GfxCore(MainFrm* parent, wxWindow* parent_win, GUIControl* control) :
120    GLACanvas(parent_win, 100),
121    m_Scale(0.0),
122    initial_scale(1.0),
123    m_ScaleBarWidth(0),
124    m_Control(control),
125    m_LabelGrid(NULL),
126    m_Parent(parent),
127    m_DoneFirstShow(false),
128    m_TiltAngle(0.0),
129    m_PanAngle(0.0),
130    m_Rotating(false),
131    m_RotationStep(0.0),
132    m_SwitchingTo(0),
133    m_Crosses(false),
134    m_Legs(true),
135    m_Splays(SHOW_FADED),
136    m_Dupes(SHOW_DASHED),
137    m_Names(false),
138    m_Scalebar(true),
139    m_ColourKey(true),
140    m_OverlappingNames(false),
141    m_Compass(true),
142    m_Clino(true),
143    m_Tubes(false),
144    m_ColourBy(COLOUR_BY_DEPTH),
145    m_HaveData(false),
146    m_HaveTerrain(true),
147    m_MouseOutsideCompass(false),
148    m_MouseOutsideElev(false),
149    m_Surface(false),
150    m_Entrances(false),
151    m_FixedPts(false),
152    m_ExportedPts(false),
153    m_Grid(false),
154    m_BoundingBox(false),
155    m_Terrain(false),
156    m_Degrees(false),
157    m_Metric(false),
158    m_Percent(false),
159    m_HitTestDebug(false),
160    m_RenderStats(false),
161    m_PointGrid(NULL),
162    m_HitTestGridValid(false),
163    m_here(NULL),
164    m_there(NULL),
165    presentation_mode(0),
166    pres_reverse(false),
167    pres_speed(0.0),
168    movie(NULL),
169    current_cursor(GfxCore::CURSOR_DEFAULT),
170    sqrd_measure_threshold(sqrd(MEASURE_THRESHOLD)),
171    dem(NULL),
172    last_time(0),
173    n_tris(0)
174{
175    AddQuad = &GfxCore::AddQuadrilateralDepth;
176    AddPoly = &GfxCore::AddPolylineDepth;
177    wxConfigBase::Get()->Read(wxT("metric"), &m_Metric, true);
178    wxConfigBase::Get()->Read(wxT("degrees"), &m_Degrees, true);
179    wxConfigBase::Get()->Read(wxT("percent"), &m_Percent, false);
180
181    for (int pen = 0; pen < NUM_COLOUR_BANDS + 1; ++pen) {
182        m_Pens[pen].SetColour(REDS[pen] / 255.0,
183                              GREENS[pen] / 255.0,
184                              BLUES[pen] / 255.0);
185    }
186
187    timer.Start();
188}
189
190GfxCore::~GfxCore()
191{
192    TryToFreeArrays();
193
194    delete[] m_PointGrid;
195}
196
197void GfxCore::TryToFreeArrays()
198{
199    // Free up any memory allocated for arrays.
200    delete[] m_LabelGrid;
201    m_LabelGrid = NULL;
202}
203
204//
205//  Initialisation methods
206//
207
208void GfxCore::Initialise(bool same_file)
209{
210    // Initialise the view from the parent holding the survey data.
211
212    TryToFreeArrays();
213
214    m_DoneFirstShow = false;
215
216    m_HitTestGridValid = false;
217    m_here = NULL;
218    m_there = NULL;
219
220    m_MouseOutsideCompass = m_MouseOutsideElev = false;
221
222    if (!same_file) {
223        // Apply default parameters unless reloading the same file.
224        DefaultParameters();
225    }
226
227    m_HaveData = true;
228
229    // Clear any cached OpenGL lists which depend on the data.
230    InvalidateList(LIST_SCALE_BAR);
231    InvalidateList(LIST_DEPTH_KEY);
232    InvalidateList(LIST_DATE_KEY);
233    InvalidateList(LIST_ERROR_KEY);
234    InvalidateList(LIST_GRADIENT_KEY);
235    InvalidateList(LIST_LENGTH_KEY);
236    InvalidateList(LIST_UNDERGROUND_LEGS);
237    InvalidateList(LIST_TUBES);
238    InvalidateList(LIST_SURFACE_LEGS);
239    InvalidateList(LIST_BLOBS);
240    InvalidateList(LIST_CROSSES);
241    InvalidateList(LIST_GRID);
242    InvalidateList(LIST_SHADOW);
243    InvalidateList(LIST_TERRAIN);
244
245    // Set diameter of the viewing volume.
246    double cave_diameter = sqrt(sqrd(m_Parent->GetXExtent()) +
247                                sqrd(m_Parent->GetYExtent()) +
248                                sqrd(m_Parent->GetZExtent()));
249
250    // Allow for terrain.
251    double diameter = max(1000.0 * 2, cave_diameter * 2);
252
253    if (!same_file) {
254        SetVolumeDiameter(diameter);
255
256        // Set initial scale based on the size of the cave.
257        initial_scale = diameter / cave_diameter;
258        SetScale(initial_scale);
259    } else {
260        // Adjust the position when restricting the view to a subsurvey (or
261        // expanding the view to show the whole survey).
262        AddTranslation(m_Parent->GetOffset() - offsets);
263
264        // Try to keep the same scale, allowing for the
265        // cave having grown (or shrunk).
266        double rescale = GetVolumeDiameter() / diameter;
267        SetVolumeDiameter(diameter);
268        SetScale(GetScale() / rescale); // ?
269        initial_scale = initial_scale * rescale;
270    }
271
272    offsets = m_Parent->GetOffset();
273
274    ForceRefresh();
275}
276
277void GfxCore::FirstShow()
278{
279    GLACanvas::FirstShow();
280
281    const unsigned int quantise(GetFontSize() / QUANTISE_FACTOR);
282    list<LabelInfo*>::iterator pos = m_Parent->GetLabelsNC();
283    while (pos != m_Parent->GetLabelsNCEnd()) {
284        LabelInfo* label = *pos++;
285        // Calculate and set the label width for use when plotting
286        // none-overlapping labels.
287        int ext_x;
288        GLACanvas::GetTextExtent(label->GetText(), &ext_x, NULL);
289        label->set_width(unsigned(ext_x) / quantise + 1);
290    }
291
292    m_DoneFirstShow = true;
293}
294
295//
296//  Recalculating methods
297//
298
299void GfxCore::SetScale(Double scale)
300{
301    if (scale < 0.05) {
302        scale = 0.05;
303    } else if (scale > GetVolumeDiameter()) {
304        scale = GetVolumeDiameter();
305    }
306
307    m_Scale = scale;
308    m_HitTestGridValid = false;
309    if (m_here && m_here == &temp_here) SetHere();
310
311    GLACanvas::SetScale(scale);
312}
313
314bool GfxCore::HasUndergroundLegs() const
315{
316    return m_Parent->HasUndergroundLegs();
317}
318
319bool GfxCore::HasSplays() const
320{
321    return m_Parent->HasSplays();
322}
323
324bool GfxCore::HasDupes() const
325{
326    return m_Parent->HasDupes();
327}
328
329bool GfxCore::HasSurfaceLegs() const
330{
331    return m_Parent->HasSurfaceLegs();
332}
333
334bool GfxCore::HasTubes() const
335{
336    return m_Parent->HasTubes();
337}
338
339void GfxCore::UpdateBlobs()
340{
341    InvalidateList(LIST_BLOBS);
342}
343
344//
345//  Event handlers
346//
347
348void GfxCore::OnLeaveWindow(wxMouseEvent&) {
349    SetHere();
350    ClearCoords();
351}
352
353void GfxCore::OnIdle(wxIdleEvent& event)
354{
355    // Handle an idle event.
356    if (Animating()) {
357        Animate();
358        // If still animating, we want more idle events.
359        if (Animating())
360            event.RequestMore();
361    } else {
362        // If we're idle, don't show a bogus FPS next time we render.
363        last_time = 0;
364    }
365}
366
367void GfxCore::OnPaint(wxPaintEvent&)
368{
369    // Redraw the window.
370
371    // Get a graphics context.
372    wxPaintDC dc(this);
373
374    if (m_HaveData) {
375        // Make sure we're initialised.
376        bool first_time = !m_DoneFirstShow;
377        if (first_time) {
378            FirstShow();
379        }
380
381        StartDrawing();
382
383        // Clear the background.
384        Clear();
385
386        // Set up model transformation matrix.
387        SetDataTransform();
388
389        if (m_Legs || m_Tubes) {
390            if (m_Tubes) {
391                EnableSmoothPolygons(true); // FIXME: allow false for wireframe view
392                DrawList(LIST_TUBES);
393                DisableSmoothPolygons();
394            }
395
396            // Draw the underground legs.  Do this last so that anti-aliasing
397            // works over polygons.
398            SetColour(col_GREEN);
399            DrawList(LIST_UNDERGROUND_LEGS);
400        }
401
402        if (m_Surface) {
403            // Draw the surface legs.
404            DrawList(LIST_SURFACE_LEGS);
405        }
406
407        if (m_BoundingBox) {
408            DrawShadowedBoundingBox();
409        }
410        if (m_Grid) {
411            // Draw the grid.
412            DrawList(LIST_GRID);
413        }
414
415        DrawList(LIST_BLOBS);
416
417        if (m_Crosses) {
418            DrawList(LIST_CROSSES);
419        }
420
421        if (m_Terrain) {
422            // Disable texturing while drawing terrain.
423            bool texturing = GetTextured();
424            if (texturing) GLACanvas::ToggleTextured();
425
426            // This is needed if blobs and/or crosses are drawn using lines -
427            // otherwise the terrain doesn't appear when they are enabled.
428            SetDataTransform();
429
430            // We don't want to be able to see the terrain through itself, so
431            // do a "Z-prepass" - plot the terrain once only updating the
432            // Z-buffer, then again with Z-clipping only plotting where the
433            // depth matches the value in the Z-buffer.
434            DrawListZPrepass(LIST_TERRAIN);
435
436            if (texturing) GLACanvas::ToggleTextured();
437        }
438
439        SetIndicatorTransform();
440
441        // Draw station names.
442        if (m_Names /*&& !m_Control->MouseDown() && !Animating()*/) {
443            SetColour(NAME_COLOUR);
444
445            if (m_OverlappingNames) {
446                SimpleDrawNames();
447            } else {
448                NattyDrawNames();
449            }
450        }
451
452        if (m_HitTestDebug) {
453            // Show the hit test grid bucket sizes...
454            SetColour(m_HitTestGridValid ? col_LIGHT_GREY : col_DARK_GREY);
455            if (m_PointGrid) {
456                for (int i = 0; i != HITTEST_SIZE; ++i) {
457                    int x = (GetXSize() + 1) * i / HITTEST_SIZE + 2;
458                    for (int j = 0; j != HITTEST_SIZE; ++j) {
459                        int square = i + j * HITTEST_SIZE;
460                        unsigned long bucket_size = m_PointGrid[square].size();
461                        if (bucket_size) {
462                            int y = (GetYSize() + 1) * (HITTEST_SIZE - 1 - j) / HITTEST_SIZE;
463                            DrawIndicatorText(x, y, wxString::Format(wxT("%lu"), bucket_size));
464                        }
465                    }
466                }
467            }
468
469            EnableDashedLines();
470            BeginLines();
471            for (int i = 0; i != HITTEST_SIZE; ++i) {
472                int x = (GetXSize() + 1) * i / HITTEST_SIZE;
473                PlaceIndicatorVertex(x, 0);
474                PlaceIndicatorVertex(x, GetYSize());
475            }
476            for (int j = 0; j != HITTEST_SIZE; ++j) {
477                int y = (GetYSize() + 1) * (HITTEST_SIZE - 1 - j) / HITTEST_SIZE;
478                PlaceIndicatorVertex(0, y);
479                PlaceIndicatorVertex(GetXSize(), y);
480            }
481            EndLines();
482            DisableDashedLines();
483        }
484
485        long now = timer.Time();
486        if (m_RenderStats) {
487            // Show stats about rendering.
488            SetColour(col_TURQUOISE);
489            int y = GetYSize() - GetFontSize();
490            if (last_time != 0.0) {
491                // timer.Time() measure in milliseconds.
492                double fps = 1000.0 / (now - last_time);
493                DrawIndicatorText(1, y, wxString::Format(wxT("FPS:% 5.1f"), fps));
494            }
495            y -= GetFontSize();
496            DrawIndicatorText(1, y, wxString::Format(wxT("▲:%lu"), (unsigned long)n_tris));
497        }
498        last_time = now;
499
500        // Draw indicators.
501        //
502        // There's no advantage in generating an OpenGL list for the
503        // indicators since they change with almost every redraw (and
504        // sometimes several times between redraws).  This way we avoid
505        // the need to track when to update the indicator OpenGL list,
506        // and also avoid indicator update bugs when we don't quite get this
507        // right...
508        DrawIndicators();
509
510        if (zoombox.active()) {
511            SetColour(SEL_COLOUR);
512            EnableDashedLines();
513            BeginPolyline();
514            glaCoord Y = GetYSize();
515            PlaceIndicatorVertex(zoombox.x1, Y - zoombox.y1);
516            PlaceIndicatorVertex(zoombox.x1, Y - zoombox.y2);
517            PlaceIndicatorVertex(zoombox.x2, Y - zoombox.y2);
518            PlaceIndicatorVertex(zoombox.x2, Y - zoombox.y1);
519            PlaceIndicatorVertex(zoombox.x1, Y - zoombox.y1);
520            EndPolyline();
521            DisableDashedLines();
522        } else if (MeasuringLineActive()) {
523            // Draw "here" and "there".
524            double hx, hy;
525            SetColour(HERE_COLOUR);
526            if (m_here) {
527                double dummy;
528                Transform(*m_here, &hx, &hy, &dummy);
529                if (m_here != &temp_here) DrawRing(hx, hy);
530            }
531            if (m_there) {
532                double tx, ty;
533                double dummy;
534                Transform(*m_there, &tx, &ty, &dummy);
535                if (m_here) {
536                    BeginLines();
537                    PlaceIndicatorVertex(hx, hy);
538                    PlaceIndicatorVertex(tx, ty);
539                    EndLines();
540                }
541                BeginBlobs();
542                DrawBlob(tx, ty);
543                EndBlobs();
544            }
545        }
546
547        FinishDrawing();
548    } else {
549        dc.SetBackground(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWFRAME));
550        dc.Clear();
551    }
552}
553
554void GfxCore::DrawBoundingBox()
555{
556    const Vector3 v = 0.5 * m_Parent->GetExtent();
557
558    SetColour(col_BLUE);
559    EnableDashedLines();
560    BeginPolyline();
561    PlaceVertex(-v.GetX(), -v.GetY(), v.GetZ());
562    PlaceVertex(-v.GetX(), v.GetY(), v.GetZ());
563    PlaceVertex(v.GetX(), v.GetY(), v.GetZ());
564    PlaceVertex(v.GetX(), -v.GetY(), v.GetZ());
565    PlaceVertex(-v.GetX(), -v.GetY(), v.GetZ());
566    EndPolyline();
567    BeginPolyline();
568    PlaceVertex(-v.GetX(), -v.GetY(), -v.GetZ());
569    PlaceVertex(-v.GetX(), v.GetY(), -v.GetZ());
570    PlaceVertex(v.GetX(), v.GetY(), -v.GetZ());
571    PlaceVertex(v.GetX(), -v.GetY(), -v.GetZ());
572    PlaceVertex(-v.GetX(), -v.GetY(), -v.GetZ());
573    EndPolyline();
574    BeginLines();
575    PlaceVertex(-v.GetX(), -v.GetY(), v.GetZ());
576    PlaceVertex(-v.GetX(), -v.GetY(), -v.GetZ());
577    PlaceVertex(-v.GetX(), v.GetY(), v.GetZ());
578    PlaceVertex(-v.GetX(), v.GetY(), -v.GetZ());
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    EndLines();
584    DisableDashedLines();
585}
586
587void GfxCore::DrawShadowedBoundingBox()
588{
589    const Vector3 v = 0.5 * m_Parent->GetExtent();
590
591    DrawBoundingBox();
592
593    PolygonOffset(true);
594    SetColour(col_DARK_GREY);
595    BeginQuadrilaterals();
596    PlaceVertex(-v.GetX(), -v.GetY(), -v.GetZ());
597    PlaceVertex(-v.GetX(), v.GetY(), -v.GetZ());
598    PlaceVertex(v.GetX(), v.GetY(), -v.GetZ());
599    PlaceVertex(v.GetX(), -v.GetY(), -v.GetZ());
600    EndQuadrilaterals();
601    PolygonOffset(false);
602
603    DrawList(LIST_SHADOW);
604}
605
606void GfxCore::DrawGrid()
607{
608    // Draw the grid.
609    SetColour(col_RED);
610
611    // Calculate the extent of the survey, in metres across the screen plane.
612    Double m_across_screen = SurveyUnitsAcrossViewport();
613    // Calculate the length of the scale bar in metres.
614    //--move this elsewhere
615    Double size_snap = pow(10.0, floor(log10(0.75 * m_across_screen)));
616    Double t = m_across_screen * 0.75 / size_snap;
617    if (t >= 5.0) {
618        size_snap *= 5.0;
619    }
620    else if (t >= 2.0) {
621        size_snap *= 2.0;
622    }
623
624    Double grid_size = size_snap * 0.1;
625    Double edge = grid_size * 2.0;
626    Double grid_z = -m_Parent->GetZExtent() * 0.5 - grid_size;
627    Double left = -m_Parent->GetXExtent() * 0.5 - edge;
628    Double right = m_Parent->GetXExtent() * 0.5 + edge;
629    Double bottom = -m_Parent->GetYExtent() * 0.5 - edge;
630    Double top = m_Parent->GetYExtent() * 0.5 + edge;
631    int count_x = (int) ceil((right - left) / grid_size);
632    int count_y = (int) ceil((top - bottom) / grid_size);
633    Double actual_right = left + count_x*grid_size;
634    Double actual_top = bottom + count_y*grid_size;
635
636    BeginLines();
637
638    for (int xc = 0; xc <= count_x; xc++) {
639        Double x = left + xc*grid_size;
640
641        PlaceVertex(x, bottom, grid_z);
642        PlaceVertex(x, actual_top, grid_z);
643    }
644
645    for (int yc = 0; yc <= count_y; yc++) {
646        Double y = bottom + yc*grid_size;
647        PlaceVertex(left, y, grid_z);
648        PlaceVertex(actual_right, y, grid_z);
649    }
650
651    EndLines();
652}
653
654int GfxCore::GetClinoOffset() const
655{
656    int result = INDICATOR_OFFSET_X;
657    if (m_Compass) {
658        result += 6 + GetCompassWidth() + INDICATOR_GAP;
659    }
660    return result;
661}
662
663void GfxCore::DrawTick(int angle_cw)
664{
665    const Double theta = rad(angle_cw);
666    const wxCoord length1 = INDICATOR_RADIUS;
667    const wxCoord length0 = length1 + TICK_LENGTH;
668    wxCoord x0 = wxCoord(length0 * sin(theta));
669    wxCoord y0 = wxCoord(length0 * cos(theta));
670    wxCoord x1 = wxCoord(length1 * sin(theta));
671    wxCoord y1 = wxCoord(length1 * cos(theta));
672
673    PlaceIndicatorVertex(x0, y0);
674    PlaceIndicatorVertex(x1, y1);
675}
676
677void GfxCore::DrawArrow(gla_colour col1, gla_colour col2) {
678    Vector3 p1(0, INDICATOR_RADIUS, 0);
679    Vector3 p2(INDICATOR_RADIUS/2, INDICATOR_RADIUS*-.866025404, 0); // 150deg
680    Vector3 p3(-INDICATOR_RADIUS/2, INDICATOR_RADIUS*-.866025404, 0); // 210deg
681    Vector3 pc(0, 0, 0);
682
683    DrawTriangle(col_LIGHT_GREY, col1, p2, p1, pc);
684    DrawTriangle(col_LIGHT_GREY, col2, p3, p1, pc);
685}
686
687void GfxCore::DrawCompass() {
688    // Ticks.
689    BeginLines();
690    for (int angle = 315; angle > 0; angle -= 45) {
691        DrawTick(angle);
692    }
693    SetColour(col_GREEN);
694    DrawTick(0);
695    EndLines();
696
697    // Compass background.
698    DrawCircle(col_LIGHT_GREY_2, col_GREY, 0, 0, INDICATOR_RADIUS);
699
700    // Compass arrow.
701    DrawArrow(col_INDICATOR_1, col_INDICATOR_2);
702}
703
704// Draw the non-rotating background to the clino.
705void GfxCore::DrawClinoBack() {
706    BeginLines();
707    for (int angle = 0; angle <= 180; angle += 90) {
708        DrawTick(angle);
709    }
710
711    SetColour(col_GREY);
712    PlaceIndicatorVertex(0, INDICATOR_RADIUS);
713    PlaceIndicatorVertex(0, -INDICATOR_RADIUS);
714    PlaceIndicatorVertex(0, 0);
715    PlaceIndicatorVertex(INDICATOR_RADIUS, 0);
716
717    EndLines();
718}
719
720void GfxCore::DrawClino() {
721    // Ticks.
722    SetColour(col_GREEN);
723    BeginLines();
724    DrawTick(0);
725    EndLines();
726
727    // Clino background.
728    DrawSemicircle(col_LIGHT_GREY_2, col_GREY, 0, 0, INDICATOR_RADIUS, 0);
729
730    // Elevation arrow.
731    DrawArrow(col_INDICATOR_2, col_INDICATOR_1);
732}
733
734void GfxCore::Draw2dIndicators()
735{
736    // Draw the compass and elevation indicators.
737
738    const int centre_y = INDICATOR_BOX_SIZE / 2 + INDICATOR_OFFSET_Y;
739
740    const int comp_centre_x = GetCompassXPosition();
741
742    if (m_Compass && !m_Parent->IsExtendedElevation()) {
743        // If the user is dragging the compass with the pointer outside the
744        // compass, we snap to 45 degree multiples, and the ticks go white.
745        SetColour(m_MouseOutsideCompass ? col_WHITE : col_LIGHT_GREY_2);
746        DrawList2D(LIST_COMPASS, comp_centre_x, centre_y, -m_PanAngle);
747    }
748
749    const int elev_centre_x = GetClinoXPosition();
750
751    if (m_Clino) {
752        // If the user is dragging the clino with the pointer outside the
753        // clino, we snap to 90 degree multiples, and the ticks go white.
754        SetColour(m_MouseOutsideElev ? col_WHITE : col_LIGHT_GREY_2);
755        DrawList2D(LIST_CLINO_BACK, elev_centre_x, centre_y, 0);
756        DrawList2D(LIST_CLINO, elev_centre_x, centre_y, 90 - m_TiltAngle);
757    }
758
759    SetColour(TEXT_COLOUR);
760
761    static int triple_zero_width = 0;
762    static int height = 0;
763    if (!triple_zero_width) {
764        GetTextExtent(wxT("000"), &triple_zero_width, &height);
765    }
766    const int y_off = INDICATOR_OFFSET_Y + INDICATOR_BOX_SIZE + height / 2;
767
768    if (m_Compass && !m_Parent->IsExtendedElevation()) {
769        wxString str;
770        int value;
771        int brg_unit;
772        if (m_Degrees) {
773            value = int(m_PanAngle);
774            /* TRANSLATORS: degree symbol - probably should be translated to
775             * itself. */
776            brg_unit = /*°*/344;
777        } else {
778            value = int(m_PanAngle * 200.0 / 180.0);
779            /* TRANSLATORS: symbol for grad (400 grad = 360 degrees = full
780             * circle). */
781            brg_unit = /*ᵍ*/345;
782        }
783        str.Printf(wxT("%03d"), value);
784        str += wmsg(brg_unit);
785        DrawIndicatorText(comp_centre_x - triple_zero_width / 2, y_off, str);
786
787        // TRANSLATORS: Used in aven above the compass indicator at the lower
788        // right of the display, with a bearing below "Facing".  This indicates the
789        // direction the viewer is "facing" in.
790        //
791        // Try to keep this translation short - ideally at most 10 characters -
792        // as otherwise the compass and clino will be moved further apart to
793        // make room. */
794        str = wmsg(/*Facing*/203);
795        int w;
796        GetTextExtent(str, &w, NULL);
797        DrawIndicatorText(comp_centre_x - w / 2, y_off + height, str);
798    }
799
800    if (m_Clino) {
801        if (m_TiltAngle == -90.0) {
802            // TRANSLATORS: Label used for "clino" in Aven when the view is
803            // from directly above.
804            //
805            // Try to keep this translation short - ideally at most 10
806            // characters - as otherwise the compass and clino will be moved
807            // further apart to make room. */
808            wxString str = wmsg(/*Plan*/432);
809            static int width = 0;
810            if (!width) {
811                GetTextExtent(str, &width, NULL);
812            }
813            int x = elev_centre_x - width / 2;
814            DrawIndicatorText(x, y_off + height / 2, str);
815        } else if (m_TiltAngle == 90.0) {
816            // TRANSLATORS: Label used for "clino" in Aven when the view is
817            // from directly below.
818            //
819            // Try to keep this translation short - ideally at most 10
820            // characters - as otherwise the compass and clino will be moved
821            // further apart to make room. */
822            wxString str = wmsg(/*Kiwi Plan*/433);
823            static int width = 0;
824            if (!width) {
825                GetTextExtent(str, &width, NULL);
826            }
827            int x = elev_centre_x - width / 2;
828            DrawIndicatorText(x, y_off + height / 2, str);
829        } else {
830            int angle;
831            wxString str;
832            int width;
833            int unit;
834            if (m_Percent) {
835                static int zero_width = 0;
836                if (!zero_width) {
837                    GetTextExtent(wxT("0"), &zero_width, NULL);
838                }
839                width = zero_width;
840                if (m_TiltAngle > 89.99) {
841                    angle = 1000000;
842                } else if (m_TiltAngle < -89.99) {
843                    angle = -1000000;
844                } else {
845                    angle = int(100 * tan(rad(m_TiltAngle)));
846                }
847                if (angle > 99999 || angle < -99999) {
848                    str = angle > 0 ? wxT("+") : wxT("-");
849                    /* TRANSLATORS: infinity symbol - used for the percentage gradient on
850                     * vertical angles. */
851                    str += wmsg(/*∞*/431);
852                } else {
853                    str = angle ? wxString::Format(wxT("%+03d"), angle) : wxT("0");
854                }
855                /* TRANSLATORS: symbol for percentage gradient (100% = 45
856                 * degrees = 50 grad). */
857                unit = /*%*/96;
858            } else if (m_Degrees) {
859                static int zero_zero_width = 0;
860                if (!zero_zero_width) {
861                    GetTextExtent(wxT("00"), &zero_zero_width, NULL);
862                }
863                width = zero_zero_width;
864                angle = int(m_TiltAngle);
865                str = angle ? wxString::Format(wxT("%+03d"), angle) : wxT("00");
866                unit = /*°*/344;
867            } else {
868                width = triple_zero_width;
869                angle = int(m_TiltAngle * 200.0 / 180.0);
870                str = angle ? wxString::Format(wxT("%+04d"), angle) : wxT("000");
871                unit = /*ᵍ*/345;
872            }
873
874            int sign_offset = 0;
875            if (unit == /*%*/96) {
876                // Right align % since the width changes so much.
877                GetTextExtent(str, &sign_offset, NULL);
878                sign_offset -= width;
879            } else if (angle < 0) {
880                // Adjust horizontal position so the left of the first digit is
881                // always in the same place.
882                static int minus_width = 0;
883                if (!minus_width) {
884                    GetTextExtent(wxT("-"), &minus_width, NULL);
885                }
886                sign_offset = minus_width;
887            } else if (angle > 0) {
888                // Adjust horizontal position so the left of the first digit is
889                // always in the same place.
890                static int plus_width = 0;
891                if (!plus_width) {
892                    GetTextExtent(wxT("+"), &plus_width, NULL);
893                }
894                sign_offset = plus_width;
895            }
896
897            str += wmsg(unit);
898            DrawIndicatorText(elev_centre_x - sign_offset - width / 2, y_off, str);
899
900            // TRANSLATORS: Label used for "clino" in Aven when the view is
901            // neither from directly above nor from directly below.  It is
902            // also used in the dialog for editing a marked position in a
903            // presentation.
904            //
905            // Try to keep this translation short - ideally at most 10
906            // characters - as otherwise the compass and clino will be moved
907            // further apart to make room. */
908            str = wmsg(/*Elevation*/118);
909            static int elevation_width = 0;
910            if (!elevation_width) {
911                GetTextExtent(str, &elevation_width, NULL);
912            }
913            int x = elev_centre_x - elevation_width / 2;
914            DrawIndicatorText(x, y_off + height, str);
915        }
916    }
917}
918
919void GfxCore::NattyDrawNames()
920{
921    // Draw station names, without overlapping.
922
923    const unsigned int quantise(GetFontSize() / QUANTISE_FACTOR);
924    const unsigned int quantised_x = GetXSize() / quantise;
925    const unsigned int quantised_y = GetYSize() / quantise;
926    const size_t buffer_size = quantised_x * quantised_y;
927
928    if (!m_LabelGrid) m_LabelGrid = new char[buffer_size];
929
930    memset((void*) m_LabelGrid, 0, buffer_size);
931
932    list<LabelInfo*>::const_iterator label = m_Parent->GetLabels();
933    for ( ; label != m_Parent->GetLabelsEnd(); ++label) {
934        if (!((m_Surface && (*label)->IsSurface()) ||
935              (m_Legs && (*label)->IsUnderground()) ||
936              (!(*label)->IsSurface() && !(*label)->IsUnderground()))) {
937            // if this station isn't to be displayed, skip to the next
938            // (last case is for stns with no legs attached)
939            continue;
940        }
941
942        double x, y, z;
943
944        Transform(**label, &x, &y, &z);
945        // Check if the label is behind us (in perspective view).
946        if (z <= 0.0 || z >= 1.0) continue;
947
948        // Apply a small shift so that translating the view doesn't make which
949        // labels are displayed change as the resulting twinkling effect is
950        // distracting.
951        double tx, ty, tz;
952        Transform(Vector3(), &tx, &ty, &tz);
953        tx -= floor(tx / quantise) * quantise;
954        ty -= floor(ty / quantise) * quantise;
955
956        tx = x - tx;
957        if (tx < 0) continue;
958
959        ty = y - ty;
960        if (ty < 0) continue;
961
962        unsigned int iy = unsigned(ty) / quantise;
963        if (iy >= quantised_y) continue;
964        unsigned int width = (*label)->get_width();
965        unsigned int ix = unsigned(tx) / quantise;
966        if (ix + width >= quantised_x) continue;
967
968        char * test = m_LabelGrid + ix + iy * quantised_x;
969        if (memchr(test, 1, width)) continue;
970
971        x += 3;
972        y -= GetFontSize() / 2;
973        DrawIndicatorText((int)x, (int)y, (*label)->GetText());
974
975        if (iy > QUANTISE_FACTOR) iy = QUANTISE_FACTOR;
976        test -= quantised_x * iy;
977        iy += 4;
978        while (--iy && test < m_LabelGrid + buffer_size) {
979            memset(test, 1, width);
980            test += quantised_x;
981        }
982    }
983}
984
985void GfxCore::SimpleDrawNames()
986{
987    // Draw all station names, without worrying about overlaps
988    list<LabelInfo*>::const_iterator label = m_Parent->GetLabels();
989    for ( ; label != m_Parent->GetLabelsEnd(); ++label) {
990        if (!((m_Surface && (*label)->IsSurface()) ||
991              (m_Legs && (*label)->IsUnderground()) ||
992              (!(*label)->IsSurface() && !(*label)->IsUnderground()))) {
993            // if this station isn't to be displayed, skip to the next
994            // (last case is for stns with no legs attached)
995            continue;
996        }
997
998        double x, y, z;
999        Transform(**label, &x, &y, &z);
1000
1001        // Check if the label is behind us (in perspective view).
1002        if (z <= 0) continue;
1003
1004        x += 3;
1005        y -= GetFontSize() / 2;
1006        DrawIndicatorText((int)x, (int)y, (*label)->GetText());
1007    }
1008}
1009
1010void GfxCore::DrawColourKey(int num_bands, const wxString & other, const wxString & units)
1011{
1012    int total_block_height =
1013        KEY_BLOCK_HEIGHT * (num_bands == 1 ? num_bands : num_bands - 1);
1014    if (!other.empty()) total_block_height += KEY_BLOCK_HEIGHT * 2;
1015    if (!units.empty()) total_block_height += KEY_BLOCK_HEIGHT;
1016
1017    const int bottom = -total_block_height;
1018
1019    int size = 0;
1020    if (!other.empty()) GetTextExtent(other, &size, NULL);
1021    int band;
1022    for (band = 0; band < num_bands; ++band) {
1023        int x;
1024        GetTextExtent(key_legends[band], &x, NULL);
1025        if (x > size) size = x;
1026    }
1027
1028    int left = -KEY_BLOCK_WIDTH - size;
1029
1030    key_lowerleft[m_ColourBy].x = left - KEY_EXTRA_LEFT_MARGIN;
1031    key_lowerleft[m_ColourBy].y = bottom;
1032
1033    int y = bottom;
1034    if (!units.empty()) y += KEY_BLOCK_HEIGHT;
1035
1036    if (!other.empty()) {
1037        DrawShadedRectangle(GetSurfacePen(), GetSurfacePen(), left, y,
1038                KEY_BLOCK_WIDTH, KEY_BLOCK_HEIGHT);
1039        SetColour(col_BLACK);
1040        BeginPolyline();
1041        PlaceIndicatorVertex(left, y);
1042        PlaceIndicatorVertex(left + KEY_BLOCK_WIDTH, y);
1043        PlaceIndicatorVertex(left + KEY_BLOCK_WIDTH, y + KEY_BLOCK_HEIGHT);
1044        PlaceIndicatorVertex(left, y + KEY_BLOCK_HEIGHT);
1045        PlaceIndicatorVertex(left, y);
1046        EndPolyline();
1047        y += KEY_BLOCK_HEIGHT * 2;
1048    }
1049
1050    int start = y;
1051    if (num_bands == 1) {
1052        DrawShadedRectangle(GetPen(0), GetPen(0), left, y,
1053                            KEY_BLOCK_WIDTH, KEY_BLOCK_HEIGHT);
1054        y += KEY_BLOCK_HEIGHT;
1055    } else {
1056        for (band = 0; band < num_bands - 1; ++band) {
1057            DrawShadedRectangle(GetPen(band), GetPen(band + 1), left, y,
1058                                KEY_BLOCK_WIDTH, KEY_BLOCK_HEIGHT);
1059            y += KEY_BLOCK_HEIGHT;
1060        }
1061    }
1062
1063    SetColour(col_BLACK);
1064    BeginPolyline();
1065    PlaceIndicatorVertex(left, y);
1066    PlaceIndicatorVertex(left + KEY_BLOCK_WIDTH, y);
1067    PlaceIndicatorVertex(left + KEY_BLOCK_WIDTH, start);
1068    PlaceIndicatorVertex(left, start);
1069    PlaceIndicatorVertex(left, y);
1070    EndPolyline();
1071
1072    SetColour(TEXT_COLOUR);
1073
1074    y = bottom;
1075    if (!units.empty()) {
1076        GetTextExtent(units, &size, NULL);
1077        DrawIndicatorText(left + (KEY_BLOCK_WIDTH - size) / 2, y, units);
1078        y += KEY_BLOCK_HEIGHT;
1079    }
1080    y -= GetFontSize() / 2;
1081    left += KEY_BLOCK_WIDTH + 5;
1082
1083    if (!other.empty()) {
1084        y += KEY_BLOCK_HEIGHT / 2;
1085        DrawIndicatorText(left, y, other);
1086        y += KEY_BLOCK_HEIGHT * 2 - KEY_BLOCK_HEIGHT / 2;
1087    }
1088
1089    if (num_bands == 1) {
1090        y += KEY_BLOCK_HEIGHT / 2;
1091        DrawIndicatorText(left, y, key_legends[0]);
1092    } else {
1093        for (band = 0; band < num_bands; ++band) {
1094            DrawIndicatorText(left, y, key_legends[band]);
1095            y += KEY_BLOCK_HEIGHT;
1096        }
1097    }
1098}
1099
1100void GfxCore::DrawDepthKey()
1101{
1102    Double z_ext = m_Parent->GetDepthExtent();
1103    int num_bands = 1;
1104    int sf = 0;
1105    if (z_ext > 0.0) {
1106        num_bands = GetNumColourBands();
1107        Double z_range = z_ext;
1108        if (!m_Metric) z_range /= METRES_PER_FOOT;
1109        sf = max(0, 1 - (int)floor(log10(z_range)));
1110    }
1111
1112    Double z_min = m_Parent->GetDepthMin() + m_Parent->GetOffset().GetZ();
1113    for (int band = 0; band < num_bands; ++band) {
1114        Double z = z_min;
1115        if (band)
1116            z += z_ext * band / (num_bands - 1);
1117
1118        if (!m_Metric)
1119            z /= METRES_PER_FOOT;
1120
1121        key_legends[band].Printf(wxT("%.*f"), sf, z);
1122    }
1123
1124    DrawColourKey(num_bands, wxString(), wmsg(m_Metric ? /*m*/424: /*ft*/428));
1125}
1126
1127void GfxCore::DrawDateKey()
1128{
1129    int num_bands;
1130    if (!HasDateInformation()) {
1131        num_bands = 0;
1132    } else {
1133        int date_ext = m_Parent->GetDateExtent();
1134        if (date_ext == 0) {
1135            num_bands = 1;
1136        } else {
1137            num_bands = GetNumColourBands();
1138        }
1139        for (int band = 0; band < num_bands; ++band) {
1140            int y, m, d;
1141            int days = m_Parent->GetDateMin();
1142            if (band)
1143                days += date_ext * band / (num_bands - 1);
1144            ymd_from_days_since_1900(days, &y, &m, &d);
1145            key_legends[band].Printf(wxT("%04d-%02d-%02d"), y, m, d);
1146        }
1147    }
1148
1149    wxString other;
1150    if (!m_Parent->HasCompleteDateInfo()) {
1151        /* TRANSLATORS: Used in the "colour key" for "colour by date" if there
1152         * are surveys without date information.  Try to keep this fairly short.
1153         */
1154        other = wmsg(/*Undated*/221);
1155    }
1156
1157    DrawColourKey(num_bands, other, wxString());
1158}
1159
1160void GfxCore::DrawErrorKey()
1161{
1162    int num_bands;
1163    if (HasErrorInformation()) {
1164        // Use fixed colours for each error factor so it's directly visually
1165        // comparable between surveys.
1166        num_bands = GetNumColourBands();
1167        for (int band = 0; band < num_bands; ++band) {
1168            double E = MAX_ERROR * band / (num_bands - 1);
1169            key_legends[band].Printf(wxT("%.2f"), E);
1170        }
1171    } else {
1172        num_bands = 0;
1173    }
1174
1175    // Always show the "Not in loop" legend for now (FIXME).
1176    /* TRANSLATORS: Used in the "colour key" for "colour by error" for surveys
1177     * which aren’t part of a loop and so have no error information. Try to keep
1178     * this fairly short. */
1179    DrawColourKey(num_bands, wmsg(/*Not in loop*/290), wxString());
1180}
1181
1182void GfxCore::DrawGradientKey()
1183{
1184    int num_bands;
1185    // Use fixed colours for each gradient so it's directly visually comparable
1186    // between surveys.
1187    num_bands = GetNumColourBands();
1188    wxString units = wmsg(m_Degrees ? /*°*/344 : /*ᵍ*/345);
1189    for (int band = 0; band < num_bands; ++band) {
1190        double gradient = double(band) / (num_bands - 1);
1191        if (m_Degrees) {
1192            gradient *= 90.0;
1193        } else {
1194            gradient *= 100.0;
1195        }
1196        key_legends[band].Printf(wxT("%.f%s"), gradient, units);
1197    }
1198
1199    DrawColourKey(num_bands, wxString(), wxString());
1200}
1201
1202void GfxCore::DrawLengthKey()
1203{
1204    int num_bands;
1205    // Use fixed colours for each length so it's directly visually comparable
1206    // between surveys.
1207    num_bands = GetNumColourBands();
1208    for (int band = 0; band < num_bands; ++band) {
1209        double len = pow(10, LOG_LEN_MAX * band / (num_bands - 1));
1210        if (!m_Metric) {
1211            len /= METRES_PER_FOOT;
1212        }
1213        key_legends[band].Printf(wxT("%.1f"), len);
1214    }
1215
1216    DrawColourKey(num_bands, wxString(), wmsg(m_Metric ? /*m*/424: /*ft*/428));
1217}
1218
1219void GfxCore::DrawScaleBar()
1220{
1221    // Draw the scalebar.
1222    if (GetPerspective()) return;
1223
1224    // Calculate how many metres of survey are currently displayed across the
1225    // screen.
1226    Double across_screen = SurveyUnitsAcrossViewport();
1227
1228    double f = double(GetClinoXPosition() - INDICATOR_BOX_SIZE / 2 - SCALE_BAR_OFFSET_X) / GetXSize();
1229    if (f > 0.75) {
1230        f = 0.75;
1231    } else if (f < 0.5) {
1232        // Stop it getting squeezed to nothing.
1233        // FIXME: In this case we should probably move the compass and clino up
1234        // to make room rather than letting stuff overlap.
1235        f = 0.5;
1236    }
1237
1238    // Convert to imperial measurements if required.
1239    Double multiplier = 1.0;
1240    if (!m_Metric) {
1241        across_screen /= METRES_PER_FOOT;
1242        multiplier = METRES_PER_FOOT;
1243        if (across_screen >= 5280.0 / f) {
1244            across_screen /= 5280.0;
1245            multiplier *= 5280.0;
1246        }
1247    }
1248
1249    // Calculate the length of the scale bar.
1250    Double size_snap = pow(10.0, floor(log10(f * across_screen)));
1251    Double t = across_screen * f / size_snap;
1252    if (t >= 5.0) {
1253        size_snap *= 5.0;
1254    } else if (t >= 2.0) {
1255        size_snap *= 2.0;
1256    }
1257
1258    if (!m_Metric) size_snap *= multiplier;
1259
1260    // Actual size of the thing in pixels:
1261    int size = int((size_snap / SurveyUnitsAcrossViewport()) * GetXSize());
1262    m_ScaleBarWidth = size;
1263
1264    // Draw it...
1265    const int end_y = SCALE_BAR_OFFSET_Y + SCALE_BAR_HEIGHT;
1266    int interval = size / 10;
1267
1268    gla_colour col = col_WHITE;
1269    for (int ix = 0; ix < 10; ix++) {
1270        int x = SCALE_BAR_OFFSET_X + int(ix * ((Double) size / 10.0));
1271
1272        DrawRectangle(col, col, x, end_y, interval + 2, SCALE_BAR_HEIGHT);
1273
1274        col = (col == col_WHITE) ? col_GREY : col_WHITE;
1275    }
1276
1277    // Add labels.
1278    wxString str;
1279    int units;
1280    if (m_Metric) {
1281        Double km = size_snap * 1e-3;
1282        if (km >= 1.0) {
1283            size_snap = km;
1284            /* TRANSLATORS: abbreviation for "kilometres" (unit of length),
1285             * used e.g.  "5km".
1286             *
1287             * If there should be a space between the number and this, include
1288             * one in the translation. */
1289            units = /*km*/423;
1290        } else if (size_snap >= 1.0) {
1291            /* TRANSLATORS: abbreviation for "metres" (unit of length), used
1292             * e.g. "10m".
1293             *
1294             * If there should be a space between the number and this, include
1295             * one in the translation. */
1296            units = /*m*/424;
1297        } else {
1298            size_snap *= 1e2;
1299            /* TRANSLATORS: abbreviation for "centimetres" (unit of length),
1300             * used e.g.  "50cm".
1301             *
1302             * If there should be a space between the number and this, include
1303             * one in the translation. */
1304            units = /*cm*/425;
1305        }
1306    } else {
1307        size_snap /= METRES_PER_FOOT;
1308        Double miles = size_snap / 5280.0;
1309        if (miles >= 1.0) {
1310            size_snap = miles;
1311            if (size_snap >= 2.0) {
1312                /* TRANSLATORS: abbreviation for "miles" (unit of length,
1313                 * plural), used e.g.  "2 miles".
1314                 *
1315                 * If there should be a space between the number and this,
1316                 * include one in the translation. */
1317                units = /* miles*/426;
1318            } else {
1319                /* TRANSLATORS: abbreviation for "mile" (unit of length,
1320                 * singular), used e.g.  "1 mile".
1321                 *
1322                 * If there should be a space between the number and this,
1323                 * include one in the translation. */
1324                units = /* mile*/427;
1325            }
1326        } else if (size_snap >= 1.0) {
1327            /* TRANSLATORS: abbreviation for "feet" (unit of length), used e.g.
1328             * as "10ft".
1329             *
1330             * If there should be a space between the number and this, include
1331             * one in the translation. */
1332            units = /*ft*/428;
1333        } else {
1334            size_snap *= 12.0;
1335            /* TRANSLATORS: abbreviation for "inches" (unit of length), used
1336             * e.g. as "6in".
1337             *
1338             * If there should be a space between the number and this, include
1339             * one in the translation. */
1340            units = /*in*/429;
1341        }
1342    }
1343    if (size_snap >= 1.0) {
1344        str.Printf(wxT("%.f%s"), size_snap, wmsg(units).c_str());
1345    } else {
1346        int sf = -(int)floor(log10(size_snap));
1347        str.Printf(wxT("%.*f%s"), sf, size_snap, wmsg(units).c_str());
1348    }
1349
1350    int text_width, text_height;
1351    GetTextExtent(str, &text_width, &text_height);
1352    const int text_y = end_y - text_height + 1;
1353    SetColour(TEXT_COLOUR);
1354    DrawIndicatorText(SCALE_BAR_OFFSET_X, text_y, wxT("0"));
1355    DrawIndicatorText(SCALE_BAR_OFFSET_X + size - text_width, text_y, str);
1356}
1357
1358bool GfxCore::CheckHitTestGrid(const wxPoint& point, bool centre)
1359{
1360    if (Animating()) return false;
1361
1362    if (point.x < 0 || point.x >= GetXSize() ||
1363        point.y < 0 || point.y >= GetYSize()) {
1364        return false;
1365    }
1366
1367    SetDataTransform();
1368
1369    if (!m_HitTestGridValid) CreateHitTestGrid();
1370
1371    int grid_x = point.x * HITTEST_SIZE / (GetXSize() + 1);
1372    int grid_y = point.y * HITTEST_SIZE / (GetYSize() + 1);
1373
1374    LabelInfo *best = NULL;
1375    int dist_sqrd = sqrd_measure_threshold;
1376    int square = grid_x + grid_y * HITTEST_SIZE;
1377    list<LabelInfo*>::iterator iter = m_PointGrid[square].begin();
1378
1379    while (iter != m_PointGrid[square].end()) {
1380        LabelInfo *pt = *iter++;
1381
1382        double cx, cy, cz;
1383
1384        Transform(*pt, &cx, &cy, &cz);
1385
1386        cy = GetYSize() - cy;
1387
1388        int dx = point.x - int(cx);
1389        int ds = dx * dx;
1390        if (ds >= dist_sqrd) continue;
1391        int dy = point.y - int(cy);
1392
1393        ds += dy * dy;
1394        if (ds >= dist_sqrd) continue;
1395
1396        dist_sqrd = ds;
1397        best = pt;
1398
1399        if (ds == 0) break;
1400    }
1401
1402    if (best) {
1403        m_Parent->ShowInfo(best, m_there);
1404        if (centre) {
1405            // FIXME: allow Ctrl-Click to not set there or something?
1406            CentreOn(*best);
1407            WarpPointer(GetXSize() / 2, GetYSize() / 2);
1408            SetThere(best);
1409            m_Parent->SelectTreeItem(best);
1410        }
1411    } else {
1412        // Left-clicking not on a survey cancels the measuring line.
1413        if (centre) {
1414            ClearTreeSelection();
1415        } else {
1416            m_Parent->ShowInfo(best, m_there);
1417            double x, y, z;
1418            ReverseTransform(point.x, GetYSize() - point.y, &x, &y, &z);
1419            temp_here.assign(Vector3(x, y, z));
1420            SetHere(&temp_here);
1421        }
1422    }
1423
1424    return best;
1425}
1426
1427void GfxCore::OnSize(wxSizeEvent& event)
1428{
1429    // Handle a change in window size.
1430    wxSize size = event.GetSize();
1431
1432    if (size.GetWidth() <= 0 || size.GetHeight() <= 0) {
1433        // Before things are fully initialised, we sometimes get a bogus
1434        // resize message...
1435        // FIXME have changes in MainFrm cured this?  It still happens with
1436        // 1.0.32 and wxGTK 2.5.2 (load a file from the command line).
1437        // With 1.1.6 and wxGTK 2.4.2 we only get negative sizes if MainFrm
1438        // is resized such that the GfxCore window isn't visible.
1439        //printf("OnSize(%d,%d)\n", size.GetWidth(), size.GetHeight());
1440        return;
1441    }
1442
1443    event.Skip();
1444
1445    if (m_DoneFirstShow) {
1446        TryToFreeArrays();
1447
1448        m_HitTestGridValid = false;
1449
1450        ForceRefresh();
1451    }
1452}
1453
1454void GfxCore::DefaultParameters()
1455{
1456    // Set default viewing parameters.
1457
1458    m_Surface = false;
1459    if (!m_Parent->HasUndergroundLegs()) {
1460        if (m_Parent->HasSurfaceLegs()) {
1461            // If there are surface legs, but no underground legs, turn
1462            // surface surveys on.
1463            m_Surface = true;
1464        } else {
1465            // If there are no legs (e.g. after loading a .pos file), turn
1466            // crosses on.
1467            m_Crosses = true;
1468        }
1469    }
1470
1471    m_PanAngle = 0.0;
1472    if (m_Parent->IsExtendedElevation()) {
1473        m_TiltAngle = 0.0;
1474    } else {
1475        m_TiltAngle = -90.0;
1476    }
1477
1478    SetRotation(m_PanAngle, m_TiltAngle);
1479    SetTranslation(Vector3());
1480
1481    m_RotationStep = 30.0;
1482    m_Rotating = false;
1483    m_SwitchingTo = 0;
1484    m_Entrances = false;
1485    m_FixedPts = false;
1486    m_ExportedPts = false;
1487    m_Grid = false;
1488    m_BoundingBox = false;
1489    m_Tubes = false;
1490    if (GetPerspective()) TogglePerspective();
1491
1492    // Set the initial scale.
1493    SetScale(initial_scale);
1494}
1495
1496void GfxCore::Defaults()
1497{
1498    // Restore default scale, rotation and translation parameters.
1499    DefaultParameters();
1500
1501    // Invalidate all the cached lists.
1502    GLACanvas::FirstShow();
1503
1504    ForceRefresh();
1505}
1506
1507void GfxCore::Animate()
1508{
1509    // Don't show pointer coordinates while animating.
1510    // FIXME : only do this when we *START* animating!  Use a static copy
1511    // of the value of "Animating()" last time we were here to track this?
1512    // MainFrm now checks if we're trying to clear already cleared labels
1513    // and just returns, but it might be simpler to check here!
1514    ClearCoords();
1515    m_Parent->ShowInfo();
1516
1517    long t;
1518    if (movie) {
1519        ReadPixels(movie->GetWidth(), movie->GetHeight(), movie->GetBuffer());
1520        if (!movie->AddFrame()) {
1521            wxGetApp().ReportError(wxString(movie->get_error_string(), wxConvUTF8));
1522            delete movie;
1523            movie = NULL;
1524            presentation_mode = 0;
1525            return;
1526        }
1527        t = 1000 / 25; // 25 frames per second
1528    } else {
1529        static long t_prev = 0;
1530        t = timer.Time();
1531        // Avoid redrawing twice in the same frame.
1532        long delta_t = (t_prev == 0 ? 1000 / MAX_FRAMERATE : t - t_prev);
1533        if (delta_t < 1000 / MAX_FRAMERATE)
1534            return;
1535        t_prev = t;
1536        if (presentation_mode == PLAYING && pres_speed != 0.0)
1537            t = delta_t;
1538    }
1539
1540    if (presentation_mode == PLAYING && pres_speed != 0.0) {
1541        // FIXME: It would probably be better to work relative to the time we
1542        // passed the last mark, but that's complicated by the speed
1543        // potentially changing (or even the direction of playback reversing)
1544        // at any point during playback.
1545        Double tick = t * 0.001 * fabs(pres_speed);
1546        while (tick >= next_mark_time) {
1547            tick -= next_mark_time;
1548            this_mark_total = 0;
1549            PresentationMark prev_mark = next_mark;
1550            if (prev_mark.angle < 0) prev_mark.angle += 360.0;
1551            else if (prev_mark.angle >= 360.0) prev_mark.angle -= 360.0;
1552            if (pres_reverse)
1553                next_mark = m_Parent->GetPresMark(MARK_PREV);
1554            else
1555                next_mark = m_Parent->GetPresMark(MARK_NEXT);
1556            if (!next_mark.is_valid()) {
1557                SetView(prev_mark);
1558                presentation_mode = 0;
1559                if (movie && !movie->Close()) {
1560                    wxGetApp().ReportError(wxString(movie->get_error_string(), wxConvUTF8));
1561                }
1562                delete movie;
1563                movie = NULL;
1564                break;
1565            }
1566
1567            double tmp = (pres_reverse ? prev_mark.time : next_mark.time);
1568            if (tmp > 0) {
1569                next_mark_time = tmp;
1570            } else {
1571                double d = (next_mark - prev_mark).magnitude();
1572                // FIXME: should ignore component of d which is unseen in
1573                // non-perspective mode?
1574                next_mark_time = sqrd(d / 30.0);
1575                double a = next_mark.angle - prev_mark.angle;
1576                if (a > 180.0) {
1577                    next_mark.angle -= 360.0;
1578                    a = 360.0 - a;
1579                } else if (a < -180.0) {
1580                    next_mark.angle += 360.0;
1581                    a += 360.0;
1582                } else {
1583                    a = fabs(a);
1584                }
1585                next_mark_time += sqrd(a / 60.0);
1586                double ta = fabs(next_mark.tilt_angle - prev_mark.tilt_angle);
1587                next_mark_time += sqrd(ta / 60.0);
1588                double s = fabs(log(next_mark.scale) - log(prev_mark.scale));
1589                next_mark_time += sqrd(s / 2.0);
1590                next_mark_time = sqrt(next_mark_time);
1591                // was: next_mark_time = max(max(d / 30, s / 2), max(a, ta) / 60);
1592                //printf("*** %.6f from (\nd: %.6f\ns: %.6f\na: %.6f\nt: %.6f )\n",
1593                //       next_mark_time, d/30.0, s/2.0, a/60.0, ta/60.0);
1594                if (tmp < 0) next_mark_time /= -tmp;
1595            }
1596        }
1597
1598        if (presentation_mode) {
1599            // Advance position towards next_mark
1600            double p = tick / next_mark_time;
1601            double q = 1 - p;
1602            PresentationMark here = GetView();
1603            if (next_mark.angle < 0) {
1604                if (here.angle >= next_mark.angle + 360.0)
1605                    here.angle -= 360.0;
1606            } else if (next_mark.angle >= 360.0) {
1607                if (here.angle <= next_mark.angle - 360.0)
1608                    here.angle += 360.0;
1609            }
1610            here.assign(q * here + p * next_mark);
1611            here.angle = q * here.angle + p * next_mark.angle;
1612            if (here.angle < 0) here.angle += 360.0;
1613            else if (here.angle >= 360.0) here.angle -= 360.0;
1614            here.tilt_angle = q * here.tilt_angle + p * next_mark.tilt_angle;
1615            here.scale = exp(q * log(here.scale) + p * log(next_mark.scale));
1616            SetView(here);
1617            this_mark_total += tick;
1618            next_mark_time -= tick;
1619        }
1620
1621        ForceRefresh();
1622        return;
1623    }
1624
1625    // When rotating...
1626    if (m_Rotating) {
1627        Double step = base_pan + (t - base_pan_time) * 1e-3 * m_RotationStep - m_PanAngle;
1628        TurnCave(step);
1629    }
1630
1631    if (m_SwitchingTo == PLAN) {
1632        // When switching to plan view...
1633        Double step = base_tilt - (t - base_tilt_time) * 1e-3 * 90.0 - m_TiltAngle;
1634        TiltCave(step);
1635        if (m_TiltAngle == -90.0) {
1636            m_SwitchingTo = 0;
1637        }
1638    } else if (m_SwitchingTo == ELEVATION) {
1639        // When switching to elevation view...
1640        Double step;
1641        if (m_TiltAngle > 0.0) {
1642            step = base_tilt - (t - base_tilt_time) * 1e-3 * 90.0 - m_TiltAngle;
1643        } else {
1644            step = base_tilt + (t - base_tilt_time) * 1e-3 * 90.0 - m_TiltAngle;
1645        }
1646        if (fabs(step) >= fabs(m_TiltAngle)) {
1647            m_SwitchingTo = 0;
1648            step = -m_TiltAngle;
1649        }
1650        TiltCave(step);
1651    } else if (m_SwitchingTo) {
1652        // Rotate the shortest way around to the destination angle.  If we're
1653        // 180 off, we favour turning anticlockwise, as auto-rotation does by
1654        // default.
1655        Double target = (m_SwitchingTo - NORTH) * 90;
1656        Double diff = target - m_PanAngle;
1657        diff = fmod(diff, 360);
1658        if (diff <= -180)
1659            diff += 360;
1660        else if (diff > 180)
1661            diff -= 360;
1662        if (m_RotationStep < 0 && diff == 180.0)
1663            diff = -180.0;
1664        Double step = base_pan - m_PanAngle;
1665        Double delta = (t - base_pan_time) * 1e-3 * fabs(m_RotationStep);
1666        if (diff > 0) {
1667            step += delta;
1668        } else {
1669            step -= delta;
1670        }
1671        step = fmod(step, 360);
1672        if (step <= -180)
1673            step += 360;
1674        else if (step > 180)
1675            step -= 360;
1676        if (fabs(step) >= fabs(diff)) {
1677            m_SwitchingTo = 0;
1678            step = diff;
1679        }
1680        TurnCave(step);
1681    }
1682
1683    ForceRefresh();
1684}
1685
1686// How much to allow around the box - this is because of the ring shape
1687// at one end of the line.
1688static const int HIGHLIGHTED_PT_SIZE = 2; // FIXME: tie in to blob and ring size
1689#define MARGIN (HIGHLIGHTED_PT_SIZE * 2 + 1)
1690void GfxCore::RefreshLine(const Point *a, const Point *b, const Point *c)
1691{
1692#ifdef __WXMSW__
1693    (void)a;
1694    (void)b;
1695    (void)c;
1696    // FIXME: We get odd redraw artifacts if we just update the line, and
1697    // redrawing the whole scene doesn't actually seem to be measurably
1698    // slower.  That may not be true with software rendering though...
1699    ForceRefresh();
1700#else
1701    // Best of all might be to copy the window contents before we draw the
1702    // line, then replace each time we redraw.
1703
1704    // Calculate the minimum rectangle which includes the old and new
1705    // measuring lines to minimise the redraw time
1706    int l = INT_MAX, r = INT_MIN, u = INT_MIN, d = INT_MAX;
1707    double X, Y, Z;
1708    if (a) {
1709        if (!Transform(*a, &X, &Y, &Z)) {
1710            printf("oops\n");
1711        } else {
1712            int x = int(X);
1713            int y = GetYSize() - 1 - int(Y);
1714            l = x;
1715            r = x;
1716            u = y;
1717            d = y;
1718        }
1719    }
1720    if (b) {
1721        if (!Transform(*b, &X, &Y, &Z)) {
1722            printf("oops\n");
1723        } else {
1724            int x = int(X);
1725            int y = GetYSize() - 1 - int(Y);
1726            l = min(l, x);
1727            r = max(r, x);
1728            u = max(u, y);
1729            d = min(d, y);
1730        }
1731    }
1732    if (c) {
1733        if (!Transform(*c, &X, &Y, &Z)) {
1734            printf("oops\n");
1735        } else {
1736            int x = int(X);
1737            int y = GetYSize() - 1 - int(Y);
1738            l = min(l, x);
1739            r = max(r, x);
1740            u = max(u, y);
1741            d = min(d, y);
1742        }
1743    }
1744    l -= MARGIN;
1745    r += MARGIN;
1746    u += MARGIN;
1747    d -= MARGIN;
1748    RefreshRect(wxRect(l, d, r - l, u - d), false);
1749#endif
1750}
1751
1752void GfxCore::SetHereFromTree(const LabelInfo * p)
1753{
1754    SetHere(p);
1755    m_Parent->ShowInfo(m_here, m_there);
1756}
1757
1758void GfxCore::SetHere(const LabelInfo *p)
1759{
1760    if (p == m_here) return;
1761    bool line_active = MeasuringLineActive();
1762    const LabelInfo * old = m_here;
1763    m_here = p;
1764    if (line_active || MeasuringLineActive())
1765        RefreshLine(old, m_there, m_here);
1766}
1767
1768void GfxCore::SetThere(const LabelInfo * p)
1769{
1770    if (p == m_there) return;
1771    const LabelInfo * old = m_there;
1772    m_there = p;
1773    RefreshLine(m_here, old, m_there);
1774}
1775
1776void GfxCore::CreateHitTestGrid()
1777{
1778    if (!m_PointGrid) {
1779        // Initialise hit-test grid.
1780        m_PointGrid = new list<LabelInfo*>[HITTEST_SIZE * HITTEST_SIZE];
1781    } else {
1782        // Clear hit-test grid.
1783        for (int i = 0; i < HITTEST_SIZE * HITTEST_SIZE; i++) {
1784            m_PointGrid[i].clear();
1785        }
1786    }
1787
1788    // Fill the grid.
1789    list<LabelInfo*>::const_iterator pos = m_Parent->GetLabels();
1790    list<LabelInfo*>::const_iterator end = m_Parent->GetLabelsEnd();
1791    while (pos != end) {
1792        LabelInfo* label = *pos++;
1793
1794        if (!((m_Surface && label->IsSurface()) ||
1795              (m_Legs && label->IsUnderground()) ||
1796              (!label->IsSurface() && !label->IsUnderground()))) {
1797            // if this station isn't to be displayed, skip to the next
1798            // (last case is for stns with no legs attached)
1799            continue;
1800        }
1801
1802        // Calculate screen coordinates.
1803        double cx, cy, cz;
1804        Transform(*label, &cx, &cy, &cz);
1805        if (cx < 0 || cx >= GetXSize()) continue;
1806        if (cy < 0 || cy >= GetYSize()) continue;
1807
1808        cy = GetYSize() - cy;
1809
1810        // On-screen, so add to hit-test grid...
1811        int grid_x = int(cx * HITTEST_SIZE / (GetXSize() + 1));
1812        int grid_y = int(cy * HITTEST_SIZE / (GetYSize() + 1));
1813
1814        m_PointGrid[grid_x + grid_y * HITTEST_SIZE].push_back(label);
1815    }
1816
1817    m_HitTestGridValid = true;
1818}
1819
1820//
1821//  Methods for controlling the orientation of the survey
1822//
1823
1824void GfxCore::TurnCave(Double angle)
1825{
1826    // Turn the cave around its z-axis by a given angle.
1827
1828    m_PanAngle += angle;
1829    // Wrap to range [0, 360):
1830    m_PanAngle = fmod(m_PanAngle, 360.0);
1831    if (m_PanAngle < 0.0) {
1832        m_PanAngle += 360.0;
1833    }
1834
1835    m_HitTestGridValid = false;
1836    if (m_here && m_here == &temp_here) SetHere();
1837
1838    SetRotation(m_PanAngle, m_TiltAngle);
1839}
1840
1841void GfxCore::TurnCaveTo(Double angle)
1842{
1843    if (m_Rotating) {
1844        // If we're rotating, jump to the specified angle.
1845        TurnCave(angle - m_PanAngle);
1846        SetPanBase();
1847        return;
1848    }
1849
1850    int new_switching_to = ((int)angle) / 90 + NORTH;
1851    if (new_switching_to == m_SwitchingTo) {
1852        // A second order to switch takes us there right away
1853        TurnCave(angle - m_PanAngle);
1854        m_SwitchingTo = 0;
1855        ForceRefresh();
1856    } else {
1857        SetPanBase();
1858        m_SwitchingTo = new_switching_to;
1859    }
1860}
1861
1862void GfxCore::TiltCave(Double tilt_angle)
1863{
1864    // Tilt the cave by a given angle.
1865    if (m_TiltAngle + tilt_angle > 90.0) {
1866        m_TiltAngle = 90.0;
1867    } else if (m_TiltAngle + tilt_angle < -90.0) {
1868        m_TiltAngle = -90.0;
1869    } else {
1870        m_TiltAngle += tilt_angle;
1871    }
1872
1873    m_HitTestGridValid = false;
1874    if (m_here && m_here == &temp_here) SetHere();
1875
1876    SetRotation(m_PanAngle, m_TiltAngle);
1877}
1878
1879void GfxCore::TranslateCave(int dx, int dy)
1880{
1881    AddTranslationScreenCoordinates(dx, dy);
1882    m_HitTestGridValid = false;
1883
1884    if (m_here && m_here == &temp_here) SetHere();
1885
1886    ForceRefresh();
1887}
1888
1889void GfxCore::DragFinished()
1890{
1891    m_MouseOutsideCompass = m_MouseOutsideElev = false;
1892    ForceRefresh();
1893}
1894
1895void GfxCore::ClearCoords()
1896{
1897    m_Parent->ClearCoords();
1898}
1899
1900void GfxCore::SetCoords(wxPoint point)
1901{
1902    // We can't work out 2D coordinates from a perspective view, and it
1903    // doesn't really make sense to show coordinates while we're animating.
1904    if (GetPerspective() || Animating()) return;
1905
1906    // Update the coordinate or altitude display, given the (x, y) position in
1907    // window coordinates.  The relevant display is updated depending on
1908    // whether we're in plan or elevation view.
1909
1910    double cx, cy, cz;
1911
1912    SetDataTransform();
1913    ReverseTransform(point.x, GetYSize() - 1 - point.y, &cx, &cy, &cz);
1914
1915    if (ShowingPlan()) {
1916        m_Parent->SetCoords(cx + m_Parent->GetOffset().GetX(),
1917                            cy + m_Parent->GetOffset().GetY(),
1918                            m_there);
1919    } else if (ShowingElevation()) {
1920        m_Parent->SetAltitude(cz + m_Parent->GetOffset().GetZ(),
1921                              m_there);
1922    } else {
1923        m_Parent->ClearCoords();
1924    }
1925}
1926
1927int GfxCore::GetCompassWidth() const
1928{
1929    static int result = 0;
1930    if (result == 0) {
1931        result = INDICATOR_BOX_SIZE;
1932        int width;
1933        const wxString & msg = wmsg(/*Facing*/203);
1934        GetTextExtent(msg, &width, NULL);
1935        if (width > result) result = width;
1936    }
1937    return result;
1938}
1939
1940int GfxCore::GetClinoWidth() const
1941{
1942    static int result = 0;
1943    if (result == 0) {
1944        result = INDICATOR_BOX_SIZE;
1945        int width;
1946        const wxString & msg1 = wmsg(/*Plan*/432);
1947        GetTextExtent(msg1, &width, NULL);
1948        if (width > result) result = width;
1949        const wxString & msg2 = wmsg(/*Kiwi Plan*/433);
1950        GetTextExtent(msg2, &width, NULL);
1951        if (width > result) result = width;
1952        const wxString & msg3 = wmsg(/*Elevation*/118);
1953        GetTextExtent(msg3, &width, NULL);
1954        if (width > result) result = width;
1955    }
1956    return result;
1957}
1958
1959int GfxCore::GetCompassXPosition() const
1960{
1961    // Return the x-coordinate of the centre of the compass in window
1962    // coordinates.
1963    return GetXSize() - INDICATOR_OFFSET_X - GetCompassWidth() / 2;
1964}
1965
1966int GfxCore::GetClinoXPosition() const
1967{
1968    // Return the x-coordinate of the centre of the compass in window
1969    // coordinates.
1970    return GetXSize() - GetClinoOffset() - GetClinoWidth() / 2;
1971}
1972
1973int GfxCore::GetIndicatorYPosition() const
1974{
1975    // Return the y-coordinate of the centre of the indicators in window
1976    // coordinates.
1977    return GetYSize() - INDICATOR_OFFSET_Y - INDICATOR_BOX_SIZE / 2;
1978}
1979
1980int GfxCore::GetIndicatorRadius() const
1981{
1982    // Return the radius of each indicator.
1983    return (INDICATOR_BOX_SIZE - INDICATOR_MARGIN * 2) / 2;
1984}
1985
1986bool GfxCore::PointWithinCompass(wxPoint point) const
1987{
1988    // Determine whether a point (in window coordinates) lies within the
1989    // compass.
1990    if (!ShowingCompass()) return false;
1991
1992    glaCoord dx = point.x - GetCompassXPosition();
1993    glaCoord dy = point.y - GetIndicatorYPosition();
1994    glaCoord radius = GetIndicatorRadius();
1995
1996    return (dx * dx + dy * dy <= radius * radius);
1997}
1998
1999bool GfxCore::PointWithinClino(wxPoint point) const
2000{
2001    // Determine whether a point (in window coordinates) lies within the clino.
2002    if (!ShowingClino()) return false;
2003
2004    glaCoord dx = point.x - GetClinoXPosition();
2005    glaCoord dy = point.y - GetIndicatorYPosition();
2006    glaCoord radius = GetIndicatorRadius();
2007
2008    return (dx * dx + dy * dy <= radius * radius);
2009}
2010
2011bool GfxCore::PointWithinScaleBar(wxPoint point) const
2012{
2013    // Determine whether a point (in window coordinates) lies within the scale
2014    // bar.
2015    if (!ShowingScaleBar()) return false;
2016
2017    return (point.x >= SCALE_BAR_OFFSET_X &&
2018            point.x <= SCALE_BAR_OFFSET_X + m_ScaleBarWidth &&
2019            point.y <= GetYSize() - SCALE_BAR_OFFSET_Y - SCALE_BAR_HEIGHT &&
2020            point.y >= GetYSize() - SCALE_BAR_OFFSET_Y - SCALE_BAR_HEIGHT*2);
2021}
2022
2023bool GfxCore::PointWithinColourKey(wxPoint point) const
2024{
2025    // Determine whether a point (in window coordinates) lies within the key.
2026    point.x -= GetXSize() - KEY_OFFSET_X;
2027    point.y = KEY_OFFSET_Y - point.y;
2028    return (point.x >= key_lowerleft[m_ColourBy].x && point.x <= 0 &&
2029            point.y >= key_lowerleft[m_ColourBy].y && point.y <= 0);
2030}
2031
2032void GfxCore::SetCompassFromPoint(wxPoint point)
2033{
2034    // Given a point in window coordinates, set the heading of the survey.  If
2035    // the point is outside the compass, it snaps to 45 degree intervals;
2036    // otherwise it operates as normal.
2037
2038    wxCoord dx = point.x - GetCompassXPosition();
2039    wxCoord dy = point.y - GetIndicatorYPosition();
2040    wxCoord radius = GetIndicatorRadius();
2041
2042    double angle = deg(atan2(double(dx), double(dy))) - 180.0;
2043    if (dx * dx + dy * dy <= radius * radius) {
2044        TurnCave(angle - m_PanAngle);
2045        m_MouseOutsideCompass = false;
2046    } else {
2047        TurnCave(int(angle / 45.0) * 45.0 - m_PanAngle);
2048        m_MouseOutsideCompass = true;
2049    }
2050
2051    ForceRefresh();
2052}
2053
2054void GfxCore::SetClinoFromPoint(wxPoint point)
2055{
2056    // Given a point in window coordinates, set the elevation of the survey.
2057    // If the point is outside the clino, it snaps to 90 degree intervals;
2058    // otherwise it operates as normal.
2059
2060    glaCoord dx = point.x - GetClinoXPosition();
2061    glaCoord dy = point.y - GetIndicatorYPosition();
2062    glaCoord radius = GetIndicatorRadius();
2063
2064    if (dx >= 0 && dx * dx + dy * dy <= radius * radius) {
2065        TiltCave(-deg(atan2(double(dy), double(dx))) - m_TiltAngle);
2066        m_MouseOutsideElev = false;
2067    } else if (dy >= INDICATOR_MARGIN) {
2068        TiltCave(-90.0 - m_TiltAngle);
2069        m_MouseOutsideElev = true;
2070    } else if (dy <= -INDICATOR_MARGIN) {
2071        TiltCave(90.0 - m_TiltAngle);
2072        m_MouseOutsideElev = true;
2073    } else {
2074        TiltCave(-m_TiltAngle);
2075        m_MouseOutsideElev = true;
2076    }
2077
2078    ForceRefresh();
2079}
2080
2081void GfxCore::SetScaleBarFromOffset(wxCoord dx)
2082{
2083    // Set the scale of the survey, given an offset as to how much the mouse has
2084    // been dragged over the scalebar since the last scale change.
2085
2086    SetScale((m_ScaleBarWidth + dx) * m_Scale / m_ScaleBarWidth);
2087    ForceRefresh();
2088}
2089
2090void GfxCore::RedrawIndicators()
2091{
2092    // Redraw the compass and clino indicators.
2093
2094    int total_width = GetCompassWidth() + INDICATOR_GAP + GetClinoWidth();
2095    RefreshRect(wxRect(GetXSize() - INDICATOR_OFFSET_X - total_width,
2096                       GetYSize() - INDICATOR_OFFSET_Y - INDICATOR_BOX_SIZE,
2097                       total_width,
2098                       INDICATOR_BOX_SIZE), false);
2099}
2100
2101void GfxCore::StartRotation()
2102{
2103    // Start the survey rotating.
2104
2105    if (m_SwitchingTo >= NORTH)
2106        m_SwitchingTo = 0;
2107    m_Rotating = true;
2108    SetPanBase();
2109}
2110
2111void GfxCore::ToggleRotation()
2112{
2113    // Toggle the survey rotation on/off.
2114
2115    if (m_Rotating) {
2116        StopRotation();
2117    } else {
2118        StartRotation();
2119    }
2120}
2121
2122void GfxCore::StopRotation()
2123{
2124    // Stop the survey rotating.
2125
2126    m_Rotating = false;
2127    ForceRefresh();
2128}
2129
2130bool GfxCore::IsExtendedElevation() const
2131{
2132    return m_Parent->IsExtendedElevation();
2133}
2134
2135void GfxCore::ReverseRotation()
2136{
2137    // Reverse the direction of rotation.
2138
2139    m_RotationStep = -m_RotationStep;
2140    if (m_Rotating)
2141        SetPanBase();
2142}
2143
2144void GfxCore::RotateSlower(bool accel)
2145{
2146    // Decrease the speed of rotation, optionally by an increased amount.
2147    if (fabs(m_RotationStep) == 1.0)
2148        return;
2149
2150    m_RotationStep *= accel ? (1 / 1.44) : (1 / 1.2);
2151
2152    if (fabs(m_RotationStep) < 1.0) {
2153        m_RotationStep = (m_RotationStep > 0 ? 1.0 : -1.0);
2154    }
2155    if (m_Rotating)
2156        SetPanBase();
2157}
2158
2159void GfxCore::RotateFaster(bool accel)
2160{
2161    // Increase the speed of rotation, optionally by an increased amount.
2162    if (fabs(m_RotationStep) == 180.0)
2163        return;
2164
2165    m_RotationStep *= accel ? 1.44 : 1.2;
2166    if (fabs(m_RotationStep) > 180.0) {
2167        m_RotationStep = (m_RotationStep > 0 ? 180.0 : -180.0);
2168    }
2169    if (m_Rotating)
2170        SetPanBase();
2171}
2172
2173void GfxCore::SwitchToElevation()
2174{
2175    // Perform an animated switch to elevation view.
2176
2177    if (m_SwitchingTo != ELEVATION) {
2178        SetTiltBase();
2179        m_SwitchingTo = ELEVATION;
2180    } else {
2181        // A second order to switch takes us there right away
2182        TiltCave(-m_TiltAngle);
2183        m_SwitchingTo = 0;
2184        ForceRefresh();
2185    }
2186}
2187
2188void GfxCore::SwitchToPlan()
2189{
2190    // Perform an animated switch to plan view.
2191
2192    if (m_SwitchingTo != PLAN) {
2193        SetTiltBase();
2194        m_SwitchingTo = PLAN;
2195    } else {
2196        // A second order to switch takes us there right away
2197        TiltCave(-90.0 - m_TiltAngle);
2198        m_SwitchingTo = 0;
2199        ForceRefresh();
2200    }
2201}
2202
2203void GfxCore::SetViewTo(Double xmin, Double xmax, Double ymin, Double ymax, Double zmin, Double zmax)
2204{
2205
2206    SetTranslation(-Vector3((xmin + xmax) / 2, (ymin + ymax) / 2, (zmin + zmax) / 2));
2207    Double scale = HUGE_VAL;
2208    const Vector3 ext = m_Parent->GetExtent();
2209    if (xmax > xmin) {
2210        Double s = ext.GetX() / (xmax - xmin);
2211        if (s < scale) scale = s;
2212    }
2213    if (ymax > ymin) {
2214        Double s = ext.GetY() / (ymax - ymin);
2215        if (s < scale) scale = s;
2216    }
2217    if (!ShowingPlan() && zmax > zmin) {
2218        Double s = ext.GetZ() / (zmax - zmin);
2219        if (s < scale) scale = s;
2220    }
2221    if (scale != HUGE_VAL) SetScale(scale);
2222    ForceRefresh();
2223}
2224
2225bool GfxCore::CanRaiseViewpoint() const
2226{
2227    // Determine if the survey can be viewed from a higher angle of elevation.
2228
2229    return GetPerspective() ? (m_TiltAngle < 90.0) : (m_TiltAngle > -90.0);
2230}
2231
2232bool GfxCore::CanLowerViewpoint() const
2233{
2234    // Determine if the survey can be viewed from a lower angle of elevation.
2235
2236    return GetPerspective() ? (m_TiltAngle > -90.0) : (m_TiltAngle < 90.0);
2237}
2238
2239bool GfxCore::HasDepth() const
2240{
2241    return m_Parent->GetDepthExtent() == 0.0;
2242}
2243
2244bool GfxCore::HasErrorInformation() const
2245{
2246    return m_Parent->HasErrorInformation();
2247}
2248
2249bool GfxCore::HasDateInformation() const
2250{
2251    return m_Parent->GetDateMin() >= 0;
2252}
2253
2254bool GfxCore::ShowingPlan() const
2255{
2256    // Determine if the survey is in plan view.
2257
2258    return (m_TiltAngle == -90.0);
2259}
2260
2261bool GfxCore::ShowingElevation() const
2262{
2263    // Determine if the survey is in elevation view.
2264
2265    return (m_TiltAngle == 0.0);
2266}
2267
2268bool GfxCore::ShowingMeasuringLine() const
2269{
2270    // Determine if the measuring line is being shown.  Only check if "there"
2271    // is valid, since that means the measuring line anchor is out.
2272
2273    return m_there;
2274}
2275
2276void GfxCore::ToggleFlag(bool* flag, int update)
2277{
2278    *flag = !*flag;
2279    if (update == UPDATE_BLOBS) {
2280        UpdateBlobs();
2281    } else if (update == UPDATE_BLOBS_AND_CROSSES) {
2282        UpdateBlobs();
2283        InvalidateList(LIST_CROSSES);
2284        m_HitTestGridValid = false;
2285    }
2286    ForceRefresh();
2287}
2288
2289int GfxCore::GetNumEntrances() const
2290{
2291    return m_Parent->GetNumEntrances();
2292}
2293
2294int GfxCore::GetNumFixedPts() const
2295{
2296    return m_Parent->GetNumFixedPts();
2297}
2298
2299int GfxCore::GetNumExportedPts() const
2300{
2301    return m_Parent->GetNumExportedPts();
2302}
2303
2304void GfxCore::ToggleTerrain()
2305{
2306    if (!m_Terrain && !dem) {
2307        // OnOpenTerrain() calls us if a file is selected.
2308        wxCommandEvent dummy;
2309        m_Parent->OnOpenTerrain(dummy);
2310        return;
2311    }
2312    ToggleFlag(&m_Terrain);
2313}
2314
2315void GfxCore::ToggleFatFinger()
2316{
2317    if (sqrd_measure_threshold == sqrd(MEASURE_THRESHOLD)) {
2318        sqrd_measure_threshold = sqrd(5 * MEASURE_THRESHOLD);
2319        wxMessageBox(wxT("Fat finger enabled"), wxT("Aven Debug"), wxOK | wxICON_INFORMATION);
2320    } else {
2321        sqrd_measure_threshold = sqrd(MEASURE_THRESHOLD);
2322        wxMessageBox(wxT("Fat finger disabled"), wxT("Aven Debug"), wxOK | wxICON_INFORMATION);
2323    }
2324}
2325
2326void GfxCore::ClearTreeSelection()
2327{
2328    m_Parent->ClearTreeSelection();
2329}
2330
2331void GfxCore::CentreOn(const Point &p)
2332{
2333    SetTranslation(-p);
2334    m_HitTestGridValid = false;
2335
2336    ForceRefresh();
2337}
2338
2339void GfxCore::ForceRefresh()
2340{
2341    Refresh(false);
2342}
2343
2344void GfxCore::GenerateList(unsigned int l)
2345{
2346    assert(m_HaveData);
2347
2348    switch (l) {
2349        case LIST_COMPASS:
2350            DrawCompass();
2351            break;
2352        case LIST_CLINO:
2353            DrawClino();
2354            break;
2355        case LIST_CLINO_BACK:
2356            DrawClinoBack();
2357            break;
2358        case LIST_SCALE_BAR:
2359            DrawScaleBar();
2360            break;
2361        case LIST_DEPTH_KEY:
2362            DrawDepthKey();
2363            break;
2364        case LIST_DATE_KEY:
2365            DrawDateKey();
2366            break;
2367        case LIST_ERROR_KEY:
2368            DrawErrorKey();
2369            break;
2370        case LIST_GRADIENT_KEY:
2371            DrawGradientKey();
2372            break;
2373        case LIST_LENGTH_KEY:
2374            DrawLengthKey();
2375            break;
2376        case LIST_UNDERGROUND_LEGS:
2377            GenerateDisplayList(false);
2378            break;
2379        case LIST_TUBES:
2380            GenerateDisplayListTubes();
2381            break;
2382        case LIST_SURFACE_LEGS:
2383            GenerateDisplayList(true);
2384            break;
2385        case LIST_BLOBS:
2386            GenerateBlobsDisplayList();
2387            break;
2388        case LIST_CROSSES: {
2389            BeginCrosses();
2390            SetColour(col_LIGHT_GREY);
2391            list<LabelInfo*>::const_iterator pos = m_Parent->GetLabels();
2392            while (pos != m_Parent->GetLabelsEnd()) {
2393                const LabelInfo* label = *pos++;
2394
2395                if ((m_Surface && label->IsSurface()) ||
2396                    (m_Legs && label->IsUnderground()) ||
2397                    (!label->IsSurface() && !label->IsUnderground())) {
2398                    // Check if this station should be displayed
2399                    // (last case is for stns with no legs attached)
2400                    DrawCross(label->GetX(), label->GetY(), label->GetZ());
2401                }
2402            }
2403            EndCrosses();
2404            break;
2405        }
2406        case LIST_GRID:
2407            DrawGrid();
2408            break;
2409        case LIST_SHADOW:
2410            GenerateDisplayListShadow();
2411            break;
2412        case LIST_TERRAIN:
2413            DrawTerrain();
2414            break;
2415        default:
2416            assert(false);
2417            break;
2418    }
2419}
2420
2421void GfxCore::ToggleSmoothShading()
2422{
2423    GLACanvas::ToggleSmoothShading();
2424    InvalidateList(LIST_TUBES);
2425    ForceRefresh();
2426}
2427
2428void GfxCore::GenerateDisplayList(bool surface)
2429{
2430    unsigned surf_or_not = surface ? img_FLAG_SURFACE : 0;
2431    // Generate the display list for the surface or underground legs.
2432    for (int f = 0; f != 8; ++f) {
2433        if ((f & img_FLAG_SURFACE) != surf_or_not) continue;
2434        const unsigned SHOW_DASHED_AND_FADED = unsigned(-1);
2435        unsigned style = SHOW_NORMAL;
2436        if ((f & img_FLAG_SPLAY) && m_Splays != SHOW_NORMAL) {
2437            style = m_Splays;
2438        } else if (f & img_FLAG_DUPLICATE) {
2439            style = m_Dupes;
2440        }
2441        if (f & img_FLAG_SURFACE) {
2442            if (style == SHOW_FADED) {
2443                style = SHOW_DASHED_AND_FADED;
2444            } else {
2445                style = SHOW_DASHED;
2446            }
2447        }
2448
2449        switch (style) {
2450            case SHOW_HIDE:
2451                continue;
2452            case SHOW_FADED:
2453                SetAlpha(0.4);
2454                break;
2455            case SHOW_DASHED:
2456                EnableDashedLines();
2457                break;
2458            case SHOW_DASHED_AND_FADED:
2459                SetAlpha(0.4);
2460                EnableDashedLines();
2461                break;
2462        }
2463
2464        void (GfxCore::* add_poly)(const traverse&);
2465        if (surface) {
2466            if (m_ColourBy == COLOUR_BY_ERROR) {
2467                add_poly = &GfxCore::AddPolylineError;
2468            } else {
2469                add_poly = &GfxCore::AddPolyline;
2470            }
2471        } else {
2472            add_poly = AddPoly;
2473        }
2474
2475        list<traverse>::const_iterator trav = m_Parent->traverses_begin(f);
2476        list<traverse>::const_iterator tend = m_Parent->traverses_end(f);
2477        while (trav != tend) {
2478            (this->*add_poly)(*trav);
2479            ++trav;
2480        }
2481
2482        switch (style) {
2483            case SHOW_FADED:
2484                SetAlpha(1.0);
2485                break;
2486            case SHOW_DASHED:
2487                DisableDashedLines();
2488                break;
2489            case SHOW_DASHED_AND_FADED:
2490                DisableDashedLines();
2491                SetAlpha(1.0);
2492                break;
2493        }
2494    }
2495}
2496
2497void GfxCore::GenerateDisplayListTubes()
2498{
2499    // Generate the display list for the tubes.
2500    list<vector<XSect> >::iterator trav = m_Parent->tubes_begin();
2501    list<vector<XSect> >::iterator tend = m_Parent->tubes_end();
2502    while (trav != tend) {
2503        SkinPassage(*trav);
2504        ++trav;
2505    }
2506}
2507
2508void GfxCore::GenerateDisplayListShadow()
2509{
2510    SetColour(col_BLACK);
2511    for (int f = 0; f != 8; ++f) {
2512        // Only include underground legs in the shadow.
2513        if ((f & img_FLAG_SURFACE) != 0) continue;
2514        list<traverse>::const_iterator trav = m_Parent->traverses_begin(f);
2515        list<traverse>::const_iterator tend = m_Parent->traverses_end(f);
2516        while (trav != tend) {
2517            AddPolylineShadow(*trav);
2518            ++trav;
2519        }
2520    }
2521}
2522
2523void
2524GfxCore::parse_hgt_filename(const wxString & lc_name)
2525{
2526    char * leaf = leaf_from_fnm(lc_name.utf8_str());
2527    const char * p = leaf;
2528    char * q;
2529    char dirn = *p++;
2530    o_y = strtoul(p, &q, 10);
2531    p = q;
2532    if (dirn == 's')
2533        o_y = -o_y;
2534    ++o_y;
2535    dirn = *p++;
2536    o_x = strtoul(p, &q, 10);
2537    if (dirn == 'w')
2538        o_x = -o_x;
2539    bigendian = true;
2540    nodata_value = -32768;
2541    osfree(leaf);
2542}
2543
2544size_t
2545GfxCore::parse_hdr(wxInputStream & is, unsigned long & skipbytes)
2546{
2547    // ESRI docs say NBITS defaults to 8.
2548    unsigned long nbits = 8;
2549    // ESRI docs say NBANDS defaults to 1.
2550    unsigned long nbands = 1;
2551    unsigned long bandrowbytes = 0;
2552    unsigned long totalrowbytes = 0;
2553    // ESRI docs say ULXMAP defaults to 0.
2554    o_x = 0.0;
2555    // ESRI docs say ULYMAP defaults to NROWS - 1.
2556    o_y = HUGE_VAL;
2557    // ESRI docs say XDIM and YDIM default to 1.
2558    step_x = step_y = 1.0;
2559    while (!is.Eof()) {
2560        wxString line;
2561        int ch;
2562        while ((ch = is.GetC()) != wxEOF) {
2563            if (ch == '\n' || ch == '\r') break;
2564            line += wxChar(ch);
2565        }
2566#define CHECK(X, COND) \
2567} else if (line.StartsWith(wxT(X " "))) { \
2568size_t v = line.find_first_not_of(wxT(' '), sizeof(X)); \
2569if (v == line.npos || !(COND)) { \
2570err += wxT("Unexpected value for " X); \
2571}
2572        wxString err;
2573        if (false) {
2574        // I = little-endian; M = big-endian
2575        CHECK("BYTEORDER", (bigendian = (line[v] == 'M')) || line[v] == 'I')
2576        // ESRI docs say LAYOUT defaults to BIL if not specified.
2577        CHECK("LAYOUT", line.substr(v) == wxT("BIL"))
2578        CHECK("NROWS", line.substr(v).ToCULong(&dem_height))
2579        CHECK("NCOLS", line.substr(v).ToCULong(&dem_width))
2580        // ESRI docs say NBANDS defaults to 1 if not specified.
2581        CHECK("NBANDS", line.substr(v).ToCULong(&nbands) && nbands == 1)
2582        CHECK("NBITS", line.substr(v).ToCULong(&nbits) && nbits == 16)
2583        CHECK("BANDROWBYTES", line.substr(v).ToCULong(&bandrowbytes))
2584        CHECK("TOTALROWBYTES", line.substr(v).ToCULong(&totalrowbytes))
2585        // PIXELTYPE is a GDAL extension, so may not be present.
2586        CHECK("PIXELTYPE", line.substr(v) == wxT("SIGNEDINT"))
2587        CHECK("ULXMAP", line.substr(v).ToCDouble(&o_x))
2588        CHECK("ULYMAP", line.substr(v).ToCDouble(&o_y))
2589        CHECK("XDIM", line.substr(v).ToCDouble(&step_x))
2590        CHECK("YDIM", line.substr(v).ToCDouble(&step_y))
2591        CHECK("NODATA", line.substr(v).ToCLong(&nodata_value))
2592        CHECK("SKIPBYTES", line.substr(v).ToCULong(&skipbytes))
2593        }
2594        if (!err.empty()) {
2595            wxMessageBox(err);
2596        }
2597    }
2598    if (o_y == HUGE_VAL) {
2599        o_y = dem_height - 1;
2600    }
2601    if (bandrowbytes != 0) {
2602        if (nbits * dem_width != bandrowbytes * 8) {
2603            wxMessageBox("BANDROWBYTES setting indicates unused bits after each band - not currently supported");
2604        }
2605    }
2606    if (totalrowbytes != 0) {
2607        // This is the ESRI default for BIL, for BIP it would be
2608        // nbands * bandrowbytes.
2609        if (nbands * nbits * dem_width != totalrowbytes * 8) {
2610            wxMessageBox("TOTALROWBYTES setting indicates unused bits after "
2611                         "each row - not currently supported");
2612        }
2613    }
2614    return ((nbits * dem_width + 7) / 8) * dem_height;
2615}
2616
2617bool
2618GfxCore::read_bil(wxInputStream & is, size_t size, unsigned long skipbytes)
2619{
2620    bool know_size = true;
2621    if (!size) {
2622        // If the stream doesn't know its size, GetSize() returns 0.
2623        size = is.GetSize();
2624        if (!size) {
2625            size = DEFAULT_HGT_SIZE;
2626            know_size = false;
2627        }
2628    }
2629    dem = new unsigned short[size / 2];
2630    if (skipbytes) {
2631        if (is.SeekI(skipbytes, wxFromStart) == ::wxInvalidOffset) {
2632            while (skipbytes) {
2633                unsigned long to_read = skipbytes;
2634                if (size < to_read) to_read = size;
2635                is.Read(reinterpret_cast<char *>(dem), to_read);
2636                size_t c = is.LastRead();
2637                if (c == 0) {
2638                    wxMessageBox(wxT("Failed to skip terrain data header"));
2639                    break;
2640                }
2641                skipbytes -= c;
2642            }
2643        }
2644    }
2645
2646#if wxCHECK_VERSION(2,9,5)
2647    if (!is.ReadAll(dem, size)) {
2648        if (know_size) {
2649            // FIXME: On __WXMSW__ currently we fail to
2650            // read any data from files in zips.
2651            delete [] dem;
2652            dem = NULL;
2653            wxMessageBox(wxT("Failed to read terrain data"));
2654            return false;
2655        }
2656        size = is.LastRead();
2657    }
2658#else
2659    char * p = reinterpret_cast<char *>(dem);
2660    while (size) {
2661        is.Read(p, size);
2662        size_t c = is.LastRead();
2663        if (c == 0) {
2664            if (!know_size) {
2665                size = DEFAULT_HGT_SIZE - size;
2666                if (size)
2667                    break;
2668            }
2669            delete [] dem;
2670            dem = NULL;
2671            wxMessageBox(wxT("Failed to read terrain data"));
2672            return false;
2673        }
2674        p += c;
2675        size -= c;
2676    }
2677#endif
2678
2679    if (dem_width == 0 && dem_height == 0) {
2680        dem_width = dem_height = sqrt(size / 2);
2681        if (dem_width * dem_height * 2 != size) {
2682            delete [] dem;
2683            dem = NULL;
2684            wxMessageBox(wxT("HGT format data doesn't form a square"));
2685            return false;
2686        }
2687        step_x = step_y = 1.0 / dem_width;
2688    }
2689
2690    return true;
2691}
2692
2693bool GfxCore::LoadDEM(const wxString & file)
2694{
2695    if (m_Parent->m_cs_proj.empty()) {
2696        wxMessageBox(wxT("No coordinate system specified in survey data"));
2697        return false;
2698    }
2699
2700    delete [] dem;
2701    dem = NULL;
2702
2703    size_t size = 0;
2704    // Default is to not skip any bytes.
2705    unsigned long skipbytes = 0;
2706    // For .hgt files, default to using filesize to determine.
2707    dem_width = dem_height = 0;
2708    // ESRI say "The default byte order is the same as that of the host machine
2709    // executing the software", but that's stupid so we default to
2710    // little-endian.
2711    bigendian = false;
2712
2713    wxFileInputStream fs(file);
2714    if (!fs.IsOk()) {
2715        wxMessageBox(wxT("Failed to open DEM file"));
2716        return false;
2717    }
2718
2719    const wxString & lc_file = file.Lower();
2720    if (lc_file.EndsWith(wxT(".hgt"))) {
2721        parse_hgt_filename(lc_file);
2722        read_bil(fs, size, skipbytes);
2723    } else if (lc_file.EndsWith(wxT(".bil"))) {
2724        wxString hdr_file = file;
2725        hdr_file.replace(file.size() - 4, 4, wxT(".hdr"));
2726        wxFileInputStream hdr_is(hdr_file);
2727        if (!hdr_is.IsOk()) {
2728            wxMessageBox(wxT("Failed to open HDR file '") + hdr_file + wxT("'"));
2729            return false;
2730        }
2731        size = parse_hdr(hdr_is, skipbytes);
2732        read_bil(fs, size, skipbytes);
2733    } else if (lc_file.EndsWith(wxT(".zip"))) {
2734        wxZipEntry * ze_data = NULL;
2735        wxZipInputStream zs(fs);
2736        wxZipEntry * ze;
2737        while ((ze = zs.GetNextEntry()) != NULL) {
2738            if (!ze->IsDir()) {
2739                const wxString & lc_name = ze->GetName().Lower();
2740                if (!ze_data && lc_name.EndsWith(wxT(".hgt"))) {
2741                    // SRTM .hgt files are raw binary data, with the filename
2742                    // encoding the coordinates.
2743                    parse_hgt_filename(lc_name);
2744                    read_bil(zs, size, skipbytes);
2745                    delete ze;
2746                    break;
2747                }
2748
2749                if (!ze_data && lc_name.EndsWith(wxT(".bil"))) {
2750                    if (size) {
2751                        read_bil(zs, size, skipbytes);
2752                        break;
2753                    }
2754                    ze_data = ze;
2755                    continue;
2756                }
2757
2758                if (lc_name.EndsWith(wxT(".hdr"))) {
2759                    size = parse_hdr(zs, skipbytes);
2760                    if (ze_data) {
2761                        if (!zs.OpenEntry(*ze_data)) {
2762                            wxMessageBox(wxT("Couldn't read DEM data from .zip file"));
2763                            break;
2764                        }
2765                        read_bil(zs, size, skipbytes);
2766                    }
2767                } else if (lc_name.EndsWith(wxT(".prj"))) {
2768                    //FIXME: check this matches the datum string we use
2769                    //Projection    GEOGRAPHIC
2770                    //Datum         WGS84
2771                    //Zunits        METERS
2772                    //Units         DD
2773                    //Spheroid      WGS84
2774                    //Xshift        0.0000000000
2775                    //Yshift        0.0000000000
2776                    //Parameters
2777                }
2778            }
2779            delete ze;
2780        }
2781        delete ze_data;
2782    }
2783
2784    if (!dem) {
2785        return false;
2786    }
2787
2788    InvalidateList(LIST_TERRAIN);
2789    ForceRefresh();
2790    return true;
2791}
2792
2793void GfxCore::DrawTerrainTriangle(const Vector3 & a, const Vector3 & b, const Vector3 & c)
2794{
2795    Vector3 n = (b - a) * (c - a);
2796    n.normalise();
2797    Double factor = dot(n, light) * .95 + .05;
2798    SetColour(col_WHITE, factor);
2799    PlaceVertex(a);
2800    PlaceVertex(b);
2801    PlaceVertex(c);
2802    ++n_tris;
2803}
2804
2805// Like wxBusyCursor, but you can cancel it early.
2806class AvenBusyCursor {
2807    bool active;
2808
2809  public:
2810    AvenBusyCursor() : active(true) {
2811        wxBeginBusyCursor();
2812    }
2813
2814    void stop() {
2815        if (active) {
2816            active = false;
2817            wxEndBusyCursor();
2818        }
2819    }
2820
2821    ~AvenBusyCursor() {
2822        stop();
2823    }
2824};
2825
2826void GfxCore::DrawTerrain()
2827{
2828    if (!dem) return;
2829
2830    AvenBusyCursor hourglass;
2831
2832    // Draw terrain to twice the extent, or at least 1km.
2833    double r_sqrd = sqrd(max(m_Parent->GetExtent().magnitude(), 1000.0));
2834#define WGS84_DATUM_STRING "+proj=longlat +ellps=WGS84 +datum=WGS84"
2835    static projPJ pj_in = pj_init_plus(WGS84_DATUM_STRING);
2836    if (!pj_in) {
2837        ToggleTerrain();
2838        delete [] dem;
2839        dem = NULL;
2840        hourglass.stop();
2841        error(/*Failed to initialise input coordinate system “%s”*/287, WGS84_DATUM_STRING);
2842        return;
2843    }
2844    static projPJ pj_out = pj_init_plus(m_Parent->m_cs_proj.c_str());
2845    if (!pj_out) {
2846        ToggleTerrain();
2847        delete [] dem;
2848        dem = NULL;
2849        hourglass.stop();
2850        error(/*Failed to initialise output coordinate system “%s”*/288, (const char *)m_Parent->m_cs_proj.c_str());
2851        return;
2852    }
2853    n_tris = 0;
2854    SetAlpha(0.3);
2855    BeginTriangles();
2856    const Vector3 & off = m_Parent->GetOffset();
2857    vector<Vector3> prevcol(dem_height + 1);
2858    for (size_t x = 0; x < dem_width; ++x) {
2859        double X_ = (o_x + x * step_x) * DEG_TO_RAD;
2860        Vector3 prev;
2861        for (size_t y = 0; y < dem_height; ++y) {
2862            unsigned short elev = dem[x + y * dem_width];
2863#ifdef WORDS_BIGENDIAN
2864            const bool MACHINE_BIGENDIAN = true;
2865#else
2866            const bool MACHINE_BIGENDIAN = false;
2867#endif
2868            if (bigendian != MACHINE_BIGENDIAN) {
2869#if defined __GNUC__ && (__GNUC__ * 100 + __GNUC_MINOR__ >= 408)
2870                elev = __builtin_bswap16(elev);
2871#else
2872                elev = (elev >> 8) | (elev << 8);
2873#endif
2874            }
2875            double Z = (short)elev;
2876            Vector3 pt;
2877            if (Z == nodata_value) {
2878                pt = Vector3(DBL_MAX, DBL_MAX, DBL_MAX);
2879            } else {
2880                double X = X_;
2881                double Y = (o_y - y * step_y) * DEG_TO_RAD;
2882                pj_transform(pj_in, pj_out, 1, 1, &X, &Y, &Z);
2883                pt = Vector3(X, Y, Z) - off;
2884                double dist_2 = sqrd(pt.GetX()) + sqrd(pt.GetY());
2885                if (dist_2 > r_sqrd) {
2886                    pt = Vector3(DBL_MAX, DBL_MAX, DBL_MAX);
2887                }
2888            }
2889            if (x > 0 && y > 0) {
2890                const Vector3 & a = prevcol[y - 1];
2891                const Vector3 & b = prevcol[y];
2892                // If all points are valid, split the quadrilateral into
2893                // triangles along the shorter 3D diagonal, which typically
2894                // looks better:
2895                //
2896                //               ----->
2897                //     prev---a    x     prev---a
2898                //   |   |P  /|            |\  S|
2899                // y |   |  / |    or      | \  |
2900                //   V   | /  |            |  \ |
2901                //       |/  Q|            |R  \|
2902                //       b----pt           b----pt
2903                //
2904                //       FORWARD           BACKWARD
2905                enum { NONE = 0, P = 1, Q = 2, R = 4, S = 8, ALL = P|Q|R|S };
2906                int valid =
2907                    ((prev.GetZ() != DBL_MAX)) |
2908                    ((a.GetZ() != DBL_MAX) << 1) |
2909                    ((b.GetZ() != DBL_MAX) << 2) |
2910                    ((pt.GetZ() != DBL_MAX) << 3);
2911                static const int tris_map[16] = {
2912                    NONE, // nothing valid
2913                    NONE, // prev
2914                    NONE, // a
2915                    NONE, // a, prev
2916                    NONE, // b
2917                    NONE, // b, prev
2918                    NONE, // b, a
2919                    P, // b, a, prev
2920                    NONE, // pt
2921                    NONE, // pt, prev
2922                    NONE, // pt, a
2923                    S, // pt, a, prev
2924                    NONE, // pt, b
2925                    R, // pt, b, prev
2926                    Q, // pt, b, a
2927                    ALL, // pt, b, a, prev
2928                };
2929                int tris = tris_map[valid];
2930                if (tris == ALL) {
2931                    // All points valid.
2932                    if ((a - b).magnitude() < (prev - pt).magnitude()) {
2933                        tris = P | Q;
2934                    } else {
2935                        tris = R | S;
2936                    }
2937                }
2938                if (tris & P)
2939                    DrawTerrainTriangle(a, prev, b);
2940                if (tris & Q)
2941                    DrawTerrainTriangle(a, b, pt);
2942                if (tris & R)
2943                    DrawTerrainTriangle(pt, prev, b);
2944                if (tris & S)
2945                    DrawTerrainTriangle(a, prev, pt);
2946            }
2947            prev = prevcol[y];
2948            prevcol[y].assign(pt);
2949        }
2950    }
2951    EndTriangles();
2952    SetAlpha(1.0);
2953    if (n_tris == 0) {
2954        ToggleTerrain();
2955        delete [] dem;
2956        dem = NULL;
2957        hourglass.stop();
2958        /* TRANSLATORS: Aven shows a circle of terrain covering the area
2959         * of the survey plus a bit, but the terrain data file didn't
2960         * contain any data inside that circle.
2961         */
2962        error(/*No terrain data near area of survey*/161);
2963    }
2964}
2965
2966// Plot blobs.
2967void GfxCore::GenerateBlobsDisplayList()
2968{
2969    if (!(m_Entrances || m_FixedPts || m_ExportedPts ||
2970          m_Parent->GetNumHighlightedPts()))
2971        return;
2972
2973    // Plot blobs.
2974    gla_colour prev_col = col_BLACK; // not a colour used for blobs
2975    list<LabelInfo*>::const_iterator pos = m_Parent->GetLabels();
2976    BeginBlobs();
2977    while (pos != m_Parent->GetLabelsEnd()) {
2978        const LabelInfo* label = *pos++;
2979
2980        // When more than one flag is set on a point:
2981        // search results take priority over entrance highlighting
2982        // which takes priority over fixed point
2983        // highlighting, which in turn takes priority over exported
2984        // point highlighting.
2985
2986        if (!((m_Surface && label->IsSurface()) ||
2987              (m_Legs && label->IsUnderground()) ||
2988              (!label->IsSurface() && !label->IsUnderground()))) {
2989            // if this station isn't to be displayed, skip to the next
2990            // (last case is for stns with no legs attached)
2991            continue;
2992        }
2993
2994        gla_colour col;
2995
2996        if (label->IsHighLighted()) {
2997            col = col_YELLOW;
2998        } else if (m_Entrances && label->IsEntrance()) {
2999            col = col_GREEN;
3000        } else if (m_FixedPts && label->IsFixedPt()) {
3001            col = col_RED;
3002        } else if (m_ExportedPts && label->IsExportedPt()) {
3003            col = col_TURQUOISE;
3004        } else {
3005            continue;
3006        }
3007
3008        // Stations are sorted by blob type, so colour changes are infrequent.
3009        if (col != prev_col) {
3010            SetColour(col);
3011            prev_col = col;
3012        }
3013        DrawBlob(label->GetX(), label->GetY(), label->GetZ());
3014    }
3015    EndBlobs();
3016}
3017
3018void GfxCore::DrawIndicators()
3019{
3020    // Draw colour key.
3021    if (m_ColourKey) {
3022        drawing_list key_list = LIST_LIMIT_;
3023        switch (m_ColourBy) {
3024            case COLOUR_BY_DEPTH:
3025                key_list = LIST_DEPTH_KEY; break;
3026            case COLOUR_BY_DATE:
3027                key_list = LIST_DATE_KEY; break;
3028            case COLOUR_BY_ERROR:
3029                key_list = LIST_ERROR_KEY; break;
3030            case COLOUR_BY_GRADIENT:
3031                key_list = LIST_GRADIENT_KEY; break;
3032            case COLOUR_BY_LENGTH:
3033                key_list = LIST_LENGTH_KEY; break;
3034        }
3035        if (key_list != LIST_LIMIT_) {
3036            DrawList2D(key_list, GetXSize() - KEY_OFFSET_X,
3037                       GetYSize() - KEY_OFFSET_Y, 0);
3038        }
3039    }
3040
3041    // Draw compass or elevation/heading indicators.
3042    if (m_Compass || m_Clino) {
3043        if (!m_Parent->IsExtendedElevation()) Draw2dIndicators();
3044    }
3045
3046    // Draw scalebar.
3047    if (m_Scalebar) {
3048        DrawList2D(LIST_SCALE_BAR, 0, 0, 0);
3049    }
3050}
3051
3052void GfxCore::PlaceVertexWithColour(const Vector3 & v,
3053                                    glaTexCoord tex_x, glaTexCoord tex_y,
3054                                    Double factor)
3055{
3056    SetColour(col_WHITE, factor);
3057    PlaceVertex(v, tex_x, tex_y);
3058}
3059
3060void GfxCore::SetDepthColour(Double z, Double factor) {
3061    // Set the drawing colour based on the altitude.
3062    Double z_ext = m_Parent->GetDepthExtent();
3063
3064    z -= m_Parent->GetDepthMin();
3065    // points arising from tubes may be slightly outside the limits...
3066    if (z < 0) z = 0;
3067    if (z > z_ext) z = z_ext;
3068
3069    if (z == 0) {
3070        SetColour(GetPen(0), factor);
3071        return;
3072    }
3073
3074    assert(z_ext > 0.0);
3075    Double how_far = z / z_ext;
3076    assert(how_far >= 0.0);
3077    assert(how_far <= 1.0);
3078
3079    int band = int(floor(how_far * (GetNumColourBands() - 1)));
3080    GLAPen pen1 = GetPen(band);
3081    if (band < GetNumColourBands() - 1) {
3082        const GLAPen& pen2 = GetPen(band + 1);
3083
3084        Double interval = z_ext / (GetNumColourBands() - 1);
3085        Double into_band = z / interval - band;
3086
3087//      printf("%g z_offset=%g interval=%g band=%d\n", into_band,
3088//             z_offset, interval, band);
3089        // FIXME: why do we need to clamp here?  Is it because the walls can
3090        // extend further up/down than the centre-line?
3091        if (into_band < 0.0) into_band = 0.0;
3092        if (into_band > 1.0) into_band = 1.0;
3093        assert(into_band >= 0.0);
3094        assert(into_band <= 1.0);
3095
3096        pen1.Interpolate(pen2, into_band);
3097    }
3098    SetColour(pen1, factor);
3099}
3100
3101void GfxCore::PlaceVertexWithDepthColour(const Vector3 &v, Double factor)
3102{
3103    SetDepthColour(v.GetZ(), factor);
3104    PlaceVertex(v);
3105}
3106
3107void GfxCore::PlaceVertexWithDepthColour(const Vector3 &v,
3108                                         glaTexCoord tex_x, glaTexCoord tex_y,
3109                                         Double factor)
3110{
3111    SetDepthColour(v.GetZ(), factor);
3112    PlaceVertex(v, tex_x, tex_y);
3113}
3114
3115void GfxCore::SplitLineAcrossBands(int band, int band2,
3116                                   const Vector3 &p, const Vector3 &q,
3117                                   Double factor)
3118{
3119    const int step = (band < band2) ? 1 : -1;
3120    for (int i = band; i != band2; i += step) {
3121        const Double z = GetDepthBoundaryBetweenBands(i, i + step);
3122
3123        // Find the intersection point of the line p -> q
3124        // with the plane parallel to the xy-plane with z-axis intersection z.
3125        assert(q.GetZ() - p.GetZ() != 0.0);
3126
3127        const Double t = (z - p.GetZ()) / (q.GetZ() - p.GetZ());
3128//      assert(0.0 <= t && t <= 1.0);           FIXME: rounding problems!
3129
3130        const Double x = p.GetX() + t * (q.GetX() - p.GetX());
3131        const Double y = p.GetY() + t * (q.GetY() - p.GetY());
3132
3133        PlaceVertexWithDepthColour(Vector3(x, y, z), factor);
3134    }
3135}
3136
3137void GfxCore::SplitPolyAcrossBands(vector<vector<Split> >& splits,
3138                                   int band, int band2,
3139                                   const Vector3 &p, const Vector3 &q,
3140                                   glaTexCoord ptx, glaTexCoord pty,
3141                                   glaTexCoord w, glaTexCoord h)
3142{
3143    const int step = (band < band2) ? 1 : -1;
3144    for (int i = band; i != band2; i += step) {
3145        const Double z = GetDepthBoundaryBetweenBands(i, i + step);
3146
3147        // Find the intersection point of the line p -> q
3148        // with the plane parallel to the xy-plane with z-axis intersection z.
3149        assert(q.GetZ() - p.GetZ() != 0.0);
3150
3151        const Double t = (z - p.GetZ()) / (q.GetZ() - p.GetZ());
3152//      assert(0.0 <= t && t <= 1.0);           FIXME: rounding problems!
3153
3154        const Double x = p.GetX() + t * (q.GetX() - p.GetX());
3155        const Double y = p.GetY() + t * (q.GetY() - p.GetY());
3156        glaTexCoord tx = ptx, ty = pty;
3157        if (w) tx += t * w;
3158        if (h) ty += t * h;
3159
3160        splits[i].push_back(Split(Vector3(x, y, z), tx, ty));
3161        splits[i + step].push_back(Split(Vector3(x, y, z), tx, ty));
3162    }
3163}
3164
3165int GfxCore::GetDepthColour(Double z) const
3166{
3167    // Return the (0-based) depth colour band index for a z-coordinate.
3168    Double z_ext = m_Parent->GetDepthExtent();
3169    z -= m_Parent->GetDepthMin();
3170    // We seem to get rounding differences causing z to sometimes be slightly
3171    // less than GetDepthMin() here, and it can certainly be true for passage
3172    // tubes, so just clamp the value to 0.
3173    if (z <= 0) return 0;
3174    // We seem to get rounding differences causing z to sometimes exceed z_ext
3175    // by a small amount here (see: https://trac.survex.com/ticket/26) and it
3176    // can certainly be true for passage tubes, so just clamp the value.
3177    if (z >= z_ext) return GetNumColourBands() - 1;
3178    return int(z / z_ext * (GetNumColourBands() - 1));
3179}
3180
3181Double GfxCore::GetDepthBoundaryBetweenBands(int a, int b) const
3182{
3183    // Return the z-coordinate of the depth colour boundary between
3184    // two adjacent depth colour bands (specified by 0-based indices).
3185
3186    assert((a == b - 1) || (a == b + 1));
3187    if (GetNumColourBands() == 1) return 0;
3188
3189    int band = (a > b) ? a : b; // boundary N lies on the bottom of band N.
3190    Double z_ext = m_Parent->GetDepthExtent();
3191    return (z_ext * band / (GetNumColourBands() - 1)) + m_Parent->GetDepthMin();
3192}
3193
3194void GfxCore::AddPolyline(const traverse & centreline)
3195{
3196    BeginPolyline();
3197    SetColour(col_WHITE);
3198    vector<PointInfo>::const_iterator i = centreline.begin();
3199    PlaceVertex(*i);
3200    ++i;
3201    while (i != centreline.end()) {
3202        PlaceVertex(*i);
3203        ++i;
3204    }
3205    EndPolyline();
3206}
3207
3208void GfxCore::AddPolylineShadow(const traverse & centreline)
3209{
3210    BeginPolyline();
3211    const double z = -0.5 * m_Parent->GetZExtent();
3212    vector<PointInfo>::const_iterator i = centreline.begin();
3213    PlaceVertex(i->GetX(), i->GetY(), z);
3214    ++i;
3215    while (i != centreline.end()) {
3216        PlaceVertex(i->GetX(), i->GetY(), z);
3217        ++i;
3218    }
3219    EndPolyline();
3220}
3221
3222void GfxCore::AddPolylineDepth(const traverse & centreline)
3223{
3224    BeginPolyline();
3225    vector<PointInfo>::const_iterator i, prev_i;
3226    i = centreline.begin();
3227    int band0 = GetDepthColour(i->GetZ());
3228    PlaceVertexWithDepthColour(*i);
3229    prev_i = i;
3230    ++i;
3231    while (i != centreline.end()) {
3232        int band = GetDepthColour(i->GetZ());
3233        if (band != band0) {
3234            SplitLineAcrossBands(band0, band, *prev_i, *i);
3235            band0 = band;
3236        }
3237        PlaceVertexWithDepthColour(*i);
3238        prev_i = i;
3239        ++i;
3240    }
3241    EndPolyline();
3242}
3243
3244void GfxCore::AddQuadrilateral(const Vector3 &a, const Vector3 &b,
3245                               const Vector3 &c, const Vector3 &d)
3246{
3247    Vector3 normal = (a - c) * (d - b);
3248    normal.normalise();
3249    Double factor = dot(normal, light) * .3 + .7;
3250    glaTexCoord w(((b - a).magnitude() + (d - c).magnitude()) * .5);
3251    glaTexCoord h(((b - c).magnitude() + (d - a).magnitude()) * .5);
3252    // FIXME: should plot triangles instead to avoid rendering glitches.
3253    BeginQuadrilaterals();
3254    PlaceVertexWithColour(a, 0, 0, factor);
3255    PlaceVertexWithColour(b, w, 0, factor);
3256    PlaceVertexWithColour(c, w, h, factor);
3257    PlaceVertexWithColour(d, 0, h, factor);
3258    EndQuadrilaterals();
3259}
3260
3261void GfxCore::AddQuadrilateralDepth(const Vector3 &a, const Vector3 &b,
3262                                    const Vector3 &c, const Vector3 &d)
3263{
3264    Vector3 normal = (a - c) * (d - b);
3265    normal.normalise();
3266    Double factor = dot(normal, light) * .3 + .7;
3267    int a_band, b_band, c_band, d_band;
3268    a_band = GetDepthColour(a.GetZ());
3269    a_band = min(max(a_band, 0), GetNumColourBands());
3270    b_band = GetDepthColour(b.GetZ());
3271    b_band = min(max(b_band, 0), GetNumColourBands());
3272    c_band = GetDepthColour(c.GetZ());
3273    c_band = min(max(c_band, 0), GetNumColourBands());
3274    d_band = GetDepthColour(d.GetZ());
3275    d_band = min(max(d_band, 0), GetNumColourBands());
3276    glaTexCoord w(((b - a).magnitude() + (d - c).magnitude()) * .5);
3277    glaTexCoord h(((b - c).magnitude() + (d - a).magnitude()) * .5);
3278    int min_band = min(min(a_band, b_band), min(c_band, d_band));
3279    int max_band = max(max(a_band, b_band), max(c_band, d_band));
3280    if (min_band == max_band) {
3281        // Simple case - the polygon is entirely within one band.
3282        BeginPolygon();
3283////    PlaceNormal(normal);
3284        PlaceVertexWithDepthColour(a, 0, 0, factor);
3285        PlaceVertexWithDepthColour(b, w, 0, factor);
3286        PlaceVertexWithDepthColour(c, w, h, factor);
3287        PlaceVertexWithDepthColour(d, 0, h, factor);
3288        EndPolygon();
3289    } else {
3290        // We need to make a separate polygon for each depth band...
3291        vector<vector<Split> > splits;
3292        splits.resize(max_band + 1);
3293        splits[a_band].push_back(Split(a, 0, 0));
3294        if (a_band != b_band) {
3295            SplitPolyAcrossBands(splits, a_band, b_band, a, b, 0, 0, w, 0);
3296        }
3297        splits[b_band].push_back(Split(b, w, 0));
3298        if (b_band != c_band) {
3299            SplitPolyAcrossBands(splits, b_band, c_band, b, c, w, 0, 0, h);
3300        }
3301        splits[c_band].push_back(Split(c, w, h));
3302        if (c_band != d_band) {
3303            SplitPolyAcrossBands(splits, c_band, d_band, c, d, w, h, -w, 0);
3304        }
3305        splits[d_band].push_back(Split(d, 0, h));
3306        if (d_band != a_band) {
3307            SplitPolyAcrossBands(splits, d_band, a_band, d, a, 0, h, 0, -h);
3308        }
3309        for (int band = min_band; band <= max_band; ++band) {
3310            BeginPolygon();
3311            for (auto&& item : splits[band]) {
3312                PlaceVertexWithDepthColour(item.vec, item.tx, item.ty, factor);
3313            }
3314            EndPolygon();
3315        }
3316    }
3317}
3318
3319void GfxCore::SetColourFromDate(int date, Double factor)
3320{
3321    // Set the drawing colour based on a date.
3322
3323    if (date == -1) {
3324        // Undated.
3325        SetColour(col_WHITE, factor);
3326        return;
3327    }
3328
3329    int date_offset = date - m_Parent->GetDateMin();
3330    if (date_offset == 0) {
3331        // Earliest date - handle as a special case for the single date case.
3332        SetColour(GetPen(0), factor);
3333        return;
3334    }
3335
3336    int date_ext = m_Parent->GetDateExtent();
3337    Double how_far = (Double)date_offset / date_ext;
3338    assert(how_far >= 0.0);
3339    assert(how_far <= 1.0);
3340    SetColourFrom01(how_far, factor);
3341}
3342
3343void GfxCore::AddPolylineDate(const traverse & centreline)
3344{
3345    BeginPolyline();
3346    vector<PointInfo>::const_iterator i, prev_i;
3347    i = centreline.begin();
3348    int date = i->GetDate();
3349    SetColourFromDate(date, 1.0);
3350    PlaceVertex(*i);
3351    prev_i = i;
3352    while (++i != centreline.end()) {
3353        int newdate = i->GetDate();
3354        if (newdate != date) {
3355            EndPolyline();
3356            BeginPolyline();
3357            date = newdate;
3358            SetColourFromDate(date, 1.0);
3359            PlaceVertex(*prev_i);
3360        }
3361        PlaceVertex(*i);
3362        prev_i = i;
3363    }
3364    EndPolyline();
3365}
3366
3367static int static_date_hack; // FIXME
3368
3369void GfxCore::AddQuadrilateralDate(const Vector3 &a, const Vector3 &b,
3370                                   const Vector3 &c, const Vector3 &d)
3371{
3372    Vector3 normal = (a - c) * (d - b);
3373    normal.normalise();
3374    Double factor = dot(normal, light) * .3 + .7;
3375    glaTexCoord w(((b - a).magnitude() + (d - c).magnitude()) * .5);
3376    glaTexCoord h(((b - c).magnitude() + (d - a).magnitude()) * .5);
3377    // FIXME: should plot triangles instead to avoid rendering glitches.
3378    BeginQuadrilaterals();
3379////    PlaceNormal(normal);
3380    SetColourFromDate(static_date_hack, factor);
3381    PlaceVertex(a, 0, 0);
3382    PlaceVertex(b, w, 0);
3383    PlaceVertex(c, w, h);
3384    PlaceVertex(d, 0, h);
3385    EndQuadrilaterals();
3386}
3387
3388static double static_E_hack; // FIXME
3389
3390void GfxCore::SetColourFromError(double E, Double factor)
3391{
3392    // Set the drawing colour based on an error value.
3393
3394    if (E < 0) {
3395        SetColour(col_WHITE, factor);
3396        return;
3397    }
3398
3399    Double how_far = E / MAX_ERROR;
3400    assert(how_far >= 0.0);
3401    if (how_far > 1.0) how_far = 1.0;
3402    SetColourFrom01(how_far, factor);
3403}
3404
3405void GfxCore::AddQuadrilateralError(const Vector3 &a, const Vector3 &b,
3406                                    const Vector3 &c, const Vector3 &d)
3407{
3408    Vector3 normal = (a - c) * (d - b);
3409    normal.normalise();
3410    Double factor = dot(normal, light) * .3 + .7;
3411    glaTexCoord w(((b - a).magnitude() + (d - c).magnitude()) * .5);
3412    glaTexCoord h(((b - c).magnitude() + (d - a).magnitude()) * .5);
3413    // FIXME: should plot triangles instead to avoid rendering glitches.
3414    BeginQuadrilaterals();
3415////    PlaceNormal(normal);
3416    SetColourFromError(static_E_hack, factor);
3417    PlaceVertex(a, 0, 0);
3418    PlaceVertex(b, w, 0);
3419    PlaceVertex(c, w, h);
3420    PlaceVertex(d, 0, h);
3421    EndQuadrilaterals();
3422}
3423
3424void GfxCore::AddPolylineError(const traverse & centreline)
3425{
3426    BeginPolyline();
3427    SetColourFromError(centreline.E, 1.0);
3428    vector<PointInfo>::const_iterator i;
3429    for(i = centreline.begin(); i != centreline.end(); ++i) {
3430        PlaceVertex(*i);
3431    }
3432    EndPolyline();
3433}
3434
3435// gradient is in *radians*.
3436void GfxCore::SetColourFromGradient(double gradient, Double factor)
3437{
3438    // Set the drawing colour based on the gradient of the leg.
3439
3440    const Double GRADIENT_MAX = M_PI_2;
3441    gradient = fabs(gradient);
3442    Double how_far = gradient / GRADIENT_MAX;
3443    SetColourFrom01(how_far, factor);
3444}
3445
3446void GfxCore::AddPolylineGradient(const traverse & centreline)
3447{
3448    vector<PointInfo>::const_iterator i, prev_i;
3449    i = centreline.begin();
3450    prev_i = i;
3451    while (++i != centreline.end()) {
3452        BeginPolyline();
3453        SetColourFromGradient((*i - *prev_i).gradient(), 1.0);
3454        PlaceVertex(*prev_i);
3455        PlaceVertex(*i);
3456        prev_i = i;
3457        EndPolyline();
3458    }
3459}
3460
3461static double static_gradient_hack; // FIXME
3462
3463void GfxCore::AddQuadrilateralGradient(const Vector3 &a, const Vector3 &b,
3464                                       const Vector3 &c, const Vector3 &d)
3465{
3466    Vector3 normal = (a - c) * (d - b);
3467    normal.normalise();
3468    Double factor = dot(normal, light) * .3 + .7;
3469    glaTexCoord w(((b - a).magnitude() + (d - c).magnitude()) * .5);
3470    glaTexCoord h(((b - c).magnitude() + (d - a).magnitude()) * .5);
3471    // FIXME: should plot triangles instead to avoid rendering glitches.
3472    BeginQuadrilaterals();
3473////    PlaceNormal(normal);
3474    SetColourFromGradient(static_gradient_hack, factor);
3475    PlaceVertex(a, 0, 0);
3476    PlaceVertex(b, w, 0);
3477    PlaceVertex(c, w, h);
3478    PlaceVertex(d, 0, h);
3479    EndQuadrilaterals();
3480}
3481
3482void GfxCore::SetColourFromLength(double length, Double factor)
3483{
3484    // Set the drawing colour based on log(length_of_leg).
3485
3486    Double log_len = log10(length);
3487    Double how_far = log_len / LOG_LEN_MAX;
3488    how_far = max(how_far, 0.0);
3489    how_far = min(how_far, 1.0);
3490    SetColourFrom01(how_far, factor);
3491}
3492
3493void GfxCore::SetColourFrom01(double how_far, Double factor)
3494{
3495    double b;
3496    double into_band = modf(how_far * (GetNumColourBands() - 1), &b);
3497    int band(b);
3498    GLAPen pen1 = GetPen(band);
3499    // With 24bit colour, interpolating by less than this can have no effect.
3500    if (into_band >= 1.0 / 512.0) {
3501        const GLAPen& pen2 = GetPen(band + 1);
3502        pen1.Interpolate(pen2, into_band);
3503    }
3504    SetColour(pen1, factor);
3505}
3506
3507void GfxCore::AddPolylineLength(const traverse & centreline)
3508{
3509    vector<PointInfo>::const_iterator i, prev_i;
3510    i = centreline.begin();
3511    prev_i = i;
3512    while (++i != centreline.end()) {
3513        BeginPolyline();
3514        SetColourFromLength((*i - *prev_i).magnitude(), 1.0);
3515        PlaceVertex(*prev_i);
3516        PlaceVertex(*i);
3517        prev_i = i;
3518        EndPolyline();
3519    }
3520}
3521
3522static double static_length_hack; // FIXME
3523
3524void GfxCore::AddQuadrilateralLength(const Vector3 &a, const Vector3 &b,
3525                                     const Vector3 &c, const Vector3 &d)
3526{
3527    Vector3 normal = (a - c) * (d - b);
3528    normal.normalise();
3529    Double factor = dot(normal, light) * .3 + .7;
3530    glaTexCoord w(((b - a).magnitude() + (d - c).magnitude()) * .5);
3531    glaTexCoord h(((b - c).magnitude() + (d - a).magnitude()) * .5);
3532    // FIXME: should plot triangles instead to avoid rendering glitches.
3533    BeginQuadrilaterals();
3534////    PlaceNormal(normal);
3535    SetColourFromLength(static_length_hack, factor);
3536    PlaceVertex(a, 0, 0);
3537    PlaceVertex(b, w, 0);
3538    PlaceVertex(c, w, h);
3539    PlaceVertex(d, 0, h);
3540    EndQuadrilaterals();
3541}
3542
3543void
3544GfxCore::SkinPassage(vector<XSect> & centreline, bool draw)
3545{
3546    assert(centreline.size() > 1);
3547    Vector3 U[4];
3548    XSect prev_pt_v;
3549    Vector3 last_right(1.0, 0.0, 0.0);
3550
3551//  FIXME: it's not simple to set the colour of a tube based on error...
3552//    static_E_hack = something...
3553    vector<XSect>::iterator i = centreline.begin();
3554    vector<XSect>::size_type segment = 0;
3555    while (i != centreline.end()) {
3556        // get the coordinates of this vertex
3557        XSect & pt_v = *i++;
3558
3559        bool cover_end = false;
3560
3561        Vector3 right, up;
3562
3563        const Vector3 up_v(0.0, 0.0, 1.0);
3564
3565        if (segment == 0) {
3566            assert(i != centreline.end());
3567            // first segment
3568
3569            // get the coordinates of the next vertex
3570            const XSect & next_pt_v = *i;
3571
3572            // calculate vector from this pt to the next one
3573            Vector3 leg_v = next_pt_v - pt_v;
3574
3575            // obtain a vector in the LRUD plane
3576            right = leg_v * up_v;
3577            if (right.magnitude() == 0) {
3578                right = last_right;
3579                // Obtain a second vector in the LRUD plane,
3580                // perpendicular to the first.
3581                //up = right * leg_v;
3582                up = up_v;
3583            } else {
3584                last_right = right;
3585                up = up_v;
3586            }
3587
3588            cover_end = true;
3589            static_date_hack = next_pt_v.GetDate();
3590        } else if (segment + 1 == centreline.size()) {
3591            // last segment
3592
3593            // Calculate vector from the previous pt to this one.
3594            Vector3 leg_v = pt_v - prev_pt_v;
3595
3596            // Obtain a horizontal vector in the LRUD plane.
3597            right = leg_v * up_v;
3598            if (right.magnitude() == 0) {
3599                right = Vector3(last_right.GetX(), last_right.GetY(), 0.0);
3600                // Obtain a second vector in the LRUD plane,
3601                // perpendicular to the first.
3602                //up = right * leg_v;
3603                up = up_v;
3604            } else {
3605                last_right = right;
3606                up = up_v;
3607            }
3608
3609            cover_end = true;
3610            static_date_hack = pt_v.GetDate();
3611        } else {
3612            assert(i != centreline.end());
3613            // Intermediate segment.
3614
3615            // Get the coordinates of the next vertex.
3616            const XSect & next_pt_v = *i;
3617
3618            // Calculate vectors from this vertex to the
3619            // next vertex, and from the previous vertex to
3620            // this one.
3621            Vector3 leg1_v = pt_v - prev_pt_v;
3622            Vector3 leg2_v = next_pt_v - pt_v;
3623
3624            // Obtain horizontal vectors perpendicular to
3625            // both legs, then normalise and average to get
3626            // a horizontal bisector.
3627            Vector3 r1 = leg1_v * up_v;
3628            Vector3 r2 = leg2_v * up_v;
3629            r1.normalise();
3630            r2.normalise();
3631            right = r1 + r2;
3632            if (right.magnitude() == 0) {
3633                // This is the "mid-pitch" case...
3634                right = last_right;
3635            }
3636            if (r1.magnitude() == 0) {
3637                up = up_v;
3638
3639                // Rotate pitch section to minimise the
3640                // "tortional stress" - FIXME: use
3641                // triangles instead of rectangles?
3642                int shift = 0;
3643                Double maxdotp = 0;
3644
3645                // Scale to unit vectors in the LRUD plane.
3646                right.normalise();
3647                up.normalise();
3648                Vector3 vec = up - right;
3649                for (int orient = 0; orient <= 3; ++orient) {
3650                    Vector3 tmp = U[orient] - prev_pt_v;
3651                    tmp.normalise();
3652                    Double dotp = dot(vec, tmp);
3653                    if (dotp > maxdotp) {
3654                        maxdotp = dotp;
3655                        shift = orient;
3656                    }
3657                }
3658                if (shift) {
3659                    if (shift != 2) {
3660                        Vector3 temp(U[0]);
3661                        U[0] = U[shift];
3662                        U[shift] = U[2];
3663                        U[2] = U[shift ^ 2];
3664                        U[shift ^ 2] = temp;
3665                    } else {
3666                        swap(U[0], U[2]);
3667                        swap(U[1], U[3]);
3668                    }
3669                }
3670#if 0
3671                // Check that the above code actually permuted
3672                // the vertices correctly.
3673                shift = 0;
3674                maxdotp = 0;
3675                for (int j = 0; j <= 3; ++j) {
3676                    Vector3 tmp = U[j] - prev_pt_v;
3677                    tmp.normalise();
3678                    Double dotp = dot(vec, tmp);
3679                    if (dotp > maxdotp) {
3680                        maxdotp = dotp + 1e-6; // Add small tolerance to stop 45 degree offset cases being flagged...
3681                        shift = j;
3682                    }
3683                }
3684                if (shift) {
3685                    printf("New shift = %d!\n", shift);
3686                    shift = 0;
3687                    maxdotp = 0;
3688                    for (int j = 0; j <= 3; ++j) {
3689                        Vector3 tmp = U[j] - prev_pt_v;
3690                        tmp.normalise();
3691                        Double dotp = dot(vec, tmp);
3692                        printf("    %d : %.8f\n", j, dotp);
3693                    }
3694                }
3695#endif
3696            } else {
3697                up = up_v;
3698            }
3699            last_right = right;
3700            static_date_hack = pt_v.GetDate();
3701        }
3702
3703        // Scale to unit vectors in the LRUD plane.
3704        right.normalise();
3705        up.normalise();
3706
3707        Double l = fabs(pt_v.GetL());
3708        Double r = fabs(pt_v.GetR());
3709        Double u = fabs(pt_v.GetU());
3710        Double d = fabs(pt_v.GetD());
3711
3712        // Produce coordinates of the corners of the LRUD "plane".
3713        Vector3 v[4];
3714        v[0] = pt_v - right * l + up * u;
3715        v[1] = pt_v + right * r + up * u;
3716        v[2] = pt_v + right * r - up * d;
3717        v[3] = pt_v - right * l - up * d;
3718
3719        if (draw) {
3720            const Vector3 & delta = pt_v - prev_pt_v;
3721            static_length_hack = delta.magnitude();
3722            static_gradient_hack = delta.gradient();
3723            if (segment > 0) {
3724                (this->*AddQuad)(v[0], v[1], U[1], U[0]);
3725                (this->*AddQuad)(v[2], v[3], U[3], U[2]);
3726                (this->*AddQuad)(v[1], v[2], U[2], U[1]);
3727                (this->*AddQuad)(v[3], v[0], U[0], U[3]);
3728            }
3729
3730            if (cover_end) {
3731                if (segment == 0) {
3732                    (this->*AddQuad)(v[0], v[1], v[2], v[3]);
3733                } else {
3734                    (this->*AddQuad)(v[3], v[2], v[1], v[0]);
3735                }
3736            }
3737        }
3738
3739        prev_pt_v = pt_v;
3740        U[0] = v[0];
3741        U[1] = v[1];
3742        U[2] = v[2];
3743        U[3] = v[3];
3744
3745        pt_v.set_right_bearing(deg(atan2(right.GetY(), right.GetX())));
3746
3747        ++segment;
3748    }
3749}
3750
3751void GfxCore::FullScreenMode()
3752{
3753    m_Parent->ViewFullScreen();
3754}
3755
3756bool GfxCore::IsFullScreen() const
3757{
3758    return m_Parent->IsFullScreen();
3759}
3760
3761bool GfxCore::FullScreenModeShowingMenus() const
3762{
3763    return m_Parent->FullScreenModeShowingMenus();
3764}
3765
3766void GfxCore::FullScreenModeShowMenus(bool show)
3767{
3768    m_Parent->FullScreenModeShowMenus(show);
3769}
3770
3771void
3772GfxCore::MoveViewer(double forward, double up, double right)
3773{
3774    double cT = cos(rad(m_TiltAngle));
3775    double sT = sin(rad(m_TiltAngle));
3776    double cP = cos(rad(m_PanAngle));
3777    double sP = sin(rad(m_PanAngle));
3778    Vector3 v_forward(cT * sP, cT * cP, sT);
3779    Vector3 v_up(sT * sP, sT * cP, -cT);
3780    Vector3 v_right(-cP, sP, 0);
3781    assert(fabs(dot(v_forward, v_up)) < 1e-6);
3782    assert(fabs(dot(v_forward, v_right)) < 1e-6);
3783    assert(fabs(dot(v_right, v_up)) < 1e-6);
3784    Vector3 move = v_forward * forward + v_up * up + v_right * right;
3785    AddTranslation(-move);
3786    // Show current position.
3787    m_Parent->SetCoords(m_Parent->GetOffset() - GetTranslation());
3788    ForceRefresh();
3789}
3790
3791PresentationMark GfxCore::GetView() const
3792{
3793    return PresentationMark(GetTranslation() + m_Parent->GetOffset(),
3794                            m_PanAngle, -m_TiltAngle, m_Scale);
3795}
3796
3797void GfxCore::SetView(const PresentationMark & p)
3798{
3799    m_SwitchingTo = 0;
3800    SetTranslation(p - m_Parent->GetOffset());
3801    m_PanAngle = p.angle;
3802    m_TiltAngle = -p.tilt_angle; // FIXME: nasty reversed sense (and above)
3803    SetRotation(m_PanAngle, m_TiltAngle);
3804    SetScale(p.scale);
3805    ForceRefresh();
3806}
3807
3808void GfxCore::PlayPres(double speed, bool change_speed) {
3809    if (!change_speed || presentation_mode == 0) {
3810        if (speed == 0.0) {
3811            presentation_mode = 0;
3812            return;
3813        }
3814        presentation_mode = PLAYING;
3815        next_mark = m_Parent->GetPresMark(MARK_FIRST);
3816        SetView(next_mark);
3817        next_mark_time = 0; // There already!
3818        this_mark_total = 0;
3819        pres_reverse = (speed < 0);
3820    }
3821
3822    if (change_speed) pres_speed = speed;
3823
3824    if (speed != 0.0) {
3825        bool new_pres_reverse = (speed < 0);
3826        if (new_pres_reverse != pres_reverse) {
3827            pres_reverse = new_pres_reverse;
3828            if (pres_reverse) {
3829                next_mark = m_Parent->GetPresMark(MARK_PREV);
3830            } else {
3831                next_mark = m_Parent->GetPresMark(MARK_NEXT);
3832            }
3833            swap(this_mark_total, next_mark_time);
3834        }
3835    }
3836}
3837
3838void GfxCore::SetColourBy(int colour_by) {
3839    m_ColourBy = colour_by;
3840    switch (colour_by) {
3841        case COLOUR_BY_DEPTH:
3842            AddQuad = &GfxCore::AddQuadrilateralDepth;
3843            AddPoly = &GfxCore::AddPolylineDepth;
3844            break;
3845        case COLOUR_BY_DATE:
3846            AddQuad = &GfxCore::AddQuadrilateralDate;
3847            AddPoly = &GfxCore::AddPolylineDate;
3848            break;
3849        case COLOUR_BY_ERROR:
3850            AddQuad = &GfxCore::AddQuadrilateralError;
3851            AddPoly = &GfxCore::AddPolylineError;
3852            break;
3853        case COLOUR_BY_GRADIENT:
3854            AddQuad = &GfxCore::AddQuadrilateralGradient;
3855            AddPoly = &GfxCore::AddPolylineGradient;
3856            break;
3857        case COLOUR_BY_LENGTH:
3858            AddQuad = &GfxCore::AddQuadrilateralLength;
3859            AddPoly = &GfxCore::AddPolylineLength;
3860            break;
3861        default: // case COLOUR_BY_NONE:
3862            AddQuad = &GfxCore::AddQuadrilateral;
3863            AddPoly = &GfxCore::AddPolyline;
3864            break;
3865    }
3866
3867    InvalidateList(LIST_UNDERGROUND_LEGS);
3868    InvalidateList(LIST_SURFACE_LEGS);
3869    InvalidateList(LIST_TUBES);
3870
3871    ForceRefresh();
3872}
3873
3874bool GfxCore::ExportMovie(const wxString & fnm)
3875{
3876    FILE* fh = wxFopen(fnm.fn_str(), wxT("wb"));
3877    if (fh == NULL) {
3878        wxGetApp().ReportError(wxString::Format(wmsg(/*Failed to open output file “%s”*/47), fnm.c_str()));
3879        return false;
3880    }
3881
3882    wxString ext;
3883    wxFileName::SplitPath(fnm, NULL, NULL, NULL, &ext, wxPATH_NATIVE);
3884
3885    int width;
3886    int height;
3887    GetSize(&width, &height);
3888    // Round up to next multiple of 2 (required by ffmpeg).
3889    width += (width & 1);
3890    height += (height & 1);
3891
3892    movie = new MovieMaker();
3893
3894    // movie takes ownership of fh.
3895    if (!movie->Open(fh, ext.utf8_str(), width, height)) {
3896        wxGetApp().ReportError(wxString(movie->get_error_string(), wxConvUTF8));
3897        delete movie;
3898        movie = NULL;
3899        return false;
3900    }
3901
3902    PlayPres(1);
3903    return true;
3904}
3905
3906void
3907GfxCore::OnPrint(const wxString &filename, const wxString &title,
3908                 const wxString &datestamp, time_t datestamp_numeric,
3909                 const wxString &cs_proj,
3910                 bool close_after_print)
3911{
3912    svxPrintDlg * p;
3913    p = new svxPrintDlg(m_Parent, filename, title, cs_proj,
3914                        datestamp, datestamp_numeric,
3915                        m_PanAngle, m_TiltAngle,
3916                        m_Names, m_Crosses, m_Legs, m_Surface, m_Splays,
3917                        m_Tubes, m_Entrances, m_FixedPts, m_ExportedPts,
3918                        true, close_after_print);
3919    p->Show(true);
3920}
3921
3922void
3923GfxCore::OnExport(const wxString &filename, const wxString &title,
3924                  const wxString &datestamp, time_t datestamp_numeric,
3925                  const wxString &cs_proj)
3926{
3927    // Fill in "right_bearing" for each cross-section.
3928    list<vector<XSect> >::iterator trav = m_Parent->tubes_begin();
3929    list<vector<XSect> >::iterator tend = m_Parent->tubes_end();
3930    while (trav != tend) {
3931        SkinPassage(*trav, false);
3932        ++trav;
3933    }
3934
3935    svxPrintDlg * p;
3936    p = new svxPrintDlg(m_Parent, filename, title, cs_proj,
3937                        datestamp, datestamp_numeric,
3938                        m_PanAngle, m_TiltAngle,
3939                        m_Names, m_Crosses, m_Legs, m_Surface, m_Splays,
3940                        m_Tubes, m_Entrances, m_FixedPts, m_ExportedPts,
3941                        false);
3942    p->Show(true);
3943}
3944
3945static wxCursor
3946make_cursor(const unsigned char * bits, const unsigned char * mask,
3947            int hotx, int hoty)
3948{
3949#ifndef __WXGTK2__
3950# ifdef __WXMAC__
3951    // The default Mac cursor is black with a white edge, so
3952    // invert our custom cursors to match.
3953    char b[128];
3954    for (int i = 0; i < 128; ++i)
3955        b[i] = bits[i] ^ 0xff;
3956# else
3957    const char * b = reinterpret_cast<const char *>(bits);
3958# endif
3959    wxBitmap cursor_bitmap(b, 32, 32);
3960    wxBitmap mask_bitmap(reinterpret_cast<const char *>(mask), 32, 32);
3961    cursor_bitmap.SetMask(new wxMask(mask_bitmap, *wxWHITE));
3962    wxImage cursor_image = cursor_bitmap.ConvertToImage();
3963    cursor_image.SetOption(wxIMAGE_OPTION_CUR_HOTSPOT_X, hotx);
3964    cursor_image.SetOption(wxIMAGE_OPTION_CUR_HOTSPOT_Y, hoty);
3965    return wxCursor(cursor_image);
3966#else
3967    return wxCursor((const char *)bits, 32, 32, hotx, hoty,
3968                    (const char *)mask, wxBLACK, wxWHITE);
3969#endif
3970}
3971
3972const
3973#include "hand.xbm"
3974const
3975#include "handmask.xbm"
3976
3977const
3978#include "brotate.xbm"
3979const
3980#include "brotatemask.xbm"
3981
3982const
3983#include "vrotate.xbm"
3984const
3985#include "vrotatemask.xbm"
3986
3987const
3988#include "rotate.xbm"
3989const
3990#include "rotatemask.xbm"
3991
3992const
3993#include "rotatezoom.xbm"
3994const
3995#include "rotatezoommask.xbm"
3996
3997void
3998GfxCore::UpdateCursor(GfxCore::cursor new_cursor)
3999{
4000    // Check if we're already showing that cursor.
4001    if (current_cursor == new_cursor) return;
4002
4003    current_cursor = new_cursor;
4004    switch (current_cursor) {
4005        case GfxCore::CURSOR_DEFAULT:
4006            GLACanvas::SetCursor(wxNullCursor);
4007            break;
4008        case GfxCore::CURSOR_POINTING_HAND:
4009            GLACanvas::SetCursor(wxCursor(wxCURSOR_HAND));
4010            break;
4011        case GfxCore::CURSOR_DRAGGING_HAND:
4012            GLACanvas::SetCursor(make_cursor(hand_bits, handmask_bits, 12, 18));
4013            break;
4014        case GfxCore::CURSOR_HORIZONTAL_RESIZE:
4015            GLACanvas::SetCursor(wxCursor(wxCURSOR_SIZEWE));
4016            break;
4017        case GfxCore::CURSOR_ROTATE_HORIZONTALLY:
4018            GLACanvas::SetCursor(make_cursor(rotate_bits, rotatemask_bits, 15, 15));
4019            break;
4020        case GfxCore::CURSOR_ROTATE_VERTICALLY:
4021            GLACanvas::SetCursor(make_cursor(vrotate_bits, vrotatemask_bits, 15, 15));
4022            break;
4023        case GfxCore::CURSOR_ROTATE_EITHER_WAY:
4024            GLACanvas::SetCursor(make_cursor(brotate_bits, brotatemask_bits, 15, 15));
4025            break;
4026        case GfxCore::CURSOR_ZOOM:
4027            GLACanvas::SetCursor(wxCursor(wxCURSOR_MAGNIFIER));
4028            break;
4029        case GfxCore::CURSOR_ZOOM_ROTATE:
4030            GLACanvas::SetCursor(make_cursor(rotatezoom_bits, rotatezoommask_bits, 15, 15));
4031            break;
4032    }
4033}
4034
4035bool GfxCore::MeasuringLineActive() const
4036{
4037    if (Animating()) return false;
4038    return HereIsReal() || m_there;
4039}
4040
4041bool GfxCore::HandleRClick(wxPoint point)
4042{
4043    if (PointWithinCompass(point)) {
4044        // Pop up menu.
4045        wxMenu menu;
4046        /* TRANSLATORS: View *looking* North */
4047        menu.Append(menu_ORIENT_MOVE_NORTH, wmsg(/*View &North*/240));
4048        /* TRANSLATORS: View *looking* East */
4049        menu.Append(menu_ORIENT_MOVE_EAST, wmsg(/*View &East*/241));
4050        /* TRANSLATORS: View *looking* South */
4051        menu.Append(menu_ORIENT_MOVE_SOUTH, wmsg(/*View &South*/242));
4052        /* TRANSLATORS: View *looking* West */
4053        menu.Append(menu_ORIENT_MOVE_WEST, wmsg(/*View &West*/243));
4054        menu.AppendSeparator();
4055        /* TRANSLATORS: Menu item which turns off the "north arrow" in aven. */
4056        menu.AppendCheckItem(menu_IND_COMPASS, wmsg(/*&Hide Compass*/387));
4057        /* TRANSLATORS: tickable menu item in View menu.
4058         *
4059         * Degrees are the angular measurement where there are 360 in a full
4060         * circle. */
4061        menu.AppendCheckItem(menu_CTL_DEGREES, wmsg(/*&Degrees*/343));
4062        menu.Connect(wxEVT_COMMAND_MENU_SELECTED, (wxObjectEventFunction)&wxEvtHandler::ProcessEvent, NULL, m_Parent->GetEventHandler());
4063        PopupMenu(&menu);
4064        return true;
4065    }
4066
4067    if (PointWithinClino(point)) {
4068        // Pop up menu.
4069        wxMenu menu;
4070        menu.Append(menu_ORIENT_PLAN, wmsg(/*&Plan View*/248));
4071        menu.Append(menu_ORIENT_ELEVATION, wmsg(/*Ele&vation*/249));
4072        menu.AppendSeparator();
4073        /* TRANSLATORS: Menu item which turns off the tilt indicator in aven. */
4074        menu.AppendCheckItem(menu_IND_CLINO, wmsg(/*&Hide Clino*/384));
4075        /* TRANSLATORS: tickable menu item in View menu.
4076         *
4077         * Degrees are the angular measurement where there are 360 in a full
4078         * circle. */
4079        menu.AppendCheckItem(menu_CTL_DEGREES, wmsg(/*&Degrees*/343));
4080        /* TRANSLATORS: tickable menu item in View menu.
4081         *
4082         * Show the tilt of the survey as a percentage gradient (100% = 45
4083         * degrees = 50 grad). */
4084        menu.AppendCheckItem(menu_CTL_PERCENT, wmsg(/*&Percent*/430));
4085        menu.Connect(wxEVT_COMMAND_MENU_SELECTED, (wxObjectEventFunction)&wxEvtHandler::ProcessEvent, NULL, m_Parent->GetEventHandler());
4086        PopupMenu(&menu);
4087        return true;
4088    }
4089
4090    if (PointWithinScaleBar(point)) {
4091        // Pop up menu.
4092        wxMenu menu;
4093        /* TRANSLATORS: Menu item which turns off the scale bar in aven. */
4094        menu.AppendCheckItem(menu_IND_SCALE_BAR, wmsg(/*&Hide scale bar*/385));
4095        /* TRANSLATORS: tickable menu item in View menu.
4096         *
4097         * "Metric" here means metres, km, etc (rather than feet, miles, etc)
4098         */
4099        menu.AppendCheckItem(menu_CTL_METRIC, wmsg(/*&Metric*/342));
4100        menu.Connect(wxEVT_COMMAND_MENU_SELECTED, (wxObjectEventFunction)&wxEvtHandler::ProcessEvent, NULL, m_Parent->GetEventHandler());
4101        PopupMenu(&menu);
4102        return true;
4103    }
4104
4105    if (PointWithinColourKey(point)) {
4106        // Pop up menu.
4107        wxMenu menu;
4108        menu.AppendCheckItem(menu_COLOUR_BY_DEPTH, wmsg(/*Colour by &Depth*/292));
4109        menu.AppendCheckItem(menu_COLOUR_BY_DATE, wmsg(/*Colour by D&ate*/293));
4110        menu.AppendCheckItem(menu_COLOUR_BY_ERROR, wmsg(/*Colour by &Error*/289));
4111        menu.AppendCheckItem(menu_COLOUR_BY_GRADIENT, wmsg(/*Colour by &Gradient*/85));
4112        menu.AppendCheckItem(menu_COLOUR_BY_LENGTH, wmsg(/*Colour by &Length*/82));
4113        menu.AppendSeparator();
4114        /* TRANSLATORS: Menu item which turns off the colour key.
4115         * The "Colour Key" is the thing in aven showing which colour
4116         * corresponds to which depth, date, survey closure error, etc. */
4117        menu.AppendCheckItem(menu_IND_COLOUR_KEY, wmsg(/*&Hide colour key*/386));
4118        if (m_ColourBy == COLOUR_BY_DEPTH || m_ColourBy == COLOUR_BY_LENGTH)
4119            menu.AppendCheckItem(menu_CTL_METRIC, wmsg(/*&Metric*/342));
4120        else if (m_ColourBy == COLOUR_BY_GRADIENT)
4121            menu.AppendCheckItem(menu_CTL_DEGREES, wmsg(/*&Degrees*/343));
4122        menu.Connect(wxEVT_COMMAND_MENU_SELECTED, (wxObjectEventFunction)&wxEvtHandler::ProcessEvent, NULL, m_Parent->GetEventHandler());
4123        PopupMenu(&menu);
4124        return true;
4125    }
4126
4127    return false;
4128}
4129
4130void GfxCore::SetZoomBox(wxPoint p1, wxPoint p2, bool centred, bool aspect)
4131{
4132    if (centred) {
4133        p1.x = p2.x + (p1.x - p2.x) * 2;
4134        p1.y = p2.y + (p1.y - p2.y) * 2;
4135    }
4136    if (aspect) {
4137#if 0 // FIXME: This needs more work.
4138        int sx = GetXSize();
4139        int sy = GetYSize();
4140        int dx = p1.x - p2.x;
4141        int dy = p1.y - p2.y;
4142        int dy_new = dx * sy / sx;
4143        if (abs(dy_new) >= abs(dy)) {
4144            p1.y += (dy_new - dy) / 2;
4145            p2.y -= (dy_new - dy) / 2;
4146        } else {
4147            int dx_new = dy * sx / sy;
4148            p1.x += (dx_new - dx) / 2;
4149            p2.x -= (dx_new - dx) / 2;
4150        }
4151#endif
4152    }
4153    zoombox.set(p1, p2);
4154    ForceRefresh();
4155}
4156
4157void GfxCore::ZoomBoxGo()
4158{
4159    if (!zoombox.active()) return;
4160
4161    int width = GetXSize();
4162    int height = GetYSize();
4163
4164    TranslateCave(-0.5 * (zoombox.x1 + zoombox.x2 - width),
4165                  -0.5 * (zoombox.y1 + zoombox.y2 - height));
4166    int box_w = abs(zoombox.x1 - zoombox.x2);
4167    int box_h = abs(zoombox.y1 - zoombox.y2);
4168
4169    double factor = min(double(width) / box_w, double(height) / box_h);
4170
4171    zoombox.unset();
4172
4173    SetScale(GetScale() * factor);
4174}
Note: See TracBrowser for help on using the repository browser.