source: git/src/gfxcore.cc @ a6401ff3

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

Improve handling of hidden splay ends

Previously, hidden splay ends still served as "targets" for snapping
the mouse pointer to, and still got crosses when crosses were
enabled.

We don't have a handy flag for "this is the outer end of a splay"
and computing that on demand isn't so easy to do, so for now we
use the "anonymous station" flag so at least these cases now behave
properly for splays to anonymous stations (which is likely to be
what people with huge numbers of splays from disto-x, etc are
using). This does mean that anonymous stations on continuation
passages will incorrectly also be off when splays are hidden, but
that seems an OK trade-off for now.

The snapping of the mouse pointer was reported by Frank Tully in #105.

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