/src/gdal/frmts/wms/minidriver_tiled_wms.cpp
Line | Count | Source |
1 | | /****************************************************************************** |
2 | | * |
3 | | * Project: tiledWMS Client Driver |
4 | | * Purpose: Implementation of the OnEarth Tiled WMS minidriver. |
5 | | * http://onearth.jpl.nasa.gov/tiled.html |
6 | | * Author: Lucian Plesea (Lucian dot Plesea at jpl.nasa.gov) |
7 | | * Adam Nowacki |
8 | | * |
9 | | ****************************************************************************** |
10 | | * Copyright (c) 2007, Adam Nowacki |
11 | | * Copyright (c) 2011-2012, Even Rouault <even dot rouault at spatialys.com> |
12 | | * |
13 | | * SPDX-License-Identifier: MIT |
14 | | ****************************************************************************/ |
15 | | |
16 | | // |
17 | | // Also known as the OnEarth tile protocol |
18 | | // |
19 | | // A few open options are supported by tiled WMS |
20 | | // |
21 | | // TiledGroupName=<Name> |
22 | | // |
23 | | // This option is only valid when the WMS file does not contain a |
24 | | // TiledGroupName. The name value should match exactly the name declared by the |
25 | | // server, including possible white spaces, otherwise the open will fail. |
26 | | // |
27 | | // Change=<key>:<value> |
28 | | // |
29 | | // If the tiled group selected supports the change key, this option will set |
30 | | // the value The <key> here does not include the brackets present in the |
31 | | // GetTileService For example, if a TiledPattern include a key of ${time}, the |
32 | | // matching open option will be Change=time:<YYYY-MM-DD> The Change open option |
33 | | // may be present multiple times, with different keys If a key is not supported |
34 | | // by the selected TilePattern, the open will fail Alternate syntax is: |
35 | | // Change=<key>=<value> |
36 | | // |
37 | | // StoreConfiguration=Yes |
38 | | // |
39 | | // This boolean option is only useful when doing a createcopy of a tiledWMS |
40 | | // dataset into another tiledWMS dataset. When set, the source tiledWMS will |
41 | | // store the server configuration into the XML metadata representation, which |
42 | | // then gets copied to the XML output. This will eliminate the need to fetch |
43 | | // the server configuration when opening the output datafile |
44 | | // |
45 | | |
46 | | #include "wmsdriver.h" |
47 | | #include "minidriver_tiled_wms.h" |
48 | | |
49 | | #include "gdal_colortable.h" |
50 | | #include "gdal_cpp_functions.h" |
51 | | |
52 | | #include <set> |
53 | | |
54 | | static const char SIG[] = "GDAL_WMS TiledWMS: "; |
55 | | |
56 | | /* |
57 | | *\brief Read a number from an xml element |
58 | | */ |
59 | | |
60 | | static double getXMLNum(const CPLXMLNode *poRoot, const char *pszPath, |
61 | | const char *pszDefault) |
62 | 0 | { // Sets errno |
63 | 0 | return CPLAtof(CPLGetXMLValue(poRoot, pszPath, pszDefault)); |
64 | 0 | } |
65 | | |
66 | | /* |
67 | | *\brief Read a ColorEntry XML node, return a GDALColorEntry structure |
68 | | * |
69 | | */ |
70 | | |
71 | | static GDALColorEntry GetXMLColorEntry(const CPLXMLNode *p) |
72 | 0 | { |
73 | 0 | GDALColorEntry ce; |
74 | 0 | ce.c1 = static_cast<short>(getXMLNum(p, "c1", "0")); |
75 | 0 | ce.c2 = static_cast<short>(getXMLNum(p, "c2", "0")); |
76 | 0 | ce.c3 = static_cast<short>(getXMLNum(p, "c3", "0")); |
77 | 0 | ce.c4 = static_cast<short>(getXMLNum(p, "c4", "255")); |
78 | 0 | return ce; |
79 | 0 | } |
80 | | |
81 | | /************************************************************************/ |
82 | | /* SearchXMLSiblings() */ |
83 | | /************************************************************************/ |
84 | | |
85 | | /* |
86 | | * \brief Search for a sibling of the root node with a given name. |
87 | | * |
88 | | * Searches only the next siblings of the node passed in for the named element |
89 | | * or attribute. If the first character of the pszElement is '=', the search |
90 | | * includes the psRoot node |
91 | | * |
92 | | * @param psRoot the root node to search. This should be a node of type |
93 | | * CXT_Element. NULL is safe. |
94 | | * |
95 | | * @param pszElement the name of the element or attribute to search for. |
96 | | * |
97 | | * |
98 | | * @return The first matching node or NULL on failure. |
99 | | */ |
100 | | |
101 | | static const CPLXMLNode *SearchXMLSiblings(const CPLXMLNode *psRoot, |
102 | | const char *pszElement) |
103 | | |
104 | 0 | { |
105 | 0 | if (psRoot == nullptr || pszElement == nullptr) |
106 | 0 | return nullptr; |
107 | | |
108 | | // If the strings starts with '=', skip it and test the root |
109 | | // If not, start testing with the next sibling |
110 | 0 | if (pszElement[0] == '=') |
111 | 0 | pszElement++; |
112 | 0 | else |
113 | 0 | psRoot = psRoot->psNext; |
114 | |
|
115 | 0 | for (; psRoot != nullptr; psRoot = psRoot->psNext) |
116 | 0 | { |
117 | 0 | if ((psRoot->eType == CXT_Element || psRoot->eType == CXT_Attribute) && |
118 | 0 | EQUAL(pszElement, psRoot->pszValue)) |
119 | 0 | return psRoot; |
120 | 0 | } |
121 | 0 | return nullptr; |
122 | 0 | } |
123 | | |
124 | | /************************************************************************/ |
125 | | /* SearchLeafGroupName() */ |
126 | | /************************************************************************/ |
127 | | |
128 | | /* |
129 | | * \brief Search for a leaf TileGroup node by name. |
130 | | * |
131 | | * @param psRoot the root node to search. This should be a node of type |
132 | | * CXT_Element. NULL is safe. |
133 | | * |
134 | | * @param pszElement the name of the TileGroup to search for. |
135 | | * |
136 | | * @return The XML node of the matching TileGroup or NULL on failure. |
137 | | */ |
138 | | |
139 | | static CPLXMLNode *SearchLeafGroupName(CPLXMLNode *psRoot, const char *name) |
140 | | |
141 | 0 | { |
142 | 0 | if (psRoot == nullptr || name == nullptr) |
143 | 0 | return nullptr; |
144 | | |
145 | | // Has to be a leaf TileGroup with the right name |
146 | 0 | if (nullptr == SearchXMLSiblings(psRoot->psChild, "TiledGroup")) |
147 | 0 | { |
148 | 0 | if (EQUAL(name, CPLGetXMLValue(psRoot, "Name", ""))) |
149 | 0 | return psRoot; |
150 | 0 | } |
151 | 0 | else |
152 | 0 | { // Is metagroup, try children then siblings |
153 | 0 | CPLXMLNode *ret = SearchLeafGroupName(psRoot->psChild, name); |
154 | 0 | if (nullptr != ret) |
155 | 0 | return ret; |
156 | 0 | } |
157 | 0 | return SearchLeafGroupName(psRoot->psNext, name); |
158 | 0 | } |
159 | | |
160 | | /************************************************************************/ |
161 | | /* BandInterp() */ |
162 | | /************************************************************************/ |
163 | | |
164 | | /* |
165 | | * \brief Utility function to calculate color band interpretation. |
166 | | * Only handles Gray, GrayAlpha, RGB and RGBA, based on total band count |
167 | | * |
168 | | * @param nbands is the total number of bands in the image |
169 | | * |
170 | | * @param band is the band number, starting with 1 |
171 | | * |
172 | | * @return GDALColorInterp of the band |
173 | | */ |
174 | | |
175 | | static GDALColorInterp BandInterp(int nbands, int band) |
176 | 0 | { |
177 | 0 | switch (nbands) |
178 | 0 | { |
179 | 0 | case 1: |
180 | 0 | return GCI_GrayIndex; |
181 | 0 | case 2: |
182 | 0 | return band == 1 ? GCI_GrayIndex : GCI_AlphaBand; |
183 | 0 | case 3: // RGB |
184 | 0 | case 4: // RBGA |
185 | 0 | if (band < 3) |
186 | 0 | return band == 1 ? GCI_RedBand : GCI_GreenBand; |
187 | 0 | return band == 3 ? GCI_BlueBand : GCI_AlphaBand; |
188 | 0 | default: |
189 | 0 | return GCI_Undefined; |
190 | 0 | } |
191 | 0 | } |
192 | | |
193 | | /************************************************************************/ |
194 | | /* FindBbox() */ |
195 | | /************************************************************************/ |
196 | | |
197 | | /* |
198 | | * \brief Utility function to find the position of the bbox parameter value |
199 | | * within a request string. The search for the bbox is case insensitive |
200 | | * |
201 | | * @param in, the string to search into |
202 | | * |
203 | | * @return The position from the beginning of the string or -1 if not found |
204 | | */ |
205 | | |
206 | | static int FindBbox(CPLString in) |
207 | 0 | { |
208 | |
|
209 | 0 | size_t pos = in.ifind("&bbox="); |
210 | 0 | if (pos == std::string::npos) |
211 | 0 | return -1; |
212 | 0 | return static_cast<int>(pos) + 6; |
213 | 0 | } |
214 | | |
215 | | /************************************************************************/ |
216 | | /* FindChangePattern() */ |
217 | | /************************************************************************/ |
218 | | |
219 | | /* |
220 | | * \brief Build the right request pattern based on the change request list |
221 | | * It only gets called on initialization |
222 | | * @param cdata, possible request strings, white space separated |
223 | | * @param substs, the list of substitutions to be applied |
224 | | * @param keys, the list of available substitution keys |
225 | | * @param ret The return value, a matching request or an empty string |
226 | | */ |
227 | | |
228 | | static void FindChangePattern(const char *cdata, const char *const *substs, |
229 | | const char *const *keys, CPLString &ret) |
230 | 0 | { |
231 | 0 | const CPLStringList aosTokens(CSLTokenizeString2( |
232 | 0 | cdata, " \t\n\r", CSLT_STRIPLEADSPACES | CSLT_STRIPENDSPACES)); |
233 | 0 | ret.clear(); |
234 | |
|
235 | 0 | int matchcount = CSLCount(substs); |
236 | 0 | int keycount = CSLCount(keys); |
237 | 0 | if (keycount < matchcount) |
238 | 0 | { |
239 | 0 | return; |
240 | 0 | } |
241 | | |
242 | | // A valid string has only the keys in the substs list and none other |
243 | 0 | for (int j = 0; j < aosTokens.size(); j++) |
244 | 0 | { |
245 | 0 | ret = aosTokens[j]; // The target string |
246 | 0 | bool matches = true; |
247 | |
|
248 | 0 | for (int k = 0; k < keycount && keys != nullptr; k++) |
249 | 0 | { |
250 | 0 | const char *key = keys[k]; |
251 | 0 | int sub_number = CSLPartialFindString(substs, key); |
252 | 0 | if (sub_number != -1) |
253 | 0 | { // It is a listed match |
254 | | // But is the match for the key position? |
255 | 0 | char *found_key = nullptr; |
256 | 0 | const char *found_value = |
257 | 0 | CPLParseNameValue(substs[sub_number], &found_key); |
258 | 0 | if (found_key != nullptr && EQUAL(found_key, key)) |
259 | 0 | { // Should exist in the request |
260 | 0 | if (std::string::npos == ret.find(key)) |
261 | 0 | matches = false; |
262 | 0 | if (matches) |
263 | | // Execute the substitution on the "ret" string |
264 | 0 | URLSearchAndReplace(&ret, key, "%s", found_value); |
265 | 0 | } |
266 | 0 | else |
267 | 0 | { |
268 | 0 | matches = false; |
269 | 0 | } |
270 | 0 | CPLFree(found_key); |
271 | 0 | } |
272 | 0 | else |
273 | 0 | { // Key not in the subst list, should not match |
274 | 0 | if (std::string::npos != ret.find(key)) |
275 | 0 | matches = false; |
276 | 0 | } |
277 | 0 | } // Key loop |
278 | 0 | if (matches) |
279 | 0 | { |
280 | 0 | return; // We got the string ready, all keys accounted for and |
281 | | // substs applied |
282 | 0 | } |
283 | 0 | } |
284 | 0 | ret.clear(); |
285 | 0 | } |
286 | | |
287 | 0 | WMSMiniDriver_TiledWMS::WMSMiniDriver_TiledWMS() = default; |
288 | | |
289 | 0 | WMSMiniDriver_TiledWMS::~WMSMiniDriver_TiledWMS() = default; |
290 | | |
291 | | // Returns the scale of a WMS request as compared to the base resolution |
292 | | double WMSMiniDriver_TiledWMS::Scale(const char *request) const |
293 | 0 | { |
294 | 0 | int bbox = FindBbox(request); |
295 | 0 | if (bbox < 0) |
296 | 0 | return 0; |
297 | 0 | double x, y, X, Y; |
298 | 0 | CPLsscanf(request + bbox, "%lf,%lf,%lf,%lf", &x, &y, &X, &Y); |
299 | 0 | return (m_data_window.m_x1 - m_data_window.m_x0) / (X - x) * m_bsx / |
300 | 0 | m_data_window.m_sx; |
301 | 0 | } |
302 | | |
303 | | // Finds, extracts, and returns the highest resolution request string from a |
304 | | // list, starting at item i |
305 | | CPLString WMSMiniDriver_TiledWMS::GetLowestScale(CPLStringList &list, |
306 | | int i) const |
307 | 0 | { |
308 | 0 | CPLString req; |
309 | 0 | double scale = -1; |
310 | 0 | int position = -1; |
311 | 0 | while (nullptr != list[i]) |
312 | 0 | { |
313 | 0 | double tscale = Scale(list[i]); |
314 | 0 | if (tscale >= scale) |
315 | 0 | { |
316 | 0 | scale = tscale; |
317 | 0 | position = i; |
318 | 0 | } |
319 | 0 | i++; |
320 | 0 | } |
321 | 0 | if (position > -1) |
322 | 0 | { |
323 | 0 | req = list[position]; |
324 | 0 | list.Assign(CSLRemoveStrings(list.StealList(), position, 1, nullptr), |
325 | 0 | true); |
326 | 0 | } |
327 | 0 | return req; |
328 | 0 | } |
329 | | |
330 | | /* |
331 | | *\Brief Initialize minidriver with info from the server |
332 | | */ |
333 | | |
334 | | CPLErr WMSMiniDriver_TiledWMS::Initialize(CPLXMLNode *config, |
335 | | CPL_UNUSED char **OpenOptions) |
336 | 0 | { |
337 | 0 | CPLErr ret = CE_None; |
338 | 0 | CPLXMLTreeCloser tileServiceConfig(nullptr); |
339 | 0 | const CPLXMLNode *TG = nullptr; |
340 | |
|
341 | 0 | CPLStringList requests; |
342 | 0 | CPLStringList substs; |
343 | 0 | CPLStringList keys; |
344 | 0 | CPLStringList changes; |
345 | |
|
346 | 0 | try |
347 | 0 | { // Parse info from the WMS Service node |
348 | | // m_end_url = CPLGetXMLValue(config, "AdditionalArgs", ""); |
349 | 0 | m_base_url = CPLGetXMLValue(config, "ServerURL", ""); |
350 | |
|
351 | 0 | if (m_base_url.empty()) |
352 | 0 | throw CPLOPrintf("%s ServerURL missing.", SIG); |
353 | | |
354 | 0 | CPLString tiledGroupName( |
355 | 0 | CSLFetchNameValueDef(OpenOptions, "TiledGroupName", "")); |
356 | 0 | tiledGroupName = |
357 | 0 | CPLGetXMLValue(config, "TiledGroupName", tiledGroupName); |
358 | 0 | if (tiledGroupName.empty()) |
359 | 0 | throw CPLOPrintf("%s TiledGroupName missing.", SIG); |
360 | | |
361 | | // Change strings, key is an attribute, value is the value of the Change |
362 | | // node Multiple keys are possible |
363 | | |
364 | | // First process the changes from open options, if present |
365 | 0 | changes = CSLFetchNameValueMultiple(OpenOptions, "Change"); |
366 | | // Transfer them to subst list |
367 | 0 | for (int i = 0; i < changes.size(); i++) |
368 | 0 | { |
369 | 0 | char *key = nullptr; |
370 | 0 | const char *value = CPLParseNameValue(changes[i], &key); |
371 | | // Add the ${} around the key |
372 | 0 | if (value != nullptr && key != nullptr) |
373 | 0 | substs.SetNameValue(CPLOPrintf("${%s}", key), value); |
374 | 0 | CPLFree(key); |
375 | 0 | } |
376 | | |
377 | | // Then process the configuration file itself |
378 | 0 | const CPLXMLNode *nodeChange = CPLSearchXMLNode(config, "Change"); |
379 | 0 | while (nodeChange != nullptr) |
380 | 0 | { |
381 | 0 | CPLString key = CPLGetXMLValue(nodeChange, "key", ""); |
382 | 0 | if (key.empty()) |
383 | 0 | throw CPLOPrintf( |
384 | 0 | "%s Change element needs a non-empty \"key\" attribute", |
385 | 0 | SIG); |
386 | 0 | substs.SetNameValue(key, CPLGetXMLValue(nodeChange, "", "")); |
387 | 0 | nodeChange = SearchXMLSiblings(nodeChange, "Change"); |
388 | 0 | } |
389 | | |
390 | 0 | m_parent_dataset->SetMetadataItem("ServerURL", m_base_url, nullptr); |
391 | 0 | m_parent_dataset->SetMetadataItem("TiledGroupName", tiledGroupName, |
392 | 0 | nullptr); |
393 | 0 | for (const char *subst : substs) |
394 | 0 | m_parent_dataset->SetMetadataItem("Change", subst, nullptr); |
395 | |
|
396 | 0 | const char *pszConfiguration = |
397 | 0 | CPLGetXMLValue(config, "Configuration", nullptr); |
398 | 0 | CPLString decodedGTS; |
399 | |
|
400 | 0 | if (pszConfiguration) |
401 | 0 | { // Probably XML encoded because it is XML itself |
402 | | // The copy will be replaced by the decoded result |
403 | 0 | decodedGTS = pszConfiguration; |
404 | 0 | WMSUtilDecode(decodedGTS, |
405 | 0 | CPLGetXMLValue(config, "Configuration.encoding", "")); |
406 | 0 | } |
407 | 0 | else |
408 | 0 | { // Not local, use the WMSdriver to fetch the server config |
409 | 0 | CPLString getTileServiceUrl = m_base_url + "request=GetTileService"; |
410 | | |
411 | | // This returns a string managed by the cfg cache, do not free |
412 | 0 | const char *pszTmp = GDALWMSDataset::GetServerConfig( |
413 | 0 | getTileServiceUrl, |
414 | 0 | const_cast<char **>(m_parent_dataset->GetHTTPRequestOpts())); |
415 | 0 | decodedGTS = pszTmp ? pszTmp : ""; |
416 | |
|
417 | 0 | if (decodedGTS.empty()) |
418 | 0 | throw CPLOPrintf("%s Can't fetch server GetTileService", SIG); |
419 | 0 | } |
420 | | |
421 | | // decodedGTS contains the GetTileService return now |
422 | 0 | tileServiceConfig.reset(CPLParseXMLString(decodedGTS)); |
423 | 0 | if (!tileServiceConfig) |
424 | 0 | throw CPLOPrintf("%s Error parsing the GetTileService response", |
425 | 0 | SIG); |
426 | | |
427 | 0 | if (nullptr == |
428 | 0 | (TG = CPLSearchXMLNode(tileServiceConfig.get(), "TiledPatterns"))) |
429 | 0 | throw CPLOPrintf( |
430 | 0 | "%s Can't locate TiledPatterns in server response.", SIG); |
431 | | |
432 | | // Get the global base_url and bounding box, these can be overwritten at |
433 | | // the tileGroup level They are just pointers into existing structures, |
434 | | // cleanup is not required |
435 | 0 | const char *global_base_url = |
436 | 0 | CPLGetXMLValue(tileServiceConfig.get(), |
437 | 0 | "TiledPatterns.OnlineResource.xlink:href", ""); |
438 | 0 | const CPLXMLNode *global_latlonbbox = CPLGetXMLNode( |
439 | 0 | tileServiceConfig.get(), "TiledPatterns.LatLonBoundingBox"); |
440 | 0 | const CPLXMLNode *global_bbox = |
441 | 0 | CPLGetXMLNode(tileServiceConfig.get(), "TiledPatterns.BoundingBox"); |
442 | 0 | const char *pszProjection = CPLGetXMLValue( |
443 | 0 | tileServiceConfig.get(), "TiledPatterns.Projection", ""); |
444 | 0 | if (pszProjection[0] != 0) |
445 | 0 | m_oSRS.SetFromUserInput( |
446 | 0 | pszProjection, |
447 | 0 | OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get()); |
448 | |
|
449 | 0 | if (nullptr == (TG = SearchLeafGroupName(TG->psChild, tiledGroupName))) |
450 | 0 | throw CPLOPrintf("%s No TiledGroup " |
451 | 0 | "%s" |
452 | 0 | " in server response.", |
453 | 0 | SIG, tiledGroupName.c_str()); |
454 | | |
455 | 0 | int band_count = atoi(CPLGetXMLValue(TG, "Bands", "3")); |
456 | |
|
457 | 0 | if (!GDALCheckBandCount(band_count, FALSE)) |
458 | 0 | throw CPLOPrintf("%s Invalid number of bands in server response", |
459 | 0 | SIG); |
460 | | |
461 | 0 | if (nullptr != CPLGetXMLNode(TG, "Key")) |
462 | 0 | { // Collect all keys defined by this tileset |
463 | 0 | const CPLXMLNode *node = CPLGetXMLNode(TG, "Key"); |
464 | 0 | while (nullptr != node) |
465 | 0 | { // the TEXT of the Key node |
466 | 0 | const char *val = CPLGetXMLValue(node, nullptr, nullptr); |
467 | 0 | if (nullptr != val) |
468 | 0 | keys.AddString(val); |
469 | 0 | node = SearchXMLSiblings(node, "Key"); |
470 | 0 | } |
471 | 0 | } |
472 | | |
473 | | // Data values are attributes, they include NoData Min and Max |
474 | 0 | if (nullptr != CPLGetXMLNode(TG, "DataValues")) |
475 | 0 | { |
476 | 0 | const char *nodata = |
477 | 0 | CPLGetXMLValue(TG, "DataValues.NoData", nullptr); |
478 | 0 | if (nodata != nullptr) |
479 | 0 | { |
480 | 0 | m_parent_dataset->WMSSetNoDataValue(nodata); |
481 | 0 | m_parent_dataset->SetTileOO("@NDV", nodata); |
482 | 0 | } |
483 | 0 | const char *min = CPLGetXMLValue(TG, "DataValues.min", nullptr); |
484 | 0 | if (min != nullptr) |
485 | 0 | m_parent_dataset->WMSSetMinValue(min); |
486 | 0 | const char *max = CPLGetXMLValue(TG, "DataValues.max", nullptr); |
487 | 0 | if (max != nullptr) |
488 | 0 | m_parent_dataset->WMSSetMaxValue(max); |
489 | 0 | } |
490 | |
|
491 | 0 | m_parent_dataset->WMSSetBandsCount(band_count); |
492 | 0 | GDALDataType dt = |
493 | 0 | GDALGetDataTypeByName(CPLGetXMLValue(TG, "DataType", "Byte")); |
494 | 0 | m_parent_dataset->WMSSetDataType(dt); |
495 | 0 | if (dt != GDT_UInt8) |
496 | 0 | m_parent_dataset->SetTileOO("@DATATYPE", GDALGetDataTypeName(dt)); |
497 | | // Let the TiledGroup override the projection |
498 | 0 | pszProjection = CPLGetXMLValue(TG, "Projection", ""); |
499 | 0 | if (pszProjection[0] != 0) |
500 | 0 | m_oSRS = ProjToSRS(pszProjection); |
501 | |
|
502 | 0 | m_base_url = |
503 | 0 | CPLGetXMLValue(TG, "OnlineResource.xlink:href", global_base_url); |
504 | 0 | if (m_base_url[0] == '\0') |
505 | 0 | throw CPLOPrintf( |
506 | 0 | "%s Can't locate OnlineResource in the server response", SIG); |
507 | | |
508 | | // Bounding box, local, global, local lat-lon, global lat-lon, in this |
509 | | // order |
510 | 0 | const CPLXMLNode *bbox = CPLGetXMLNode(TG, "BoundingBox"); |
511 | 0 | if (nullptr == bbox) |
512 | 0 | bbox = global_bbox; |
513 | 0 | if (nullptr == bbox) |
514 | 0 | bbox = CPLGetXMLNode(TG, "LatLonBoundingBox"); |
515 | 0 | if (nullptr == bbox) |
516 | 0 | bbox = global_latlonbbox; |
517 | 0 | if (nullptr == bbox) |
518 | 0 | throw CPLOPrintf( |
519 | 0 | "%s Can't locate the LatLonBoundingBox in server response", |
520 | 0 | SIG); |
521 | | |
522 | | // Check for errors during conversion |
523 | 0 | errno = 0; |
524 | 0 | int err = 0; |
525 | 0 | m_data_window.m_x0 = getXMLNum(bbox, "minx", "0"); |
526 | 0 | err |= errno; |
527 | 0 | m_data_window.m_x1 = getXMLNum(bbox, "maxx", "-1"); |
528 | 0 | err |= errno; |
529 | 0 | m_data_window.m_y0 = getXMLNum(bbox, "maxy", "0"); |
530 | 0 | err |= errno; |
531 | 0 | m_data_window.m_y1 = getXMLNum(bbox, "miny", "-1"); |
532 | 0 | err |= errno; |
533 | 0 | if (err) |
534 | 0 | throw CPLOPrintf("%s Can't parse LatLonBoundingBox", SIG); |
535 | | |
536 | 0 | if ((m_data_window.m_x1 - m_data_window.m_x0) <= 0 || |
537 | 0 | (m_data_window.m_y0 - m_data_window.m_y1) <= 0) |
538 | 0 | throw CPLOPrintf( |
539 | 0 | "%s Coordinate order in BBox problem in server response", SIG); |
540 | | |
541 | | // Is there a palette? |
542 | | // |
543 | | // Format is |
544 | | // <Palette> |
545 | | // <Size>N</Size> : Optional |
546 | | // <Model>RGBA|RGB</Model> : Optional, defaults to RGB |
547 | | // <Entry idx=i c1=v1 c2=v2 c3=v3 c4=v4/> :Optional |
548 | | // <Entry .../> |
549 | | // </Palette> |
550 | | // the idx attribute is optional, it autoincrements |
551 | | // The entries are vertices, interpolation takes place in between if the |
552 | | // indices are not successive index values have to be in increasing |
553 | | // order The palette starts initialized with zeros |
554 | | // |
555 | | |
556 | 0 | bool bHasColorTable = false; |
557 | |
|
558 | 0 | if ((band_count == 1) && CPLGetXMLNode(TG, "Palette")) |
559 | 0 | { |
560 | 0 | const CPLXMLNode *node = CPLGetXMLNode(TG, "Palette"); |
561 | |
|
562 | 0 | int entries = static_cast<int>(getXMLNum(node, "Size", "255")); |
563 | 0 | GDALPaletteInterp eInterp = GPI_RGB; // RGB and RGBA are the same |
564 | |
|
565 | 0 | CPLString pModel = CPLGetXMLValue(node, "Model", "RGB"); |
566 | 0 | if (!pModel.empty() && pModel.find("RGB") == std::string::npos) |
567 | 0 | throw CPLOPrintf( |
568 | 0 | "%s Palette Model %s is unknown, use RGB or RGBA", SIG, |
569 | 0 | pModel.c_str()); |
570 | | |
571 | 0 | if ((entries < 1) || (entries > 256)) |
572 | 0 | throw CPLOPrintf("%s Palette definition error", SIG); |
573 | | |
574 | | // Create it and initialize it to nothing |
575 | 0 | int start_idx; |
576 | 0 | int end_idx; |
577 | 0 | GDALColorEntry ce_start = {0, 0, 0, 255}; |
578 | 0 | GDALColorEntry ce_end = {0, 0, 0, 255}; |
579 | |
|
580 | 0 | auto poColorTable = std::make_unique<GDALColorTable>(eInterp); |
581 | 0 | poColorTable->CreateColorRamp(0, &ce_start, entries - 1, &ce_end); |
582 | | // Read the values |
583 | 0 | const CPLXMLNode *p = CPLGetXMLNode(node, "Entry"); |
584 | 0 | if (p) |
585 | 0 | { |
586 | | // Initialize the first entry |
587 | 0 | start_idx = static_cast<int>(getXMLNum(p, "idx", "0")); |
588 | 0 | ce_start = GetXMLColorEntry(p); |
589 | |
|
590 | 0 | if (start_idx < 0) |
591 | 0 | throw CPLOPrintf("%s Palette index %d not allowed", SIG, |
592 | 0 | start_idx); |
593 | | |
594 | 0 | poColorTable->SetColorEntry(start_idx, &ce_start); |
595 | 0 | while (nullptr != (p = SearchXMLSiblings(p, "Entry"))) |
596 | 0 | { |
597 | | // For every entry, create a ramp |
598 | 0 | ce_end = GetXMLColorEntry(p); |
599 | 0 | end_idx = static_cast<int>( |
600 | 0 | getXMLNum(p, "idx", CPLOPrintf("%d", start_idx + 1))); |
601 | 0 | if ((end_idx <= start_idx) || (start_idx >= entries)) |
602 | 0 | throw CPLOPrintf("%s Index Error at index %d", SIG, |
603 | 0 | end_idx); |
604 | | |
605 | 0 | poColorTable->CreateColorRamp(start_idx, &ce_start, end_idx, |
606 | 0 | &ce_end); |
607 | 0 | ce_start = ce_end; |
608 | 0 | start_idx = end_idx; |
609 | 0 | } |
610 | 0 | } |
611 | | |
612 | | // Dataset has ownership |
613 | 0 | m_parent_dataset->SetColorTable(poColorTable.release()); |
614 | 0 | bHasColorTable = true; |
615 | 0 | } // If palette |
616 | | |
617 | 0 | int overview_count = 0; |
618 | 0 | const CPLXMLNode *Pattern = TG->psChild; |
619 | |
|
620 | 0 | m_bsx = -1; |
621 | 0 | m_bsy = -1; |
622 | 0 | m_data_window.m_sx = 0; |
623 | 0 | m_data_window.m_sy = 0; |
624 | |
|
625 | 0 | while ( |
626 | 0 | (nullptr != Pattern) && |
627 | 0 | (nullptr != (Pattern = SearchXMLSiblings(Pattern, "=TilePattern")))) |
628 | 0 | { |
629 | 0 | int mbsx, mbsy, sx, sy; |
630 | 0 | double x, y, X, Y; |
631 | |
|
632 | 0 | CPLString request; |
633 | 0 | FindChangePattern(Pattern->psChild->pszValue, substs, keys, |
634 | 0 | request); |
635 | 0 | if (request.empty()) |
636 | 0 | break; // No point to drag, this level doesn't match the keys |
637 | | |
638 | 0 | const CPLStringList aosTokens(CSLTokenizeString2(request, "&", 0)); |
639 | |
|
640 | 0 | const char *pszWIDTH = aosTokens.FetchNameValue("WIDTH"); |
641 | 0 | const char *pszHEIGHT = aosTokens.FetchNameValue("HEIGHT"); |
642 | 0 | if (pszWIDTH == nullptr || pszHEIGHT == nullptr) |
643 | 0 | throw CPLOPrintf( |
644 | 0 | "%s Cannot find width or height parameters in %s", SIG, |
645 | 0 | request.c_str()); |
646 | | |
647 | 0 | mbsx = atoi(pszWIDTH); |
648 | 0 | mbsy = atoi(pszHEIGHT); |
649 | | // If unset until now, try to get the projection from the |
650 | | // pattern |
651 | 0 | if (m_oSRS.IsEmpty()) |
652 | 0 | { |
653 | 0 | const char *pszSRS = aosTokens.FetchNameValueDef("SRS", ""); |
654 | 0 | if (pszSRS[0] != 0) |
655 | 0 | m_oSRS = ProjToSRS(pszSRS); |
656 | 0 | } |
657 | |
|
658 | 0 | if (-1 == m_bsx) |
659 | 0 | m_bsx = mbsx; |
660 | 0 | if (-1 == m_bsy) |
661 | 0 | m_bsy = mbsy; |
662 | 0 | if ((m_bsx != mbsx) || (m_bsy != mbsy)) |
663 | 0 | throw CPLOPrintf("%s Tileset uses different block sizes", SIG); |
664 | | |
665 | 0 | if (CPLsscanf(aosTokens.FetchNameValueDef("BBOX", ""), |
666 | 0 | "%lf,%lf,%lf,%lf", &x, &y, &X, &Y) != 4) |
667 | 0 | throw CPLOPrintf("%s Error parsing BBOX, pattern %d\n", SIG, |
668 | 0 | overview_count + 1); |
669 | | |
670 | | // Pick the largest size |
671 | 0 | sx = static_cast<int>((m_data_window.m_x1 - m_data_window.m_x0) / |
672 | 0 | (X - x) * m_bsx); |
673 | 0 | sy = static_cast<int>(fabs( |
674 | 0 | (m_data_window.m_y1 - m_data_window.m_y0) / (Y - y) * m_bsy)); |
675 | 0 | if (sx > m_data_window.m_sx) |
676 | 0 | m_data_window.m_sx = sx; |
677 | 0 | if (sy > m_data_window.m_sy) |
678 | 0 | m_data_window.m_sy = sy; |
679 | | |
680 | | // Only use overlays where the top coordinate is within a pixel from |
681 | | // the top of coverage |
682 | 0 | double pix_off, temp; |
683 | 0 | pix_off = |
684 | 0 | m_bsy * modf(fabs((Y - m_data_window.m_y0) / (Y - y)), &temp); |
685 | 0 | if ((pix_off < 1) || ((m_bsy - pix_off) < 1)) |
686 | 0 | { |
687 | 0 | requests.AddString(request); |
688 | 0 | overview_count++; |
689 | 0 | } |
690 | 0 | else |
691 | 0 | { // Just a warning |
692 | 0 | CPLError(CE_Warning, CPLE_AppDefined, |
693 | 0 | "%s Overlay size %dX%d can't be used due to alignment", |
694 | 0 | SIG, sx, sy); |
695 | 0 | } |
696 | |
|
697 | 0 | Pattern = Pattern->psNext; |
698 | 0 | } // Search for matching TilePattern |
699 | | |
700 | | // Did we find anything |
701 | 0 | if (requests.empty()) |
702 | 0 | throw CPLOPrintf("Can't find any usable TilePattern, maybe the " |
703 | 0 | "Changes are not correct?"); |
704 | | |
705 | | // The tlevel is needed, the tx and ty are not used by this minidriver |
706 | 0 | m_data_window.m_tlevel = 0; |
707 | 0 | m_data_window.m_tx = 0; |
708 | 0 | m_data_window.m_ty = 0; |
709 | | |
710 | | // Make sure the parent_dataset values are set before creating the bands |
711 | 0 | m_parent_dataset->WMSSetBlockSize(m_bsx, m_bsy); |
712 | 0 | m_parent_dataset->WMSSetRasterSize(m_data_window.m_sx, |
713 | 0 | m_data_window.m_sy); |
714 | |
|
715 | 0 | m_parent_dataset->WMSSetDataWindow(m_data_window); |
716 | | // m_parent_dataset->WMSSetOverviewCount(overview_count); |
717 | 0 | m_parent_dataset->WMSSetClamp(false); |
718 | | |
719 | | // Ready for the Rasterband creation |
720 | 0 | for (int i = 0; i < overview_count; i++) |
721 | 0 | { |
722 | 0 | CPLString request = GetLowestScale(requests, i); |
723 | 0 | double scale = Scale(request); |
724 | | |
725 | | // Base scale should be very close to 1 |
726 | 0 | if ((0 == i) && (fabs(scale - 1) > 1e-6)) |
727 | 0 | throw CPLOPrintf("%s Base resolution pattern missing", SIG); |
728 | | |
729 | | // Prepare the request and insert it back into the list |
730 | | // Find returns an answer relative to the original string start! |
731 | 0 | size_t startBbox = FindBbox(request); |
732 | 0 | size_t endBbox = request.find('&', startBbox); |
733 | 0 | if (endBbox == std::string::npos) |
734 | 0 | endBbox = request.size(); |
735 | 0 | request.replace(startBbox, endBbox - startBbox, "${GDAL_BBOX}"); |
736 | 0 | requests.InsertString(i, request); |
737 | | |
738 | | // Create the Rasterband or overview |
739 | 0 | for (int j = 1; j <= band_count; j++) |
740 | 0 | { |
741 | 0 | if (i != 0) |
742 | 0 | { |
743 | 0 | m_parent_dataset->mGetBand(j)->AddOverview(scale); |
744 | 0 | } |
745 | 0 | else |
746 | 0 | { // Base resolution |
747 | 0 | GDALWMSRasterBand *band = |
748 | 0 | new GDALWMSRasterBand(m_parent_dataset, j, 1); |
749 | 0 | if (bHasColorTable) |
750 | 0 | band->SetColorInterpretation(GCI_PaletteIndex); |
751 | 0 | else |
752 | 0 | band->SetColorInterpretation(BandInterp(band_count, j)); |
753 | 0 | m_parent_dataset->mSetBand(j, band); |
754 | 0 | } |
755 | 0 | } |
756 | 0 | } |
757 | | |
758 | 0 | if ((overview_count == 0) || (m_bsx < 1) || (m_bsy < 1)) |
759 | 0 | throw CPLOPrintf("%s No usable TilePattern elements found", SIG); |
760 | | |
761 | | // Do we need to modify the output XML |
762 | 0 | if (0 != CSLCount(OpenOptions)) |
763 | 0 | { |
764 | | // Get the proposed XML, it will exist at this point |
765 | 0 | CPLXMLTreeCloser cfg_root(CPLParseXMLString( |
766 | 0 | m_parent_dataset->GetMetadataItem("XML", "WMS"))); |
767 | 0 | char *pszXML = nullptr; |
768 | |
|
769 | 0 | if (cfg_root) |
770 | 0 | { |
771 | 0 | bool modified = false; |
772 | | |
773 | | // Set openoption StoreConfiguration to Yes to save the server |
774 | | // GTS in the output XML |
775 | 0 | if (CSLFetchBoolean(OpenOptions, "StoreConfiguration", 0) && |
776 | 0 | nullptr == |
777 | 0 | CPLGetXMLNode(cfg_root.get(), "Service.Configuration")) |
778 | 0 | { |
779 | 0 | char *xmlencodedGTS = CPLEscapeString( |
780 | 0 | decodedGTS, static_cast<int>(decodedGTS.size()), |
781 | 0 | CPLES_XML); |
782 | | |
783 | | // It doesn't have a Service.Configuration element, safe to |
784 | | // add one |
785 | 0 | CPLXMLNode *scfg = CPLCreateXMLElementAndValue( |
786 | 0 | CPLGetXMLNode(cfg_root.get(), "Service"), |
787 | 0 | "Configuration", xmlencodedGTS); |
788 | 0 | CPLAddXMLAttributeAndValue(scfg, "encoding", "XMLencoded"); |
789 | 0 | modified = true; |
790 | 0 | CPLFree(xmlencodedGTS); |
791 | 0 | } |
792 | | |
793 | | // Set the TiledGroupName if it's not already there and we have |
794 | | // it as an open option |
795 | 0 | if (!CPLGetXMLNode(cfg_root.get(), "Service.TiledGroupName") && |
796 | 0 | nullptr != CSLFetchNameValue(OpenOptions, "TiledGroupName")) |
797 | 0 | { |
798 | 0 | CPLCreateXMLElementAndValue( |
799 | 0 | CPLGetXMLNode(cfg_root.get(), "Service"), |
800 | 0 | "TiledGroupName", |
801 | 0 | CSLFetchNameValue(OpenOptions, "TiledGroupName")); |
802 | 0 | modified = true; |
803 | 0 | } |
804 | |
|
805 | 0 | if (!substs.empty()) |
806 | 0 | { |
807 | | // Get all the existing Change elements |
808 | 0 | std::set<std::string> oExistingKeys; |
809 | 0 | auto nodechange = |
810 | 0 | CPLGetXMLNode(cfg_root.get(), "Service.Change"); |
811 | 0 | while (nodechange) |
812 | 0 | { |
813 | 0 | const char *key = |
814 | 0 | CPLGetXMLValue(nodechange, "Key", nullptr); |
815 | 0 | if (key) |
816 | 0 | oExistingKeys.insert(key); |
817 | 0 | nodechange = nodechange->psNext; |
818 | 0 | } |
819 | |
|
820 | 0 | for (const char *subst : substs) |
821 | 0 | { |
822 | 0 | CPLString kv(subst); |
823 | 0 | auto sep_pos = |
824 | 0 | kv.find_first_of("=:"); // It should find it |
825 | 0 | if (sep_pos == CPLString::npos) |
826 | 0 | continue; |
827 | 0 | CPLString key(kv.substr(0, sep_pos)); |
828 | 0 | CPLString val(kv.substr(sep_pos + 1)); |
829 | | // Add to the cfg_root if this change is not already |
830 | | // there |
831 | 0 | if (oExistingKeys.find(key) == oExistingKeys.end()) |
832 | 0 | { |
833 | 0 | auto cnode = CPLCreateXMLElementAndValue( |
834 | 0 | CPLGetXMLNode(cfg_root.get(), "Service"), |
835 | 0 | "Change", val); |
836 | 0 | CPLAddXMLAttributeAndValue(cnode, "Key", key); |
837 | 0 | modified = true; |
838 | 0 | } |
839 | 0 | } |
840 | 0 | } |
841 | |
|
842 | 0 | if (modified) |
843 | 0 | { |
844 | 0 | pszXML = CPLSerializeXMLTree(cfg_root.get()); |
845 | 0 | m_parent_dataset->SetXML(pszXML); |
846 | 0 | } |
847 | 0 | } |
848 | |
|
849 | 0 | CPLFree(pszXML); |
850 | 0 | } |
851 | 0 | } |
852 | 0 | catch (const CPLString &msg) |
853 | 0 | { |
854 | 0 | ret = CE_Failure; |
855 | 0 | CPLError(ret, CPLE_AppDefined, "%s", msg.c_str()); |
856 | 0 | } |
857 | | |
858 | 0 | m_requests = std::move(requests); |
859 | 0 | return ret; |
860 | 0 | } |
861 | | |
862 | | CPLErr WMSMiniDriver_TiledWMS::TiledImageRequest( |
863 | | WMSHTTPRequest &request, const GDALWMSImageRequestInfo &iri, |
864 | | const GDALWMSTiledImageRequestInfo &tiri) |
865 | 0 | { |
866 | 0 | CPLString &url = request.URL; |
867 | 0 | url = m_base_url; |
868 | 0 | URLPrepare(url); |
869 | 0 | url += CSLGetField(m_requests.List(), -tiri.m_level); |
870 | 0 | URLSearchAndReplace(&url, "${GDAL_BBOX}", "%013.8f,%013.8f,%013.8f,%013.8f", |
871 | 0 | iri.m_x0, iri.m_y1, iri.m_x1, iri.m_y0); |
872 | 0 | return CE_None; |
873 | 0 | } |