/src/gdal/frmts/wcs/wcsdataset110.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /****************************************************************************** |
2 | | * |
3 | | * Project: WCS Client Driver |
4 | | * Purpose: Implementation of Dataset class for WCS 1.1. |
5 | | * Author: Frank Warmerdam, warmerdam@pobox.com |
6 | | * |
7 | | ****************************************************************************** |
8 | | * Copyright (c) 2006, Frank Warmerdam |
9 | | * Copyright (c) 2008-2013, Even Rouault <even dot rouault at spatialys.com> |
10 | | * Copyright (c) 2017, Ari Jolma |
11 | | * Copyright (c) 2017, Finnish Environment Institute |
12 | | * |
13 | | * SPDX-License-Identifier: MIT |
14 | | ****************************************************************************/ |
15 | | |
16 | | #include "cpl_string.h" |
17 | | #include "cpl_minixml.h" |
18 | | #include "cpl_http.h" |
19 | | #include "gmlutils.h" |
20 | | #include "gdal_frmts.h" |
21 | | #include "gdal_pam.h" |
22 | | #include "ogr_spatialref.h" |
23 | | #include "gmlcoverage.h" |
24 | | |
25 | | #include <algorithm> |
26 | | |
27 | | #include "wcsdataset.h" |
28 | | #include "wcsutils.h" |
29 | | |
30 | | using namespace WCSUtils; |
31 | | |
32 | | /************************************************************************/ |
33 | | /* GetExtent() */ |
34 | | /* */ |
35 | | /************************************************************************/ |
36 | | |
37 | | std::vector<double> WCSDataset110::GetExtent(int nXOff, int nYOff, int nXSize, |
38 | | int nYSize, |
39 | | CPL_UNUSED int nBufXSize, |
40 | | CPL_UNUSED int nBufYSize) |
41 | 0 | { |
42 | 0 | std::vector<double> extent; |
43 | | |
44 | | // outer edges of outer pixels. |
45 | 0 | extent.push_back(adfGeoTransform[0] + (nXOff)*adfGeoTransform[1]); |
46 | 0 | extent.push_back(adfGeoTransform[3] + |
47 | 0 | (nYOff + nYSize) * adfGeoTransform[5]); |
48 | 0 | extent.push_back(adfGeoTransform[0] + |
49 | 0 | (nXOff + nXSize) * adfGeoTransform[1]); |
50 | 0 | extent.push_back(adfGeoTransform[3] + (nYOff)*adfGeoTransform[5]); |
51 | |
|
52 | 0 | bool no_shrink = CPLGetXMLBoolean(psService, "OuterExtents"); |
53 | | |
54 | | // WCS 1.1 extents are centers of outer pixels. |
55 | 0 | if (!no_shrink) |
56 | 0 | { |
57 | 0 | extent[2] -= adfGeoTransform[1] * 0.5; |
58 | 0 | extent[0] += adfGeoTransform[1] * 0.5; |
59 | 0 | extent[1] -= adfGeoTransform[5] * 0.5; |
60 | 0 | extent[3] += adfGeoTransform[5] * 0.5; |
61 | 0 | } |
62 | |
|
63 | 0 | double dfXStep, dfYStep; |
64 | |
|
65 | 0 | if (!no_shrink) |
66 | 0 | { |
67 | 0 | dfXStep = (nXSize / (double)nBufXSize) * adfGeoTransform[1]; |
68 | 0 | dfYStep = (nYSize / (double)nBufYSize) * adfGeoTransform[5]; |
69 | | // Carefully adjust bounds for pixel centered values at new |
70 | | // sampling density. |
71 | 0 | if (nBufXSize != nXSize || nBufYSize != nYSize) |
72 | 0 | { |
73 | 0 | dfXStep = (nXSize / (double)nBufXSize) * adfGeoTransform[1]; |
74 | 0 | dfYStep = (nYSize / (double)nBufYSize) * adfGeoTransform[5]; |
75 | |
|
76 | 0 | extent[0] = |
77 | 0 | nXOff * adfGeoTransform[1] + adfGeoTransform[0] + dfXStep * 0.5; |
78 | 0 | extent[2] = extent[0] + (nBufXSize - 1) * dfXStep; |
79 | |
|
80 | 0 | extent[3] = |
81 | 0 | nYOff * adfGeoTransform[5] + adfGeoTransform[3] + dfYStep * 0.5; |
82 | 0 | extent[1] = extent[3] + (nBufYSize - 1) * dfYStep; |
83 | 0 | } |
84 | 0 | } |
85 | 0 | else |
86 | 0 | { |
87 | 0 | double adjust = |
88 | 0 | CPLAtof(CPLGetXMLValue(psService, "BufSizeAdjust", "0.0")); |
89 | 0 | dfXStep = (nXSize / ((double)nBufXSize + adjust)) * adfGeoTransform[1]; |
90 | 0 | dfYStep = (nYSize / ((double)nBufYSize + adjust)) * adfGeoTransform[5]; |
91 | 0 | } |
92 | |
|
93 | 0 | extent.push_back(dfXStep); |
94 | 0 | extent.push_back(dfYStep); |
95 | |
|
96 | 0 | return extent; |
97 | 0 | } |
98 | | |
99 | | /************************************************************************/ |
100 | | /* GetCoverageRequest() */ |
101 | | /* */ |
102 | | /************************************************************************/ |
103 | | |
104 | | std::string WCSDataset110::GetCoverageRequest(bool scaled, int /* nBufXSize */, |
105 | | int /* nBufYSize */, |
106 | | const std::vector<double> &extent, |
107 | | const std::string &osBandList) |
108 | 0 | { |
109 | 0 | CPLString osRequest; |
110 | | |
111 | | /* -------------------------------------------------------------------- */ |
112 | | /* URL encode strings that could have questionable characters. */ |
113 | | /* -------------------------------------------------------------------- */ |
114 | 0 | CPLString osCoverage = CPLGetXMLValue(psService, "CoverageName", ""); |
115 | |
|
116 | 0 | char *pszEncoded = CPLEscapeString(osCoverage, -1, CPLES_URL); |
117 | 0 | osCoverage = pszEncoded; |
118 | 0 | CPLFree(pszEncoded); |
119 | |
|
120 | 0 | CPLString osFormat = CPLGetXMLValue(psService, "PreferredFormat", ""); |
121 | |
|
122 | 0 | pszEncoded = CPLEscapeString(osFormat, -1, CPLES_URL); |
123 | 0 | osFormat = pszEncoded; |
124 | 0 | CPLFree(pszEncoded); |
125 | |
|
126 | 0 | CPLString osRangeSubset = CPLGetXMLValue(psService, "FieldName", ""); |
127 | | |
128 | | // todo: MapServer seems to require interpolation |
129 | |
|
130 | 0 | CPLString interpolation = CPLGetXMLValue(psService, "Interpolation", ""); |
131 | 0 | if (interpolation == "") |
132 | 0 | { |
133 | | // old undocumented key for interpolation in service |
134 | 0 | interpolation = CPLGetXMLValue(psService, "Resample", ""); |
135 | 0 | } |
136 | 0 | if (interpolation != "") |
137 | 0 | { |
138 | 0 | osRangeSubset += ":" + interpolation; |
139 | 0 | } |
140 | |
|
141 | 0 | if (osBandList != "") |
142 | 0 | { |
143 | 0 | if (osBandIdentifier != "") |
144 | 0 | { |
145 | 0 | osRangeSubset += CPLString().Printf( |
146 | 0 | "[%s[%s]]", osBandIdentifier.c_str(), osBandList.c_str()); |
147 | 0 | } |
148 | 0 | } |
149 | |
|
150 | 0 | osRangeSubset = "&RangeSubset=" + URLEncode(osRangeSubset); |
151 | |
|
152 | 0 | double bbox_0 = extent[0], // min X |
153 | 0 | bbox_1 = extent[1], // min Y |
154 | 0 | bbox_2 = extent[2], // max X |
155 | 0 | bbox_3 = extent[3]; // max Y |
156 | |
|
157 | 0 | if (axis_order_swap) |
158 | 0 | { |
159 | 0 | bbox_0 = extent[1]; // min Y |
160 | 0 | bbox_1 = extent[0]; // min X |
161 | 0 | bbox_2 = extent[3]; // max Y |
162 | 0 | bbox_3 = extent[2]; // max X |
163 | 0 | } |
164 | 0 | std::string request = CPLGetXMLValue(psService, "ServiceURL", ""); |
165 | 0 | request = CPLURLAddKVP(request.c_str(), "SERVICE", "WCS"); |
166 | 0 | request += CPLString().Printf( |
167 | 0 | "&VERSION=%s&REQUEST=GetCoverage&IDENTIFIER=%s" |
168 | 0 | "&FORMAT=%s&BOUNDINGBOX=%.15g,%.15g,%.15g,%.15g,%s%s", |
169 | 0 | CPLGetXMLValue(psService, "Version", ""), osCoverage.c_str(), |
170 | 0 | osFormat.c_str(), bbox_0, bbox_1, bbox_2, bbox_3, osCRS.c_str(), |
171 | 0 | osRangeSubset.c_str()); |
172 | 0 | double origin_1 = extent[0], // min X |
173 | 0 | origin_2 = extent[3], // max Y |
174 | 0 | offset_1 = extent[4], // dX |
175 | 0 | offset_2 = extent[5]; // dY |
176 | |
|
177 | 0 | if (axis_order_swap) |
178 | 0 | { |
179 | 0 | origin_1 = extent[3]; // max Y |
180 | 0 | origin_2 = extent[0]; // min X |
181 | 0 | offset_1 = extent[5]; // dY |
182 | 0 | offset_2 = extent[4]; // dX |
183 | 0 | } |
184 | 0 | CPLString offsets; |
185 | 0 | if (CPLGetXMLBoolean(psService, "OffsetsPositive")) |
186 | 0 | { |
187 | 0 | offset_1 = fabs(offset_1); |
188 | 0 | offset_2 = fabs(offset_2); |
189 | 0 | } |
190 | 0 | if (EQUAL(CPLGetXMLValue(psService, "NrOffsets", "4"), "2")) |
191 | 0 | { |
192 | 0 | offsets = CPLString().Printf("%.15g,%.15g", offset_1, offset_2); |
193 | 0 | } |
194 | 0 | else |
195 | 0 | { |
196 | 0 | if (axis_order_swap) |
197 | 0 | { |
198 | | // Only tested with GeoServer but this is the correct offset(?) |
199 | 0 | offsets = CPLString().Printf("0,%.15g,%.15g,0", offset_2, offset_1); |
200 | 0 | } |
201 | 0 | else |
202 | 0 | { |
203 | 0 | offsets = CPLString().Printf("%.15g,0,0,%.15g", offset_1, offset_2); |
204 | 0 | } |
205 | 0 | } |
206 | 0 | bool do_not_include = |
207 | 0 | CPLGetXMLBoolean(psService, "GridCRSOptional") && !scaled; |
208 | 0 | if (!do_not_include) |
209 | 0 | { |
210 | 0 | request += CPLString().Printf( |
211 | 0 | "&GridBaseCRS=%s" |
212 | 0 | "&GridCS=urn:ogc:def:cs:OGC:0.0:Grid2dSquareCS" |
213 | 0 | "&GridType=urn:ogc:def:method:WCS:1.1:2dGridIn2dCrs" |
214 | 0 | "&GridOrigin=%.15g,%.15g" |
215 | 0 | "&GridOffsets=%s", |
216 | 0 | osCRS.c_str(), origin_1, origin_2, offsets.c_str()); |
217 | 0 | } |
218 | 0 | CPLString extra = CPLGetXMLValue(psService, "Parameters", ""); |
219 | 0 | if (extra != "") |
220 | 0 | { |
221 | 0 | std::vector<std::string> pairs = Split(extra.c_str(), "&"); |
222 | 0 | for (unsigned int i = 0; i < pairs.size(); ++i) |
223 | 0 | { |
224 | 0 | std::vector<std::string> pair = Split(pairs[i].c_str(), "="); |
225 | 0 | request = |
226 | 0 | CPLURLAddKVP(request.c_str(), pair[0].c_str(), pair[1].c_str()); |
227 | 0 | } |
228 | 0 | } |
229 | 0 | extra = CPLGetXMLValue(psService, "GetCoverageExtra", ""); |
230 | 0 | if (extra != "") |
231 | 0 | { |
232 | 0 | std::vector<std::string> pairs = Split(extra.c_str(), "&"); |
233 | 0 | for (unsigned int i = 0; i < pairs.size(); ++i) |
234 | 0 | { |
235 | 0 | std::vector<std::string> pair = Split(pairs[i].c_str(), "="); |
236 | 0 | request = |
237 | 0 | CPLURLAddKVP(request.c_str(), pair[0].c_str(), pair[1].c_str()); |
238 | 0 | } |
239 | 0 | } |
240 | 0 | CPLDebug("WCS", "Requesting %s", request.c_str()); |
241 | 0 | return request; |
242 | 0 | } |
243 | | |
244 | | /************************************************************************/ |
245 | | /* DescribeCoverageRequest() */ |
246 | | /* */ |
247 | | /************************************************************************/ |
248 | | |
249 | | std::string WCSDataset110::DescribeCoverageRequest() |
250 | 0 | { |
251 | 0 | std::string request = CPLGetXMLValue(psService, "ServiceURL", ""); |
252 | 0 | request = CPLURLAddKVP(request.c_str(), "SERVICE", "WCS"); |
253 | 0 | request = CPLURLAddKVP(request.c_str(), "REQUEST", "DescribeCoverage"); |
254 | 0 | request = CPLURLAddKVP(request.c_str(), "VERSION", |
255 | 0 | CPLGetXMLValue(psService, "Version", "1.1.0")); |
256 | 0 | request = CPLURLAddKVP(request.c_str(), "IDENTIFIERS", |
257 | 0 | CPLGetXMLValue(psService, "CoverageName", "")); |
258 | 0 | CPLString extra = CPLGetXMLValue(psService, "Parameters", ""); |
259 | 0 | if (extra != "") |
260 | 0 | { |
261 | 0 | std::vector<std::string> pairs = Split(extra.c_str(), "&"); |
262 | 0 | for (unsigned int i = 0; i < pairs.size(); ++i) |
263 | 0 | { |
264 | 0 | std::vector<std::string> pair = Split(pairs[i].c_str(), "="); |
265 | 0 | request = |
266 | 0 | CPLURLAddKVP(request.c_str(), pair[0].c_str(), pair[1].c_str()); |
267 | 0 | } |
268 | 0 | } |
269 | 0 | extra = CPLGetXMLValue(psService, "DescribeCoverageExtra", ""); |
270 | 0 | if (extra != "") |
271 | 0 | { |
272 | 0 | std::vector<std::string> pairs = Split(extra.c_str(), "&"); |
273 | 0 | for (unsigned int i = 0; i < pairs.size(); ++i) |
274 | 0 | { |
275 | 0 | std::vector<std::string> pair = Split(pairs[i].c_str(), "="); |
276 | 0 | request = |
277 | 0 | CPLURLAddKVP(request.c_str(), pair[0].c_str(), pair[1].c_str()); |
278 | 0 | } |
279 | 0 | } |
280 | 0 | return request; |
281 | 0 | } |
282 | | |
283 | | /************************************************************************/ |
284 | | /* CoverageOffering() */ |
285 | | /* */ |
286 | | /************************************************************************/ |
287 | | |
288 | | CPLXMLNode *WCSDataset110::CoverageOffering(CPLXMLNode *psDC) |
289 | 0 | { |
290 | 0 | return CPLGetXMLNode(psDC, "=CoverageDescriptions.CoverageDescription"); |
291 | 0 | } |
292 | | |
293 | | /************************************************************************/ |
294 | | /* ExtractGridInfo() */ |
295 | | /* */ |
296 | | /* Collect info about grid from describe coverage for WCS 1.1. */ |
297 | | /* */ |
298 | | /************************************************************************/ |
299 | | |
300 | | bool WCSDataset110::ExtractGridInfo() |
301 | | |
302 | 0 | { |
303 | 0 | CPLXMLNode *psCO = CPLGetXMLNode(psService, "CoverageDescription"); |
304 | |
|
305 | 0 | if (psCO == nullptr) |
306 | 0 | return false; |
307 | | |
308 | | /* -------------------------------------------------------------------- */ |
309 | | /* We need to strip off name spaces so it is easier to */ |
310 | | /* searchfor plain gml names. */ |
311 | | /* -------------------------------------------------------------------- */ |
312 | 0 | CPLStripXMLNamespace(psCO, nullptr, TRUE); |
313 | | |
314 | | /* -------------------------------------------------------------------- */ |
315 | | /* Verify we have a SpatialDomain and GridCRS. */ |
316 | | /* -------------------------------------------------------------------- */ |
317 | 0 | CPLXMLNode *psSD = CPLGetXMLNode(psCO, "Domain.SpatialDomain"); |
318 | 0 | CPLXMLNode *psGCRS = CPLGetXMLNode(psSD, "GridCRS"); |
319 | |
|
320 | 0 | if (psSD == nullptr || psGCRS == nullptr) |
321 | 0 | { |
322 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
323 | 0 | "Unable to find GridCRS in CoverageDescription,\n" |
324 | 0 | "unable to process WCS Coverage."); |
325 | 0 | return false; |
326 | 0 | } |
327 | | |
328 | | /* -------------------------------------------------------------------- */ |
329 | | /* Establish our coordinate system. */ |
330 | | /* This is needed before geometry since we may have axis order swap. */ |
331 | | /* -------------------------------------------------------------------- */ |
332 | 0 | CPLString crs = ParseCRS(psGCRS); |
333 | |
|
334 | 0 | if (crs.empty()) |
335 | 0 | { |
336 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
337 | 0 | "Unable to find GridCRS.GridBaseCRS"); |
338 | 0 | return false; |
339 | 0 | } |
340 | | |
341 | | // SetCRS should fail only if the CRS is really unknown to GDAL |
342 | 0 | if (!SetCRS(crs, true)) |
343 | 0 | { |
344 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
345 | 0 | "Unable to interpret GridBaseCRS '%s'.", crs.c_str()); |
346 | 0 | return false; |
347 | 0 | } |
348 | | |
349 | | /* -------------------------------------------------------------------- */ |
350 | | /* Collect size, origin, and offsets for SetGeometry() */ |
351 | | /* */ |
352 | | /* Extract Geotransform from GridCRS. */ |
353 | | /* */ |
354 | | /* -------------------------------------------------------------------- */ |
355 | 0 | const char *pszGridType = CPLGetXMLValue( |
356 | 0 | psGCRS, "GridType", "urn:ogc:def:method:WCS::2dSimpleGrid"); |
357 | 0 | bool swap = |
358 | 0 | axis_order_swap && !CPLGetXMLBoolean(psService, "NoGridAxisSwap"); |
359 | 0 | std::vector<double> origin = |
360 | 0 | Flist(Split(CPLGetXMLValue(psGCRS, "GridOrigin", ""), " ", swap)); |
361 | |
|
362 | 0 | std::vector<std::string> offset_1 = |
363 | 0 | Split(CPLGetXMLValue(psGCRS, "GridOffsets", ""), " "); |
364 | 0 | std::vector<std::string> offset_2; |
365 | 0 | size_t n = offset_1.size(); |
366 | 0 | if (n % 2 != 0) |
367 | 0 | { |
368 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
369 | 0 | "GridOffsets has incorrect amount of coefficients.\n" |
370 | 0 | "Unable to process WCS coverage."); |
371 | 0 | return false; |
372 | 0 | } |
373 | 0 | for (unsigned int i = 0; i < n / 2; ++i) |
374 | 0 | { |
375 | 0 | CPLString s = offset_1.back(); |
376 | 0 | offset_1.erase(offset_1.end() - 1); |
377 | 0 | #if defined(__GNUC__) |
378 | 0 | #pragma GCC diagnostic push |
379 | 0 | #pragma GCC diagnostic ignored "-Wnull-dereference" |
380 | 0 | #endif |
381 | 0 | offset_2.insert(offset_2.begin(), s); |
382 | 0 | #if defined(__GNUC__) |
383 | 0 | #pragma GCC diagnostic pop |
384 | 0 | #endif |
385 | 0 | } |
386 | 0 | std::vector<std::vector<double>> offsets; |
387 | 0 | if (swap) |
388 | 0 | { |
389 | 0 | offsets.push_back(Flist(offset_2)); |
390 | 0 | offsets.push_back(Flist(offset_1)); |
391 | 0 | } |
392 | 0 | else |
393 | 0 | { |
394 | 0 | offsets.push_back(Flist(offset_1)); |
395 | 0 | offsets.push_back(Flist(offset_2)); |
396 | 0 | } |
397 | |
|
398 | 0 | if (strstr(pszGridType, ":2dGridIn2dCrs") || |
399 | 0 | strstr(pszGridType, ":2dGridin2dCrs")) |
400 | 0 | { |
401 | 0 | if (!(offset_1.size() == 2 && origin.size() == 2)) |
402 | 0 | { |
403 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
404 | 0 | "2dGridIn2dCrs does not have expected GridOrigin or\n" |
405 | 0 | "GridOffsets values - unable to process WCS coverage."); |
406 | 0 | return false; |
407 | 0 | } |
408 | 0 | } |
409 | | |
410 | 0 | else if (strstr(pszGridType, ":2dGridIn3dCrs")) |
411 | 0 | { |
412 | 0 | if (!(offset_1.size() == 3 && origin.size() == 3)) |
413 | 0 | { |
414 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
415 | 0 | "2dGridIn3dCrs does not have expected GridOrigin or\n" |
416 | 0 | "GridOffsets values - unable to process WCS coverage."); |
417 | 0 | return false; |
418 | 0 | } |
419 | 0 | } |
420 | | |
421 | 0 | else if (strstr(pszGridType, ":2dSimpleGrid")) |
422 | 0 | { |
423 | 0 | if (!(offset_1.size() == 1 && origin.size() == 2)) |
424 | 0 | { |
425 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
426 | 0 | "2dSimpleGrid does not have expected GridOrigin or\n" |
427 | 0 | "GridOffsets values - unable to process WCS coverage."); |
428 | 0 | return false; |
429 | 0 | } |
430 | 0 | } |
431 | | |
432 | 0 | else |
433 | 0 | { |
434 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
435 | 0 | "Unrecognized GridCRS.GridType value '%s',\n" |
436 | 0 | "unable to process WCS coverage.", |
437 | 0 | pszGridType); |
438 | 0 | return false; |
439 | 0 | } |
440 | | |
441 | | /* -------------------------------------------------------------------- */ |
442 | | /* Search for an ImageCRS for raster size. */ |
443 | | /* -------------------------------------------------------------------- */ |
444 | 0 | std::vector<int> size; |
445 | 0 | CPLXMLNode *psNode; |
446 | |
|
447 | 0 | for (psNode = psSD->psChild; psNode != nullptr && size.size() == 0; |
448 | 0 | psNode = psNode->psNext) |
449 | 0 | { |
450 | 0 | if (psNode->eType != CXT_Element || |
451 | 0 | !EQUAL(psNode->pszValue, "BoundingBox")) |
452 | 0 | continue; |
453 | | |
454 | 0 | CPLString osBBCRS = ParseCRS(psNode); |
455 | 0 | if (strstr(osBBCRS, ":imageCRS")) |
456 | 0 | { |
457 | 0 | std::vector<std::string> bbox = ParseBoundingBox(psNode); |
458 | 0 | if (bbox.size() >= 2) |
459 | 0 | { |
460 | 0 | std::vector<int> low = Ilist(Split(bbox[0].c_str(), " "), 0, 2); |
461 | 0 | std::vector<int> high = |
462 | 0 | Ilist(Split(bbox[1].c_str(), " "), 0, 2); |
463 | 0 | if (low[0] == 0 && low[1] == 0) |
464 | 0 | { |
465 | 0 | size.push_back(high[0]); |
466 | 0 | size.push_back(high[1]); |
467 | 0 | } |
468 | 0 | } |
469 | 0 | } |
470 | 0 | } |
471 | | |
472 | | /* -------------------------------------------------------------------- */ |
473 | | /* Otherwise we search for a bounding box in our coordinate */ |
474 | | /* system and derive the size from that. */ |
475 | | /* -------------------------------------------------------------------- */ |
476 | 0 | for (psNode = psSD->psChild; psNode != nullptr && size.size() == 0; |
477 | 0 | psNode = psNode->psNext) |
478 | 0 | { |
479 | 0 | if (psNode->eType != CXT_Element || |
480 | 0 | !EQUAL(psNode->pszValue, "BoundingBox")) |
481 | 0 | continue; |
482 | | |
483 | 0 | CPLString osBBCRS = ParseCRS(psNode); |
484 | 0 | if (osBBCRS == osCRS) |
485 | 0 | { |
486 | 0 | std::vector<std::string> bbox = ParseBoundingBox(psNode); |
487 | 0 | bool not_rot = |
488 | 0 | (offsets[0].size() == 1 && offsets[1].size() == 1) || |
489 | 0 | ((swap && offsets[0][0] == 0.0 && offsets[1][1] == 0.0) || |
490 | 0 | (!swap && offsets[0][1] == 0.0 && offsets[1][0] == 0.0)); |
491 | 0 | if (bbox.size() >= 2 && not_rot) |
492 | 0 | { |
493 | 0 | std::vector<double> low = |
494 | 0 | Flist(Split(bbox[0].c_str(), " ", axis_order_swap), 0, 2); |
495 | 0 | std::vector<double> high = |
496 | 0 | Flist(Split(bbox[1].c_str(), " ", axis_order_swap), 0, 2); |
497 | 0 | double c1 = offsets[0][0]; |
498 | 0 | double c2 = |
499 | 0 | offsets[1].size() == 1 ? offsets[1][0] : offsets[1][1]; |
500 | 0 | size.push_back((int)((high[0] - low[0]) / c1 + 1.01)); |
501 | 0 | size.push_back((int)((high[1] - low[1]) / fabs(c2) + 1.01)); |
502 | 0 | } |
503 | 0 | } |
504 | 0 | } |
505 | |
|
506 | 0 | if (size.size() < 2) |
507 | 0 | { |
508 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
509 | 0 | "Could not determine the size of the grid."); |
510 | 0 | return false; |
511 | 0 | } |
512 | | |
513 | 0 | SetGeometry(size, origin, offsets); |
514 | | |
515 | | /* -------------------------------------------------------------------- */ |
516 | | /* Do we have a coordinate system override? */ |
517 | | /* -------------------------------------------------------------------- */ |
518 | 0 | const char *pszProjOverride = CPLGetXMLValue(psService, "SRS", nullptr); |
519 | |
|
520 | 0 | if (pszProjOverride) |
521 | 0 | { |
522 | 0 | if (m_oSRS.SetFromUserInput( |
523 | 0 | pszProjOverride, |
524 | 0 | OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get()) != |
525 | 0 | OGRERR_NONE) |
526 | 0 | { |
527 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
528 | 0 | "<SRS> element contents not parsable:\n%s", |
529 | 0 | pszProjOverride); |
530 | 0 | return false; |
531 | 0 | } |
532 | 0 | } |
533 | | |
534 | | /* -------------------------------------------------------------------- */ |
535 | | /* Pick a format type if we don't already have one selected. */ |
536 | | /* */ |
537 | | /* We will prefer anything that sounds like TIFF, otherwise */ |
538 | | /* falling back to the first supported format. Should we */ |
539 | | /* consider preferring the nativeFormat if available? */ |
540 | | /* -------------------------------------------------------------------- */ |
541 | 0 | if (CPLGetXMLValue(psService, "PreferredFormat", nullptr) == nullptr) |
542 | 0 | { |
543 | 0 | CPLString osPreferredFormat; |
544 | |
|
545 | 0 | for (psNode = psCO->psChild; psNode != nullptr; psNode = psNode->psNext) |
546 | 0 | { |
547 | 0 | if (psNode->eType == CXT_Element && |
548 | 0 | EQUAL(psNode->pszValue, "SupportedFormat") && psNode->psChild && |
549 | 0 | psNode->psChild->eType == CXT_Text) |
550 | 0 | { |
551 | 0 | if (osPreferredFormat.empty()) |
552 | 0 | osPreferredFormat = psNode->psChild->pszValue; |
553 | |
|
554 | 0 | if (strstr(psNode->psChild->pszValue, "tiff") != nullptr || |
555 | 0 | strstr(psNode->psChild->pszValue, "TIFF") != nullptr || |
556 | 0 | strstr(psNode->psChild->pszValue, "Tiff") != nullptr) |
557 | 0 | { |
558 | 0 | osPreferredFormat = psNode->psChild->pszValue; |
559 | 0 | break; |
560 | 0 | } |
561 | 0 | } |
562 | 0 | } |
563 | |
|
564 | 0 | if (!osPreferredFormat.empty()) |
565 | 0 | { |
566 | 0 | bServiceDirty = true; |
567 | 0 | CPLCreateXMLElementAndValue(psService, "PreferredFormat", |
568 | 0 | osPreferredFormat); |
569 | 0 | } |
570 | 0 | } |
571 | | |
572 | | /* -------------------------------------------------------------------- */ |
573 | | /* Try to identify a nodata value. For now we only support the */ |
574 | | /* singleValue mechanism. */ |
575 | | /* -------------------------------------------------------------------- */ |
576 | 0 | if (CPLGetXMLValue(psService, "NoDataValue", nullptr) == nullptr) |
577 | 0 | { |
578 | 0 | const char *pszSV = |
579 | 0 | CPLGetXMLValue(psCO, "Range.Field.NullValue", nullptr); |
580 | |
|
581 | 0 | if (pszSV != nullptr && (CPLAtof(pszSV) != 0.0 || *pszSV == DIGIT_ZERO)) |
582 | 0 | { |
583 | 0 | bServiceDirty = true; |
584 | 0 | CPLCreateXMLElementAndValue(psService, "NoDataValue", pszSV); |
585 | 0 | } |
586 | 0 | } |
587 | | |
588 | | /* -------------------------------------------------------------------- */ |
589 | | /* Grab the field name, if possible. */ |
590 | | /* -------------------------------------------------------------------- */ |
591 | 0 | if (CPLGetXMLValue(psService, "FieldName", nullptr) == nullptr) |
592 | 0 | { |
593 | 0 | CPLString osFieldName = |
594 | 0 | CPLGetXMLValue(psCO, "Range.Field.Identifier", ""); |
595 | |
|
596 | 0 | if (!osFieldName.empty()) |
597 | 0 | { |
598 | 0 | bServiceDirty = true; |
599 | 0 | CPLCreateXMLElementAndValue(psService, "FieldName", osFieldName); |
600 | 0 | } |
601 | 0 | else |
602 | 0 | { |
603 | 0 | CPLError( |
604 | 0 | CE_Failure, CPLE_AppDefined, |
605 | 0 | "Unable to find required Identifier name %s for Range Field.", |
606 | 0 | osCRS.c_str()); |
607 | 0 | return false; |
608 | 0 | } |
609 | 0 | } |
610 | | |
611 | | /* -------------------------------------------------------------------- */ |
612 | | /* Do we have a "Band" axis? If so try to grab the bandcount */ |
613 | | /* and data type from it. */ |
614 | | /* -------------------------------------------------------------------- */ |
615 | 0 | osBandIdentifier = CPLGetXMLValue(psService, "BandIdentifier", ""); |
616 | 0 | CPLXMLNode *psAxis = |
617 | 0 | CPLGetXMLNode(psService, "CoverageDescription.Range.Field.Axis"); |
618 | |
|
619 | 0 | if (osBandIdentifier.empty() && |
620 | 0 | (EQUAL(CPLGetXMLValue(psAxis, "Identifier", ""), "Band") || |
621 | 0 | EQUAL(CPLGetXMLValue(psAxis, "Identifier", ""), "Bands")) && |
622 | 0 | CPLGetXMLNode(psAxis, "AvailableKeys") != nullptr) |
623 | 0 | { |
624 | 0 | osBandIdentifier = CPLGetXMLValue(psAxis, "Identifier", ""); |
625 | | |
626 | | // verify keys are ascending starting at 1 |
627 | 0 | CPLXMLNode *psValues = CPLGetXMLNode(psAxis, "AvailableKeys"); |
628 | 0 | CPLXMLNode *psSV; |
629 | 0 | int iBand; |
630 | |
|
631 | 0 | for (psSV = psValues->psChild, iBand = 1; psSV != nullptr; |
632 | 0 | psSV = psSV->psNext, iBand++) |
633 | 0 | { |
634 | 0 | if (psSV->eType != CXT_Element || !EQUAL(psSV->pszValue, "Key") || |
635 | 0 | psSV->psChild == nullptr || psSV->psChild->eType != CXT_Text || |
636 | 0 | atoi(psSV->psChild->pszValue) != iBand) |
637 | 0 | { |
638 | 0 | osBandIdentifier = ""; |
639 | 0 | break; |
640 | 0 | } |
641 | 0 | } |
642 | |
|
643 | 0 | if (!osBandIdentifier.empty()) |
644 | 0 | { |
645 | 0 | if (CPLGetXMLValue(psService, "BandIdentifier", nullptr) == nullptr) |
646 | 0 | { |
647 | 0 | bServiceDirty = true; |
648 | 0 | CPLSetXMLValue(psService, "BandIdentifier", |
649 | 0 | osBandIdentifier.c_str()); |
650 | 0 | } |
651 | |
|
652 | 0 | if (CPLGetXMLValue(psService, "BandCount", nullptr) == nullptr) |
653 | 0 | { |
654 | 0 | bServiceDirty = true; |
655 | 0 | CPLSetXMLValue(psService, "BandCount", |
656 | 0 | CPLString().Printf("%d", iBand - 1)); |
657 | 0 | } |
658 | 0 | } |
659 | | |
660 | | // Is this an ESRI server returning a GDAL recognised data type? |
661 | 0 | CPLString osDataType = CPLGetXMLValue(psAxis, "DataType", ""); |
662 | 0 | if (GDALGetDataTypeByName(osDataType) != GDT_Unknown && |
663 | 0 | CPLGetXMLValue(psService, "BandType", nullptr) == nullptr) |
664 | 0 | { |
665 | 0 | bServiceDirty = true; |
666 | 0 | CPLCreateXMLElementAndValue(psService, "BandType", osDataType); |
667 | 0 | } |
668 | 0 | } |
669 | |
|
670 | 0 | return true; |
671 | 0 | } |
672 | | |
673 | | /************************************************************************/ |
674 | | /* ParseCapabilities() */ |
675 | | /************************************************************************/ |
676 | | |
677 | | CPLErr WCSDataset110::ParseCapabilities(CPLXMLNode *Capabilities, |
678 | | const std::string &url) |
679 | 0 | { |
680 | 0 | CPLStripXMLNamespace(Capabilities, nullptr, TRUE); |
681 | | |
682 | | // make sure this is a capabilities document |
683 | 0 | if (strcmp(Capabilities->pszValue, "Capabilities") != 0) |
684 | 0 | { |
685 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
686 | 0 | "Error in capabilities document.\n"); |
687 | 0 | return CE_Failure; |
688 | 0 | } |
689 | | |
690 | 0 | char **metadata = nullptr; |
691 | 0 | std::string path = "WCS_GLOBAL#"; |
692 | |
|
693 | 0 | CPLString key = path + "version"; |
694 | 0 | metadata = CSLSetNameValue(metadata, key, Version()); |
695 | |
|
696 | 0 | for (CPLXMLNode *node = Capabilities->psChild; node != nullptr; |
697 | 0 | node = node->psNext) |
698 | 0 | { |
699 | 0 | const char *attr = node->pszValue; |
700 | 0 | if (node->eType == CXT_Attribute && EQUAL(attr, "updateSequence")) |
701 | 0 | { |
702 | 0 | key = path + "updateSequence"; |
703 | 0 | CPLString value = CPLGetXMLValue(node, nullptr, ""); |
704 | 0 | metadata = CSLSetNameValue(metadata, key, value); |
705 | 0 | } |
706 | 0 | } |
707 | | |
708 | | // identification metadata |
709 | 0 | std::string path2 = path; |
710 | 0 | CPLXMLNode *service = AddSimpleMetaData( |
711 | 0 | &metadata, Capabilities, path2, "ServiceIdentification", |
712 | 0 | {"Title", "Abstract", "Fees", "AccessConstraints"}); |
713 | 0 | CPLString kw = GetKeywords(service, "Keywords", "Keyword"); |
714 | 0 | if (kw != "") |
715 | 0 | { |
716 | 0 | CPLString name = path + "Keywords"; |
717 | 0 | metadata = CSLSetNameValue(metadata, name, kw); |
718 | 0 | } |
719 | 0 | CPLString profiles = GetKeywords(service, "", "Profile"); |
720 | 0 | if (profiles != "") |
721 | 0 | { |
722 | 0 | CPLString name = path + "Profiles"; |
723 | 0 | metadata = CSLSetNameValue(metadata, name, profiles); |
724 | 0 | } |
725 | | |
726 | | // provider metadata |
727 | 0 | path2 = path; |
728 | 0 | CPLXMLNode *provider = AddSimpleMetaData( |
729 | 0 | &metadata, Capabilities, path2, "ServiceProvider", {"ProviderName"}); |
730 | 0 | if (provider) |
731 | 0 | { |
732 | 0 | CPLXMLNode *site = CPLGetXMLNode(provider, "ProviderSite"); |
733 | 0 | if (site) |
734 | 0 | { |
735 | 0 | std::string path3 = path2 + "ProviderSite"; |
736 | 0 | CPLString value = |
737 | 0 | CPLGetXMLValue(CPLGetXMLNode(site, "href"), nullptr, ""); |
738 | 0 | metadata = CSLSetNameValue(metadata, path3.c_str(), value); |
739 | 0 | } |
740 | 0 | std::string path3 = std::move(path2); |
741 | 0 | CPLXMLNode *contact = |
742 | 0 | AddSimpleMetaData(&metadata, provider, path3, "ServiceContact", |
743 | 0 | {"IndividualName", "PositionName", "Role"}); |
744 | 0 | if (contact) |
745 | 0 | { |
746 | 0 | std::string path4 = std::move(path3); |
747 | 0 | CPLXMLNode *info = |
748 | 0 | AddSimpleMetaData(&metadata, contact, path4, "ContactInfo", |
749 | 0 | {"HoursOfService", "ContactInstructions"}); |
750 | 0 | if (info) |
751 | 0 | { |
752 | 0 | std::string path5 = path4; |
753 | 0 | std::string path6 = path4; |
754 | 0 | AddSimpleMetaData(&metadata, info, path5, "Address", |
755 | 0 | {"DeliveryPoint", "City", |
756 | 0 | "AdministrativeArea", "PostalCode", |
757 | 0 | "Country", "ElectronicMailAddress"}); |
758 | 0 | AddSimpleMetaData(&metadata, info, path6, "Phone", |
759 | 0 | {"Voice", "Facsimile"}); |
760 | 0 | CPL_IGNORE_RET_VAL(path4); |
761 | 0 | } |
762 | 0 | } |
763 | 0 | } |
764 | | |
765 | | // operations metadata |
766 | 0 | CPLString DescribeCoverageURL = ""; |
767 | 0 | CPLXMLNode *service2 = CPLGetXMLNode(Capabilities, "OperationsMetadata"); |
768 | 0 | if (service2) |
769 | 0 | { |
770 | 0 | for (CPLXMLNode *operation = service2->psChild; operation != nullptr; |
771 | 0 | operation = operation->psNext) |
772 | 0 | { |
773 | 0 | if (operation->eType != CXT_Element || |
774 | 0 | !EQUAL(operation->pszValue, "Operation")) |
775 | 0 | { |
776 | 0 | continue; |
777 | 0 | } |
778 | 0 | if (EQUAL(CPLGetXMLValue(CPLGetXMLNode(operation, "name"), nullptr, |
779 | 0 | ""), |
780 | 0 | "DescribeCoverage")) |
781 | 0 | { |
782 | 0 | DescribeCoverageURL = CPLGetXMLValue( |
783 | 0 | CPLGetXMLNode(CPLSearchXMLNode(operation, "Get"), "href"), |
784 | 0 | nullptr, ""); |
785 | 0 | } |
786 | 0 | } |
787 | 0 | } |
788 | | // if DescribeCoverageURL looks wrong, we change it |
789 | 0 | if (DescribeCoverageURL.find("localhost") != std::string::npos) |
790 | 0 | { |
791 | 0 | DescribeCoverageURL = URLRemoveKey(url.c_str(), "request"); |
792 | 0 | } |
793 | | |
794 | | // service metadata (in 2.0) |
795 | 0 | CPLString ext = "ServiceMetadata"; |
796 | 0 | CPLString formats = GetKeywords(Capabilities, ext, "formatSupported"); |
797 | 0 | if (formats != "") |
798 | 0 | { |
799 | 0 | CPLString name = path + "formatSupported"; |
800 | 0 | metadata = CSLSetNameValue(metadata, name, formats); |
801 | 0 | } |
802 | | // wcs:Extensions: interpolation, CRS, others? |
803 | 0 | ext += ".Extension"; |
804 | 0 | CPLString interpolation = |
805 | 0 | GetKeywords(Capabilities, ext, "interpolationSupported"); |
806 | 0 | if (interpolation == "") |
807 | 0 | { |
808 | 0 | interpolation = |
809 | 0 | GetKeywords(Capabilities, ext + ".InterpolationMetadata", |
810 | 0 | "InterpolationSupported"); |
811 | 0 | } |
812 | 0 | if (interpolation != "") |
813 | 0 | { |
814 | 0 | CPLString name = path + "InterpolationSupported"; |
815 | 0 | metadata = CSLSetNameValue(metadata, name, interpolation); |
816 | 0 | } |
817 | 0 | CPLString crs = GetKeywords(Capabilities, ext, "crsSupported"); |
818 | 0 | if (crs == "") |
819 | 0 | { |
820 | 0 | crs = GetKeywords(Capabilities, ext + ".CrsMetadata", "crsSupported"); |
821 | 0 | } |
822 | 0 | if (crs != "") |
823 | 0 | { |
824 | 0 | CPLString name = path + "crsSupported"; |
825 | 0 | metadata = CSLSetNameValue(metadata, name, crs); |
826 | 0 | } |
827 | |
|
828 | 0 | this->SetMetadata(metadata, ""); |
829 | 0 | CSLDestroy(metadata); |
830 | 0 | metadata = nullptr; |
831 | | |
832 | | // contents metadata |
833 | 0 | CPLXMLNode *contents = CPLGetXMLNode(Capabilities, "Contents"); |
834 | 0 | if (contents) |
835 | 0 | { |
836 | 0 | int index = 1; |
837 | 0 | for (CPLXMLNode *summary = contents->psChild; summary != nullptr; |
838 | 0 | summary = summary->psNext) |
839 | 0 | { |
840 | 0 | if (summary->eType != CXT_Element || |
841 | 0 | !EQUAL(summary->pszValue, "CoverageSummary")) |
842 | 0 | { |
843 | 0 | continue; |
844 | 0 | } |
845 | 0 | CPLString path3; |
846 | 0 | path3.Printf("SUBDATASET_%d_", index); |
847 | 0 | index += 1; |
848 | | |
849 | | // the name and description of the subdataset: |
850 | | // GDAL Data Model: |
851 | | // The value of the _NAME is a string that can be passed to |
852 | | // GDALOpen() to access the file. |
853 | |
|
854 | 0 | CPLString key2 = path3 + "NAME"; |
855 | |
|
856 | 0 | CPLString name = DescribeCoverageURL; |
857 | 0 | name = CPLURLAddKVP(name, "version", this->Version()); |
858 | |
|
859 | 0 | CPLXMLNode *node = CPLGetXMLNode(summary, "CoverageId"); |
860 | 0 | std::string id; |
861 | 0 | if (node) |
862 | 0 | { |
863 | 0 | id = CPLGetXMLValue(node, nullptr, ""); |
864 | 0 | } |
865 | 0 | else |
866 | 0 | { |
867 | 0 | node = CPLGetXMLNode(summary, "Identifier"); |
868 | 0 | if (node) |
869 | 0 | { |
870 | 0 | id = CPLGetXMLValue(node, nullptr, ""); |
871 | 0 | } |
872 | 0 | else |
873 | 0 | { |
874 | | // todo: maybe not an error since CoverageSummary may be |
875 | | // within CoverageSummary (07-067r5 Fig4) |
876 | 0 | CSLDestroy(metadata); |
877 | 0 | CPLError(CE_Failure, CPLE_AppDefined, |
878 | 0 | "Error in capabilities document.\n"); |
879 | 0 | return CE_Failure; |
880 | 0 | } |
881 | 0 | } |
882 | 0 | name = CPLURLAddKVP(name, "coverage", id.c_str()); |
883 | 0 | name = "WCS:" + name; |
884 | 0 | metadata = CSLSetNameValue(metadata, key2, name); |
885 | |
|
886 | 0 | key2 = path3 + "DESC"; |
887 | |
|
888 | 0 | node = CPLGetXMLNode(summary, "Title"); |
889 | 0 | if (node) |
890 | 0 | { |
891 | 0 | metadata = CSLSetNameValue(metadata, key2, |
892 | 0 | CPLGetXMLValue(node, nullptr, "")); |
893 | 0 | } |
894 | 0 | else |
895 | 0 | { |
896 | 0 | metadata = CSLSetNameValue(metadata, key2, id.c_str()); |
897 | 0 | } |
898 | | |
899 | | // todo: compose global bounding box from WGS84BoundingBox and |
900 | | // BoundingBox |
901 | | |
902 | | // further subdataset (coverage) parameters are parsed in |
903 | | // ParseCoverageCapabilities |
904 | 0 | } |
905 | 0 | } |
906 | 0 | this->SetMetadata(metadata, "SUBDATASETS"); |
907 | 0 | CSLDestroy(metadata); |
908 | 0 | return CE_None; |
909 | 0 | } |
910 | | |
911 | | void WCSDataset110::ParseCoverageCapabilities(CPLXMLNode *capabilities, |
912 | | const std::string &coverage, |
913 | | CPLXMLNode *metadata) |
914 | 0 | { |
915 | 0 | CPLStripXMLNamespace(capabilities, nullptr, TRUE); |
916 | 0 | CPLXMLNode *contents = CPLGetXMLNode(capabilities, "Contents"); |
917 | 0 | if (contents) |
918 | 0 | { |
919 | 0 | for (CPLXMLNode *summary = contents->psChild; summary != nullptr; |
920 | 0 | summary = summary->psNext) |
921 | 0 | { |
922 | 0 | if (summary->eType != CXT_Element || |
923 | 0 | !EQUAL(summary->pszValue, "CoverageSummary")) |
924 | 0 | { |
925 | 0 | continue; |
926 | 0 | } |
927 | 0 | CPLXMLNode *node = CPLGetXMLNode(summary, "CoverageId"); |
928 | 0 | CPLString id; |
929 | 0 | if (node) |
930 | 0 | { |
931 | 0 | id = CPLGetXMLValue(node, nullptr, ""); |
932 | 0 | } |
933 | 0 | else |
934 | 0 | { |
935 | 0 | node = CPLGetXMLNode(summary, "Identifier"); |
936 | 0 | if (node) |
937 | 0 | { |
938 | 0 | id = CPLGetXMLValue(node, nullptr, ""); |
939 | 0 | } |
940 | 0 | else |
941 | 0 | { |
942 | 0 | id = ""; |
943 | 0 | } |
944 | 0 | } |
945 | 0 | if (id != coverage) |
946 | 0 | { |
947 | 0 | continue; |
948 | 0 | } |
949 | | |
950 | | // Description |
951 | | // todo: there could be Title and Abstract for each supported |
952 | | // language |
953 | 0 | XMLCopyMetadata(summary, metadata, "Title"); |
954 | 0 | XMLCopyMetadata(summary, metadata, "Abstract"); |
955 | | |
956 | | // 2.0.1 stuff |
957 | 0 | XMLCopyMetadata(summary, metadata, "CoverageSubtype"); |
958 | | |
959 | | // Keywords |
960 | 0 | CPLString kw = GetKeywords(summary, "Keywords", "Keyword"); |
961 | 0 | CPLAddXMLAttributeAndValue( |
962 | 0 | CPLCreateXMLElementAndValue(metadata, "MDI", kw), "key", |
963 | 0 | "Keywords"); |
964 | | |
965 | | // WCSContents |
966 | 0 | const char *tags[] = {"SupportedCRS", "SupportedFormat", |
967 | 0 | "OtherSource"}; |
968 | 0 | for (unsigned int i = 0; i < CPL_ARRAYSIZE(tags); i++) |
969 | 0 | { |
970 | 0 | kw = GetKeywords(summary, "", tags[i]); |
971 | 0 | CPLAddXMLAttributeAndValue( |
972 | 0 | CPLCreateXMLElementAndValue(metadata, "MDI", kw), "key", |
973 | 0 | tags[i]); |
974 | 0 | } |
975 | | |
976 | | // skipping WGS84BoundingBox, BoundingBox, Metadata, Extension |
977 | | // since those we'll get from coverage description |
978 | 0 | } |
979 | 0 | } |
980 | 0 | } |