/src/gdal/alg/contour.cpp
Line | Count | Source |
1 | | /****************************************************************************** |
2 | | * |
3 | | * Project: Contour Generation |
4 | | * Purpose: Core algorithm implementation for contour line generation. |
5 | | * Author: Frank Warmerdam, warmerdam@pobox.com |
6 | | * |
7 | | ****************************************************************************** |
8 | | * Copyright (c) 2003, Frank Warmerdam <warmerdam@pobox.com> |
9 | | * Copyright (c) 2003, Applied Coherent Technology Corporation, www.actgate.com |
10 | | * Copyright (c) 2007-2013, Even Rouault <even dot rouault at spatialys.com> |
11 | | * Copyright (c) 2018, Oslandia <infos at oslandia dot com> |
12 | | * |
13 | | * SPDX-License-Identifier: MIT |
14 | | ****************************************************************************/ |
15 | | |
16 | | #include "level_generator.h" |
17 | | #include "polygon_ring_appender.h" |
18 | | #include "utility.h" |
19 | | #include "contour_generator.h" |
20 | | #include "segment_merger.h" |
21 | | #include <algorithm> |
22 | | |
23 | | #include "gdal.h" |
24 | | #include "gdal_alg.h" |
25 | | #include "cpl_conv.h" |
26 | | #include "cpl_error_internal.h" |
27 | | #include "cpl_string.h" |
28 | | #include "ogr_api.h" |
29 | | #include "ogr_srs_api.h" |
30 | | #include "ogr_geometry.h" |
31 | | |
32 | | #include <climits> |
33 | | #include <limits> |
34 | | |
35 | | static CPLErr OGRPolygonContourWriter(double dfLevelMin, double dfLevelMax, |
36 | | const OGRMultiPolygon &multipoly, |
37 | | void *pInfo) |
38 | | |
39 | 0 | { |
40 | 0 | OGRContourWriterInfo *poInfo = static_cast<OGRContourWriterInfo *>(pInfo); |
41 | |
|
42 | 0 | OGRFeatureDefnH hFDefn = |
43 | 0 | OGR_L_GetLayerDefn(static_cast<OGRLayerH>(poInfo->hLayer)); |
44 | |
|
45 | 0 | OGRFeatureH hFeat = OGR_F_Create(hFDefn); |
46 | |
|
47 | 0 | if (poInfo->nIDField != -1) |
48 | 0 | OGR_F_SetFieldInteger(hFeat, poInfo->nIDField, poInfo->nNextID++); |
49 | |
|
50 | 0 | if (poInfo->nElevFieldMin != -1) |
51 | 0 | OGR_F_SetFieldDouble(hFeat, poInfo->nElevFieldMin, dfLevelMin); |
52 | |
|
53 | 0 | if (poInfo->nElevFieldMax != -1) |
54 | 0 | OGR_F_SetFieldDouble(hFeat, poInfo->nElevFieldMax, dfLevelMax); |
55 | |
|
56 | 0 | const bool bHasZ = wkbHasZ(OGR_FD_GetGeomType(hFDefn)); |
57 | 0 | OGRGeometryH hGeom = |
58 | 0 | OGR_G_CreateGeometry(bHasZ ? wkbMultiPolygon25D : wkbMultiPolygon); |
59 | |
|
60 | 0 | for (int iPart = 0; iPart < multipoly.getNumGeometries(); iPart++) |
61 | 0 | { |
62 | 0 | OGRPolygon *poNewPoly = new OGRPolygon(); |
63 | 0 | const OGRPolygon *poPolygon = |
64 | 0 | static_cast<const OGRPolygon *>(multipoly.getGeometryRef(iPart)); |
65 | |
|
66 | 0 | for (int iRing = 0; iRing < poPolygon->getNumInteriorRings() + 1; |
67 | 0 | iRing++) |
68 | 0 | { |
69 | 0 | const OGRLinearRing *poRing = |
70 | 0 | iRing == 0 ? poPolygon->getExteriorRing() |
71 | 0 | : poPolygon->getInteriorRing(iRing - 1); |
72 | |
|
73 | 0 | OGRLinearRing *poNewRing = new OGRLinearRing(); |
74 | 0 | for (int iPoint = 0; iPoint < poRing->getNumPoints(); iPoint++) |
75 | 0 | { |
76 | 0 | const double dfX = |
77 | 0 | poInfo->adfGeoTransform[0] + |
78 | 0 | poInfo->adfGeoTransform[1] * poRing->getX(iPoint) + |
79 | 0 | poInfo->adfGeoTransform[2] * poRing->getY(iPoint); |
80 | 0 | const double dfY = |
81 | 0 | poInfo->adfGeoTransform[3] + |
82 | 0 | poInfo->adfGeoTransform[4] * poRing->getX(iPoint) + |
83 | 0 | poInfo->adfGeoTransform[5] * poRing->getY(iPoint); |
84 | 0 | if (bHasZ) |
85 | 0 | OGR_G_SetPoint(OGRGeometry::ToHandle(poNewRing), iPoint, |
86 | 0 | dfX, dfY, dfLevelMax); |
87 | 0 | else |
88 | 0 | OGR_G_SetPoint_2D(OGRGeometry::ToHandle(poNewRing), iPoint, |
89 | 0 | dfX, dfY); |
90 | 0 | } |
91 | 0 | poNewPoly->addRingDirectly(poNewRing); |
92 | 0 | } |
93 | 0 | OGR_G_AddGeometryDirectly(hGeom, OGRGeometry::ToHandle(poNewPoly)); |
94 | 0 | } |
95 | |
|
96 | 0 | OGR_F_SetGeometryDirectly(hFeat, hGeom); |
97 | |
|
98 | 0 | OGRErr eErr = |
99 | 0 | OGR_L_CreateFeature(static_cast<OGRLayerH>(poInfo->hLayer), hFeat); |
100 | 0 | OGR_F_Destroy(hFeat); |
101 | |
|
102 | 0 | if (eErr == OGRERR_NONE && poInfo->nTransactionCommitInterval > 0) |
103 | 0 | { |
104 | 0 | if (++poInfo->nWrittenFeatureCountSinceLastCommit == |
105 | 0 | poInfo->nTransactionCommitInterval) |
106 | 0 | { |
107 | 0 | poInfo->nWrittenFeatureCountSinceLastCommit = 0; |
108 | | // CPLDebug("CONTOUR", "Flush transaction"); |
109 | 0 | eErr = |
110 | 0 | OGR_L_CommitTransaction(static_cast<OGRLayerH>(poInfo->hLayer)); |
111 | 0 | if (eErr == OGRERR_NONE) |
112 | 0 | { |
113 | 0 | eErr = OGR_L_StartTransaction( |
114 | 0 | static_cast<OGRLayerH>(poInfo->hLayer)); |
115 | 0 | } |
116 | 0 | } |
117 | 0 | } |
118 | |
|
119 | 0 | return eErr == OGRERR_NONE ? CE_None : CE_Failure; |
120 | 0 | } |
121 | | |
122 | | struct PolygonContourWriter |
123 | | { |
124 | | CPL_DISALLOW_COPY_ASSIGN(PolygonContourWriter) |
125 | | |
126 | | explicit PolygonContourWriter(OGRContourWriterInfo *poInfo, double minLevel) |
127 | 0 | : poInfo_(poInfo), currentLevel_(minLevel) |
128 | 0 | { |
129 | 0 | } |
130 | | |
131 | | void startPolygon(double level) |
132 | 0 | { |
133 | 0 | previousLevel_ = currentLevel_; |
134 | 0 | currentGeometry_.reset(new OGRMultiPolygon()); |
135 | 0 | currentLevel_ = level; |
136 | 0 | } |
137 | | |
138 | | void endPolygon() |
139 | 0 | { |
140 | 0 | if (currentPart_) |
141 | 0 | { |
142 | 0 | currentGeometry_->addGeometryDirectly(currentPart_); |
143 | 0 | } |
144 | |
|
145 | 0 | if (currentGeometry_->getNumGeometries() > 0) |
146 | 0 | { |
147 | 0 | OGRPolygonContourWriter(previousLevel_, currentLevel_, |
148 | 0 | *currentGeometry_, poInfo_); |
149 | 0 | } |
150 | |
|
151 | 0 | currentGeometry_.reset(nullptr); |
152 | 0 | currentPart_ = nullptr; |
153 | 0 | } |
154 | | |
155 | | void addPart(const marching_squares::LineString &ring) |
156 | 0 | { |
157 | 0 | if (currentPart_) |
158 | 0 | { |
159 | 0 | currentGeometry_->addGeometryDirectly(currentPart_); |
160 | 0 | } |
161 | |
|
162 | 0 | OGRLinearRing *poNewRing = new OGRLinearRing(); |
163 | 0 | poNewRing->setNumPoints(int(ring.size())); |
164 | 0 | int iPoint = 0; |
165 | 0 | for (const auto &p : ring) |
166 | 0 | { |
167 | 0 | poNewRing->setPoint(iPoint, p.x, p.y); |
168 | 0 | iPoint++; |
169 | 0 | } |
170 | 0 | currentPart_ = new OGRPolygon(); |
171 | 0 | currentPart_->addRingDirectly(poNewRing); |
172 | 0 | } |
173 | | |
174 | | void addInteriorRing(const marching_squares::LineString &ring) |
175 | 0 | { |
176 | 0 | OGRLinearRing *poNewRing = new OGRLinearRing(); |
177 | 0 | for (const auto &p : ring) |
178 | 0 | { |
179 | 0 | poNewRing->addPoint(p.x, p.y); |
180 | 0 | } |
181 | 0 | currentPart_->addRingDirectly(poNewRing); |
182 | 0 | } |
183 | | |
184 | | std::unique_ptr<OGRMultiPolygon> currentGeometry_ = {}; |
185 | | OGRPolygon *currentPart_ = nullptr; |
186 | | OGRContourWriterInfo *poInfo_ = nullptr; |
187 | | double currentLevel_ = 0; |
188 | | double previousLevel_ = 0; |
189 | | }; |
190 | | |
191 | | struct GDALRingAppender |
192 | | { |
193 | | CPL_DISALLOW_COPY_ASSIGN(GDALRingAppender) |
194 | | |
195 | | GDALRingAppender(GDALContourWriter write, void *data) |
196 | 0 | : write_(write), data_(data) |
197 | 0 | { |
198 | 0 | } |
199 | | |
200 | | void addLine(double level, marching_squares::LineString &ls, |
201 | | bool /*closed*/) |
202 | 0 | { |
203 | 0 | const size_t sz = ls.size(); |
204 | 0 | std::vector<double> xs(sz), ys(sz); |
205 | 0 | size_t i = 0; |
206 | 0 | for (const auto &pt : ls) |
207 | 0 | { |
208 | 0 | xs[i] = pt.x; |
209 | 0 | ys[i] = pt.y; |
210 | 0 | i++; |
211 | 0 | } |
212 | |
|
213 | 0 | if (write_(level, int(sz), &xs[0], &ys[0], data_) != CE_None) |
214 | 0 | CPLError(CE_Failure, CPLE_AppDefined, "cannot write linestring"); |
215 | 0 | } |
216 | | |
217 | | private: |
218 | | GDALContourWriter write_; |
219 | | void *data_; |
220 | | }; |
221 | | |
222 | | /************************************************************************/ |
223 | | /* ==================================================================== */ |
224 | | /* Additional C Callable Functions */ |
225 | | /* ==================================================================== */ |
226 | | /************************************************************************/ |
227 | | |
228 | | /************************************************************************/ |
229 | | /* OGRContourWriter() */ |
230 | | /************************************************************************/ |
231 | | |
232 | | CPLErr OGRContourWriter(double dfLevel, int nPoints, double *padfX, |
233 | | double *padfY, void *pInfo) |
234 | | |
235 | 0 | { |
236 | 0 | OGRContourWriterInfo *poInfo = static_cast<OGRContourWriterInfo *>(pInfo); |
237 | |
|
238 | 0 | OGRFeatureDefnH hFDefn = |
239 | 0 | OGR_L_GetLayerDefn(static_cast<OGRLayerH>(poInfo->hLayer)); |
240 | |
|
241 | 0 | OGRFeatureH hFeat = OGR_F_Create(hFDefn); |
242 | |
|
243 | 0 | if (poInfo->nIDField != -1) |
244 | 0 | OGR_F_SetFieldInteger(hFeat, poInfo->nIDField, poInfo->nNextID++); |
245 | |
|
246 | 0 | if (poInfo->nElevField != -1) |
247 | 0 | OGR_F_SetFieldDouble(hFeat, poInfo->nElevField, dfLevel); |
248 | |
|
249 | 0 | const bool bHasZ = wkbHasZ(OGR_FD_GetGeomType(hFDefn)); |
250 | 0 | OGRGeometryH hGeom = |
251 | 0 | OGR_G_CreateGeometry(bHasZ ? wkbLineString25D : wkbLineString); |
252 | |
|
253 | 0 | for (int iPoint = nPoints - 1; iPoint >= 0; iPoint--) |
254 | 0 | { |
255 | 0 | const double dfX = poInfo->adfGeoTransform[0] + |
256 | 0 | poInfo->adfGeoTransform[1] * padfX[iPoint] + |
257 | 0 | poInfo->adfGeoTransform[2] * padfY[iPoint]; |
258 | 0 | const double dfY = poInfo->adfGeoTransform[3] + |
259 | 0 | poInfo->adfGeoTransform[4] * padfX[iPoint] + |
260 | 0 | poInfo->adfGeoTransform[5] * padfY[iPoint]; |
261 | 0 | if (bHasZ) |
262 | 0 | OGR_G_SetPoint(hGeom, iPoint, dfX, dfY, dfLevel); |
263 | 0 | else |
264 | 0 | OGR_G_SetPoint_2D(hGeom, iPoint, dfX, dfY); |
265 | 0 | } |
266 | |
|
267 | 0 | OGR_F_SetGeometryDirectly(hFeat, hGeom); |
268 | |
|
269 | 0 | const OGRErr eErr = |
270 | 0 | OGR_L_CreateFeature(static_cast<OGRLayerH>(poInfo->hLayer), hFeat); |
271 | 0 | OGR_F_Destroy(hFeat); |
272 | |
|
273 | 0 | return eErr == OGRERR_NONE ? CE_None : CE_Failure; |
274 | 0 | } |
275 | | |
276 | | /************************************************************************/ |
277 | | /* GDALContourGenerate() */ |
278 | | /************************************************************************/ |
279 | | |
280 | | /** |
281 | | * Create vector contours from raster DEM. |
282 | | * |
283 | | * This function is kept for compatibility reason and will call the new |
284 | | * variant GDALContourGenerateEx that is more extensible and provide more |
285 | | * options. |
286 | | * |
287 | | * Details about the algorithm are also given in the documentation of the |
288 | | * new GDALContourenerateEx function. |
289 | | * |
290 | | * @param hBand The band to read raster data from. The whole band will be |
291 | | * processed. |
292 | | * |
293 | | * @param dfContourInterval The elevation interval between contours generated. |
294 | | * |
295 | | * @param dfContourBase The "base" relative to which contour intervals are |
296 | | * applied. This is normally zero, but could be different. To generate 10m |
297 | | * contours at 5, 15, 25, ... the ContourBase would be 5. |
298 | | * |
299 | | * @param nFixedLevelCount The number of fixed levels. If this is greater than |
300 | | * zero, then fixed levels will be used, and ContourInterval and ContourBase |
301 | | * are ignored. |
302 | | * |
303 | | * @param padfFixedLevels The list of fixed contour levels at which contours |
304 | | * should be generated. It will contain FixedLevelCount entries, and may be |
305 | | * NULL if fixed levels are disabled (FixedLevelCount = 0). |
306 | | * |
307 | | * @param bUseNoData If TRUE the dfNoDataValue will be used. |
308 | | * |
309 | | * @param dfNoDataValue The value to use as a "nodata" value. That is, a |
310 | | * pixel value which should be ignored in generating contours as if the value |
311 | | * of the pixel were not known. |
312 | | * |
313 | | * @param hLayer The layer to which new contour vectors will be written. |
314 | | * Each contour will have a LINESTRING geometry attached to it. This |
315 | | * is really of type OGRLayerH, but void * is used to avoid pulling the |
316 | | * ogr_api.h file in here. |
317 | | * |
318 | | * @param iIDField If not -1 this will be used as a field index to indicate |
319 | | * where a unique id should be written for each feature (contour) written. |
320 | | * |
321 | | * @param iElevField If not -1 this will be used as a field index to indicate |
322 | | * where the elevation value of the contour should be written. |
323 | | * |
324 | | * @param pfnProgress A GDALProgressFunc that may be used to report progress |
325 | | * to the user, or to interrupt the algorithm. May be NULL if not required. |
326 | | * |
327 | | * @param pProgressArg The callback data for the pfnProgress function. |
328 | | * |
329 | | * @return CE_None on success or CE_Failure if an error occurs. |
330 | | */ |
331 | | |
332 | | CPLErr GDALContourGenerate(GDALRasterBandH hBand, double dfContourInterval, |
333 | | double dfContourBase, int nFixedLevelCount, |
334 | | double *padfFixedLevels, int bUseNoData, |
335 | | double dfNoDataValue, void *hLayer, int iIDField, |
336 | | int iElevField, GDALProgressFunc pfnProgress, |
337 | | void *pProgressArg) |
338 | 0 | { |
339 | 0 | char **options = nullptr; |
340 | 0 | if (nFixedLevelCount > 0) |
341 | 0 | { |
342 | 0 | std::string values = "FIXED_LEVELS="; |
343 | 0 | for (int i = 0; i < nFixedLevelCount; i++) |
344 | 0 | { |
345 | 0 | const int sz = 32; |
346 | 0 | char *newValue = new char[sz + 1]; |
347 | 0 | if (i == nFixedLevelCount - 1) |
348 | 0 | { |
349 | 0 | CPLsnprintf(newValue, sz + 1, "%f", padfFixedLevels[i]); |
350 | 0 | } |
351 | 0 | else |
352 | 0 | { |
353 | 0 | CPLsnprintf(newValue, sz + 1, "%f,", padfFixedLevels[i]); |
354 | 0 | } |
355 | 0 | values = values + std::string(newValue); |
356 | 0 | delete[] newValue; |
357 | 0 | } |
358 | 0 | options = CSLAddString(options, values.c_str()); |
359 | 0 | } |
360 | 0 | else if (dfContourInterval != 0.0) |
361 | 0 | { |
362 | 0 | options = |
363 | 0 | CSLAppendPrintf(options, "LEVEL_INTERVAL=%f", dfContourInterval); |
364 | 0 | } |
365 | |
|
366 | 0 | if (dfContourBase != 0.0) |
367 | 0 | { |
368 | 0 | options = CSLAppendPrintf(options, "LEVEL_BASE=%f", dfContourBase); |
369 | 0 | } |
370 | |
|
371 | 0 | if (bUseNoData) |
372 | 0 | { |
373 | 0 | options = CSLAppendPrintf(options, "NODATA=%.19g", dfNoDataValue); |
374 | 0 | } |
375 | 0 | if (iIDField != -1) |
376 | 0 | { |
377 | 0 | options = CSLAppendPrintf(options, "ID_FIELD=%d", iIDField); |
378 | 0 | } |
379 | 0 | if (iElevField != -1) |
380 | 0 | { |
381 | 0 | options = CSLAppendPrintf(options, "ELEV_FIELD=%d", iElevField); |
382 | 0 | } |
383 | |
|
384 | 0 | CPLErr err = GDALContourGenerateEx(hBand, hLayer, options, pfnProgress, |
385 | 0 | pProgressArg); |
386 | 0 | CSLDestroy(options); |
387 | |
|
388 | 0 | return err; |
389 | 0 | } |
390 | | |
391 | | /** |
392 | | * Create vector contours from raster DEM. |
393 | | * |
394 | | * This algorithm is an implementation of "Marching squares" [1] that will |
395 | | * generate contour vectors for the input raster band on the requested set |
396 | | * of contour levels. The vector contours are written to the passed in OGR |
397 | | * vector layer. Also, a NODATA value may be specified to identify pixels |
398 | | * that should not be considered in contour line generation. |
399 | | * |
400 | | * The gdal/apps/gdal_contour_bin.cpp mainline can be used as an example of |
401 | | * how to use this function. |
402 | | * |
403 | | * [1] see https://en.wikipedia.org/wiki/Marching_squares |
404 | | * |
405 | | * ALGORITHM RULES |
406 | | |
407 | | For contouring purposes raster pixel values are assumed to represent a point |
408 | | value at the center of the corresponding pixel region. For the purpose of |
409 | | contour generation we virtually connect each pixel center to the values to |
410 | | the left, right, top and bottom. We assume that the pixel value is linearly |
411 | | interpolated between the pixel centers along each line, and determine where |
412 | | (if any) contour lines will appear along these line segments. Then the |
413 | | contour crossings are connected. |
414 | | |
415 | | This means that contour lines' nodes will not actually be on pixel edges, but |
416 | | rather along vertical and horizontal lines connecting the pixel centers. |
417 | | |
418 | | \verbatim |
419 | | General Case: |
420 | | |
421 | | 5 | | 3 |
422 | | -- + ---------------- + -- |
423 | | | | |
424 | | | | |
425 | | | | |
426 | | | | |
427 | | 10 + | |
428 | | |\ | |
429 | | | \ | |
430 | | -- + -+-------------- + -- |
431 | | 12 | 10 | 1 |
432 | | |
433 | | Saddle Point: |
434 | | |
435 | | 5 | | 12 |
436 | | -- + -------------+-- + -- |
437 | | | \ | |
438 | | | \| |
439 | | | + |
440 | | | | |
441 | | + | |
442 | | |\ | |
443 | | | \ | |
444 | | -- + -+-------------- + -- |
445 | | 12 | | 1 |
446 | | |
447 | | or: |
448 | | |
449 | | 5 | | 12 |
450 | | -- + -------------+-- + -- |
451 | | | __/ | |
452 | | | ___/ | |
453 | | | ___/ __+ |
454 | | | / __/ | |
455 | | +' __/ | |
456 | | | __/ | |
457 | | | ,__/ | |
458 | | -- + -+-------------- + -- |
459 | | 12 | | 1 |
460 | | \endverbatim |
461 | | |
462 | | Nodata: |
463 | | |
464 | | In the "nodata" case we treat the whole nodata pixel as a no-mans land. |
465 | | We extend the corner pixels near the nodata out to half way and then |
466 | | construct extra lines from those points to the center which is assigned |
467 | | an averaged value from the two nearby points (in this case (12+3+5)/3). |
468 | | |
469 | | \verbatim |
470 | | 5 | | 3 |
471 | | -- + ---------------- + -- |
472 | | | | |
473 | | | | |
474 | | | 6.7 | |
475 | | | +---------+ 3 |
476 | | 10 +___ | |
477 | | | \____+ 10 |
478 | | | | |
479 | | -- + -------+ + |
480 | | 12 | 12 (nodata) |
481 | | |
482 | | \endverbatim |
483 | | |
484 | | * |
485 | | * @param hBand The band to read raster data from. The whole band will be |
486 | | * processed. |
487 | | * |
488 | | * @param hLayer The layer to which new contour vectors will be written. |
489 | | * Each contour will have a LINESTRING geometry attached to it |
490 | | * (or POLYGON if POLYGONIZE=YES). This is really of type OGRLayerH, but |
491 | | * void * is used to avoid pulling the ogr_api.h file in here. |
492 | | * |
493 | | * @param pfnProgress A GDALProgressFunc that may be used to report progress |
494 | | * to the user, or to interrupt the algorithm. May be NULL if not required. |
495 | | * |
496 | | * @param pProgressArg The callback data for the pfnProgress function. |
497 | | * |
498 | | * @param options List of options |
499 | | * |
500 | | * Options: |
501 | | * |
502 | | * LEVEL_INTERVAL=f |
503 | | * |
504 | | * The elevation interval between contours generated. |
505 | | * |
506 | | * LEVEL_BASE=f |
507 | | * |
508 | | * The "base" relative to which contour intervals are |
509 | | * applied. This is normally zero, but could be different. To generate 10m |
510 | | * contours at 5, 15, 25, ... the LEVEL_BASE would be 5. |
511 | | * |
512 | | * LEVEL_EXP_BASE=f |
513 | | * |
514 | | * If greater than 0, contour levels are generated on an |
515 | | * exponential scale. Levels will then be generated by LEVEL_EXP_BASE^k |
516 | | * where k is a positive integer. |
517 | | * |
518 | | * FIXED_LEVELS=f[,f]* |
519 | | * |
520 | | * The list of fixed contour levels at which contours should be generated. |
521 | | * This option has precedence on LEVEL_INTERVAL. MIN and MAX can be used |
522 | | * as special values to represent the minimum and maximum values of the |
523 | | * raster. |
524 | | * |
525 | | * NODATA=f |
526 | | * |
527 | | * The value to use as a "nodata" value. That is, a pixel value which |
528 | | * should be ignored in generating contours as if the value of the pixel |
529 | | * were not known. |
530 | | * |
531 | | * ID_FIELD=d |
532 | | * |
533 | | * This will be used as a field index to indicate where a unique id should |
534 | | * be written for each feature (contour) written. |
535 | | * |
536 | | * ELEV_FIELD=d |
537 | | * |
538 | | * This will be used as a field index to indicate where the elevation value |
539 | | * of the contour should be written. Only used in line contouring mode. |
540 | | * |
541 | | * ELEV_FIELD_MIN=d |
542 | | * |
543 | | * This will be used as a field index to indicate where the minimum elevation |
544 | | value |
545 | | * of the polygon contour should be written. Only used in polygonal contouring |
546 | | mode. |
547 | | * |
548 | | * ELEV_FIELD_MAX=d |
549 | | * |
550 | | * This will be used as a field index to indicate where the maximum elevation |
551 | | value |
552 | | * of the polygon contour should be written. Only used in polygonal contouring |
553 | | mode. |
554 | | * |
555 | | * POLYGONIZE=YES|NO |
556 | | * |
557 | | * If YES, contour polygons will be created, rather than polygon lines. |
558 | | * |
559 | | * |
560 | | * COMMIT_INTERVAL=num |
561 | | * |
562 | | * (GDAL >= 3.10) Interval in number of features at which transactions must be |
563 | | * flushed. The default value of 0 means that no transactions are opened. |
564 | | * A negative value means a single transaction. The function takes care of |
565 | | * issuing the starting transaction and committing the final one. |
566 | | * |
567 | | * @return CE_None on success or CE_Failure if an error occurs. |
568 | | */ |
569 | | CPLErr GDALContourGenerateEx(GDALRasterBandH hBand, void *hLayer, |
570 | | CSLConstList options, GDALProgressFunc pfnProgress, |
571 | | void *pProgressArg) |
572 | 0 | { |
573 | 0 | VALIDATE_POINTER1(hBand, "GDALContourGenerateEx", CE_Failure); |
574 | | |
575 | 0 | if (pfnProgress == nullptr) |
576 | 0 | pfnProgress = GDALDummyProgress; |
577 | |
|
578 | 0 | double contourInterval = 0.0; |
579 | 0 | const char *opt = CSLFetchNameValue(options, "LEVEL_INTERVAL"); |
580 | 0 | if (opt) |
581 | 0 | { |
582 | 0 | contourInterval = CPLAtof(opt); |
583 | | // Written this way to catch NaN as well. |
584 | 0 | if (!(contourInterval > 0)) |
585 | 0 | { |
586 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
587 | 0 | "Invalid value for LEVEL_INTERVAL. Should be strictly " |
588 | 0 | "positive."); |
589 | 0 | return CE_Failure; |
590 | 0 | } |
591 | 0 | } |
592 | | |
593 | 0 | double contourBase = 0.0; |
594 | 0 | opt = CSLFetchNameValue(options, "LEVEL_BASE"); |
595 | 0 | if (opt) |
596 | 0 | { |
597 | 0 | contourBase = CPLAtof(opt); |
598 | 0 | } |
599 | |
|
600 | 0 | double expBase = 0.0; |
601 | 0 | opt = CSLFetchNameValue(options, "LEVEL_EXP_BASE"); |
602 | 0 | if (opt) |
603 | 0 | { |
604 | 0 | expBase = CPLAtof(opt); |
605 | 0 | } |
606 | |
|
607 | 0 | std::vector<double> fixedLevels; |
608 | 0 | opt = CSLFetchNameValue(options, "FIXED_LEVELS"); |
609 | 0 | if (opt) |
610 | 0 | { |
611 | 0 | const CPLStringList aosLevels( |
612 | 0 | CSLTokenizeStringComplex(opt, ",", FALSE, FALSE)); |
613 | 0 | fixedLevels.resize(aosLevels.size()); |
614 | 0 | for (size_t i = 0; i < fixedLevels.size(); i++) |
615 | 0 | { |
616 | | // Handle min/max values |
617 | 0 | if (EQUAL(aosLevels[i], "MIN")) |
618 | 0 | { |
619 | 0 | fixedLevels[i] = std::numeric_limits<double>::lowest(); |
620 | 0 | } |
621 | 0 | else if (EQUAL(aosLevels[i], "MAX")) |
622 | 0 | { |
623 | 0 | fixedLevels[i] = std::numeric_limits<double>::max(); |
624 | 0 | } |
625 | 0 | else |
626 | 0 | { |
627 | 0 | fixedLevels[i] = CPLAtof(aosLevels[i]); |
628 | 0 | } |
629 | 0 | if (i > 0 && !(fixedLevels[i] >= fixedLevels[i - 1])) |
630 | 0 | { |
631 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
632 | 0 | "FIXED_LEVELS should be strictly increasing"); |
633 | 0 | return CE_Failure; |
634 | 0 | } |
635 | 0 | } |
636 | 0 | } |
637 | | |
638 | 0 | bool useNoData = false; |
639 | 0 | double noDataValue = 0.0; |
640 | 0 | opt = CSLFetchNameValue(options, "NODATA"); |
641 | 0 | if (opt) |
642 | 0 | { |
643 | 0 | useNoData = true; |
644 | 0 | noDataValue = CPLAtof(opt); |
645 | 0 | if (GDALGetRasterDataType(hBand) == GDT_Float32) |
646 | 0 | { |
647 | 0 | int bClamped = FALSE; |
648 | 0 | int bRounded = FALSE; |
649 | 0 | noDataValue = GDALAdjustValueToDataType(GDT_Float32, noDataValue, |
650 | 0 | &bClamped, &bRounded); |
651 | 0 | } |
652 | 0 | } |
653 | |
|
654 | 0 | int idField = -1; |
655 | 0 | opt = CSLFetchNameValue(options, "ID_FIELD"); |
656 | 0 | if (opt) |
657 | 0 | { |
658 | 0 | idField = atoi(opt); |
659 | 0 | } |
660 | |
|
661 | 0 | int elevField = -1; |
662 | 0 | opt = CSLFetchNameValue(options, "ELEV_FIELD"); |
663 | 0 | if (opt) |
664 | 0 | { |
665 | 0 | elevField = atoi(opt); |
666 | 0 | } |
667 | |
|
668 | 0 | int elevFieldMin = -1; |
669 | 0 | opt = CSLFetchNameValue(options, "ELEV_FIELD_MIN"); |
670 | 0 | if (opt) |
671 | 0 | { |
672 | 0 | elevFieldMin = atoi(opt); |
673 | 0 | } |
674 | |
|
675 | 0 | int elevFieldMax = -1; |
676 | 0 | opt = CSLFetchNameValue(options, "ELEV_FIELD_MAX"); |
677 | 0 | if (opt) |
678 | 0 | { |
679 | 0 | elevFieldMax = atoi(opt); |
680 | 0 | } |
681 | |
|
682 | 0 | bool polygonize = CPLFetchBool(options, "POLYGONIZE", false); |
683 | |
|
684 | 0 | int bSuccessMin = FALSE; |
685 | 0 | double dfMinimum = GDALGetRasterMinimum(hBand, &bSuccessMin); |
686 | 0 | int bSuccessMax = FALSE; |
687 | 0 | double dfMaximum = GDALGetRasterMaximum(hBand, &bSuccessMax); |
688 | 0 | if ((!bSuccessMin || !bSuccessMax)) |
689 | 0 | { |
690 | 0 | double adfMinMax[2]; |
691 | 0 | CPLErrorAccumulator oAccumulator; |
692 | 0 | bool bGotMinMax; |
693 | 0 | { |
694 | 0 | CPLErrorStateBackuper oBackuper; |
695 | 0 | auto oAccumulatorContext = oAccumulator.InstallForCurrentScope(); |
696 | 0 | CPL_IGNORE_RET_VAL(oAccumulatorContext); |
697 | 0 | bGotMinMax = |
698 | 0 | (GDALComputeRasterMinMax(hBand, false, adfMinMax) == CE_None); |
699 | 0 | } |
700 | 0 | if (bGotMinMax) |
701 | 0 | { |
702 | 0 | dfMinimum = adfMinMax[0]; |
703 | 0 | dfMaximum = adfMinMax[1]; |
704 | 0 | } |
705 | 0 | else |
706 | 0 | { |
707 | 0 | bool bNoValidPixels = false; |
708 | 0 | for (const auto &sError : oAccumulator.GetErrors()) |
709 | 0 | { |
710 | 0 | if (sError.msg.find("no valid pixels found in sampling") != |
711 | 0 | std::string::npos) |
712 | 0 | { |
713 | 0 | bNoValidPixels = true; |
714 | 0 | } |
715 | 0 | else if (sError.type == CE_Debug) |
716 | 0 | { |
717 | 0 | CPLDebug("GDAL", "%s", sError.msg.c_str()); |
718 | 0 | } |
719 | 0 | else |
720 | 0 | { |
721 | 0 | CPLError(sError.type, sError.no, "%s", sError.msg.c_str()); |
722 | 0 | } |
723 | 0 | } |
724 | 0 | if (bNoValidPixels && oAccumulator.GetErrors().size() == 1) |
725 | 0 | { |
726 | 0 | CPLDebug("GDAL", |
727 | 0 | "contour: no valid pixels found in input band"); |
728 | 0 | return CE_None; |
729 | 0 | } |
730 | 0 | return CE_Failure; |
731 | 0 | } |
732 | 0 | } |
733 | | |
734 | 0 | using namespace marching_squares; |
735 | |
|
736 | 0 | OGRContourWriterInfo oCWI; |
737 | 0 | oCWI.hLayer = static_cast<OGRLayerH>(hLayer); |
738 | 0 | oCWI.nElevField = elevField; |
739 | 0 | oCWI.nElevFieldMin = elevFieldMin; |
740 | 0 | oCWI.nElevFieldMax = elevFieldMax; |
741 | 0 | oCWI.nIDField = idField; |
742 | 0 | oCWI.adfGeoTransform[0] = 0.0; |
743 | 0 | oCWI.adfGeoTransform[1] = 1.0; |
744 | 0 | oCWI.adfGeoTransform[2] = 0.0; |
745 | 0 | oCWI.adfGeoTransform[3] = 0.0; |
746 | 0 | oCWI.adfGeoTransform[4] = 0.0; |
747 | 0 | oCWI.adfGeoTransform[5] = 1.0; |
748 | 0 | GDALDatasetH hSrcDS = GDALGetBandDataset(hBand); |
749 | 0 | if (hSrcDS != nullptr) |
750 | 0 | GDALGetGeoTransform(hSrcDS, oCWI.adfGeoTransform); |
751 | 0 | oCWI.nNextID = 0; |
752 | 0 | oCWI.nWrittenFeatureCountSinceLastCommit = 0; |
753 | 0 | oCWI.nTransactionCommitInterval = |
754 | 0 | CPLAtoGIntBig(CSLFetchNameValueDef(options, "COMMIT_INTERVAL", "0")); |
755 | |
|
756 | 0 | if (oCWI.nTransactionCommitInterval < 0) |
757 | 0 | oCWI.nTransactionCommitInterval = std::numeric_limits<GIntBig>::max(); |
758 | 0 | if (oCWI.nTransactionCommitInterval > 0) |
759 | 0 | { |
760 | 0 | if (OGR_L_StartTransaction(static_cast<OGRLayerH>(hLayer)) != |
761 | 0 | OGRERR_NONE) |
762 | 0 | { |
763 | 0 | oCWI.nTransactionCommitInterval = 0; |
764 | 0 | } |
765 | 0 | } |
766 | |
|
767 | 0 | bool ok = true; |
768 | | |
769 | | // Replace fixed levels min/max values with raster min/max values |
770 | 0 | if (!fixedLevels.empty()) |
771 | 0 | { |
772 | 0 | if (fixedLevels[0] == std::numeric_limits<double>::lowest()) |
773 | 0 | { |
774 | 0 | fixedLevels[0] = dfMinimum; |
775 | 0 | } |
776 | 0 | if (fixedLevels.back() == std::numeric_limits<double>::max()) |
777 | 0 | { |
778 | 0 | fixedLevels.back() = dfMaximum; |
779 | 0 | } |
780 | 0 | } |
781 | |
|
782 | 0 | try |
783 | 0 | { |
784 | 0 | if (polygonize) |
785 | 0 | { |
786 | |
|
787 | 0 | if (!fixedLevels.empty()) |
788 | 0 | { |
789 | | |
790 | | // If the minimum raster value is larger than the first requested |
791 | | // level, select the requested level that is just below the |
792 | | // minimum raster value |
793 | 0 | if (fixedLevels[0] < dfMinimum) |
794 | 0 | { |
795 | 0 | for (size_t i = 1; i < fixedLevels.size(); ++i) |
796 | 0 | { |
797 | 0 | if (fixedLevels[i] >= dfMinimum) |
798 | 0 | { |
799 | 0 | dfMinimum = fixedLevels[i - 1]; |
800 | 0 | break; |
801 | 0 | } |
802 | 0 | } |
803 | 0 | } |
804 | | |
805 | | // Largest requested level (levels are sorted) |
806 | 0 | const double maxLevel{fixedLevels.back()}; |
807 | | |
808 | | // If the maximum raster value is smaller than the last requested |
809 | | // level, select the requested level that is just above the |
810 | | // maximum raster value |
811 | 0 | if (maxLevel > dfMaximum) |
812 | 0 | { |
813 | 0 | for (size_t i = fixedLevels.size() - 1; i > 0; --i) |
814 | 0 | { |
815 | 0 | if (fixedLevels[i] <= dfMaximum) |
816 | 0 | { |
817 | 0 | dfMaximum = fixedLevels[i + 1]; |
818 | 0 | break; |
819 | 0 | } |
820 | 0 | } |
821 | 0 | } |
822 | | |
823 | | // If the maximum raster value is equal to the last requested |
824 | | // level, add a small value to the maximum to avoid skipping the |
825 | | // last level polygons |
826 | 0 | if (maxLevel == dfMaximum) |
827 | 0 | { |
828 | 0 | dfMaximum = std::nextafter( |
829 | 0 | dfMaximum, std::numeric_limits<double>::infinity()); |
830 | 0 | } |
831 | 0 | } |
832 | |
|
833 | 0 | PolygonContourWriter w(&oCWI, dfMinimum); |
834 | 0 | typedef PolygonRingAppender<PolygonContourWriter> RingAppender; |
835 | 0 | RingAppender appender(w); |
836 | |
|
837 | 0 | if (expBase > 0.0) |
838 | 0 | { |
839 | | // Do not provide the actual minimum value to level iterator |
840 | | // in polygonal case, otherwise it can result in a polygon |
841 | | // with a degenerate min=max range. |
842 | 0 | ExponentialLevelRangeIterator generator( |
843 | 0 | expBase, -std::numeric_limits<double>::infinity()); |
844 | 0 | auto levelIt{generator.range(dfMinimum, dfMaximum)}; |
845 | 0 | for (auto i = levelIt.begin(); i != levelIt.end(); ++i) |
846 | 0 | { |
847 | 0 | const double level = (*i).second; |
848 | 0 | fixedLevels.push_back(level); |
849 | 0 | } |
850 | | // Append minimum value to fixed levels |
851 | 0 | fixedLevels.push_back(dfMinimum); |
852 | | // Append maximum value to fixed levels |
853 | 0 | fixedLevels.push_back(dfMaximum); |
854 | 0 | } |
855 | 0 | else if (contourInterval != 0) |
856 | 0 | { |
857 | | // Do not provide the actual minimum value to level iterator |
858 | | // in polygonal case, otherwise it can result in a polygon |
859 | | // with a degenerate min=max range. |
860 | 0 | IntervalLevelRangeIterator generator( |
861 | 0 | contourBase, contourInterval, |
862 | 0 | -std::numeric_limits<double>::infinity()); |
863 | 0 | auto levelIt{generator.range(dfMinimum, dfMaximum)}; |
864 | 0 | for (auto i = levelIt.begin(); i != levelIt.end(); ++i) |
865 | 0 | { |
866 | 0 | const double level = (*i).second; |
867 | 0 | fixedLevels.push_back(level); |
868 | 0 | } |
869 | | // Append minimum value to fixed levels |
870 | 0 | fixedLevels.push_back(dfMinimum); |
871 | | // Append maximum value to fixed levels |
872 | 0 | fixedLevels.push_back(dfMaximum); |
873 | 0 | } |
874 | |
|
875 | 0 | if (!fixedLevels.empty()) |
876 | 0 | { |
877 | 0 | std::sort(fixedLevels.begin(), fixedLevels.end()); |
878 | 0 | auto uniqueIt = |
879 | 0 | std::unique(fixedLevels.begin(), fixedLevels.end()); |
880 | 0 | fixedLevels.erase(uniqueIt, fixedLevels.end()); |
881 | | // Do not provide the actual minimum value to level iterator |
882 | | // in polygonal case, otherwise it can result in a polygon |
883 | | // with a degenerate min=max range. |
884 | 0 | FixedLevelRangeIterator levels( |
885 | 0 | &fixedLevels[0], fixedLevels.size(), |
886 | 0 | -std::numeric_limits<double>::infinity(), dfMaximum); |
887 | 0 | SegmentMerger<RingAppender, FixedLevelRangeIterator> writer( |
888 | 0 | appender, levels, /* polygonize */ true); |
889 | 0 | std::vector<int> aoiSkipLevels; |
890 | | // Skip first and last levels (min/max) in polygonal case |
891 | 0 | aoiSkipLevels.push_back(0); |
892 | 0 | aoiSkipLevels.push_back(static_cast<int>(levels.levelsCount())); |
893 | 0 | writer.setSkipLevels(aoiSkipLevels); |
894 | 0 | ContourGeneratorFromRaster<decltype(writer), |
895 | 0 | FixedLevelRangeIterator> |
896 | 0 | cg(hBand, useNoData, noDataValue, writer, levels); |
897 | 0 | ok = cg.process(pfnProgress, pProgressArg); |
898 | 0 | } |
899 | 0 | } |
900 | 0 | else |
901 | 0 | { |
902 | 0 | GDALRingAppender appender(OGRContourWriter, &oCWI); |
903 | | |
904 | | // Append all exp levels to fixed levels |
905 | 0 | if (expBase > 0.0) |
906 | 0 | { |
907 | 0 | ExponentialLevelRangeIterator generator(expBase, dfMinimum); |
908 | 0 | auto levelIt{generator.range(dfMinimum, dfMaximum)}; |
909 | 0 | for (auto i = levelIt.begin(); i != levelIt.end(); ++i) |
910 | 0 | { |
911 | 0 | const double level = (*i).second; |
912 | 0 | fixedLevels.push_back(level); |
913 | 0 | } |
914 | 0 | } |
915 | 0 | else if (contourInterval != 0) |
916 | 0 | { |
917 | 0 | IntervalLevelRangeIterator levels(contourBase, contourInterval, |
918 | 0 | dfMinimum); |
919 | 0 | auto levelIt{levels.range(dfMinimum, dfMaximum)}; |
920 | 0 | for (auto i = levelIt.begin(); i != levelIt.end(); ++i) |
921 | 0 | { |
922 | 0 | const double level = (*i).second; |
923 | 0 | fixedLevels.push_back(level); |
924 | 0 | } |
925 | 0 | } |
926 | |
|
927 | 0 | if (!fixedLevels.empty()) |
928 | 0 | { |
929 | 0 | std::sort(fixedLevels.begin(), fixedLevels.end()); |
930 | 0 | auto uniqueIt = |
931 | 0 | std::unique(fixedLevels.begin(), fixedLevels.end()); |
932 | 0 | fixedLevels.erase(uniqueIt, fixedLevels.end()); |
933 | 0 | FixedLevelRangeIterator levels( |
934 | 0 | &fixedLevels[0], fixedLevels.size(), dfMinimum, dfMaximum); |
935 | 0 | SegmentMerger<GDALRingAppender, FixedLevelRangeIterator> writer( |
936 | 0 | appender, levels, /* polygonize */ false); |
937 | 0 | ContourGeneratorFromRaster<decltype(writer), |
938 | 0 | FixedLevelRangeIterator> |
939 | 0 | cg(hBand, useNoData, noDataValue, writer, levels); |
940 | 0 | ok = cg.process(pfnProgress, pProgressArg); |
941 | 0 | } |
942 | 0 | } |
943 | 0 | } |
944 | 0 | catch (const std::exception &e) |
945 | 0 | { |
946 | 0 | CPLError(CE_Failure, CPLE_AppDefined, "%s", e.what()); |
947 | 0 | return CE_Failure; |
948 | 0 | } |
949 | | |
950 | 0 | if (oCWI.nTransactionCommitInterval > 0) |
951 | 0 | { |
952 | | // CPLDebug("CONTOUR", "Flush transaction"); |
953 | 0 | if (OGR_L_CommitTransaction(static_cast<OGRLayerH>(hLayer)) != |
954 | 0 | OGRERR_NONE) |
955 | 0 | { |
956 | 0 | ok = false; |
957 | 0 | } |
958 | 0 | } |
959 | |
|
960 | 0 | if (ok) |
961 | 0 | { |
962 | 0 | pfnProgress(1.0, "", pProgressArg); |
963 | 0 | } |
964 | |
|
965 | 0 | return ok ? CE_None : CE_Failure; |
966 | 0 | } |
967 | | |
968 | | /************************************************************************/ |
969 | | /* GDAL_CG_Create() */ |
970 | | /************************************************************************/ |
971 | | |
972 | | namespace marching_squares |
973 | | { |
974 | | |
975 | | // Opaque type used by the C API |
976 | | struct ContourGeneratorOpaque |
977 | | { |
978 | | typedef SegmentMerger<GDALRingAppender, IntervalLevelRangeIterator> |
979 | | SegmentMergerT; |
980 | | typedef ContourGenerator<SegmentMergerT, IntervalLevelRangeIterator> |
981 | | ContourGeneratorT; |
982 | | |
983 | | ContourGeneratorOpaque(int nWidth, int nHeight, int bNoDataSet, |
984 | | double dfNoDataValue, double dfContourInterval, |
985 | | double dfContourBase, GDALContourWriter pfnWriter, |
986 | | void *pCBData) |
987 | 0 | : levels(dfContourBase, dfContourInterval, |
988 | 0 | -std::numeric_limits<double>::infinity()), |
989 | 0 | writer(pfnWriter, pCBData), |
990 | 0 | merger(writer, levels, /* polygonize */ false), |
991 | 0 | contourGenerator(nWidth, nHeight, bNoDataSet != 0, dfNoDataValue, |
992 | 0 | merger, levels) |
993 | 0 | { |
994 | 0 | } |
995 | | |
996 | | IntervalLevelRangeIterator levels; |
997 | | GDALRingAppender writer; |
998 | | SegmentMergerT merger; |
999 | | ContourGeneratorT contourGenerator; |
1000 | | }; |
1001 | | |
1002 | | } // namespace marching_squares |
1003 | | |
1004 | | /** Create contour generator */ |
1005 | | GDALContourGeneratorH GDAL_CG_Create(int nWidth, int nHeight, int bNoDataSet, |
1006 | | double dfNoDataValue, |
1007 | | double dfContourInterval, |
1008 | | double dfContourBase, |
1009 | | GDALContourWriter pfnWriter, void *pCBData) |
1010 | | |
1011 | 0 | { |
1012 | 0 | auto cg = new marching_squares::ContourGeneratorOpaque( |
1013 | 0 | nWidth, nHeight, bNoDataSet, dfNoDataValue, dfContourInterval, |
1014 | 0 | dfContourBase, pfnWriter, pCBData); |
1015 | |
|
1016 | 0 | return reinterpret_cast<GDALContourGeneratorH>(cg); |
1017 | 0 | } |
1018 | | |
1019 | | /************************************************************************/ |
1020 | | /* GDAL_CG_FeedLine() */ |
1021 | | /************************************************************************/ |
1022 | | |
1023 | | /** Feed a line to the contour generator */ |
1024 | | CPLErr GDAL_CG_FeedLine(GDALContourGeneratorH hCG, double *padfScanline) |
1025 | | |
1026 | 0 | { |
1027 | 0 | VALIDATE_POINTER1(hCG, "GDAL_CG_FeedLine", CE_Failure); |
1028 | 0 | return reinterpret_cast<marching_squares::ContourGeneratorOpaque *>(hCG) |
1029 | 0 | ->contourGenerator.feedLine(padfScanline); |
1030 | 0 | } |
1031 | | |
1032 | | /************************************************************************/ |
1033 | | /* GDAL_CG_Destroy() */ |
1034 | | /************************************************************************/ |
1035 | | |
1036 | | /** Destroy contour generator */ |
1037 | | void GDAL_CG_Destroy(GDALContourGeneratorH hCG) |
1038 | | |
1039 | 0 | { |
1040 | 0 | delete reinterpret_cast<marching_squares::ContourGeneratorOpaque *>(hCG); |
1041 | 0 | } |