OpenCPN Partial API docs
Loading...
Searching...
No Matches
mbtiles.cpp
1/******************************************************************************
2 *
3 * Project: OpenCPN
4 * Purpose: MBTiles chart type support
5 * Author: David Register
6 *
7 ***************************************************************************
8 * Copyright (C) 2018 by David S. Register *
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 *
22 * Free Software Foundation, Inc., *
23 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
24 ***************************************************************************
25 *
26 */
27
28// ============================================================================
29// declarations
30// ============================================================================
31
32// ----------------------------------------------------------------------------
33// headers
34// ----------------------------------------------------------------------------
35
36// For compilers that support precompilation, includes "wx.h".
37#include <wx/wxprec.h>
38
39#ifndef WX_PRECOMP
40#include <wx/wx.h>
41#endif // precompiled headers
42
43// Why are these not in wx/prec.h?
44#include <wx/dir.h>
45#include <wx/stream.h>
46#include <wx/wfstream.h>
47#include <wx/tokenzr.h>
48#include <wx/filename.h>
49#include <wx/image.h>
50#include <wx/fileconf.h>
51#include <wx/mstream.h>
52#include <sys/stat.h>
53#include <sstream>
54#include <map>
55#include <unordered_map>
56
57#include <sqlite3.h> //We need some defines
58#include <SQLiteCpp/SQLiteCpp.h>
59
60#include "mbtiles.h"
61#include "chcanv.h"
62#include "glChartCanvas.h"
63#include "ocpn_frame.h"
64#include "shaders.h"
65
66// Missing from MSW include files
67#ifdef _MSC_VER
68typedef __int32 int32_t;
69typedef unsigned __int32 uint32_t;
70typedef __int64 int64_t;
71typedef unsigned __int64 uint64_t;
72#endif
73
74// ----------------------------------------------------------------------------
75// Random Prototypes
76// ----------------------------------------------------------------------------
77
78#if !defined(NAN)
79static const long long lNaN = 0xfff8000000000000;
80#define NAN (*(double *)&lNaN)
81#endif
82
83#ifdef OCPN_USE_CONFIG
84class MyConfig;
85extern MyConfig *pConfig;
86#endif
87
88#define LON_UNDEF NAN
89#define LAT_UNDEF NAN
90
91// The OpenStreetMaps zommlevel translation tables
92// https://wiki.openstreetmap.org/wiki/Zoom_levels
93
94/*Level Degree Area m / pixel ~Scale # Tiles
950 360 whole world 156,412 1:500 million 1
961 180 78,206 1:250 million 4
972 90 39,103 1:150 million 16
983 45 19,551 1:70 million 64
994 22.5 9,776 1:35 million 256
1005 11.25 4,888 1:15 million 1,024
1016 5.625 2,444 1:10 million 4,096
1027 2.813 1,222 1:4 million 16,384
1038 1.406 610.984 1:2 million 65,536
1049 0.703 wide area 305.492 1:1 million 262,144
10510 0.352 152.746 1:500,000 1,048,576
10611 0.176 area 76.373 1:250,000 4,194,304
10712 0.088 38.187 1:150,000 16,777,216
10813 0.044 village or town 19.093 1:70,000 67,108,864
10914 0.022 9.547 1:35,000 268,435,456
11015 0.011 4.773 1:15,000 1,073,741,824
11116 0.005 small road 2.387 1:8,000 4,294,967,296
11217 0.003 1.193 1:4,000 17,179,869,184
11318 0.001 0.596 1:2,000 68,719,476,736
11419 0.0005 0.298 1:1,000 274,877,906,944
115*/
116
117// A "nominal" scale value, by zoom factor. Estimated at equator, with monitor
118// pixel size of 0.3mm
119static const double OSM_zoomScale[] = {
120 5e8, 2.5e8, 1.5e8, 7.0e7, 3.5e7, 1.5e7, 1.0e7, 4.0e6, 2.0e6, 1.0e6,
121 5.0e5, 2.5e5, 1.5e5, 7.0e4, 3.5e4, 1.5e4, 8.0e3, 4.0e3, 2.0e3, 1.0e3,
122 5.0e2, 2.5e2
123};
124
125// Meters per pixel, by zoom factor
126static const double OSM_zoomMPP[] = {
127 156412, 78206, 39103, 19551, 9776, 4888, 2444,
128 1222, 610, 984, 305.492, 152.746, 76.373, 38.187,
129 19.093, 9.547, 4.773, 2.387, 1.193, 0.596, 0.298,
130 0.149, 0.075
131};
132
133static const double eps = 6e-6; // about 1cm on earth's surface at equator
134extern MyFrame *gFrame;
135
136// Private tile shader source
137static const GLchar* tile_vertex_shader_source =
138 "attribute vec2 aPos;\n"
139 "attribute vec2 aUV;\n"
140 "uniform mat4 MVMatrix;\n"
141 "varying vec2 varCoord;\n"
142 "void main() {\n"
143 " gl_Position = MVMatrix * vec4(aPos, 0.0, 1.0);\n"
144 " varCoord = aUV;\n"
145 "}\n";
146
147static const GLchar* tile_fragment_shader_source =
148 "precision lowp float;\n"
149 "uniform sampler2D uTex;\n"
150 "varying vec2 varCoord;\n"
151 "void main() {\n"
152 " gl_FragColor = texture2D(uTex, varCoord);\n"
153 "}\n";
154
155
156GLShaderProgram *g_tile_shader_program;
157
158#if defined(__UNIX__) && \
159 !defined(__WXOSX__) // high resolution stopwatch for profiling
160class OCPNStopWatch {
161public:
162 OCPNStopWatch() { Reset(); }
163 void Reset() { clock_gettime(CLOCK_REALTIME, &tp); }
164
165 double GetTime() {
166 timespec tp_end;
167 clock_gettime(CLOCK_REALTIME, &tp_end);
168 return (tp_end.tv_sec - tp.tv_sec) * 1.e3 +
169 (tp_end.tv_nsec - tp.tv_nsec) / 1.e6;
170 }
171
172private:
173 timespec tp;
174};
175#endif
176
177// *********************************************
178// Utility Functions
179// *********************************************
180
181// https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#C.2FC.2B.2B
182static int long2tilex(double lon, int z) {
183 if (lon < -180) lon += 360;
184
185 return (int)(floor((lon + 180.0) / 360.0 * pow(2.0, z)));
186}
187
188static int lat2tiley(double lat, int z) {
189 int y = (int)(floor(
190 (1.0 -
191 log(tan(lat * M_PI / 180.0) + 1.0 / cos(lat * M_PI / 180.0)) / M_PI) /
192 2.0 * pow(2.0, z)));
193 int ymax = 1 << z;
194 y = ymax - y - 1;
195 return y;
196}
197
198static double tilex2long(int x, int z) { return x / pow(2.0, z) * 360.0 - 180; }
199
200static double tiley2lat(int y, int z) {
201 double n = pow(2.0, z);
202 int ymax = 1 << z;
203 y = ymax - y - 1;
204 double latRad = atan(sinh(M_PI * (1 - (2 * y / n))));
205 return 180.0 / M_PI * latRad;
206}
207
208// ----------------------------------------------------------------------------
209// private classes
210// ----------------------------------------------------------------------------
211
212// Per tile descriptor
214public:
216 glTextureName = 0;
217 m_bAvailable = false;
218 m_bgeomSet = false;
219 }
220
221 virtual ~mbTileDescriptor() {}
222
223 int tile_x, tile_y;
224 int m_zoomLevel;
225 float latmin, lonmin, latmax, lonmax;
226 LLBBox box;
227
228 GLuint glTextureName;
229 bool m_bAvailable;
230 bool m_bgeomSet;
231};
232
233// Per zoomlevel descriptor of tile array for that zoomlevel
235public:
237 virtual ~mbTileZoomDescriptor() {}
238
239 int tile_x_min, tile_x_max;
240 int tile_y_min, tile_y_max;
241
242 int nx_tile, ny_tile;
243
244 // std::map<unsigned int, mbTileDescriptor *> tileMap;
245 std::unordered_map<unsigned int, mbTileDescriptor *> tileMap;
246};
247
248// ============================================================================
249// ChartMBTiles implementation
250// ============================================================================
251
252ChartMBTiles::ChartMBTiles() {
253 // Init some private data
254 m_ChartFamily = CHART_FAMILY_RASTER;
255 m_ChartType = CHART_TYPE_MBTILES;
256
257 m_Chart_Skew = 0.0;
258
259 m_datum_str = _T("WGS84"); // assume until proven otherwise
260 m_projection = PROJECTION_WEB_MERCATOR;
261 m_imageType = wxBITMAP_TYPE_ANY;
262
263 m_b_cdebug = 0;
264
265 m_minZoom = 0;
266 m_maxZoom = 21;
267
268 m_nNoCOVREntries = 0;
269 m_nCOVREntries = 0;
270 m_pCOVRTablePoints = NULL;
271 m_pCOVRTable = NULL;
272 m_pNoCOVRTablePoints = NULL;
273 m_pNoCOVRTable = NULL;
274 m_tileArray = NULL;
275
276 m_LonMin = LON_UNDEF;
277 m_LonMax = LON_UNDEF;
278 m_LatMin = LAT_UNDEF;
279 m_LatMax = LAT_UNDEF;
280
281#ifdef OCPN_USE_CONFIG
282 wxFileConfig *pfc = (wxFileConfig *)pConfig;
283 pfc->SetPath(_T ( "/Settings" ));
284 pfc->Read(_T ( "DebugMBTiles" ), &m_b_cdebug, 0);
285#endif
286 m_pDB = NULL;
287}
288
289ChartMBTiles::~ChartMBTiles() {
290 FlushTiles();
291 if (m_pDB) {
292 delete m_pDB;
293 }
294}
295
296//-------------------------------------------------------------------------------------------------
297// Get the Chart thumbnail data structure
298// Creating the thumbnail bitmap as required
299//-------------------------------------------------------------------------------------------------
300
301ThumbData *ChartMBTiles::GetThumbData() { return NULL; }
302
303ThumbData *ChartMBTiles::GetThumbData(int tnx, int tny, float lat, float lon) {
304 return NULL;
305}
306
307bool ChartMBTiles::UpdateThumbData(double lat, double lon) { return true; }
308
309bool ChartMBTiles::AdjustVP(ViewPort &vp_last, ViewPort &vp_proposed) {
310 return true;
311}
312
313// Report recommended minimum and maximum scale values for which use of this
314// chart is valid
315
316double ChartMBTiles::GetNormalScaleMin(double canvas_scale_factor,
317 bool b_allow_overzoom) {
318 // if(b_allow_overzoom)
319 return (canvas_scale_factor / m_ppm_avg) /
320 132; // allow wide range overzoom overscale
321 // else
322 // return (canvas_scale_factor / m_ppm_avg) / 2; // don't
323 // suggest too much overscale
324}
325
326double ChartMBTiles::GetNormalScaleMax(double canvas_scale_factor,
327 int canvas_width) {
328 return (canvas_scale_factor / m_ppm_avg) *
329 40.0; // excessive underscale is slow, and unreadable
330}
331
332double ChartMBTiles::GetNearestPreferredScalePPM(double target_scale_ppm) {
333 return target_scale_ppm;
334}
335
336// Checks/corrects/completes the initialization based on real data from the
337// tiles table
338void ChartMBTiles::InitFromTiles(const wxString &name) {
339 try {
340 // Open the MBTiles database file
341 const char *name_UTF8 = "";
342 wxCharBuffer utf8CB = name.ToUTF8(); // the UTF-8 buffer
343 if (utf8CB.data()) name_UTF8 = utf8CB.data();
344
345 SQLite::Database db(name_UTF8);
346
347 // Check if tiles with advertised min and max zoom level really exist, or
348 // correct the defaults We can't blindly use what we find though - the DB
349 // often contains empty cells at very low zoom levels, so if we have some
350 // info from metadata, we will use that if more conservative...
351 SQLite::Statement query(db,
352 "SELECT min(zoom_level) AS min_zoom, "
353 "max(zoom_level) AS max_zoom FROM tiles");
354 while (query.executeStep()) {
355 const char *colMinZoom = query.getColumn(0);
356 const char *colMaxZoom = query.getColumn(1);
357
358 int min_zoom = 0, max_zoom = 0;
359 sscanf(colMinZoom, "%i", &min_zoom);
360 m_minZoom = wxMax(m_minZoom, min_zoom);
361 sscanf(colMaxZoom, "%i", &max_zoom);
362 m_maxZoom = wxMin(m_maxZoom, max_zoom);
363 if (m_minZoom > m_maxZoom) {
364 // We are looking at total nonsense with wrong metatadata and actual
365 // tile coverage out of it, better use what's really in the data to be
366 // able to show at least something
367 m_minZoom = min_zoom;
368 m_maxZoom = max_zoom;
369 }
370 }
371
372 // std::cout << name.c_str() << " zoom_min: " << m_minZoom << "
373 // zoom_max: " << m_maxZoom << std::endl;
374
375 // Traversing the entire tile table can be expensive....
376 // Use declared bounds if present.
377
378 if (!std::isnan(m_LatMin) && !std::isnan(m_LatMax) &&
379 !std::isnan(m_LonMin) && !std::isnan(m_LonMax))
380 return;
381
382 // Try to guess the coverage extents from the tiles. This will be hard to
383 // get right -
384 // the finest resolution likely does not cover the whole area, while the
385 // lowest resolution tiles probably contain a lot of theoretical space which
386 // actually is not covered. And some resolutions may be actually missing...
387 // What do we use?
388 // If we have the metadata and it is not completely off, we should probably
389 // prefer it.
390 SQLite::Statement query1(
391 db,
392 wxString::Format(
393 "SELECT min(tile_row) AS min_row, max(tile_row) as max_row, "
394 "min(tile_column) as min_column, max(tile_column) as max_column, "
395 "count(*) as cnt, zoom_level FROM tiles WHERE zoom_level >= %d "
396 "AND zoom_level <= %d GROUP BY zoom_level ORDER BY zoom_level ASC",
397 m_minZoom, m_maxZoom)
398 .c_str());
399 float minLat = 999., maxLat = -999.0, minLon = 999., maxLon = -999.0;
400 while (query1.executeStep()) {
401 const char *colMinRow = query1.getColumn(0);
402 const char *colMaxRow = query1.getColumn(1);
403 const char *colMinCol = query1.getColumn(2);
404 const char *colMaxCol = query1.getColumn(3);
405 const char *colCnt = query1.getColumn(4);
406 const char *colZoom = query1.getColumn(5);
407
408 int minRow, maxRow, minCol, maxCol, cnt, zoom;
409 sscanf(colMinRow, "%i", &minRow);
410 sscanf(colMaxRow, "%i", &maxRow);
411 sscanf(colMinCol, "%i", &minCol);
412 sscanf(colMaxCol, "%i", &maxCol);
413 sscanf(colMinRow, "%i", &minRow);
414 sscanf(colMaxRow, "%i", &maxRow);
415 sscanf(colCnt, "%i", &cnt);
416 sscanf(colZoom, "%i", &zoom);
417
418 // Let's try to use the simplest possible algo and just look for the zoom
419 // level with largest extent (Which probably be the one with lowest
420 // resolution?)...
421 minLat = wxMin(minLat, tiley2lat(minRow, zoom));
422 maxLat = wxMax(maxLat, tiley2lat(maxRow - 1, zoom));
423 minLon = wxMin(minLon, tilex2long(minCol, zoom));
424 maxLon = wxMax(maxLon, tilex2long(maxCol + 1, zoom));
425 // std::cout << "Zoom: " << zoom << " minlat: " << tiley2lat(minRow, zoom)
426 // << " maxlat: " << tiley2lat(maxRow - 1, zoom) << " minlon: " <<
427 // tilex2long(minCol, zoom) << " maxlon: " << tilex2long(maxCol + 1, zoom)
428 // << std::endl;
429 }
430
431 // ... and use what we found only in case we miss some of the values from
432 // metadata...
433 if (std::isnan(m_LatMin)) m_LatMin = minLat;
434 if (std::isnan(m_LatMax)) m_LatMax = maxLat;
435 if (std::isnan(m_LonMin)) m_LonMin = minLon;
436 if (std::isnan(m_LonMax)) m_LonMax = maxLon;
437 } catch (std::exception &e) {
438 const char *t = e.what();
439 wxLogMessage("mbtiles exception: %s", e.what());
440 }
441}
442
443InitReturn ChartMBTiles::Init(const wxString &name, ChartInitFlag init_flags) {
444 m_global_color_scheme = GLOBAL_COLOR_SCHEME_RGB;
445
446 m_FullPath = name;
447 m_Description = m_FullPath;
448
449 try {
450 // Open the MBTiles database file
451 const char *name_UTF8 = "";
452 wxCharBuffer utf8CB = name.ToUTF8(); // the UTF-8 buffer
453 if (utf8CB.data()) name_UTF8 = utf8CB.data();
454
455 SQLite::Database db(name_UTF8);
456
457 // Compile a SQL query, getting everything from the "metadata" table
458 SQLite::Statement query(db, "SELECT * FROM metadata ");
459
460 // Loop to execute the query step by step, to get rows of result
461 while (query.executeStep()) {
462 const char *colName = query.getColumn(0);
463 const char *colValue = query.getColumn(1);
464
465 // Get the geometric extent of the data
466 if (!strncmp(colName, "bounds", 6)) {
467 float lon1, lat1, lon2, lat2;
468 sscanf(colValue, "%g,%g,%g,%g", &lon1, &lat1, &lon2, &lat2);
469
470 // There is some confusion over the layout of this field...
471 m_LatMax = wxMax(lat1, lat2);
472 m_LatMin = wxMin(lat1, lat2);
473 m_LonMax = wxMax(lon1, lon2);
474 m_LonMin = wxMin(lon1, lon2);
475
476 }
477
478
479 else if(!strncmp(colName, "format", 6) ){
480 m_format = std::string(colValue);
481 }
482
483 // Get the min and max zoom values present in the db
484 else if (!strncmp(colName, "minzoom", 7)) {
485 sscanf(colValue, "%i", &m_minZoom);
486 } else if (!strncmp(colName, "maxzoom", 7)) {
487 sscanf(colValue, "%i", &m_maxZoom);
488 }
489
490 else if (!strncmp(colName, "description", 11)) {
491 m_Description = wxString(colValue, wxConvUTF8);
492 } else if (!strncmp(colName, "name", 11)) {
493 m_Name = wxString(colValue, wxConvUTF8);
494 } else if (!strncmp(colName, "type", 11)) {
495 m_Type = wxString(colValue, wxConvUTF8).Upper().IsSameAs("OVERLAY")
496 ? MBTilesType::OVERLAY
497 : MBTilesType::BASE;
498 } else if (!strncmp(colName, "scheme", 11)) {
499 m_Scheme = wxString(colValue, wxConvUTF8).Upper().IsSameAs("XYZ")
500 ? MBTilesScheme::XYZ
501 : MBTilesScheme::TMS;
502 }
503 }
504 } catch (std::exception &e) {
505 const char *t = e.what();
506 wxLogMessage("mbtiles exception: %s", e.what());
507 return INIT_FAIL_REMOVE;
508 }
509
510 // Fix the missing/wrong metadata values
511 InitFromTiles(name);
512
513 // set the chart scale parameters based on the max zoom factor
514 m_ppm_avg = 1.0 / OSM_zoomMPP[m_minZoom];
515 m_Chart_Scale = OSM_zoomScale[m_maxZoom];
516
517 PrepareTiles(); // Initialize the tile data structures
518
519 LLRegion covrRegion;
520
521 LLBBox extentBox;
522 extentBox.Set(m_LatMin, m_LonMin, m_LatMax, m_LonMax);
523
524 const char *name_UTF8 = "";
525 wxCharBuffer utf8CB = name.ToUTF8(); // the UTF-8 buffer
526 if (utf8CB.data()) name_UTF8 = utf8CB.data();
527
528 SQLite::Database db(name_UTF8);
529
530 int zoomFactor = m_minZoom;
531 int minRegionZoom = -1;
532 bool covr_populated = false;
533
534 m_nTiles = 0;
535 while ((zoomFactor <= m_maxZoom) && (minRegionZoom < 0)) {
536 LLRegion covrRegionZoom;
537 wxRegion regionZoom;
538 char qrs[100];
539
540 // Protect against trying to create the exact coverage for the brutal large
541 // scale layers contianing tens of thousand tiles.
542 sprintf(qrs, "select count(*) from tiles where zoom_level = %d ",
543 zoomFactor);
544 SQLite::Statement query_size(db, qrs);
545
546 if (query_size.executeStep()) {
547 const char *colValue = query_size.getColumn(0);
548 int tile_at_zoom = atoi(colValue);
549 m_nTiles += tile_at_zoom;
550
551 if (tile_at_zoom > 1000) {
552 zoomFactor++;
553 if (!covr_populated) {
554 covr_populated = true;
555 covrRegion = extentBox;
556 }
557 continue;
558 }
559 }
560
561 // query the database
562 sprintf(qrs,
563 "select tile_column, tile_row from tiles where zoom_level = %d ",
564 zoomFactor);
565
566 // Compile a SQL query, getting the specific data
567 SQLite::Statement query(db, qrs);
568 covr_populated = true;
569
570 while (query.executeStep()) {
571 const char *colValue = query.getColumn(0);
572 const char *c2 = query.getColumn(1);
573 int tile_x_found = atoi(colValue); // tile_x
574 int tile_y_found = atoi(c2); // tile_y
575
576 regionZoom.Union(tile_x_found, tile_y_found - 1, 1, 1);
577
578 } // inner while
579
580 wxRegionIterator upd(regionZoom); // get the rect list
581 double eps_factor = eps * 100; // roughly 1 m
582
583 while (upd) {
584 wxRect rect = upd.GetRect();
585
586 double lonmin =
587 round(tilex2long(rect.x, zoomFactor) / eps_factor) * eps_factor;
588 double lonmax =
589 round(tilex2long(rect.x + rect.width, zoomFactor) / eps_factor) *
590 eps_factor;
591 double latmin =
592 round(tiley2lat(rect.y, zoomFactor) / eps_factor) * eps_factor;
593 double latmax =
594 round(tiley2lat(rect.y + rect.height, zoomFactor) / eps_factor) *
595 eps_factor;
596
597 LLBBox box;
598 box.Set(latmin, lonmin, latmax, lonmax);
599
600 LLRegion tileRegion(box);
601 // if(i <= 1)
602 covrRegionZoom.Union(tileRegion);
603
604 upd++;
605 minRegionZoom = zoomFactor; // We take the first populated (lowest) zoom
606 // level region as the final chart region
607 }
608
609 covrRegion.Union(covrRegionZoom);
610
611 zoomFactor++;
612
613 } // while
614
615 // The coverage region must be reduced if necessary to include only the db
616 // specified bounds.
617 covrRegion.Intersect(extentBox);
618
619 m_minZoomRegion = covrRegion;
620
621 // Populate M_COVR entries for the OCPN chart database
622 if (covrRegion.contours.size()) { // Check for no intersection caused by ??
623 m_nCOVREntries = covrRegion.contours.size();
624 m_pCOVRTablePoints = (int *)malloc(m_nCOVREntries * sizeof(int));
625 m_pCOVRTable = (float **)malloc(m_nCOVREntries * sizeof(float *));
626 std::list<poly_contour>::iterator it = covrRegion.contours.begin();
627 for (int i = 0; i < m_nCOVREntries; i++) {
628 m_pCOVRTablePoints[i] = it->size();
629 m_pCOVRTable[i] =
630 (float *)malloc(m_pCOVRTablePoints[i] * 2 * sizeof(float));
631 std::list<contour_pt>::iterator jt = it->begin();
632 for (int j = 0; j < m_pCOVRTablePoints[i]; j++) {
633 m_pCOVRTable[i][2 * j + 0] = jt->y;
634 m_pCOVRTable[i][2 * j + 1] = jt->x;
635 jt++;
636 }
637 it++;
638 }
639 }
640
641 if (init_flags == HEADER_ONLY) return INIT_OK;
642
643 InitReturn pi_ret = PostInit();
644 if (pi_ret != INIT_OK)
645 return pi_ret;
646 else
647 return INIT_OK;
648}
649
650InitReturn ChartMBTiles::PreInit(const wxString &name, ChartInitFlag init_flags,
651 ColorScheme cs) {
652 m_global_color_scheme = cs;
653 return INIT_OK;
654}
655
656InitReturn ChartMBTiles::PostInit(void) {
657 // Create the persistent MBTiles database file
658 const char *name_UTF8 = "";
659 wxCharBuffer utf8CB = m_FullPath.ToUTF8(); // the UTF-8 buffer
660 if (utf8CB.data()) name_UTF8 = utf8CB.data();
661
662 m_pDB = new SQLite::Database(name_UTF8);
663 m_pDB->exec("PRAGMA locking_mode=EXCLUSIVE");
664 m_pDB->exec("PRAGMA cache_size=-50000");
665
666 bReadyToRender = true;
667 return INIT_OK;
668}
669
670void ChartMBTiles::PrepareTiles() {
671 // OCPNStopWatch sw;
672 m_tileArray = new mbTileZoomDescriptor *[(m_maxZoom - m_minZoom) + 1];
673
674 for (int i = 0; i < (m_maxZoom - m_minZoom) + 1; i++) {
675 PrepareTilesForZoom(
676 m_minZoom + i,
677 (i == 0)); // Preset the geometry only on the minZoom tiles
678 }
679 // printf("PrepareTiles time: %f\n", sw.GetTime());
680}
681
682void ChartMBTiles::FlushTiles() {
683 if (!bReadyToRender || m_tileArray == nullptr) return;
684 for (int iz = 0; iz < (m_maxZoom - m_minZoom) + 1; iz++) {
685 mbTileZoomDescriptor *tzd = m_tileArray[iz];
686
687 for (auto const &it : tzd->tileMap) {
688 mbTileDescriptor *tile = it.second;
689 if (tile) {
690 if (tile->glTextureName > 0) glDeleteTextures(1, &tile->glTextureName);
691 delete tile;
692 }
693 }
694 delete tzd;
695 }
696}
697
698void ChartMBTiles::FlushTextures() {
699 if (m_tileArray == nullptr) {
700 return;
701 }
702 for (int iz = 0; iz < (m_maxZoom - m_minZoom) + 1; iz++) {
703 mbTileZoomDescriptor *tzd = m_tileArray[iz];
704
705 for (auto const &it : tzd->tileMap) {
706 mbTileDescriptor *tile = it.second;
707 if (tile && tile->glTextureName > 0) {
708 glDeleteTextures(1, &tile->glTextureName);
709 tile->glTextureName = 0;
710 }
711 }
712 }
713}
714
715void ChartMBTiles::PrepareTilesForZoom(int zoomFactor, bool bset_geom) {
717
718 m_tileArray[zoomFactor - m_minZoom] = tzd;
719
720 // Calculate the tile counts in x and y, based on zoomfactor and chart extents
721 tzd->tile_x_min = long2tilex(m_LonMin + eps, zoomFactor);
722 tzd->tile_x_max = long2tilex(m_LonMax - eps, zoomFactor);
723 tzd->tile_y_min = lat2tiley(m_LatMin + eps, zoomFactor);
724 tzd->tile_y_max = lat2tiley(m_LatMax - eps, zoomFactor);
725
726 tzd->nx_tile = abs(tzd->tile_x_max - tzd->tile_x_min) + 1;
727 tzd->ny_tile = tzd->tile_y_max - tzd->tile_y_min + 1;
728
729 return;
730}
731
732bool ChartMBTiles::GetChartExtent(Extent *pext) {
733 pext->NLAT = m_LatMax;
734 pext->SLAT = m_LatMin;
735 pext->ELON = m_LonMax;
736 pext->WLON = m_LonMin;
737
738 return true;
739}
740
741void ChartMBTiles::SetColorScheme(ColorScheme cs, bool bApplyImmediate) {
742 if (m_global_color_scheme != cs) {
743 m_global_color_scheme = cs;
744 FlushTextures();
745 }
746}
747
748void ChartMBTiles::GetValidCanvasRegion(const ViewPort &VPoint,
749 OCPNRegion *pValidRegion) {
750 pValidRegion->Clear();
751 pValidRegion->Union(0, 0, VPoint.pix_width, VPoint.pix_height);
752 return;
753}
754
755LLRegion ChartMBTiles::GetValidRegion() { return m_minZoomRegion; }
756
757bool ChartMBTiles::RenderViewOnDC(wxMemoryDC &dc, const ViewPort &VPoint) {
758 return true;
759}
760
761bool ChartMBTiles::getTileTexture(mbTileDescriptor *tile) {
762 if (!m_pDB) return false;
763
764 // Is the texture ready?
765 if (tile->glTextureName > 0) {
766 glBindTexture(GL_TEXTURE_2D, tile->glTextureName);
767
768 return true;
769 } else {
770 if (!tile->m_bAvailable) return false;
771 // fetch the tile data from the mbtile database
772 try {
773 char qrs[2100];
774 sprintf(qrs,
775 "select tile_data, length(tile_data) from tiles where zoom_level "
776 "= %d AND tile_column=%d AND tile_row=%d",
777 tile->m_zoomLevel, tile->tile_x, tile->tile_y);
778
779 // Compile a SQL query, getting the specific blob
780 SQLite::Statement query(*m_pDB, qrs);
781
782 int queryResult = query.tryExecuteStep();
783 if (SQLITE_DONE == queryResult) {
784 tile->m_bAvailable = false;
785 return false; // requested ROW not found, should never happen
786 } else {
787 SQLite::Column blobColumn = query.getColumn(0); // Get the blob
788 const void *blob = blobColumn.getBlob();
789
790 int length = query.getColumn(1); // Get the length
791
792 wxMemoryInputStream blobStream(blob, length);
793 wxImage blobImage;
794
795 blobImage = wxImage(blobStream, wxBITMAP_TYPE_ANY);
796 int blobWidth, blobHeight;
797 unsigned char *imgdata;
798
799 if (blobImage.IsOk()){
800 blobWidth = blobImage.GetWidth();
801 blobHeight = blobImage.GetHeight();
802 // Support MapTiler HiDPI tiles, 512x512
803 if ((blobWidth != 256) || (blobHeight != 256))
804 blobImage.Rescale(256, 256, wxIMAGE_QUALITY_NORMAL);
805 imgdata = blobImage.GetData();
806 }
807 else
808 return false;
809
810 if ((m_global_color_scheme != GLOBAL_COLOR_SCHEME_RGB) &&
811 (m_global_color_scheme != GLOBAL_COLOR_SCHEME_DAY)) {
812 double dimLevel;
813 switch (m_global_color_scheme) {
814 case GLOBAL_COLOR_SCHEME_DUSK: {
815 dimLevel = 0.8;
816 break;
817 }
818 case GLOBAL_COLOR_SCHEME_NIGHT: {
819 dimLevel = 0.3;
820 break;
821 }
822 default: {
823 dimLevel = 1.0;
824 break;
825 }
826 }
827
828 // for( int iy = 0; iy < blobHeight; iy++ ) {
829 // for( int ix = 0; ix < blobWidth; ix++ ) {
830 // wxImage::RGBValue rgb(
831 // blobImage.GetRed( ix, iy ),
832 // blobImage.GetGreen( ix, iy ),
833 // blobImage.GetBlue( ix, iy ) );
834 // wxImage::HSVValue hsv =
835 // wxImage::RGBtoHSV( rgb );
836 // hsv.value = hsv.value * dimLevel;
837 // wxImage::RGBValue nrgb =
838 // wxImage::HSVtoRGB( hsv );
839 // blobImage.SetRGB( ix, iy,
840 // nrgb.red, nrgb.green, nrgb.blue );
841 // }
842 // }
843
844 for (int j = 0; j < blobHeight * blobWidth; j++) {
845 unsigned char *d = &imgdata[3 * j];
846 wxImage::RGBValue rgb(*d, *(d + 1), *(d + 2));
847 wxImage::HSVValue hsv = wxImage::RGBtoHSV(rgb);
848 hsv.value = hsv.value * dimLevel;
849 wxImage::RGBValue nrgb = wxImage::HSVtoRGB(hsv);
850 *d = nrgb.red;
851 *(d + 1) = nrgb.green;
852 *(d + 2) = nrgb.blue;
853 }
854 }
855
856 int stride = 4;
857 int tex_w = 256;
858 int tex_h = 256;
859 if (!imgdata) return false;
860
861 unsigned char *teximage =
862 (unsigned char *)malloc(stride * tex_w * tex_h);
863 bool transparent = blobImage.HasAlpha();
864
865 for (int j = 0; j < tex_w * tex_h; j++) {
866 for (int k = 0; k < 3; k++)
867 teximage[j * stride + k] = imgdata[3 * j + k];
868
869 // Some NOAA Tilesets do not give transparent tiles, so we detect
870 // NOAA's idea of blank as RGB(1,0,0) and force alpha = 0;
871 if (imgdata[3 * j] == 1 && imgdata[3 * j + 1] == 0 &&
872 imgdata[3 * j + 2] == 0) {
873 teximage[j * stride + 3] = 0;
874 } else {
875 if (transparent) {
876 teximage[j * stride + 3] =
877 blobImage.GetAlpha(j % tex_w, j / tex_w);
878 } else {
879 teximage[j * stride + 3] = 255;
880 }
881 }
882 }
883
884 glGenTextures(1, &tile->glTextureName);
885 glBindTexture(GL_TEXTURE_2D, tile->glTextureName);
886
887 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
888 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
889 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
890 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
891
892 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tex_w, tex_h, 0, GL_RGBA,
893 GL_UNSIGNED_BYTE, teximage);
894
895 free(teximage);
896
897 return true;
898 }
899
900 } catch (std::exception &e) {
901 const char *t = e.what();
902 wxLogMessage("mbtiles exception: %s", e.what());
903 }
904 }
905
906 return false;
907}
908
909class wxPoint2DDouble;
910
911wxPoint2DDouble ChartMBTiles::GetDoublePixFromLL(ViewPort &vp, double lat,
912 double lon) {
913 double easting = 0;
914 double northing = 0;
915 double xlon = lon - eps;
916
917 switch (vp.m_projection_type) {
918 case PROJECTION_MERCATOR:
919 case PROJECTION_WEB_MERCATOR:
920 default:
921 const double z = WGS84_semimajor_axis_meters * mercator_k0;
922
923 easting = (xlon - vp.clon) * DEGREE * z;
924
925 // y =.5 ln( (1 + sin t) / (1 - sin t) )
926 const double s = sin(lat * DEGREE);
927 const double y3 = (.5 * log((1 + s) / (1 - s))) * z;
928
929 const double s0 = sin(vp.clat * DEGREE);
930 const double y30 = (.5 * log((1 + s0) / (1 - s0))) * z;
931 northing = y3 - y30;
932
933 break;
934 }
935
936 double epix = easting * vp.view_scale_ppm;
937 double npix = northing * vp.view_scale_ppm;
938 double dxr = epix;
939 double dyr = npix;
940
941 // Apply VP Rotation
942 double angle = vp.rotation;
943
944 if (angle) {
945 dxr = epix * cos(angle) + npix * sin(angle);
946 dyr = npix * cos(angle) - epix * sin(angle);
947 }
948
949 // printf(" gdpll: %g %g %g\n", vp.clon, (vp.pix_width / 2.0 ) + dxr, (
950 // vp.pix_height / 2.0 ) - dyr);
951
952 return wxPoint2DDouble((vp.pix_width / 2.0) + dxr,
953 (vp.pix_height / 2.0) - dyr);
954}
955
956bool ChartMBTiles::RenderTile(mbTileDescriptor *tile, int zoomLevel,
957 const ViewPort &VPoint) {
958 ViewPort vp = VPoint;
959
960 bool btexture = getTileTexture(tile);
961 if (!btexture) { // failed to load, draw NODTA on the minimum zoom
962 glDisable(GL_TEXTURE_2D);
963 return false;
964 } else {
965#if !defined(USE_ANDROID_GLES2) && !defined(ocpnUSE_GLSL)
966 glColor4f(1,1,1,1);
967#endif
968 glEnable(GL_TEXTURE_2D);
969 glEnable(GL_BLEND);
970 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
971 }
972
973 float coords[8];
974 float texcoords[] = {0., 1., 0., 0., 1., 0., 1., 1.};
975
976 ViewPort mvp = vp;
977
978 wxPoint2DDouble p;
979
980 p = GetDoublePixFromLL(mvp, tile->latmin, tile->lonmin);
981 coords[0] = p.m_x;
982 coords[1] = p.m_y;
983 p = GetDoublePixFromLL(mvp, tile->latmax, tile->lonmin);
984 coords[2] = p.m_x;
985 coords[3] = p.m_y;
986 p = GetDoublePixFromLL(mvp, tile->latmax, tile->lonmax);
987 coords[4] = p.m_x;
988 coords[5] = p.m_y;
989 p = GetDoublePixFromLL(mvp, tile->latmin, tile->lonmax);
990 coords[6] = p.m_x;
991 coords[7] = p.m_y;
992
993 if (!g_tile_shader_program) {
994 GLShaderProgram *shaderProgram = new GLShaderProgram;
995 shaderProgram->addShaderFromSource(tile_vertex_shader_source, GL_VERTEX_SHADER);
996 shaderProgram->addShaderFromSource(tile_fragment_shader_source, GL_FRAGMENT_SHADER);
997 shaderProgram->linkProgram();
998 g_tile_shader_program = shaderProgram;
999 }
1000
1001 GLShaderProgram *shader = g_tile_shader_program;
1002 shader->Bind();
1003
1004 // Set up the texture sampler to texture unit 0
1005 shader->SetUniform1i("uTex", 0);
1006
1007 shader->SetUniformMatrix4fv("MVMatrix", (GLfloat *)vp.vp_matrix_transform);
1008
1009 float co1[8];
1010 float tco1[8];
1011
1012 shader->SetAttributePointerf("aPos", co1);
1013 shader->SetAttributePointerf("aUV", tco1);
1014
1015 // Perform the actual drawing.
1016
1017// For some reason, glDrawElements is busted on Android
1018// So we do this a hard ugly way, drawing two triangles...
1019#if 0
1020 GLushort indices1[] = {0,1,3,2};
1021 glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT, indices1);
1022#else
1023
1024 co1[0] = coords[0];
1025 co1[1] = coords[1];
1026 co1[2] = coords[2];
1027 co1[3] = coords[3];
1028 co1[4] = coords[6];
1029 co1[5] = coords[7];
1030 co1[6] = coords[4];
1031 co1[7] = coords[5];
1032
1033 tco1[0] = texcoords[0];
1034 tco1[1] = texcoords[1];
1035 tco1[2] = texcoords[2];
1036 tco1[3] = texcoords[3];
1037 tco1[4] = texcoords[6];
1038 tco1[5] = texcoords[7];
1039 tco1[6] = texcoords[4];
1040 tco1[7] = texcoords[5];
1041
1042 glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
1043#endif
1044
1045 shader->UnBind();
1046
1047 glDisable(GL_BLEND);
1048
1049 return true;
1050}
1051
1052bool ChartMBTiles::RenderRegionViewOnGL(const wxGLContext &glc,
1053 const ViewPort &VPoint,
1054 const OCPNRegion &RectRegion,
1055 const LLRegion &Region) {
1056 // Do not render if significantly underzoomed
1057 if (VPoint.chart_scale > (20 * OSM_zoomScale[m_minZoom])) {
1058 if (m_nTiles > 500) {
1059 return true;
1060 }
1061 }
1062
1063 ViewPort vp = VPoint;
1064
1065 OCPNRegion screen_region(wxRect(0, 0, vp.pix_width, vp.pix_height));
1066 LLRegion screenLLRegion = vp.GetLLRegion(screen_region);
1067 LLBBox screenBox = screenLLRegion.GetBox();
1068
1069 if ((m_LonMax - m_LonMin) > 180) { // big chart
1070 LLRegion validRegion = m_minZoomRegion;
1071 validRegion.Intersect(screenLLRegion);
1072 glChartCanvas::SetClipRegion(vp, validRegion);
1073 }
1074 else
1075 glChartCanvas::SetClipRegion(vp, m_minZoomRegion);
1076
1077 /* setup opengl parameters */
1078 glEnable(GL_TEXTURE_2D);
1079
1080 int viewZoom = m_maxZoom;
1081 double zoomMod =
1082 2.0; // decrease to get more detail, nominal 4?, 2 works OK for NOAA.
1083
1084 for (int kz = m_minZoom; kz <= 19; kz++) {
1085 double db_mpp = OSM_zoomMPP[kz];
1086 double vp_mpp = 1. / vp.view_scale_ppm;
1087
1088 if (db_mpp < vp_mpp * zoomMod) {
1089 viewZoom = kz;
1090 break;
1091 }
1092 }
1093
1094 viewZoom = wxMin(viewZoom, m_maxZoom);
1095 // printf("viewZoomCalc: %d %g %g\n", viewZoom, VPoint.view_scale_ppm, 1.
1096 // / VPoint.view_scale_ppm);
1097
1098 int zoomFactor = m_minZoom;
1099
1100 // DEBUG TODO Show single zoom
1101 // zoomFactor = 5; //m_minZoom;
1102 // viewZoom = zoomFactor;
1103
1104 int maxrenZoom = m_minZoom;
1105
1106 LLBBox box = Region.GetBox();
1107
1108 // if the full screen box spans IDL,
1109 // we need to render the entire screen in two passes.
1110 bool btwoPass = false;
1111 if (((screenBox.GetMinLon() < -180) && (screenBox.GetMaxLon() > -180)) ||
1112 ((screenBox.GetMinLon() < 180) && (screenBox.GetMaxLon() > 180))) {
1113 // printf("\nTwoPass\n");
1114 btwoPass = true;
1115 box = screenBox;
1116 }
1117
1118 while (zoomFactor <= viewZoom) {
1119 // printf("zoomFactor: %d viewZoom: %d\n", zoomFactor, viewZoom);
1120 mbTileZoomDescriptor *tzd = m_tileArray[zoomFactor - m_minZoom];
1121
1122 // Get the tile numbers of the box corners of this render region, at this
1123 // zoom level
1124 int topTile =
1125 wxMin(tzd->tile_y_max, lat2tiley(box.GetMaxLat(), zoomFactor));
1126 int botTile =
1127 wxMax(tzd->tile_y_min, lat2tiley(box.GetMinLat(), zoomFactor));
1128 int leftTile = long2tilex(box.GetMinLon(), zoomFactor);
1129 int rightTile = long2tilex(box.GetMaxLon(), zoomFactor);
1130
1131 if (btwoPass) {
1132 leftTile = long2tilex(-180 + eps, zoomFactor);
1133 rightTile = long2tilex(box.GetMaxLon(), zoomFactor);
1134 vp = VPoint;
1135 if (vp.clon > 0) vp.clon -= 360;
1136
1137 } else
1138 vp = VPoint;
1139
1140 // botTile -= 1;
1141 topTile += 1;
1142
1143 // printf("limits: {%d %d} {%d %d}\n", botTile, topTile, leftTile,
1144 // rightTile);
1145
1146 for (int i = botTile; i < topTile; i++) {
1147 if ((i > tzd->tile_y_max) || (i < tzd->tile_y_min)) continue;
1148
1149 for (int j = leftTile; j < rightTile + 1; j++) {
1150 if ((tzd->tile_x_max >= tzd->tile_x_min) &&
1151 ((j > tzd->tile_x_max) || (j < tzd->tile_x_min)))
1152 continue;
1153
1154 unsigned int index = ((i - tzd->tile_y_min) * (tzd->nx_tile + 1)) + j;
1155 // printf("pass 1: %d %d %d\n", zoomFactor, i, j);
1156 mbTileDescriptor *tile = NULL;
1157
1158 if (tzd->tileMap.find(index) != tzd->tileMap.end())
1159 tile = tzd->tileMap[index];
1160 if (NULL == tile) {
1161 tile = new mbTileDescriptor;
1162 tile->tile_x = j;
1163 tile->tile_y = i;
1164 tile->m_zoomLevel = zoomFactor;
1165 tile->m_bAvailable = true;
1166
1167 tzd->tileMap[index] = tile;
1168 }
1169
1170 if (!tile->m_bgeomSet) {
1171 tile->lonmin =
1172 round(tilex2long(tile->tile_x, zoomFactor) / eps) * eps;
1173 tile->lonmax =
1174 round(tilex2long(tile->tile_x + 1, zoomFactor) / eps) * eps;
1175 tile->latmin =
1176 round(tiley2lat(tile->tile_y - 1, zoomFactor) / eps) * eps;
1177 tile->latmax = round(tiley2lat(tile->tile_y, zoomFactor) / eps) * eps;
1178
1179 tile->box.Set(tile->latmin, tile->lonmin, tile->latmax, tile->lonmax);
1180 tile->m_bgeomSet = true;
1181 }
1182
1183 if (!Region.IntersectOut(tile->box)) {
1184 if (RenderTile(tile, zoomFactor, vp)) maxrenZoom = zoomFactor;
1185 }
1186 }
1187 } // for
1188
1189 // second pass
1190 if (btwoPass) {
1191 vp = VPoint;
1192 if (vp.clon < 0) vp.clon += 360;
1193
1194 // Get the tile numbers of the box corners of this render region, at this
1195 // zoom level
1196 int topTile =
1197 wxMin(tzd->tile_y_max, lat2tiley(box.GetMaxLat(), zoomFactor));
1198 int botTile =
1199 wxMax(tzd->tile_y_min, lat2tiley(box.GetMinLat(), zoomFactor));
1200 int leftTile = long2tilex(box.GetMinLon(), zoomFactor);
1201 int rightTile = long2tilex(-180 - eps /*box.GetMaxLon()*/, zoomFactor);
1202
1203 if (rightTile < leftTile) rightTile = leftTile;
1204 topTile += 1;
1205
1206 for (int i = botTile; i < topTile; i++) {
1207 for (int j = leftTile; j < rightTile + 1; j++) {
1208 unsigned int index = ((i - tzd->tile_y_min) * (tzd->nx_tile + 1)) + j;
1209
1210 mbTileDescriptor *tile = NULL;
1211
1212 // printf("pass 2: %d %d %d\n", zoomFactor, i, j);
1213
1214 if (tzd->tileMap.find(index) != tzd->tileMap.end())
1215 tile = tzd->tileMap[index];
1216 if (NULL == tile) {
1217 tile = new mbTileDescriptor;
1218 tile->tile_x = j;
1219 tile->tile_y = i;
1220 tile->m_zoomLevel = zoomFactor;
1221 tile->m_bAvailable = true;
1222
1223 tzd->tileMap[index] = tile;
1224 }
1225
1226 if (!tile->m_bgeomSet) {
1227 tile->lonmin =
1228 round(tilex2long(tile->tile_x, zoomFactor) / eps) * eps;
1229 tile->lonmax =
1230 round(tilex2long(tile->tile_x + 1, zoomFactor) / eps) * eps;
1231 tile->latmin =
1232 round(tiley2lat(tile->tile_y - 1, zoomFactor) / eps) * eps;
1233 tile->latmax =
1234 round(tiley2lat(tile->tile_y, zoomFactor) / eps) * eps;
1235
1236 tile->box.Set(tile->latmin, tile->lonmin, tile->latmax,
1237 tile->lonmax);
1238 tile->m_bgeomSet = true;
1239 }
1240
1241 if (!Region.IntersectOut(tile->box)) RenderTile(tile, zoomFactor, vp);
1242 }
1243 } // for
1244 }
1245
1246 zoomFactor++;
1247 }
1248
1249 glDisable(GL_TEXTURE_2D);
1250
1251 m_zoomScaleFactor = 2.0 * OSM_zoomMPP[maxrenZoom] * VPoint.view_scale_ppm;
1252
1253 glChartCanvas::DisableClipRegion();
1254
1255 return true;
1256}
1257
1258bool ChartMBTiles::RenderRegionViewOnDC(wxMemoryDC &dc, const ViewPort &VPoint,
1259 const OCPNRegion &Region) {
1260 gFrame->GetPrimaryCanvas()->SetAlertString(
1261 _("MBTile requires OpenGL to be enabled"));
1262
1263 return true;
1264}