Line data Source code
1 : /****************************************************************************/
2 : // Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3 : // Copyright (C) 2011-2026 German Aerospace Center (DLR) and others.
4 : // This program and the accompanying materials are made available under the
5 : // terms of the Eclipse Public License 2.0 which is available at
6 : // https://www.eclipse.org/legal/epl-2.0/
7 : // This Source Code may also be made available under the following Secondary
8 : // Licenses when the conditions for such availability set forth in the Eclipse
9 : // Public License 2.0 are satisfied: GNU General Public License, version 2
10 : // or later which is available at
11 : // https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
12 : // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
13 : /****************************************************************************/
14 : /// @file NBHeightMapper.cpp
15 : /// @author Jakob Erdmann
16 : /// @author Laura Bieker
17 : /// @author Michael Behrisch
18 : /// @date Sept 2011
19 : ///
20 : // Set z-values for all network positions based on data from a height map
21 : /****************************************************************************/
22 : #include <config.h>
23 :
24 : #include <string>
25 : #include <utils/common/MsgHandler.h>
26 : #include <utils/common/ToString.h>
27 : #include <utils/common/StringUtils.h>
28 : #include <utils/options/OptionsCont.h>
29 : #include <utils/geom/GeomHelper.h>
30 : #include "NBHeightMapper.h"
31 : #include <utils/geom/GeoConvHelper.h>
32 : #include <utils/common/RGBColor.h>
33 :
34 : #ifdef HAVE_GDAL
35 : #ifdef _MSC_VER
36 : #pragma warning(push)
37 : #pragma warning(disable: 4435 5219 5220)
38 : #endif
39 : #if __GNUC__ > 3
40 : #pragma GCC diagnostic push
41 : #pragma GCC diagnostic ignored "-Wpedantic"
42 : #endif
43 : #include <ogrsf_frmts.h>
44 : #include <ogr_api.h>
45 : #include <gdal_priv.h>
46 : #if __GNUC__ > 3
47 : #pragma GCC diagnostic pop
48 : #endif
49 : #ifdef _MSC_VER
50 : #pragma warning(pop)
51 : #endif
52 : #endif
53 :
54 : // ===========================================================================
55 : // static members
56 : // ===========================================================================
57 : NBHeightMapper NBHeightMapper::myInstance;
58 :
59 :
60 : // ===========================================================================
61 : // method definitions
62 : // ===========================================================================
63 2525 : NBHeightMapper::NBHeightMapper():
64 2525 : myRTree(&Triangle::addSelf) {
65 2525 : }
66 :
67 :
68 2525 : NBHeightMapper::~NBHeightMapper() {
69 2525 : clearData();
70 2525 : }
71 :
72 :
73 : const NBHeightMapper&
74 593682 : NBHeightMapper::get() {
75 593682 : return myInstance;
76 : }
77 :
78 :
79 : bool
80 595217 : NBHeightMapper::ready() const {
81 595217 : return myRasters.size() > 0 || myTriangles.size() > 0;
82 : }
83 :
84 :
85 : double
86 1535 : NBHeightMapper::getZ(const Position& geo) const {
87 1535 : if (!ready()) {
88 0 : WRITE_WARNING(TL("Cannot supply height since no height data was loaded"));
89 0 : return 0;
90 : }
91 2383 : for (auto& item : myRasters) {
92 1532 : const Boundary& boundary = item.boundary;
93 1532 : float* raster = item.raster;
94 : double result = -1e6;
95 :
96 1532 : double x = geo.x();
97 1532 : double y = geo.y();
98 :
99 : #ifdef HAVE_GDAL
100 : // Transform geo coordinates to the coordinate system of this
101 : // raster image for lookup in its raster, if applicable.
102 1532 : if (item.transform != nullptr) {
103 : // Since the input coordinates are always WGS84 (they may be
104 : // transformed to it in NBNetBuilder::transformCoordinate), and
105 : // WGS84 uses latitude-longitude order (y-x), we have to swap the
106 : // input coordinates here.
107 : std::swap(x, y);
108 :
109 1532 : item.transform->Transform(1, &x, &y);
110 : }
111 : #endif
112 :
113 1532 : if (boundary.around2D(x, y)) {
114 684 : const int xSize = item.xSize;
115 684 : const double normX = (x - boundary.xmin()) / mySizeOfPixel.x();
116 684 : const double normY = (y - boundary.ymax()) / mySizeOfPixel.y();
117 684 : PositionVector corners;
118 684 : corners.push_back(Position(floor(normX) + 0.5, floor(normY) + 0.5, raster[(int)normY * xSize + (int)normX]));
119 684 : if (normX - floor(normX) > 0.5) {
120 294 : corners.push_back(Position(floor(normX) + 1.5, floor(normY) + 0.5, raster[(int)normY * xSize + (int)normX + 1]));
121 : } else {
122 390 : corners.push_back(Position(floor(normX) - 0.5, floor(normY) + 0.5, raster[(int)normY * xSize + (int)normX - 1]));
123 : }
124 684 : if (normY - floor(normY) > 0.5 && ((int)normY + 1) < item.ySize) {
125 325 : corners.push_back(Position(floor(normX) + 0.5, floor(normY) + 1.5, raster[((int)normY + 1) * xSize + (int)normX]));
126 : } else {
127 359 : corners.push_back(Position(floor(normX) + 0.5, floor(normY) - 0.5, raster[((int)normY - 1) * xSize + (int)normX]));
128 : }
129 684 : result = Triangle(corners).getZ(Position(normX, normY));
130 684 : }
131 684 : if (result > -1e5 && result < 1e5) {
132 684 : return result;
133 : }
134 : }
135 : // coordinates in degrees hence a small search window
136 : float minB[2];
137 : float maxB[2];
138 851 : minB[0] = (float)geo.x() - 0.00001f;
139 851 : minB[1] = (float)geo.y() - 0.00001f;
140 851 : maxB[0] = (float)geo.x() + 0.00001f;
141 851 : maxB[1] = (float)geo.y() + 0.00001f;
142 : QueryResult queryResult;
143 851 : int hits = myRTree.Search(minB, maxB, queryResult);
144 851 : Triangles result = queryResult.triangles;
145 : assert(hits == (int)result.size());
146 : UNUSED_PARAMETER(hits); // only used for assertion
147 :
148 852 : for (Triangles::iterator it = result.begin(); it != result.end(); it++) {
149 4 : const Triangle* triangle = *it;
150 4 : if (triangle->contains(geo)) {
151 3 : return triangle->getZ(geo);
152 : }
153 : }
154 1696 : WRITE_WARNINGF(TL("Could not get height data for coordinate %"), toString(geo));
155 848 : return 0;
156 851 : }
157 :
158 :
159 : void
160 3 : NBHeightMapper::addTriangle(PositionVector corners) {
161 3 : Triangle* triangle = new Triangle(corners);
162 3 : myTriangles.push_back(triangle);
163 3 : Boundary b = corners.getBoxBoundary();
164 3 : const float cmin[2] = {(float) b.xmin(), (float) b.ymin()};
165 3 : const float cmax[2] = {(float) b.xmax(), (float) b.ymax()};
166 3 : myRTree.Insert(cmin, cmax, triangle);
167 3 : }
168 :
169 :
170 : void
171 2373 : NBHeightMapper::loadIfSet(OptionsCont& oc) {
172 4746 : if (oc.isSet("heightmap.geotiff")) {
173 : // parse file(s)
174 10 : std::vector<std::string> files = oc.getStringVector("heightmap.geotiff");
175 10 : for (std::vector<std::string>::const_iterator file = files.begin(); file != files.end(); ++file) {
176 15 : PROGRESS_BEGIN_MESSAGE("Parsing from GeoTIFF '" + *file + "'");
177 5 : int numFeatures = myInstance.loadTiff(*file);
178 10 : MsgHandler::getMessageInstance()->endProcessMsg(
179 10 : " done (parsed " + toString(numFeatures) +
180 15 : " features, Boundary: " + toString(myInstance.getBoundary()) + ").");
181 : }
182 5 : }
183 4746 : if (oc.isSet("heightmap.shapefiles")) {
184 : // parse file(s)
185 0 : std::vector<std::string> files = oc.getStringVector("heightmap.shapefiles");
186 0 : for (std::vector<std::string>::const_iterator file = files.begin(); file != files.end(); ++file) {
187 0 : PROGRESS_BEGIN_MESSAGE("Parsing from shape-file '" + *file + "'");
188 0 : int numFeatures = myInstance.loadShapeFile(*file);
189 0 : MsgHandler::getMessageInstance()->endProcessMsg(
190 0 : " done (parsed " + toString(numFeatures) +
191 0 : " features, Boundary: " + toString(myInstance.getBoundary()) + ").");
192 : }
193 0 : }
194 2373 : }
195 :
196 :
197 : int
198 0 : NBHeightMapper::loadShapeFile(const std::string& file) {
199 : #ifdef HAVE_GDAL
200 : #if GDAL_VERSION_MAJOR < 2
201 : OGRRegisterAll();
202 : OGRDataSource* ds = OGRSFDriverRegistrar::Open(file.c_str(), FALSE);
203 : #else
204 0 : GDALAllRegister();
205 0 : GDALDataset* ds = (GDALDataset*)GDALOpenEx(file.c_str(), GDAL_OF_VECTOR | GA_ReadOnly, nullptr, nullptr, nullptr);
206 : #endif
207 0 : if (ds == nullptr) {
208 0 : throw ProcessError(TLF("Could not open shape file '%'.", file));
209 : }
210 :
211 : // begin file parsing
212 0 : OGRLayer* layer = ds->GetLayer(0);
213 0 : layer->ResetReading();
214 :
215 : // triangle coordinates are stored in WGS84 and later matched with network coordinates in WGS84
216 : // build coordinate transformation
217 : #if GDAL_VERSION_MAJOR < 3
218 : OGRSpatialReference* sr_src = layer->GetSpatialRef();
219 : #else
220 0 : const OGRSpatialReference* sr_src = layer->GetSpatialRef();
221 : #endif
222 0 : OGRSpatialReference sr_dest;
223 0 : sr_dest.SetWellKnownGeogCS("WGS84");
224 0 : OGRCoordinateTransformation* toWGS84 = OGRCreateCoordinateTransformation(sr_src, &sr_dest);
225 0 : if (toWGS84 == nullptr) {
226 0 : WRITE_WARNING(TL("Could not create geocoordinates converter; check whether proj.4 is installed."));
227 : }
228 :
229 : int numFeatures = 0;
230 : OGRFeature* feature;
231 0 : layer->ResetReading();
232 0 : while ((feature = layer->GetNextFeature()) != nullptr) {
233 0 : OGRGeometry* geom = feature->GetGeometryRef();
234 : assert(geom != 0);
235 :
236 0 : OGRwkbGeometryType gtype = geom->getGeometryType();
237 0 : if (gtype == wkbPolygon) {
238 : assert(std::string(geom->getGeometryName()) == std::string("POLYGON"));
239 : // try transform to wgs84
240 0 : geom->transform(toWGS84);
241 0 : OGRLinearRing* cgeom = ((OGRPolygon*) geom)->getExteriorRing();
242 : // assume TIN with with 4 points and point0 == point3
243 : assert(cgeom->getNumPoints() == 4);
244 0 : PositionVector corners;
245 0 : for (int j = 0; j < 3; j++) {
246 0 : Position pos((double) cgeom->getX(j), (double) cgeom->getY(j), (double) cgeom->getZ(j));
247 0 : corners.push_back(pos);
248 0 : myBoundary.add(pos);
249 : }
250 0 : addTriangle(corners);
251 0 : numFeatures++;
252 0 : } else {
253 0 : WRITE_WARNINGF(TL("Ignored heightmap feature type %"), geom->getGeometryName());
254 : }
255 :
256 : /*
257 : switch (gtype) {
258 : case wkbPolygon: {
259 : break;
260 : }
261 : case wkbPoint: {
262 : WRITE_WARNING(TL("got wkbPoint"));
263 : break;
264 : }
265 : case wkbLineString: {
266 : WRITE_WARNING(TL("got wkbLineString"));
267 : break;
268 : }
269 : case wkbMultiPoint: {
270 : WRITE_WARNING(TL("got wkbMultiPoint"));
271 : break;
272 : }
273 : case wkbMultiLineString: {
274 : WRITE_WARNING(TL("got wkbMultiLineString"));
275 : break;
276 : }
277 : case wkbMultiPolygon: {
278 : WRITE_WARNING(TL("got wkbMultiPolygon"));
279 : break;
280 : }
281 : default:
282 : WRITE_WARNING(TL("Unsupported shape type occurred"));
283 : break;
284 : }
285 : */
286 0 : OGRFeature::DestroyFeature(feature);
287 : }
288 : #if GDAL_VERSION_MAJOR < 2
289 : OGRDataSource::DestroyDataSource(ds);
290 : #else
291 0 : GDALClose(ds);
292 : #endif
293 0 : OCTDestroyCoordinateTransformation(reinterpret_cast<OGRCoordinateTransformationH>(toWGS84));
294 0 : OGRCleanupAll();
295 0 : return numFeatures;
296 : #else
297 : UNUSED_PARAMETER(file);
298 : WRITE_ERROR(TL("Cannot load shape file since SUMO was compiled without GDAL support."));
299 : return 0;
300 : #endif
301 0 : }
302 :
303 :
304 : int
305 5 : NBHeightMapper::loadTiff(const std::string& file) {
306 : #ifdef HAVE_GDAL
307 5 : GDALAllRegister();
308 5 : GDALDataset* poDataset = (GDALDataset*)GDALOpen(file.c_str(), GA_ReadOnly);
309 5 : if (poDataset == 0) {
310 0 : WRITE_ERROR(TL("Cannot load GeoTIFF file."));
311 0 : return 0;
312 : }
313 5 : Boundary boundary;
314 5 : const int xSize = poDataset->GetRasterXSize();
315 5 : const int ySize = poDataset->GetRasterYSize();
316 : double adfGeoTransform[6];
317 5 : if (poDataset->GetGeoTransform(adfGeoTransform) == CE_None) {
318 5 : Position topLeft(adfGeoTransform[0], adfGeoTransform[3]);
319 5 : mySizeOfPixel.set(adfGeoTransform[1], adfGeoTransform[5]);
320 5 : const double horizontalSize = xSize * mySizeOfPixel.x();
321 5 : const double verticalSize = ySize * mySizeOfPixel.y();
322 5 : boundary.add(topLeft);
323 5 : boundary.add(topLeft.x() + horizontalSize, topLeft.y() + verticalSize);
324 : } else {
325 0 : WRITE_ERRORF(TL("Could not parse geo information from %."), file);
326 0 : return 0;
327 : }
328 5 : const int picSize = xSize * ySize;
329 5 : float* raster = (float*)CPLMalloc(sizeof(float) * picSize);
330 : bool ok = true;
331 10 : for (int i = 1; i <= poDataset->GetRasterCount(); i++) {
332 5 : GDALRasterBand* poBand = poDataset->GetRasterBand(i);
333 5 : if (poBand->GetColorInterpretation() != GCI_GrayIndex) {
334 0 : WRITE_ERRORF(TL("Unknown color band in %."), file);
335 0 : clearData();
336 : ok = false;
337 : break;
338 : }
339 : assert(xSize == poBand->GetXSize() && ySize == poBand->GetYSize());
340 5 : if (poBand->RasterIO(GF_Read, 0, 0, xSize, ySize, raster, xSize, ySize, GDT_Float32, 0, 0) == CE_Failure) {
341 0 : WRITE_ERRORF(TL("Failure in reading %."), file);
342 0 : clearData();
343 : ok = false;
344 : break;
345 : }
346 : }
347 5 : double min = std::numeric_limits<double>::max();
348 5 : double max = -std::numeric_limits<double>::max();
349 75605 : for (int i = 0; i < picSize; i++) {
350 75600 : min = MIN2(min, (double)raster[i]);
351 75850 : max = MAX2(max, (double)raster[i]);
352 : }
353 :
354 : // Make a copy, GDALClose will destroy the original
355 : #if GDAL_VERSION_MAJOR < 3
356 : OGRSpatialReference spatialRef;
357 : char* wkt = const_cast<char*>(poDataset->GetProjectionRef());
358 : spatialRef.importFromWkt(&wkt);
359 : #else
360 5 : OGRSpatialReference spatialRef(*poDataset->GetSpatialRef());
361 : #endif
362 5 : GDALClose(poDataset);
363 5 : if (ok) {
364 35 : WRITE_MESSAGE("Read geotiff heightmap with size " + toString(xSize) + "," + toString(ySize)
365 : + " for geo boundary [" + toString(boundary)
366 : + "] with elevation range [" + toString(min) + "," + toString(max) + "].");
367 5 : OGRSpatialReference wgs;
368 5 : wgs.SetWellKnownGeogCS("WGS84");
369 5 : myRasters.push_back(RasterData{raster, boundary, xSize, ySize, OGRCreateCoordinateTransformation(&wgs, &spatialRef)});
370 : return picSize;
371 5 : }
372 : return 0;
373 : #else
374 : UNUSED_PARAMETER(file);
375 : WRITE_ERROR(TL("Cannot load GeoTIFF file since SUMO was compiled without GDAL support."));
376 : return 0;
377 : #endif
378 5 : }
379 :
380 :
381 : void
382 2526 : NBHeightMapper::clearData() {
383 2529 : for (Triangles::iterator it = myTriangles.begin(); it != myTriangles.end(); it++) {
384 6 : delete *it;
385 : }
386 : myTriangles.clear();
387 : #ifdef HAVE_GDAL
388 2531 : for (auto& item : myRasters) {
389 5 : CPLFree(item.raster);
390 5 : if (item.transform != nullptr) {
391 5 : delete item.transform;
392 : }
393 : }
394 : myRasters.clear();
395 : #endif
396 2526 : myBoundary.reset();
397 2526 : }
398 :
399 :
400 : // ===========================================================================
401 : // Triangle member methods
402 : // ===========================================================================
403 687 : NBHeightMapper::Triangle::Triangle(const PositionVector& corners):
404 : myCorners(corners) {
405 : assert(myCorners.size() == 3);
406 : // @todo assert non-colinearity
407 687 : }
408 :
409 :
410 : void
411 5 : NBHeightMapper::Triangle::addSelf(const QueryResult& queryResult) const {
412 5 : queryResult.triangles.push_back(this);
413 5 : }
414 :
415 :
416 : bool
417 4 : NBHeightMapper::Triangle::contains(const Position& pos) const {
418 4 : return myCorners.around(pos);
419 : }
420 :
421 :
422 : double
423 687 : NBHeightMapper::Triangle::getZ(const Position& geo) const {
424 : // en.wikipedia.org/wiki/Line-plane_intersection
425 687 : Position p0 = myCorners.front();
426 : Position line(0, 0, 1);
427 : p0.sub(geo); // p0 - l0
428 687 : Position normal = normalVector();
429 687 : return p0.dotProduct(normal) / line.dotProduct(normal);
430 : }
431 :
432 :
433 : Position
434 687 : NBHeightMapper::Triangle::normalVector() const {
435 : // @todo maybe cache result to avoid multiple computations?
436 687 : Position side1 = myCorners[1] - myCorners[0];
437 687 : Position side2 = myCorners[2] - myCorners[0];
438 687 : return side1.crossProduct(side2);
439 : }
440 :
441 :
442 : /****************************************************************************/
|