source: git/src/gfxcore.cc @ f2d6d32

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

Split tube polygon across depth bands properly

Previous we just set the colours of the corners and let OpenGL
interpolate, which gives odd looking results for polygons which
cross several depth bands - e.g. high chambers and long legs
down deep pitches. The issue is worse for surveys without much
vertical range.

Currently the texturing of the split polygons isn't handled
correctly, but it wasn't before either.

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