source: git/src/gfxcore.cc @ 4a0e0623

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

Reliably disable scale bar in perspective view

This is supposed to happen (because the scale across the screen
varies in perspective view) but actually the scale bar stayed around
until an update was forced for another reason.

Spotted thanks to Pedro Silva Pinto.

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