OpenCPN Partial API docs
Loading...
Searching...
No Matches
shapefile_basemap.cpp
1/******************************************************************************
2 *
3 * Project: OpenCPN
4 * Purpose: Shapefile basemap
5 *
6 ***************************************************************************
7 * Copyright (C) 2012-2023 by David S. Register *
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 * This program is distributed in the hope that it will be useful, *
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
17 * GNU General Public License for more details. *
18 * *
19 * You should have received a copy of the GNU General Public License *
20 * along with this program; if not, write to the *
21 * Free Software Foundation, Inc., *
22 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
23 ***************************************************************************
24 *
25 *
26 */
27
28// Include OCPNPlatform.h before shapefile_basemap.h to prevent obscure syntax
29// error when compiling with VS2022
30#include "OCPNPlatform.h"
31#include "shapefile_basemap.h"
32#include "chartbase.h"
33#include "glChartCanvas.h"
34
35#include "model/logger.h"
36
37#ifdef ocpnUSE_GL
38#include "shaders.h"
39#endif
40
41#ifdef __WXMSW__
42#define __CALL_CONVENTION //__stdcall
43#else
44#define __CALL_CONVENTION
45#endif
46
47extern OCPNPlatform *g_Platform;
48extern wxString gWorldShapefileLocation;
49
50#ifdef ocpnUSE_GL
51
52typedef union {
53 GLdouble data[6];
54 struct sGLvertex {
55 GLdouble x;
56 GLdouble y;
57 GLdouble z;
58 GLdouble r;
59 GLdouble g;
60 GLdouble b;
61 } info;
62} GLvertexshp;
63#include <list>
64
65static std::list<float_2Dpt> g_pvshp;
66static std::list<GLvertexshp *> g_vertexesshp;
67static int g_typeshp, g_posshp;
68static float_2Dpt g_p1shp, g_p2shp;
69
70void __CALL_CONVENTION shpscombineCallback(GLdouble coords[3],
71 GLdouble *vertex_data[4],
72 GLfloat weight[4],
73 GLdouble **dataOut) {
74 GLvertexshp *vertex;
75
76 vertex = new GLvertexshp();
77 g_vertexesshp.push_back(vertex);
78
79 vertex->info.x = coords[0];
80 vertex->info.y = coords[1];
81
82 *dataOut = vertex->data;
83}
84
85void __CALL_CONVENTION shpserrorCallback(GLenum errorCode) {
86 const GLubyte *estring;
87 estring = gluErrorString(errorCode);
88 // wxLogMessage( _T("OpenGL Tessellation Error: %s"), estring );
89}
90
91void __CALL_CONVENTION shpsbeginCallback(GLenum type) {
92 switch (type) {
93 case GL_TRIANGLES:
94 case GL_TRIANGLE_STRIP:
95 case GL_TRIANGLE_FAN:
96 g_typeshp = type;
97 break;
98 default:
99 printf("tess unhandled begin type: %d\n", type);
100 }
101
102 g_posshp = 0;
103}
104
105void __CALL_CONVENTION shpsendCallback() {}
106
107void __CALL_CONVENTION shpsvertexCallback(GLvoid *arg) {
108 GLvertexshp *vertex;
109 vertex = (GLvertexshp *)arg;
110 float_2Dpt p;
111 p.y = vertex->info.x;
112 p.x = vertex->info.y;
113
114 // convert strips and fans into triangles
115 if (g_typeshp != GL_TRIANGLES) {
116 if (g_posshp > 2) {
117 g_pvshp.push_back(g_p1shp);
118 g_pvshp.push_back(g_p2shp);
119 }
120
121 if (g_typeshp == GL_TRIANGLE_STRIP)
122 g_p1shp = g_p2shp;
123 else if (g_posshp == 0)
124 g_p1shp = p;
125 g_p2shp = p;
126 }
127
128 g_pvshp.push_back(p);
129 g_posshp++;
130}
131#endif
132
133ShapeBaseChartSet::ShapeBaseChartSet() : _loaded(false) {
134 land_color = wxColor(170, 175, 80);
135}
136void ShapeBaseChartSet::SetBasemapLandColor(wxColor color) {
137 land_color = color;
138}
139wxColor ShapeBaseChartSet::GetBasemapLandColor() { return land_color; }
140
141wxPoint2DDouble ShapeBaseChartSet::GetDoublePixFromLL(ViewPort &vp, double lat,
142 double lon) {
143 wxPoint2DDouble p = vp.GetDoublePixFromLL(lat, lon);
144 p.m_x -= vp.rv_rect.x, p.m_y -= vp.rv_rect.y;
145 return p;
146}
147
148ShapeBaseChart &ShapeBaseChartSet::LowestQualityBaseMap() {
149 if (_basemap_map.find(Quality::crude) != _basemap_map.end()) {
150 return _basemap_map.at(Quality::crude);
151 }
152 if (_basemap_map.find(Quality::low) != _basemap_map.end()) {
153 return _basemap_map.at(Quality::low);
154 }
155 if (_basemap_map.find(Quality::medium) != _basemap_map.end()) {
156 return _basemap_map.at(Quality::medium);
157 }
158 if (_basemap_map.find(Quality::high) != _basemap_map.end()) {
159 return _basemap_map.at(Quality::high);
160 }
161 return _basemap_map.at(Quality::full);
162}
163
164ShapeBaseChart &ShapeBaseChartSet::HighestQualityBaseMap() {
165 if (_basemap_map.find(Quality::full) != _basemap_map.end()) {
166 return _basemap_map.at(Quality::full);
167 }
168 if (_basemap_map.find(Quality::high) != _basemap_map.end()) {
169 return _basemap_map.at(Quality::high);
170 }
171 if (_basemap_map.find(Quality::medium) != _basemap_map.end()) {
172 return _basemap_map.at(Quality::medium);
173 }
174 if (_basemap_map.find(Quality::low) != _basemap_map.end()) {
175 return _basemap_map.at(Quality::low);
176 }
177 return _basemap_map.at(Quality::crude);
178}
179
180ShapeBaseChart &ShapeBaseChartSet::SelectBaseMap(const size_t &scale) {
181 if (_basemap_map.find(Quality::full) != _basemap_map.end() &&
182 _basemap_map.at(Quality::full).IsUsable() &&
183 scale <= _basemap_map.at(Quality::full).MinScale()) {
184 return _basemap_map.at(Quality::full);
185 } else if (_basemap_map.find(Quality::high) != _basemap_map.end() &&
186 _basemap_map.at(Quality::high).IsUsable() &&
187 scale <= _basemap_map.at(Quality::high).MinScale()) {
188 return _basemap_map.at(Quality::high);
189 } else if (_basemap_map.find(Quality::medium) != _basemap_map.end() &&
190 _basemap_map.at(Quality::medium).IsUsable() &&
191 scale <= _basemap_map.at(Quality::medium).MinScale()) {
192 return _basemap_map.at(Quality::medium);
193 } else if (_basemap_map.find(Quality::low) != _basemap_map.end() &&
194 _basemap_map.at(Quality::low).IsUsable() &&
195 scale <= _basemap_map.at(Quality::low).MinScale()) {
196 return _basemap_map.at(Quality::low);
197 }
198 return LowestQualityBaseMap();
199}
200
201void ShapeBaseChartSet::Reset() {
202 // Establish the Shapefile basemap location
203 wxString basemap_dir;
204 if (gWorldShapefileLocation.empty()) {
205 basemap_dir = g_Platform->GetSharedDataDir();
206 basemap_dir.Append("basemap_shp");
207 } else {
208 basemap_dir = gWorldShapefileLocation;
209 }
210
211 LoadBasemaps(basemap_dir.ToStdString());
212}
213void ShapeBaseChartSet::LoadBasemaps(const std::string &dir) {
214 _loaded = false;
215 _basemap_map.clear();
216
217 if (fs::exists(ShapeBaseChart::ConstructPath(dir, "crude_10x10"))) {
218 auto c = ShapeBaseChart(ShapeBaseChart::ConstructPath(dir, "crude_10x10"),
219 300000000, land_color);
220 c._dmod = 10;
221 _basemap_map.insert(std::make_pair(Quality::crude, c));
222 }
223
224 if (fs::exists(ShapeBaseChart::ConstructPath(dir, "low"))) {
225 _basemap_map.insert(std::make_pair(
226 Quality::low, ShapeBaseChart(ShapeBaseChart::ConstructPath(dir, "low"),
227 15000000, land_color)));
228 }
229 if (fs::exists(ShapeBaseChart::ConstructPath(dir, "medium"))) {
230 _basemap_map.insert(std::make_pair(
231 Quality::medium,
232 ShapeBaseChart(ShapeBaseChart::ConstructPath(dir, "medium"), 1000000,
233 land_color)));
234 }
235 if (fs::exists(ShapeBaseChart::ConstructPath(dir, "high"))) {
236 _basemap_map.insert(std::make_pair(
237 Quality::high,
238 ShapeBaseChart(ShapeBaseChart::ConstructPath(dir, "high"), 300000,
239 land_color)));
240 }
241 if (fs::exists(ShapeBaseChart::ConstructPath(dir, "full"))) {
242 _basemap_map.insert(std::make_pair(
243 Quality::full,
244 ShapeBaseChart(ShapeBaseChart::ConstructPath(dir, "full"), 50000,
245 land_color)));
246 }
247 _loaded = true;
248 // if(_basemap_map.size())
249 // LowestQualityBaseMap().LoadSHP();
250}
251
252bool ShapeBaseChart::LoadSHP() {
253 if (!fs::exists(_filename)) {
254 _is_usable = false;
255 return false;
256 }
257 std::unique_ptr<shp::ShapefileReader> temp_reader(
258 new shp::ShapefileReader(_filename));
259 if (!temp_reader->isOpen()) {
260 MESSAGE_LOG << "Shapefile " << _filename << " is not opened";
261 _is_usable = false;
262 return false;
263 }
264 if (!_loading) {
265 // Check if loading was cancelled
266 return false;
267 }
268 auto bounds = temp_reader->getBounds();
269 _is_usable = temp_reader->getCount() > 1 && bounds.getMaxX() <= 180 &&
270 bounds.getMinX() >= -180 && bounds.getMinY() >= -90 &&
271 bounds.getMaxY() <=
272 90; // TODO - Do we care whether the planet is covered?
273 if (!_loading) {
274 // Check if loading was cancelled
275 _is_usable = false;
276 return false;
277 }
278 _is_usable &= temp_reader->getGeometryType() == shp::GeometryType::Polygon;
279 bool has_x = false;
280 bool has_y = false;
281 for (auto field : temp_reader->getFields()) {
282 if (field.getName() == "x") {
283 has_x = true;
284 } else if (field.getName() == "y") {
285 has_y = true;
286 }
287 }
288 _is_tiled = (has_x && has_y);
289 if (_is_usable && _is_tiled) {
290 size_t feat{0};
291 for (auto const &feature : *temp_reader) {
292 if (!_loading) {
293 // Check if loading was cancelled
294 _is_usable = false;
295 return false;
296 }
297 auto f1 = feature.getAttributes();
298 _tiles[LatLonKey(std::any_cast<int>(feature.getAttributes()["y"]),
299 std::any_cast<int>(feature.getAttributes()["x"]))]
300 .push_back(feat);
301 feat++;
302 }
303 }
304 if (_loading) { // Only set reader if loading wasn't cancelled
305 _reader = temp_reader.release();
306 }
307 return _is_usable;
308}
309
310void ShapeBaseChart::DoDrawPolygonFilled(ocpnDC &pnt, ViewPort &vp,
311 const shp::Feature &feature) {
312 double old_x = -9999999.0, old_y = -9999999.0;
313 auto polygon = static_cast<shp::Polygon *>(feature.getGeometry());
314 pnt.SetBrush(_color);
315 for (auto &ring : polygon->getRings()) {
316 wxPoint *poly_pt = new wxPoint[ring.getPoints().size()];
317 size_t cnt{0};
318 auto bbox = vp.GetBBox();
319 for (auto &point : ring.getPoints()) {
320 // if (bbox.ContainsMarge(point.getY(), point.getX(), 0.05)) {
321 wxPoint2DDouble q =
322 ShapeBaseChartSet::GetDoublePixFromLL(vp, point.getY(), point.getX());
323 if (round(q.m_x) != round(old_x) || round(q.m_y) != round(old_y)) {
324 poly_pt[cnt].x = round(q.m_x);
325 poly_pt[cnt].y = round(q.m_y);
326 cnt++;
327 }
328 old_x = q.m_x;
329 old_y = q.m_y;
330 //}
331 }
332 if (cnt > 1) {
333 pnt.DrawPolygonTessellated(cnt, poly_pt, 0, 0);
334 }
335 delete[] poly_pt;
336 }
337}
338
339void ShapeBaseChart::AddPointToTessList(shp::Point &point, ViewPort &vp,
340 GLUtesselator *tobj, bool idl) {
341#ifdef ocpnUSE_GL
342
343 wxPoint2DDouble q;
344 if (glChartCanvas::HasNormalizedViewPort(vp)) {
345 q = ShapeBaseChartSet::GetDoublePixFromLL(vp, point.getY(), point.getX());
346 } else { // tesselation directly from lat/lon
347 q.m_x = point.getY(), q.m_y = point.getX();
348 }
349 GLvertexshp *vertex = new GLvertexshp();
350 g_vertexesshp.push_back(vertex);
351 if (vp.m_projection_type != PROJECTION_POLAR) {
352 // need to correctly pick +180 or -180 longitude for projections
353 // that have a discontiguous date line
354
355 if (idl && (point.getX() == 180)) {
356 if (vp.m_projection_type == PROJECTION_MERCATOR ||
357 vp.m_projection_type == PROJECTION_EQUIRECTANGULAR) {
358 // q.m_x -= 40058986 * 4096.0; // 360 degrees in normalized
359 // viewport
360 } else {
361 q.m_x -= 360; // lat/lon coordinates
362 }
363 }
364 }
365 vertex->info.x = q.m_x;
366 vertex->info.y = q.m_y;
367
368 gluTessVertex(tobj, (GLdouble *)vertex, (GLdouble *)vertex);
369#endif
370}
371
372void ShapeBaseChart::DoDrawPolygonFilledGL(ocpnDC &pnt, ViewPort &vp,
373 const shp::Feature &feature) {
374#ifdef ocpnUSE_GL
375
376 bool idl =
377 vp.GetBBox().GetMinLon() <= -180 || vp.GetBBox().GetMaxLon() >= 180;
378 auto polygon = static_cast<shp::Polygon *>(feature.getGeometry());
379 for (auto &ring : polygon->getRings()) {
380 size_t cnt{0};
381 GLUtesselator *tobj = gluNewTess();
382
383 gluTessCallback(tobj, GLU_TESS_VERTEX, (_GLUfuncptr)&shpsvertexCallback);
384 gluTessCallback(tobj, GLU_TESS_BEGIN, (_GLUfuncptr)&shpsbeginCallback);
385 gluTessCallback(tobj, GLU_TESS_END, (_GLUfuncptr)&shpsendCallback);
386 gluTessCallback(tobj, GLU_TESS_COMBINE, (_GLUfuncptr)&shpscombineCallback);
387 gluTessCallback(tobj, GLU_TESS_ERROR, (_GLUfuncptr)&shpserrorCallback);
388
389 gluTessNormal(tobj, 0, 0, 1);
390 gluTessProperty(tobj, GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_NONZERO);
391
392 gluTessBeginPolygon(tobj, NULL);
393 gluTessBeginContour(tobj);
394 for (auto &point : ring.getPoints()) {
395 AddPointToTessList(point, vp, tobj, idl);
396 cnt++;
397 }
398 gluTessEndContour(tobj);
399 gluTessEndPolygon(tobj);
400 gluDeleteTess(tobj);
401
402 for (auto ver : g_vertexesshp) delete ver;
403 g_vertexesshp.clear();
404 }
405 float_2Dpt *polyv = new float_2Dpt[g_pvshp.size()];
406 int cnt = 0;
407 for (auto pt : g_pvshp) {
408 polyv[cnt++] = pt;
409 }
410 size_t polycnt = g_pvshp.size();
411 g_pvshp.clear();
412
413 GLuint vbo = 0;
414
415 // Build the shader viewport transform matrix
416 mat4x4 m, mvp;
417 mat4x4_identity(m);
418 mat4x4_scale_aniso(mvp, m, 2.0 / (float)vp.pix_width,
419 2.0 / (float)vp.pix_height, 1.0);
420 mat4x4_translate_in_place(mvp, -vp.pix_width / 2, vp.pix_height / 2, 0);
421
422 float *pvt = new float[2 * (polycnt)];
423 for (size_t i = 0; i < polycnt; i++) {
424 float_2Dpt *pc = polyv + i;
425 // wxPoint2DDouble q(pc->y, pc->x);// = vp.GetDoublePixFromLL(pc->y, pc->x);
426 wxPoint2DDouble q = vp.GetDoublePixFromLL(pc->y, pc->x);
427
428 pvt[i * 2] = q.m_x;
429 pvt[(i * 2) + 1] = q.m_y;
430 }
431
432 GLShaderProgram *shader = pcolor_tri_shader_program[pnt.m_canvasIndex];
433 shader->Bind();
434
435 float colorv[4];
436 colorv[0] = _color.Red() / float(256);
437 colorv[1] = _color.Green() / float(256);
438 colorv[2] = _color.Blue() / float(256);
439 colorv[3] = 1.0;
440 shader->SetUniform4fv("color", colorv);
441
442 shader->SetAttributePointerf("position", pvt);
443
444 glDrawArrays(GL_TRIANGLES, 0, polycnt);
445
446 delete[] pvt;
447 delete[] polyv;
448
449 glDeleteBuffers(1, &vbo);
450 shader->UnBind();
451#endif
452}
453
454void ShapeBaseChart::DrawPolygonFilled(ocpnDC &pnt, ViewPort &vp) {
455 if (!_is_usable) {
456 return;
457 }
458 if (!_reader) {
459 _loading = true;
460 _loaded = std::async(std::launch::async, [&]() {
461 bool ret = LoadSHP();
462 _loading = false;
463 return ret;
464 });
465 }
466 if (_loading) {
467 if (_loaded.wait_for(std::chrono::milliseconds(0)) ==
468 std::future_status::ready) {
469 _is_usable = _loaded.get();
470 } else {
471 return; // not yet loaded
472 }
473 }
474
475 int pmod = _dmod;
476 LLBBox bbox = vp.GetBBox();
477
478 int lat_start = floor(bbox.GetMinLat());
479 if (lat_start < 0)
480 lat_start = lat_start - (pmod + (lat_start % pmod));
481 else
482 lat_start = lat_start - (lat_start % pmod);
483
484 int lon_start = floor(bbox.GetMinLon());
485 if (lon_start < 0)
486 lon_start = lon_start - (pmod + (lon_start % pmod));
487 else
488 lon_start = lon_start - (lon_start % pmod);
489
490 if (_is_tiled) {
491 for (int i = lat_start; i < ceil(bbox.GetMaxLat()) + pmod; i += pmod) {
492 for (int j = lon_start; j < ceil(bbox.GetMaxLon()) + pmod; j += pmod) {
493 int lon{j};
494 if (j < -180) {
495 lon = j + 360;
496 } else if (j >= 180) {
497 lon = j - 360;
498 }
499 for (auto fid : _tiles[LatLonKey(i, lon)]) {
500 auto const &feature = _reader->getFeature(fid);
501 if (pnt.GetDC()) {
502 DoDrawPolygonFilled(pnt, vp,
503 feature); // Parallelize using std::async?
504 } else {
505 DoDrawPolygonFilledGL(pnt, vp,
506 feature); // Parallelize using std::async?
507 }
508 }
509 }
510 }
511 } else {
512 for (auto const &feature : *_reader) {
513 if (pnt.GetDC()) {
514 DoDrawPolygonFilled(pnt, vp,
515 feature); // Parallelize using std::async?
516 } else {
517 DoDrawPolygonFilledGL(pnt, vp,
518 feature); // Parallelize using std::async?
519 }
520 }
521 }
522}
523
524bool ShapeBaseChart::CrossesLand(double &lat1, double &lon1, double &lat2,
525 double &lon2) {
526 double latmin = std::min(lat1, lat2);
527 double lonmin = std::min(lon1, lon2);
528 double latmax = std::min(lat1, lat2);
529 double lonmax = std::min(lon1, lon2);
530
531 auto A = std::make_pair(lat1, lon1);
532 auto B = std::make_pair(lat2, lon2);
533
534 if (_is_tiled) {
535 for (int i = floor(latmin); i < ceil(latmax); i++) {
536 for (int j = floor(lonmin); j < ceil(lonmax); j++) {
537 int lon{j};
538 if (j < -180) {
539 lon = j + 360;
540 } else if (j >= 180) {
541 lon = j - 360;
542 }
543 for (auto fid : _tiles[LatLonKey(i, lon)]) {
544 auto const &feature = _reader->getFeature(fid);
545 if (PolygonLineIntersect(feature, A, B)) {
546 return true;
547 }
548 }
549 }
550 }
551 } else {
552 for (auto const &feature : *_reader) {
553 if (PolygonLineIntersect(feature, A, B)) {
554 return true;
555 }
556 }
557 }
558
559 return false;
560}
561
563 if (_loading) {
564 _loading = false;
565 if (_loaded.valid()) {
566 _loaded.wait(); // Wait for async operation to complete
567 }
568 }
569}
570
571bool ShapeBaseChart::LineLineIntersect(const std::pair<double, double> &A,
572 const std::pair<double, double> &B,
573 const std::pair<double, double> &C,
574 const std::pair<double, double> &D) {
575 // Line AB represented as a1x + b1y = c1
576 double a1 = B.second - A.second;
577 double b1 = A.first - B.first;
578 double c1 = a1 * (A.first) + b1 * (A.second);
579
580 // Line CD represented as a2x + b2y = c2
581 double a2 = D.second - C.second;
582 double b2 = C.first - D.first;
583 double c2 = a2 * (C.first) + b2 * (C.second);
584
585 double determinant = a1 * b2 - a2 * b1;
586
587 if (determinant == 0) {
588 // The lines are parallel
589 return false;
590 } else {
591 // The lines intersect at x,y
592 double x = (b2 * c1 - b1 * c2) / determinant;
593 double y = (a1 * c2 - a2 * c1) / determinant;
594 // x,y must be on both the segments we are checking
595 if (std::min(A.first, B.first) <= x && x <= std::max(A.first, B.first) &&
596 std::min(A.second, B.second) <= y &&
597 y <= std::max(A.second, B.second) && std::min(C.first, D.first) <= x &&
598 x <= std::max(C.first, D.first) && std::min(C.second, D.second) <= y &&
599 y <= std::max(C.second, D.second)) {
600 return true;
601 }
602 }
603 return false;
604}
605
606bool ShapeBaseChart::PolygonLineIntersect(const shp::Feature &feature,
607 const std::pair<double, double> &A,
608 const std::pair<double, double> &B) {
609 auto polygon = static_cast<shp::Polygon *>(feature.getGeometry());
610 std::pair<double, double> previous_point;
611 for (auto &ring : polygon->getRings()) {
612 size_t cnt{0};
613 for (auto &point : ring.getPoints()) {
614 auto pnt = std::make_pair(point.getY(), point.getX());
615 if (cnt > 0) {
616 // TODO: Is it faster to first check if we are in the boundong box of
617 // the line?
618 if (LineLineIntersect(A, B, previous_point, pnt)) {
619 return true;
620 }
621 }
622 previous_point = pnt;
623 cnt++;
624 }
625 }
626 return false;
627}
628
629void ShapeBaseChartSet::RenderViewOnDC(ocpnDC &dc, ViewPort &vp) {
630 if (IsUsable()) {
631 ShapeBaseChart &chart = SelectBaseMap(vp.chart_scale);
632 chart.SetColor(land_color);
633 chart.RenderViewOnDC(dc, vp);
634 }
635}
Wrapper class for OpenGL shader programs.
Definition shaders.h:57
latitude/longitude key for 1 degree cells
Provides platform-specific support utilities for OpenCPN.
Represents a basemap chart based on shapefile data.
void CancelLoading()
Cancel the chart loading operation.
Represents the view port for chart display in OpenCPN.
Definition viewport.h:84
int pix_height
Height of the viewport in physical pixels.
Definition viewport.h:221
int pix_width
Width of the viewport in physical pixels.
Definition viewport.h:219
wxPoint2DDouble GetDoublePixFromLL(double lat, double lon)
Convert latitude and longitude on the ViewPort to physical pixel coordinates with double precision.
Definition viewport.cpp:145
double chart_scale
Chart scale denominator (e.g., 50000 for a 1:50000 scale).
Definition viewport.h:214
Device context class that can use either wxDC or OpenGL for drawing.
Definition ocpndc.h:64
Enhanced logging interface on top of wx/log.h.