• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geographika / mapserver / 10826014663

12 Sep 2024 07:15AM UTC coverage: 40.289%. First build
10826014663

push

github

geographika
PNG support of OGC Maps

1 of 20 new or added lines in 1 file covered. (5.0%)

58226 of 144522 relevant lines covered (40.29%)

25417.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.96
/src/mapogcapi.cpp
1
/**********************************************************************
2
 * $id$
3
 *
4
 * Project:  MapServer
5
 * Purpose:  OGCAPI Implementation
6
 * Author:   Steve Lime and the MapServer team.
7
 *
8
 **********************************************************************
9
 * Copyright (c) 1996-2005 Regents of the University of Minnesota.
10
 *
11
 * Permission is hereby granted, free of charge, to any person obtaining a
12
 * copy of this software and associated documentation files (the "Software"),
13
 * to deal in the Software without restriction, including without limitation
14
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
15
 * and/or sell copies of the Software, and to permit persons to whom the
16
 * Software is furnished to do so, subject to the following conditions:
17
 *
18
 * The above copyright notice and this permission notice shall be included in
19
 * all copies of this Software or works derived from this Software.
20
 *
21
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
24
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27
 ****************************************************************************/
28
#include "mapserver.h"
29
#include "mapogcapi.h"
30
#include "mapows.h"
31
#include "mapgml.h"
32
#include "maptime.h"
33

34
#include "cpl_conv.h"
35

36
#include "third-party/include_nlohmann_json.hpp"
37
#include "third-party/include_pantor_inja.hpp"
38

39
#include <algorithm>
40
#include <map>
41
#include <string>
42
#include <iostream>
43
#include <utility>
44

45
using namespace inja;
46
using json = nlohmann::json;
47

48
#define OGCAPI_DEFAULT_TITLE "MapServer OGC API"
49

50
/*
51
** HTML Templates
52
*/
53
#define OGCAPI_TEMPLATE_HTML_LANDING "landing.html"
54
#define OGCAPI_TEMPLATE_HTML_CONFORMANCE "conformance.html"
55
#define OGCAPI_TEMPLATE_HTML_COLLECTION "collection.html"
56
#define OGCAPI_TEMPLATE_HTML_COLLECTIONS "collections.html"
57
#define OGCAPI_TEMPLATE_HTML_COLLECTION_ITEMS "collection-items.html"
58
#define OGCAPI_TEMPLATE_HTML_COLLECTION_ITEM "collection-item.html"
59
#define OGCAPI_TEMPLATE_HTML_OPENAPI "openapi.html"
60

61
enum class OGCAPIFormat { JSON, GeoJSON, OpenAPI_V3, HTML, PNG };
62

63
#define OGCAPI_MIMETYPE_JSON "application/json"
64
#define OGCAPI_MIMETYPE_GEOJSON "application/geo+json"
65
#define OGCAPI_MIMETYPE_OPENAPI_V3                                             \
66
  "application/vnd.oai.openapi+json;version=3.0"
67
#define OGCAPI_MIMETYPE_HTML "text/html"
68

69
#define OGCAPI_DEFAULT_LIMIT 10 // by specification
70
#define OGCAPI_MAX_LIMIT 10000
71

72
#define OGCAPI_DEFAULT_GEOMETRY_PRECISION 6
73

74
constexpr const char *EPSG_PREFIX_URL =
75
    "http://www.opengis.net/def/crs/EPSG/0/";
76
constexpr const char *CRS84_URL =
77
    "http://www.opengis.net/def/crs/OGC/1.3/CRS84";
78

79
#ifdef USE_OGCAPI_SVR
80

81
// Error types
82
typedef enum {
83
  OGCAPI_SERVER_ERROR = 0,
84
  OGCAPI_CONFIG_ERROR = 1,
85
  OGCAPI_PARAM_ERROR = 2,
86
  OGCAPI_NOT_FOUND_ERROR = 3,
87
} OGCAPIErrorType;
88

89
/*
90
** Returns a JSON object using and a description.
91
*/
92
static void outputError(OGCAPIErrorType errorType,
9✔
93
                        const std::string &description) {
94
  const char *code = "";
9✔
95
  const char *status = "";
96
  switch (errorType) {
9✔
97
  case OGCAPI_SERVER_ERROR: {
×
98
    code = "ServerError";
×
99
    status = "500";
100
    break;
×
101
  }
102
  case OGCAPI_CONFIG_ERROR: {
2✔
103
    code = "ConfigError";
2✔
104
    status = "500";
105
    break;
2✔
106
  }
107
  case OGCAPI_PARAM_ERROR: {
6✔
108
    code = "InvalidParameterValue";
6✔
109
    status = "400";
110
    break;
6✔
111
  }
112
  case OGCAPI_NOT_FOUND_ERROR: {
1✔
113
    code = "NotFound";
1✔
114
    status = "404";
115
    break;
1✔
116
  }
117
  }
118

119
  json j = {{"code", code}, {"description", description}};
36✔
120

121
  msIO_setHeader("Content-Type", "%s", OGCAPI_MIMETYPE_JSON);
9✔
122
  msIO_setHeader("Status", "%s", status);
9✔
123
  msIO_sendHeaders();
9✔
124
  msIO_printf("%s\n", j.dump().c_str());
18✔
125
}
9✔
126

127
static int includeLayer(mapObj *map, layerObj *layer) {
27✔
128
  if (!msOWSRequestIsEnabled(map, layer, "AO", "OGCAPI", MS_FALSE) ||
54✔
129
      !msIsLayerSupportedForWFSOrOAPIF(layer) || !msIsLayerQueryable(layer)) {
54✔
130
    return MS_FALSE;
×
131
  } else {
132
    return MS_TRUE;
133
  }
134
}
135

136
/*
137
** Get stuff...
138
*/
139

140
/*
141
** Returns the value associated with an item from the request's query string and
142
*NULL if the item was not found.
143
*/
144
static const char *getRequestParameter(cgiRequestObj *request,
122✔
145
                                       const char *item) {
146
  int i;
147

148
  for (i = 0; i < request->NumParams; i++) {
290✔
149
    if (strcmp(item, request->ParamNames[i]) == 0)
222✔
150
      return request->ParamValues[i];
54✔
151
  }
152

153
  return NULL;
154
}
155

156
static int getMaxLimit(mapObj *map, layerObj *layer) {
19✔
157
  int max_limit = OGCAPI_MAX_LIMIT;
19✔
158
  const char *value;
159

160
  // check metadata, layer then map
161
  value = msOWSLookupMetadata(&(layer->metadata), "A", "max_limit");
19✔
162
  if (value == NULL)
19✔
163
    value = msOWSLookupMetadata(&(map->web.metadata), "A", "max_limit");
19✔
164

165
  if (value != NULL) {
19✔
166
    int status = msStringToInt(value, &max_limit, 10);
15✔
167
    if (status != MS_SUCCESS)
15✔
168
      max_limit = OGCAPI_MAX_LIMIT; // conversion failed
×
169
  }
170

171
  return max_limit;
19✔
172
}
173

174
static int getDefaultLimit(mapObj *map, layerObj *layer) {
21✔
175
  int default_limit = OGCAPI_DEFAULT_LIMIT;
21✔
176

177
  // check metadata, layer then map
178
  const char *value =
179
      msOWSLookupMetadata(&(layer->metadata), "A", "default_limit");
21✔
180
  if (value == NULL)
21✔
181
    value = msOWSLookupMetadata(&(map->web.metadata), "A", "default_limit");
21✔
182

183
  if (value != NULL) {
21✔
184
    int status = msStringToInt(value, &default_limit, 10);
13✔
185
    if (status != MS_SUCCESS)
13✔
186
      default_limit = OGCAPI_DEFAULT_LIMIT; // conversion failed
×
187
  }
188

189
  return default_limit;
21✔
190
}
191

192
/*
193
** Returns the limit as an int - between 1 and getMaxLimit(). We always return a
194
*valid value...
195
*/
196
static int getLimit(mapObj *map, cgiRequestObj *request, layerObj *layer,
19✔
197
                    int *limit) {
198
  int status;
199
  const char *p;
200

201
  int max_limit;
202
  max_limit = getMaxLimit(map, layer);
19✔
203

204
  p = getRequestParameter(request, "limit");
19✔
205
  if (!p || (p && strlen(p) == 0)) { // missing or empty
19✔
206
    *limit = MS_MIN(getDefaultLimit(map, layer),
9✔
207
                    max_limit); // max could be smaller than the default
208
  } else {
209
    status = msStringToInt(p, limit, 10);
10✔
210
    if (status != MS_SUCCESS)
10✔
211
      return MS_FAILURE;
212

213
    if (*limit <= 0) {
10✔
214
      *limit = MS_MIN(getDefaultLimit(map, layer),
×
215
                      max_limit); // max could be smaller than the default
216
    } else {
217
      *limit = MS_MIN(*limit, max_limit);
10✔
218
    }
219
  }
220

221
  return MS_SUCCESS;
222
}
223

224
// Return the content of the "crs" member of the /collections/{name} response
225
static json getCrsList(mapObj *map, layerObj *layer) {
13✔
226
  char *pszSRSList = NULL;
13✔
227
  msOWSGetEPSGProj(&(layer->projection), &(layer->metadata), "AOF", MS_FALSE,
13✔
228
                   &pszSRSList);
229
  if (!pszSRSList)
13✔
230
    msOWSGetEPSGProj(&(map->projection), &(map->web.metadata), "AOF", MS_FALSE,
×
231
                     &pszSRSList);
232
  json jCrsList;
233
  if (pszSRSList) {
13✔
234
    const auto tokens = msStringSplit(pszSRSList, ' ');
13✔
235
    for (const auto &crs : tokens) {
37✔
236
      if (crs.find("EPSG:") == 0) {
24✔
237
        if (jCrsList.empty()) {
35✔
238
          jCrsList.push_back(CRS84_URL);
26✔
239
        }
240
        const std::string url =
241
            std::string(EPSG_PREFIX_URL) + crs.substr(strlen("EPSG:"));
48✔
242
        jCrsList.push_back(url);
48✔
243
      }
244
    }
245
    msFree(pszSRSList);
13✔
246
  }
247
  return jCrsList;
13✔
248
}
249

250
// Return the content of the "storageCrs" member of the /collections/{name}
251
// response
252
static std::string getStorageCrs(layerObj *layer) {
5✔
253
  std::string storageCrs;
254
  char *pszFirstSRS = nullptr;
5✔
255
  msOWSGetEPSGProj(&(layer->projection), &(layer->metadata), "AOF", MS_TRUE,
5✔
256
                   &pszFirstSRS);
257
  if (pszFirstSRS) {
5✔
258
    if (std::string(pszFirstSRS).find("EPSG:") == 0) {
10✔
259
      storageCrs =
260
          std::string(EPSG_PREFIX_URL) + (pszFirstSRS + strlen("EPSG:"));
10✔
261
    }
262
    msFree(pszFirstSRS);
5✔
263
  }
264
  return storageCrs;
5✔
265
}
266

267
/*
268
** Returns the bbox in output CRS (CRS84 by default, or "crs" request parameter
269
*when specified)
270
*/
271
static bool getBbox(mapObj *map, layerObj *layer, cgiRequestObj *request,
17✔
272
                    rectObj *bbox, projectionObj *outputProj) {
273
  int status;
274

275
  const char *bboxParam = getRequestParameter(request, "bbox");
17✔
276
  if (!bboxParam ||
17✔
277
      strlen(bboxParam) == 0) { // missing or empty - assign map->extent (no
5✔
278
                                // projection necessary)
279
    bbox->minx = map->extent.minx;
12✔
280
    bbox->miny = map->extent.miny;
12✔
281
    bbox->maxx = map->extent.maxx;
12✔
282
    bbox->maxy = map->extent.maxy;
12✔
283
  } else {
284
    const auto tokens = msStringSplit(bboxParam, ',');
8✔
285
    if (tokens.size() != 4) {
5✔
286
      outputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
287
      return false;
2✔
288
    }
289

290
    double values[4];
291
    for (int i = 0; i < 4; i++) {
25✔
292
      status = msStringToDouble(tokens[i].c_str(), &values[i]);
20✔
293
      if (status != MS_SUCCESS) {
20✔
294
        outputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
295
        return false;
×
296
      }
297
    }
298

299
    bbox->minx = values[0]; // assign
5✔
300
    bbox->miny = values[1];
5✔
301
    bbox->maxx = values[2];
5✔
302
    bbox->maxy = values[3];
5✔
303

304
    // validate bbox is well-formed (degenerate is ok)
305
    if (MS_VALID_SEARCH_EXTENT(*bbox) != MS_TRUE) {
5✔
306
      outputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
307
      return false;
×
308
    }
309

310
    std::string bboxCrs = "EPSG:4326";
5✔
311
    bool axisInverted =
312
        false; // because above EPSG:4326 is meant to be OGC:CRS84 actually
313
    const char *bboxCrsParam = getRequestParameter(request, "bbox-crs");
5✔
314
    if (bboxCrsParam) {
5✔
315
      bool isExpectedCrs = false;
316
      for (const auto &crsItem : getCrsList(map, layer)) {
26✔
317
        if (bboxCrsParam == crsItem.get<std::string>()) {
11✔
318
          isExpectedCrs = true;
319
          break;
320
        }
321
      }
322
      if (!isExpectedCrs) {
4✔
323
        outputError(OGCAPI_PARAM_ERROR, "Bad value for bbox-crs.");
2✔
324
        return false;
2✔
325
      }
326
      if (std::string(bboxCrsParam) != CRS84_URL) {
4✔
327
        if (std::string(bboxCrsParam).find(EPSG_PREFIX_URL) == 0) {
4✔
328
          const char *code = bboxCrsParam + strlen(EPSG_PREFIX_URL);
2✔
329
          bboxCrs = std::string("EPSG:") + code;
4✔
330
          axisInverted = msIsAxisInverted(atoi(code));
2✔
331
        }
332
      }
333
    }
334
    if (axisInverted) {
2✔
335
      std::swap(bbox->minx, bbox->miny);
336
      std::swap(bbox->maxx, bbox->maxy);
337
    }
338

339
    projectionObj bboxProj;
340
    msInitProjection(&bboxProj);
3✔
341
    msProjectionInheritContextFrom(&bboxProj, &(map->projection));
3✔
342
    if (msLoadProjectionString(&bboxProj, bboxCrs.c_str()) != 0) {
3✔
343
      msFreeProjection(&bboxProj);
×
344
      outputError(OGCAPI_SERVER_ERROR, "Cannot process bbox-crs.");
×
345
      return false;
×
346
    }
347

348
    status = msProjectRect(&bboxProj, outputProj, bbox);
3✔
349
    msFreeProjection(&bboxProj);
3✔
350
    if (status != MS_SUCCESS) {
3✔
351
      outputError(OGCAPI_SERVER_ERROR,
×
352
                  "Cannot reproject bbox from bbox-crs to output CRS.");
353
      return false;
×
354
    }
355
  }
356

357
  return true;
358
}
359

360
/*
361
** Returns the template directory location or NULL if it isn't set.
362
*/
363
static std::string getTemplateDirectory(mapObj *map, const char *key,
4✔
364
                                        const char *envvar) {
365
  const char *directory;
366

367
  directory = msOWSLookupMetadata(&(map->web.metadata), "A", key);
4✔
368

369
  if (directory == NULL) {
4✔
370
    directory = CPLGetConfigOption(envvar, NULL);
×
371
  }
372

373
  std::string s;
374
  if (directory != NULL) {
4✔
375
    s = directory;
376
    if (!s.empty() && (s.back() != '/' && s.back() != '\\')) {
4✔
377
      // add a trailing slash if missing
378
      std::string slash = "/";
2✔
379
#ifdef _WIN32
380
      slash = "\\";
381
#endif
382
      s += slash;
383
    }
384
  }
385

386
  return s;
4✔
387
}
388

389
/*
390
** Returns the service title from oga_{key} and/or ows_{key} or a default value
391
*if not set.
392
*/
393
static const char *getWebMetadata(mapObj *map, const char *domain,
394
                                  const char *key, const char *defaultVal) {
395
  const char *value;
396

397
  if ((value = msOWSLookupMetadata(&(map->web.metadata), domain, key)) != NULL)
18✔
398
    return value;
399
  else
400
    return defaultVal;
3✔
401
}
402

403
/*
404
** Returns the service title from oga|ows_title or a default value if not set.
405
*/
406
static const char *getTitle(mapObj *map) {
407
  return getWebMetadata(map, "OA", "title", OGCAPI_DEFAULT_TITLE);
408
}
409

410
/*
411
** Returns the API root URL from oga_onlineresource or builds a value if not
412
*set.
413
*/
414
static std::string getApiRootUrl(mapObj *map) {
27✔
415
  const char *root;
416

417
  if ((root = msOWSLookupMetadata(&(map->web.metadata), "A",
27✔
418
                                  "onlineresource")) != NULL)
419
    return std::string(root);
27✔
420
  else
421
    return "http://" + std::string(getenv("SERVER_NAME")) + ":" +
×
422
           std::string(getenv("SERVER_PORT")) +
×
423
           std::string(getenv("SCRIPT_NAME")) +
×
424
           std::string(getenv("PATH_INFO"));
×
425
}
426

427
static json getFeatureConstant(const gmlConstantObj *constant) {
×
428
  json j; // empty (null)
429

430
  if (!constant)
×
431
    throw std::runtime_error("Null constant metadata.");
×
432
  if (!constant->value)
×
433
    return j;
434

435
  // initialize
436
  j = {{constant->name, constant->value}};
×
437

438
  return j;
×
439
}
440

441
static json getFeatureItem(const gmlItemObj *item, const char *value) {
156✔
442
  json j; // empty (null)
443
  const char *key;
444

445
  if (!item)
156✔
446
    throw std::runtime_error("Null item metadata.");
×
447
  if (!item->visible)
156✔
448
    return j;
449

450
  if (item->alias)
104✔
451
    key = item->alias;
64✔
452
  else
453
    key = item->name;
40✔
454

455
  // initialize
456
  j = {{key, value}};
520✔
457

458
  if (item->type &&
104✔
459
      (EQUAL(item->type, "Date") || EQUAL(item->type, "DateTime") ||
64✔
460
       EQUAL(item->type, "Time"))) {
48✔
461
    struct tm tm;
462
    if (msParseTime(value, &tm) == MS_TRUE) {
16✔
463
      char tmpValue[64];
464
      if (EQUAL(item->type, "Date"))
16✔
465
        snprintf(tmpValue, sizeof(tmpValue), "%04d-%02d-%02d",
×
466
                 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
×
467
      else if (EQUAL(item->type, "Time"))
16✔
468
        snprintf(tmpValue, sizeof(tmpValue), "%02d:%02d:%02dZ", tm.tm_hour,
×
469
                 tm.tm_min, tm.tm_sec);
470
      else
471
        snprintf(tmpValue, sizeof(tmpValue), "%04d-%02d-%02dT%02d:%02d:%02dZ",
16✔
472
                 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour,
16✔
473
                 tm.tm_min, tm.tm_sec);
474

475
      j = {{key, tmpValue}};
80✔
476
    }
16✔
477
  } else if (item->type &&
88✔
478
             (EQUAL(item->type, "Integer") || EQUAL(item->type, "Long"))) {
48✔
479
    try {
480
      j = {{key, std::stoll(value)}};
96✔
481
    } catch (const std::exception &) {
×
482
    }
483
  } else if (item->type && EQUAL(item->type, "Real")) {
72✔
484
    try {
485
      j = {{key, std::stod(value)}};
208✔
486
    } catch (const std::exception &) {
×
487
    }
488
  } else if (item->type && EQUAL(item->type, "Boolean")) {
40✔
489
    if (EQUAL(value, "0") || EQUAL(value, "false")) {
×
490
      j = {{key, false}};
×
491
    } else {
492
      j = {{key, true}};
×
493
    }
494
  }
495

496
  return j;
497
}
498

499
static double round_down(double value, int decimal_places) {
10✔
500
  const double multiplier = std::pow(10.0, decimal_places);
501
  return std::floor(value * multiplier) / multiplier;
10✔
502
}
503
// https://stackoverflow.com/questions/25925290/c-round-a-double-up-to-2-decimal-places
504
static double round_up(double value, int decimal_places) {
30,520✔
505
  const double multiplier = std::pow(10.0, decimal_places);
506
  return std::ceil(value * multiplier) / multiplier;
30,520✔
507
}
508

509
static json getFeatureGeometry(shapeObj *shape, int precision,
20✔
510
                               bool outputCrsAxisInverted) {
511
  json geometry; // empty (null)
512
  int *outerList = NULL, numOuterRings = 0;
513

514
  if (!shape)
20✔
515
    throw std::runtime_error("Null shape.");
×
516

517
  switch (shape->type) {
20✔
518
  case (MS_SHAPE_POINT):
×
519
    if (shape->numlines == 0 ||
×
520
        shape->line[0].numpoints == 0) // not enough info for a point
×
521
      return geometry;
522

523
    if (shape->line[0].numpoints == 1) {
×
524
      geometry["type"] = "Point";
×
525
      double x = shape->line[0].point[0].x;
×
526
      double y = shape->line[0].point[0].y;
×
527
      if (outputCrsAxisInverted)
×
528
        std::swap(x, y);
529
      geometry["coordinates"] = {round_up(x, precision),
×
530
                                 round_up(y, precision)};
×
531
    } else {
532
      geometry["type"] = "MultiPoint";
×
533
      geometry["coordinates"] = json::array();
×
534
      for (int j = 0; j < shape->line[0].numpoints; j++) {
×
535
        double x = shape->line[0].point[j].x;
×
536
        double y = shape->line[0].point[j].y;
×
537
        if (outputCrsAxisInverted)
×
538
          std::swap(x, y);
539
        geometry["coordinates"].push_back(
×
540
            {round_up(x, precision), round_up(y, precision)});
×
541
      }
542
    }
543
    break;
544
  case (MS_SHAPE_LINE):
×
545
    if (shape->numlines == 0 ||
×
546
        shape->line[0].numpoints < 2) // not enough info for a line
×
547
      return geometry;
548

549
    if (shape->numlines == 1) {
×
550
      geometry["type"] = "LineString";
×
551
      geometry["coordinates"] = json::array();
×
552
      for (int j = 0; j < shape->line[0].numpoints; j++) {
×
553
        double x = shape->line[0].point[j].x;
×
554
        double y = shape->line[0].point[j].y;
×
555
        if (outputCrsAxisInverted)
×
556
          std::swap(x, y);
557
        geometry["coordinates"].push_back(
×
558
            {round_up(x, precision), round_up(y, precision)});
×
559
      }
560
    } else {
561
      geometry["type"] = "MultiLineString";
×
562
      geometry["coordinates"] = json::array();
×
563
      for (int i = 0; i < shape->numlines; i++) {
×
564
        json part = json::array();
×
565
        for (int j = 0; j < shape->line[i].numpoints; j++) {
×
566
          double x = shape->line[i].point[j].x;
×
567
          double y = shape->line[i].point[j].y;
×
568
          if (outputCrsAxisInverted)
×
569
            std::swap(x, y);
570
          part.push_back({round_up(x, precision), round_up(y, precision)});
×
571
        }
572
        geometry["coordinates"].push_back(part);
×
573
      }
574
    }
575
    break;
576
  case (MS_SHAPE_POLYGON):
20✔
577
    if (shape->numlines == 0 ||
20✔
578
        shape->line[0].numpoints <
20✔
579
            4) // not enough info for a polygon (first=last)
580
      return geometry;
581

582
    outerList = msGetOuterList(shape);
20✔
583
    if (outerList == NULL)
20✔
584
      throw std::runtime_error("Unable to allocate list of outer rings.");
×
585
    for (int k = 0; k < shape->numlines; k++) {
40✔
586
      if (outerList[k] == MS_TRUE)
20✔
587
        numOuterRings++;
20✔
588
    }
589

590
    if (numOuterRings == 1) {
20✔
591
      geometry["type"] = "Polygon";
40✔
592
      geometry["coordinates"] = json::array();
40✔
593
      for (int i = 0; i < shape->numlines; i++) {
40✔
594
        json part = json::array();
20✔
595
        for (int j = 0; j < shape->line[i].numpoints; j++) {
15,275✔
596
          double x = shape->line[i].point[j].x;
15,255✔
597
          double y = shape->line[i].point[j].y;
15,255✔
598
          if (outputCrsAxisInverted)
15,255✔
599
            std::swap(x, y);
600
          part.push_back({round_up(x, precision), round_up(y, precision)});
91,530✔
601
        }
602
        geometry["coordinates"].push_back(part);
20✔
603
      }
604
    } else {
605
      geometry["type"] = "MultiPolygon";
×
606
      geometry["coordinates"] = json::array();
×
607

608
      for (int k = 0; k < shape->numlines; k++) {
×
609
        if (outerList[k] ==
×
610
            MS_TRUE) { // outer ring: generate polygon and add to coordinates
611
          int *innerList = msGetInnerList(shape, k, outerList);
×
612
          if (innerList == NULL) {
×
613
            msFree(outerList);
×
614
            throw std::runtime_error("Unable to allocate list of inner rings.");
×
615
          }
616

617
          json polygon = json::array();
×
618
          for (int i = 0; i < shape->numlines; i++) {
×
619
            if (i == k ||
×
620
                outerList[i] ==
×
621
                    MS_TRUE) { // add outer ring (k) and any inner rings
622
              json part = json::array();
×
623
              for (int j = 0; j < shape->line[i].numpoints; j++) {
×
624
                double x = shape->line[i].point[j].x;
×
625
                double y = shape->line[i].point[j].y;
×
626
                if (outputCrsAxisInverted)
×
627
                  std::swap(x, y);
628
                part.push_back(
×
629
                    {round_up(x, precision), round_up(y, precision)});
×
630
              }
631
              polygon.push_back(part);
×
632
            }
633
          }
634

635
          msFree(innerList);
×
636
          geometry["coordinates"].push_back(polygon);
×
637
        }
638
      }
639
    }
640
    msFree(outerList);
20✔
641
    break;
20✔
642
  default:
×
643
    throw std::runtime_error("Invalid shape type.");
×
644
    break;
645
  }
646

647
  return geometry;
648
}
649

650
/*
651
** Return a GeoJSON representation of a shape.
652
*/
653
static json getFeature(layerObj *layer, shapeObj *shape, gmlItemListObj *items,
20✔
654
                       gmlConstantListObj *constants, int geometry_precision,
655
                       bool outputCrsAxisInverted) {
656
  int i;
657
  json feature; // empty (null)
658

659
  if (!layer || !shape)
20✔
660
    throw std::runtime_error("Null arguments.");
×
661

662
  // initialize
663
  feature = {{"type", "Feature"}, {"properties", json::object()}};
180✔
664

665
  // id
666
  const char *featureIdItem =
667
      msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
20✔
668
  if (featureIdItem == NULL)
20✔
669
    throw std::runtime_error(
670
        "Missing required featureid metadata."); // should have been trapped
×
671
                                                 // earlier
672
  for (i = 0; i < items->numitems; i++) {
88✔
673
    if (strcasecmp(featureIdItem, items->items[i].name) == 0) {
88✔
674
      feature["id"] = shape->values[i];
40✔
675
      break;
20✔
676
    }
677
  }
678

679
  if (i == items->numitems)
20✔
680
    throw std::runtime_error("Feature id not found.");
×
681

682
  // properties - build from items and constants, no group support for now
683

684
  for (int i = 0; i < items->numitems; i++) {
176✔
685
    try {
686
      json item = getFeatureItem(&(items->items[i]), shape->values[i]);
156✔
687
      if (!item.is_null())
156✔
688
        feature["properties"].insert(item.begin(), item.end());
104✔
689
    } catch (const std::runtime_error &) {
×
690
      throw std::runtime_error("Error fetching item.");
×
691
    }
692
  }
693

694
  for (int i = 0; i < constants->numconstants; i++) {
20✔
695
    try {
696
      json constant = getFeatureConstant(&(constants->constants[i]));
×
697
      if (!constant.is_null())
×
698
        feature["properties"].insert(constant.begin(), constant.end());
×
699
    } catch (const std::runtime_error &) {
×
700
      throw std::runtime_error("Error fetching constant.");
×
701
    }
702
  }
703

704
  // geometry
705
  try {
706
    json geometry =
707
        getFeatureGeometry(shape, geometry_precision, outputCrsAxisInverted);
20✔
708
    if (!geometry.is_null())
20✔
709
      feature["geometry"] = std::move(geometry);
40✔
710
  } catch (const std::runtime_error &) {
×
711
    throw std::runtime_error("Error fetching geometry.");
×
712
  }
713

714
  return feature;
20✔
715
}
716

717
static json getLink(hashTableObj *metadata, const std::string &name) {
11✔
718
  json link;
719

720
  const char *href =
721
      msOWSLookupMetadata(metadata, "A", (name + "_href").c_str());
11✔
722
  if (!href)
11✔
723
    throw std::runtime_error("Missing required link href property.");
×
724

725
  const char *title =
726
      msOWSLookupMetadata(metadata, "A", (name + "_title").c_str());
11✔
727
  const char *type =
728
      msOWSLookupMetadata(metadata, "A", (name + "_type").c_str());
22✔
729

730
  link = {{"href", href},
731
          {"title", title ? title : href},
11✔
732
          {"type", type ? type : "text/html"}};
154✔
733

734
  return link;
11✔
735
}
736

737
static const char *getCollectionDescription(layerObj *layer) {
14✔
738
  const char *description =
739
      msOWSLookupMetadata(&(layer->metadata), "A", "description");
14✔
740
  if (!description)
14✔
741
    description = msOWSLookupMetadata(&(layer->metadata), "OF",
×
742
                                      "abstract"); // fallback on abstract
743
  if (!description)
14✔
744
    description =
745
        "<!-- Warning: unable to set the collection description. -->"; // finally
746
                                                                       // a
747
                                                                       // warning...
748
  return description;
14✔
749
}
750

751
static const char *getCollectionTitle(layerObj *layer) {
752
  const char *title = msOWSLookupMetadata(&(layer->metadata), "AOF", "title");
11✔
753
  if (!title)
16✔
754
    title = layer->name; // revert to layer name if no title found
×
755
  return title;
756
}
757

758
static int getGeometryPrecision(mapObj *map, layerObj *layer) {
18✔
759
  int geometry_precision = OGCAPI_DEFAULT_GEOMETRY_PRECISION;
760
  if (msOWSLookupMetadata(&(layer->metadata), "AF", "geometry_precision")) {
18✔
761
    geometry_precision = atoi(
×
762
        msOWSLookupMetadata(&(layer->metadata), "AF", "geometry_precision"));
763
  } else if (msOWSLookupMetadata(&map->web.metadata, "AF",
18✔
764
                                 "geometry_precision")) {
765
    geometry_precision = atoi(
18✔
766
        msOWSLookupMetadata(&map->web.metadata, "AF", "geometry_precision"));
767
  }
768
  return geometry_precision;
18✔
769
}
770

771
static json getCollection(mapObj *map, layerObj *layer, OGCAPIFormat format) {
5✔
772
  json collection; // empty (null)
773
  rectObj bbox;
774

775
  if (!map || !layer)
5✔
776
    return collection;
777

778
  if (!includeLayer(map, layer))
5✔
779
    return collection;
780

781
  // initialize some things
782
  std::string api_root = getApiRootUrl(map);
5✔
783

784
  if (msOWSGetLayerExtent(map, layer, "AOF", &bbox) == MS_SUCCESS) {
5✔
785
    if (layer->projection.numargs > 0)
5✔
786
      msOWSProjectToWGS84(&layer->projection, &bbox);
5✔
787
    else if (map->projection.numargs > 0)
×
788
      msOWSProjectToWGS84(&map->projection, &bbox);
×
789
    else
790
      throw std::runtime_error(
791
          "Unable to transform bounding box, no projection defined.");
×
792
  } else {
793
    throw std::runtime_error(
794
        "Unable to get collection bounding box."); // might be too harsh since
×
795
                                                   // extent is optional
796
  }
797

798
  const char *description = getCollectionDescription(layer);
5✔
799
  const char *title = getCollectionTitle(layer);
5✔
800

801
  const char *id = layer->name;
5✔
802
  char *id_encoded = msEncodeUrl(id); // free after use
5✔
803

804
  const int geometry_precision = getGeometryPrecision(map, layer);
5✔
805

806
  // build collection object
807
  collection = {{"id", id},
808
                {"description", description},
809
                {"title", title},
810
                {"extent",
811
                 {{"spatial",
812
                   {{"bbox",
813
                     {{round_down(bbox.minx, geometry_precision),
5✔
814
                       round_down(bbox.miny, geometry_precision),
5✔
815
                       round_up(bbox.maxx, geometry_precision),
5✔
816
                       round_up(bbox.maxy, geometry_precision)}}},
5✔
817
                    {"crs", CRS84_URL}}}}},
818
                {"links",
819
                 {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
6✔
820
                   {"type", OGCAPI_MIMETYPE_JSON},
821
                   {"title", "This collection as JSON"},
822
                   {"href", api_root + "/collections/" +
10✔
823
                                std::string(id_encoded) + "?f=json"}},
20✔
824
                  {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
9✔
825
                   {"type", OGCAPI_MIMETYPE_HTML},
826
                   {"title", "This collection as HTML"},
827
                   {"href", api_root + "/collections/" +
10✔
828
                                std::string(id_encoded) + "?f=html"}},
20✔
829
                  {{"rel", "items"},
830
                   {"type", OGCAPI_MIMETYPE_GEOJSON},
831
                   {"title", "Items for this collection as GeoJSON"},
832
                   {"href", api_root + "/collections/" +
10✔
833
                                std::string(id_encoded) + "/items?f=json"}},
20✔
834
                  {{"rel", "items"},
835
                   {"type", OGCAPI_MIMETYPE_HTML},
836
                   {"title", "Items for this collection as HTML"},
837
                   {"href", api_root + "/collections/" +
10✔
838
                                std::string(id_encoded) + "/items?f=html"}}
20✔
839

840
                 }},
841
                {"itemType", "feature"}};
555✔
842

843
  msFree(id_encoded); // done
5✔
844

845
  // handle optional configuration (keywords and links)
846
  const char *value = msOWSLookupMetadata(&(layer->metadata), "A", "keywords");
5✔
847
  if (!value)
5✔
848
    value = msOWSLookupMetadata(&(layer->metadata), "OF",
×
849
                                "keywordlist"); // fallback on keywordlist
850
  if (value) {
5✔
851
    std::vector<std::string> keywords = msStringSplit(value, ',');
10✔
852
    for (const std::string &keyword : keywords) {
24✔
853
      collection["keywords"].push_back(keyword);
57✔
854
    }
855
  }
856

857
  value = msOWSLookupMetadata(&(layer->metadata), "A", "links");
5✔
858
  if (value) {
5✔
859
    std::vector<std::string> names = msStringSplit(value, ',');
10✔
860
    for (const std::string &name : names) {
10✔
861
      try {
862
        json link = getLink(&(layer->metadata), name);
5✔
863
        collection["links"].push_back(link);
5✔
864
      } catch (const std::runtime_error &e) {
×
865
        throw e;
×
866
      }
867
    }
868
  }
869

870
  // Part 2 - CRS support
871
  // Inspect metadata to set the "crs": [] member and "storageCrs" member
872

873
  json jCrsList = getCrsList(map, layer);
5✔
874
  if (!jCrsList.empty()) {
10✔
875
    collection["crs"] = std::move(jCrsList);
5✔
876

877
    std::string storageCrs = getStorageCrs(layer);
5✔
878
    if (!storageCrs.empty()) {
5✔
879
      collection["storageCrs"] = std::move(storageCrs);
10✔
880
    }
881
  }
882

883
  return collection;
884
}
885

886
/*
887
** Output stuff...
888
*/
889

890
static void outputJson(const json &j, const char *mimetype,
17✔
891
                       const std::map<std::string, std::string> &extraHeaders) {
892
  std::string js;
893

894
  try {
895
    js = j.dump();
17✔
896
  } catch (...) {
1✔
897
    outputError(OGCAPI_CONFIG_ERROR, "Invalid UTF-8 data, check encoding.");
2✔
898
    return;
899
  }
900

901
  msIO_setHeader("Content-Type", "%s", mimetype);
16✔
902
  for (const auto &kvp : extraHeaders) {
26✔
903
    msIO_setHeader(kvp.first.c_str(), "%s", kvp.second.c_str());
10✔
904
  }
905
  msIO_sendHeaders();
16✔
906
  msIO_printf("%s\n", js.c_str());
16✔
907
}
908

909
static void outputTemplate(const char *directory, const char *filename,
4✔
910
                           const json &j, const char *mimetype) {
911
  std::string _directory(directory);
4✔
912
  std::string _filename(filename);
4✔
913
  Environment env{_directory}; // catch
7✔
914

915
  // ERB-style instead of Mustache (we'll see)
916
  // env.set_expression("<%=", "%>");
917
  // env.set_statement("<%", "%>");
918

919
  // callbacks, need:
920
  //   - match (regex)
921
  //   - contains (substring)
922
  //   - URL encode
923

924
  try {
925
    std::string js = j.dump();
4✔
926
  } catch (...) {
1✔
927
    outputError(OGCAPI_CONFIG_ERROR, "Invalid UTF-8 data, check encoding.");
2✔
928
    return;
929
  }
930

931
  try {
932
    Template t = env.parse_template(_filename); // catch
6✔
933
    std::string result = env.render(t, j);
3✔
934

935
    msIO_setHeader("Content-Type", "%s", mimetype);
3✔
936
    msIO_sendHeaders();
3✔
937
    msIO_printf("%s\n", result.c_str());
3✔
938
  } catch (const inja::RenderError &e) {
×
939
    outputError(OGCAPI_CONFIG_ERROR, "Template rendering error. " +
×
940
                                         std::string(e.what()) + " (" +
×
941
                                         std::string(filename) + ").");
×
942
    return;
943
  } catch (const inja::InjaError &e) {
×
944
    outputError(OGCAPI_CONFIG_ERROR, "InjaError error. " +
×
945
                                         std::string(e.what()) + " (" +
×
946
                                         std::string(filename) + ")." + " (" +
×
947
                                         std::string(directory) + ").");
×
948
    return;
949
  } catch (...) {
×
950
    outputError(OGCAPI_SERVER_ERROR, "General template handling error.");
×
951
    return;
952
  }
953
}
954

955
/*
956
** Generic response output.
957
*/
958
static void
959
outputResponse(mapObj *map, cgiRequestObj *request, OGCAPIFormat format,
21✔
960
               const char *filename, const json &response,
961
               const std::map<std::string, std::string> &extraHeaders =
962
                   std::map<std::string, std::string>()) {
963
  std::string path;
964
  char fullpath[MS_MAXPATHLEN];
965

966
  if (format == OGCAPIFormat::JSON) {
21✔
967
    outputJson(response, OGCAPI_MIMETYPE_JSON, extraHeaders);
5✔
968
  } else if (format == OGCAPIFormat::GeoJSON) {
16✔
969
    outputJson(response, OGCAPI_MIMETYPE_GEOJSON, extraHeaders);
11✔
970
  } else if (format == OGCAPIFormat::OpenAPI_V3) {
5✔
971
    outputJson(response, OGCAPI_MIMETYPE_OPENAPI_V3, extraHeaders);
1✔
972
  } else if (format == OGCAPIFormat::HTML) {
4✔
973
    path = getTemplateDirectory(map, "html_template_directory",
8✔
974
                                "OGCAPI_HTML_TEMPLATE_DIRECTORY");
4✔
975
    if (path.empty()) {
4✔
976
      outputError(OGCAPI_CONFIG_ERROR, "Template directory not set.");
×
977
      return; // bail
×
978
    }
979
    msBuildPath(fullpath, map->mappath, path.c_str());
4✔
980

981
    json j;
982

983
    j["response"] = response; // nest the response so we could write the whole
8✔
984
                              // object in the template
985

986
    // extend the JSON with a few things that we need for templating
987
    j["template"] = {{"path", json::array()},
12✔
988
                     {"params", json::object()},
8✔
989
                     {"api_root", getApiRootUrl(map)},
8✔
990
                     {"title", getTitle(map)},
4✔
991
                     {"tags", json::object()}};
92✔
992

993
    // api path
994
    for (int i = 0; i < request->api_path_length; i++)
20✔
995
      j["template"]["path"].push_back(request->api_path[i]);
48✔
996

997
    // parameters (optional)
998
    for (int i = 0; i < request->NumParams; i++) {
7✔
999
      if (request->ParamValues[i] &&
3✔
1000
          strlen(request->ParamValues[i]) > 0) { // skip empty params
3✔
1001
        j["template"]["params"].update(
18✔
1002
            {{request->ParamNames[i], request->ParamValues[i]}});
3✔
1003
      }
1004
    }
1005

1006
    // add custom tags (optional)
1007
    const char *tags =
1008
        msOWSLookupMetadata(&(map->web.metadata), "A", "html_tags");
4✔
1009
    if (tags) {
4✔
1010
      std::vector<std::string> names = msStringSplit(tags, ',');
4✔
1011
      for (std::string name : names) {
6✔
1012
        const char *value = msOWSLookupMetadata(&(map->web.metadata), "A",
4✔
1013
                                                ("tag_" + name).c_str());
8✔
1014
        if (value) {
4✔
1015
          j["template"]["tags"].update({{name, value}}); // add object
24✔
1016
        }
1017
      }
1018
    }
1019

1020
    outputTemplate(fullpath, filename, j, OGCAPI_MIMETYPE_HTML);
4✔
1021
  } else {
1022
    outputError(OGCAPI_PARAM_ERROR, "Unsupported format requested.");
×
1023
  }
1024
}
1025

1026
/*
1027
** Process stuff...
1028
*/
1029
static int processLandingRequest(mapObj *map, cgiRequestObj *request,
3✔
1030
                                 OGCAPIFormat format) {
1031
  json response;
1032

1033
  // define ambiguous elements
1034
  const char *description =
1035
      msOWSLookupMetadata(&(map->web.metadata), "A", "description");
3✔
1036
  if (!description)
3✔
1037
    description =
1038
        msOWSLookupMetadata(&(map->web.metadata), "OF",
×
1039
                            "abstract"); // fallback on abstract if necessary
1040

1041
  // define api root url
1042
  std::string api_root = getApiRootUrl(map);
3✔
1043

1044
  // build response object
1045
  //   - consider conditionally excluding links for HTML format
1046
  response = {
1047
      {"title", getTitle(map)},
3✔
1048
      {"description", description ? description : ""},
3✔
1049
      {"links",
1050
       {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
4✔
1051
         {"type", OGCAPI_MIMETYPE_JSON},
1052
         {"title", "This document as JSON"},
1053
         {"href", api_root + "?f=json"}},
6✔
1054
        {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
5✔
1055
         {"type", OGCAPI_MIMETYPE_HTML},
1056
         {"title", "This document as HTML"},
1057
         {"href", api_root + "?f=html"}},
6✔
1058
        {{"rel", "conformance"},
1059
         {"type", OGCAPI_MIMETYPE_JSON},
1060
         {"title",
1061
          "OCG API conformance classes implemented by this server (JSON)"},
1062
         {"href", api_root + "/conformance?f=json"}},
6✔
1063
        {{"rel", "conformance"},
1064
         {"type", OGCAPI_MIMETYPE_HTML},
1065
         {"title", "OCG API conformance classes implemented by this server"},
1066
         {"href", api_root + "/conformance?f=html"}},
6✔
1067
        {{"rel", "data"},
1068
         {"type", OGCAPI_MIMETYPE_JSON},
1069
         {"title", "Information about feature collections available from this "
1070
                   "server (JSON)"},
1071
         {"href", api_root + "/collections?f=json"}},
6✔
1072
        {{"rel", "data"},
1073
         {"type", OGCAPI_MIMETYPE_HTML},
1074
         {"title",
1075
          "Information about feature collections available from this server"},
1076
         {"href", api_root + "/collections?f=html"}},
6✔
1077
        {{"rel", "service-desc"},
1078
         {"type", OGCAPI_MIMETYPE_OPENAPI_V3},
1079
         {"title", "OpenAPI document"},
1080
         {"href", api_root + "/api?f=json"}},
6✔
1081
        {{"rel", "service-doc"},
1082
         {"type", OGCAPI_MIMETYPE_HTML},
1083
         {"title", "API documentation"},
1084
         {"href", api_root + "/api?f=html"}}}}};
465✔
1085

1086
  // handle custom links (optional)
1087
  const char *links = msOWSLookupMetadata(&(map->web.metadata), "A", "links");
3✔
1088
  if (links) {
3✔
1089
    std::vector<std::string> names = msStringSplit(links, ',');
6✔
1090
    for (std::string name : names) {
9✔
1091
      try {
1092
        json link = getLink(&(map->web.metadata), name);
6✔
1093
        response["links"].push_back(link);
6✔
1094
      } catch (const std::runtime_error &e) {
×
1095
        outputError(OGCAPI_CONFIG_ERROR, std::string(e.what()));
×
1096
        return MS_SUCCESS;
1097
      }
1098
    }
1099
  }
1100

1101
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_LANDING, response);
3✔
1102
  return MS_SUCCESS;
3✔
1103
}
1104

1105
static int processConformanceRequest(mapObj *map, cgiRequestObj *request,
1✔
1106
                                     OGCAPIFormat format) {
1107
  json response;
1108

1109
  // build response object
1110
  response = {
1111
      {"conformsTo",
1112
       {
1113
           "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core",
1114
           "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections",
1115
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core",
1116
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30",
1117
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/html",
1118
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson",
1119
           "http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs",
1120
       }}};
12✔
1121

1122
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_CONFORMANCE,
2✔
1123
                 response);
1124
  return MS_SUCCESS;
1✔
1125
}
1126

1127
static int processCollectionItemsRequest(mapObj *map, cgiRequestObj *request,
19✔
1128
                                         const char *collectionId,
1129
                                         const char *featureId,
1130
                                         OGCAPIFormat format) {
1131
  json response;
1132
  int i;
1133
  layerObj *layer;
1134

1135
  int limit;
1136
  rectObj bbox;
1137

1138
  int numberMatched = 0;
1139

1140
  // find the right layer
1141
  for (i = 0; i < map->numlayers; i++) {
21✔
1142
    if (strcmp(map->layers[i]->name, collectionId) == 0)
21✔
1143
      break; // match
1144
  }
1145

1146
  if (i == map->numlayers) { // invalid collectionId
19✔
1147
    outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1148
    return MS_SUCCESS;
×
1149
  }
1150

1151
  layer = map->layers[i]; // for convenience
19✔
1152
  layer->status = MS_ON;  // force on (do we need to save and reset?)
19✔
1153

1154
  if (!includeLayer(map, layer)) {
19✔
1155
    outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1156
    return MS_SUCCESS;
×
1157
  }
1158

1159
  //
1160
  // handle parameters specific to this endpoint
1161
  //
1162
  if (getLimit(map, request, layer, &limit) != MS_SUCCESS) {
19✔
1163
    outputError(OGCAPI_PARAM_ERROR, "Bad value for limit.");
×
1164
    return MS_SUCCESS;
×
1165
  }
1166

1167
  const char *crs = getRequestParameter(request, "crs");
19✔
1168

1169
  std::string outputCrs = "EPSG:4326";
19✔
1170
  bool outputCrsAxisInverted =
1171
      false; // because above EPSG:4326 is meant to be OGC:CRS84 actually
1172
  std::map<std::string, std::string> extraHeaders;
1173
  if (crs) {
19✔
1174
    bool isExpectedCrs = false;
1175
    for (const auto &crsItem : getCrsList(map, layer)) {
26✔
1176
      if (crs == crsItem.get<std::string>()) {
11✔
1177
        isExpectedCrs = true;
1178
        break;
1179
      }
1180
    }
1181
    if (!isExpectedCrs) {
4✔
1182
      outputError(OGCAPI_PARAM_ERROR, "Bad value for crs.");
2✔
1183
      return MS_SUCCESS;
2✔
1184
    }
1185
    extraHeaders["Content-Crs"] = '<' + std::string(crs) + '>';
4✔
1186
    if (std::string(crs) != CRS84_URL) {
4✔
1187
      if (std::string(crs).find(EPSG_PREFIX_URL) == 0) {
4✔
1188
        const char *code = crs + strlen(EPSG_PREFIX_URL);
2✔
1189
        outputCrs = std::string("EPSG:") + code;
4✔
1190
        outputCrsAxisInverted = msIsAxisInverted(atoi(code));
2✔
1191
      }
1192
    }
1193
  } else {
1194
    extraHeaders["Content-Crs"] = '<' + std::string(CRS84_URL) + '>';
30✔
1195
  }
1196

1197
  struct ReprojectionObjects {
1198
    reprojectionObj *reprojector = NULL;
1199
    projectionObj proj;
1200

1201
    ReprojectionObjects() { msInitProjection(&proj); }
17✔
1202

1203
    ~ReprojectionObjects() {
×
1204
      msProjectDestroyReprojector(reprojector);
17✔
1205
      msFreeProjection(&proj);
17✔
1206
    }
17✔
1207

1208
    int executeQuery(mapObj *map) {
24✔
1209
      projectionObj backupMapProjection = map->projection;
24✔
1210
      map->projection = proj;
24✔
1211
      int ret = msExecuteQuery(map);
24✔
1212
      map->projection = backupMapProjection;
24✔
1213
      return ret;
24✔
1214
    }
1215
  };
1216
  ReprojectionObjects reprObjs;
1217

1218
  msProjectionInheritContextFrom(&reprObjs.proj, &(map->projection));
17✔
1219
  if (msLoadProjectionString(&reprObjs.proj, outputCrs.c_str()) != 0) {
17✔
1220
    outputError(OGCAPI_SERVER_ERROR, "Cannot instantiate output CRS.");
×
1221
    return MS_SUCCESS;
×
1222
  }
1223

1224
  if (layer->projection.numargs > 0) {
17✔
1225
    if (msProjectionsDiffer(&(layer->projection), &reprObjs.proj)) {
17✔
1226
      reprObjs.reprojector =
12✔
1227
          msProjectCreateReprojector(&(layer->projection), &reprObjs.proj);
12✔
1228
      if (reprObjs.reprojector == NULL) {
12✔
1229
        outputError(OGCAPI_SERVER_ERROR, "Error creating re-projector.");
×
1230
        return MS_SUCCESS;
×
1231
      }
1232
    }
1233
  } else if (map->projection.numargs > 0) {
×
1234
    if (msProjectionsDiffer(&(map->projection), &reprObjs.proj)) {
×
1235
      reprObjs.reprojector =
×
1236
          msProjectCreateReprojector(&(map->projection), &reprObjs.proj);
×
1237
      if (reprObjs.reprojector == NULL) {
×
1238
        outputError(OGCAPI_SERVER_ERROR, "Error creating re-projector.");
×
1239
        return MS_SUCCESS;
×
1240
      }
1241
    }
1242
  } else {
1243
    outputError(OGCAPI_CONFIG_ERROR,
×
1244
                "Unable to transform geometries, no projection defined.");
1245
    return MS_SUCCESS;
×
1246
  }
1247

1248
  if (map->projection.numargs > 0) {
17✔
1249
    msProjectRect(&(map->projection), &reprObjs.proj, &map->extent);
17✔
1250
  }
1251

1252
  if (!getBbox(map, layer, request, &bbox, &reprObjs.proj)) {
17✔
1253
    return MS_SUCCESS;
1254
  }
1255

1256
  int offset = 0;
15✔
1257
  if (featureId) {
15✔
1258
    const char *featureIdItem =
1259
        msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
2✔
1260
    if (featureIdItem == NULL) {
2✔
1261
      outputError(OGCAPI_CONFIG_ERROR, "Missing required featureid metadata.");
×
1262
      return MS_SUCCESS;
×
1263
    }
1264

1265
    // TODO: does featureIdItem exist in the data?
1266

1267
    // optional validation
1268
    const char *featureIdValidation =
1269
        msLookupHashTable(&(layer->validation), featureIdItem);
2✔
1270
    if (featureIdValidation &&
4✔
1271
        msValidateParameter(featureId, featureIdValidation, NULL, NULL, NULL) !=
2✔
1272
            MS_SUCCESS) {
1273
      outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid feature id.");
1✔
1274
      return MS_SUCCESS;
1✔
1275
    }
1276

1277
    map->query.type = MS_QUERY_BY_FILTER;
1✔
1278
    map->query.mode = MS_QUERY_SINGLE;
1✔
1279
    map->query.layer = i;
1✔
1280
    map->query.rect = bbox;
1✔
1281
    map->query.filteritem = strdup(featureIdItem);
1✔
1282

1283
    msInitExpression(&map->query.filter);
1✔
1284
    map->query.filter.type = MS_STRING;
1✔
1285
    map->query.filter.string = strdup(featureId);
1✔
1286

1287
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
1✔
1288
      outputError(OGCAPI_NOT_FOUND_ERROR, "Collection items id query failed.");
×
1289
      return MS_SUCCESS;
×
1290
    }
1291

1292
    if (!layer->resultcache || layer->resultcache->numresults != 1) {
1✔
1293
      outputError(OGCAPI_NOT_FOUND_ERROR, "Collection items id query failed.");
×
1294
      return MS_SUCCESS;
×
1295
    }
1296
  } else { // bbox query
1297

1298
    const char *compliance_mode =
1299
        msOWSLookupMetadata(&(map->web.metadata), "A", "compliance_mode");
13✔
1300
    if (compliance_mode != NULL && strcasecmp(compliance_mode, "true") == 0) {
13✔
1301
      for (int j = 0; j < request->NumParams; j++) {
40✔
1302
        const char *paramName = request->ParamNames[j];
28✔
1303
        if (strcmp(paramName, "f") == 0 || strcmp(paramName, "bbox") == 0 ||
28✔
1304
            strcmp(paramName, "bbox-crs") == 0 ||
12✔
1305
            strcmp(paramName, "datetime") == 0 ||
10✔
1306
            strcmp(paramName, "limit") == 0 ||
10✔
1307
            strcmp(paramName, "offset") == 0 || strcmp(paramName, "crs") == 0) {
4✔
1308
          // ok
1309
        } else {
1310
          outputError(
2✔
1311
              OGCAPI_PARAM_ERROR,
1312
              (std::string("Unknown query parameter: ") + paramName).c_str());
2✔
1313
          return MS_SUCCESS;
1✔
1314
        }
1315
      }
1316
    }
1317

1318
    map->query.type = MS_QUERY_BY_RECT;
12✔
1319
    map->query.mode = MS_QUERY_MULTIPLE;
12✔
1320
    map->query.layer = i;
12✔
1321
    map->query.rect = bbox;
12✔
1322
    map->query.only_cache_result_count = MS_TRUE;
12✔
1323

1324
    // get number matched
1325
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
12✔
1326
      outputError(OGCAPI_NOT_FOUND_ERROR, "Collection items query failed.");
×
1327
      return MS_SUCCESS;
×
1328
    }
1329

1330
    if (!layer->resultcache) {
12✔
1331
      outputError(OGCAPI_NOT_FOUND_ERROR, "Collection items query failed.");
×
1332
      return MS_SUCCESS;
×
1333
    }
1334

1335
    numberMatched = layer->resultcache->numresults;
12✔
1336

1337
    if (numberMatched > 0) {
12✔
1338
      map->query.only_cache_result_count = MS_FALSE;
11✔
1339
      map->query.maxfeatures = limit;
11✔
1340

1341
      const char *offsetStr = getRequestParameter(request, "offset");
11✔
1342
      if (offsetStr) {
11✔
1343
        if (msStringToInt(offsetStr, &offset, 10) != MS_SUCCESS) {
1✔
1344
          outputError(OGCAPI_PARAM_ERROR, "Bad value for offset.");
×
1345
          return MS_SUCCESS;
×
1346
        }
1347

1348
        if (offset < 0 || offset >= numberMatched) {
1✔
1349
          outputError(OGCAPI_PARAM_ERROR, "Offset out of range.");
×
1350
          return MS_SUCCESS;
×
1351
        }
1352

1353
        // msExecuteQuery() use a 1-based offset convention, whereas the API
1354
        // uses a 0-based offset convention.
1355
        map->query.startindex = 1 + offset;
1✔
1356
        layer->startindex = 1 + offset;
1✔
1357
      }
1358

1359
      if (reprObjs.executeQuery(map) != MS_SUCCESS || !layer->resultcache) {
11✔
1360
        outputError(OGCAPI_NOT_FOUND_ERROR, "Collection items query failed.");
×
1361
        return MS_SUCCESS;
×
1362
      }
1363
    }
1364
  }
1365

1366
  // build response object
1367
  if (!featureId) {
13✔
1368
    std::string api_root = getApiRootUrl(map);
12✔
1369
    const char *id = layer->name;
12✔
1370
    char *id_encoded = msEncodeUrl(id); // free after use
12✔
1371

1372
    std::string extra_kvp = "&limit=" + std::to_string(limit);
12✔
1373
    extra_kvp += "&offset=" + std::to_string(offset);
24✔
1374

1375
    std::string other_extra_kvp;
1376
    if (crs)
12✔
1377
      other_extra_kvp += "&crs=" + std::string(crs);
4✔
1378
    const char *bbox = getRequestParameter(request, "bbox");
12✔
1379
    if (bbox)
12✔
1380
      other_extra_kvp += "&bbox=" + std::string(bbox);
6✔
1381
    const char *bboxCrs = getRequestParameter(request, "bbox-crs");
12✔
1382
    if (bboxCrs)
12✔
1383
      other_extra_kvp += "&bbox-crs=" + std::string(bboxCrs);
4✔
1384

1385
    response = {
1386
        {"type", "FeatureCollection"},
1387
        {"numberMatched", numberMatched},
1388
        {"numberReturned", layer->resultcache->numresults},
12✔
1389
        {"features", json::array()},
24✔
1390
        {"links",
1391
         {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
14✔
1392
           {"type", OGCAPI_MIMETYPE_GEOJSON},
1393
           {"title", "Items for this collection as GeoJSON"},
1394
           {"href", api_root + "/collections/" + std::string(id_encoded) +
24✔
1395
                        "/items?f=json" + extra_kvp + other_extra_kvp}},
24✔
1396
          {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
22✔
1397
           {"type", OGCAPI_MIMETYPE_HTML},
1398
           {"title", "Items for this collection as HTML"},
1399
           {"href", api_root + "/collections/" + std::string(id_encoded) +
24✔
1400
                        "/items?f=html" + extra_kvp + other_extra_kvp}}}}};
696✔
1401

1402
    if (offset + layer->resultcache->numresults < numberMatched) {
12✔
1403
      response["links"].push_back(
133✔
1404
          {{"rel", "next"},
1405
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
14✔
1406
                                                 : OGCAPI_MIMETYPE_HTML},
1407
           {"title", "next page"},
1408
           {"href",
1409
            api_root + "/collections/" + std::string(id_encoded) +
14✔
1410
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
21✔
1411
                "&limit=" + std::to_string(limit) + "&offset=" +
28✔
1412
                std::to_string(offset + limit) + other_extra_kvp}});
28✔
1413
    }
1414

1415
    if (offset > 0) {
12✔
1416
      response["links"].push_back(
19✔
1417
          {{"rel", "prev"},
1418
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
2✔
1419
                                                 : OGCAPI_MIMETYPE_HTML},
1420
           {"title", "previous page"},
1421
           {"href",
1422
            api_root + "/collections/" + std::string(id_encoded) +
2✔
1423
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
3✔
1424
                "&limit=" + std::to_string(limit) +
4✔
1425
                "&offset=" + std::to_string(MS_MAX(0, (offset - limit))) +
4✔
1426
                other_extra_kvp}});
1427
    }
1428

1429
    msFree(id_encoded); // done
12✔
1430
  }
1431

1432
  // features (items)
1433
  {
1434
    shapeObj shape;
1435
    msInitShape(&shape);
13✔
1436

1437
    // we piggyback on GML configuration
1438
    gmlItemListObj *items = msGMLGetItems(layer, "AG");
13✔
1439
    gmlConstantListObj *constants = msGMLGetConstants(layer, "AG");
13✔
1440

1441
    if (!items || !constants) {
13✔
1442
      msGMLFreeItems(items);
×
1443
      msGMLFreeConstants(constants);
×
1444
      outputError(OGCAPI_SERVER_ERROR,
×
1445
                  "Error fetching layer attribute metadata.");
1446
      return MS_SUCCESS;
×
1447
    }
1448

1449
    const int geometry_precision = getGeometryPrecision(map, layer);
13✔
1450

1451
    for (i = 0; i < layer->resultcache->numresults; i++) {
33✔
1452
      int status =
1453
          msLayerGetShape(layer, &shape, &(layer->resultcache->results[i]));
20✔
1454
      if (status != MS_SUCCESS) {
20✔
1455
        msGMLFreeItems(items);
×
1456
        msGMLFreeConstants(constants);
×
1457
        outputError(OGCAPI_SERVER_ERROR, "Error fetching feature.");
×
1458
        return MS_SUCCESS;
×
1459
      }
1460

1461
      if (reprObjs.reprojector) {
20✔
1462
        status = msProjectShapeEx(reprObjs.reprojector, &shape);
15✔
1463
        if (status != MS_SUCCESS) {
15✔
1464
          msGMLFreeItems(items);
×
1465
          msGMLFreeConstants(constants);
×
1466
          msFreeShape(&shape);
×
1467
          outputError(OGCAPI_SERVER_ERROR, "Error reprojecting feature.");
×
1468
          return MS_SUCCESS;
×
1469
        }
1470
      }
1471

1472
      try {
1473
        json feature = getFeature(layer, &shape, items, constants,
1474
                                  geometry_precision, outputCrsAxisInverted);
20✔
1475
        if (featureId) {
20✔
1476
          response = std::move(feature);
1✔
1477
        } else {
1478
          response["features"].emplace_back(std::move(feature));
19✔
1479
        }
1480
      } catch (const std::runtime_error &e) {
×
1481
        msGMLFreeItems(items);
×
1482
        msGMLFreeConstants(constants);
×
1483
        msFreeShape(&shape);
×
1484
        outputError(OGCAPI_SERVER_ERROR,
×
1485
                    "Error getting feature. " + std::string(e.what()));
×
1486
        return MS_SUCCESS;
1487
      }
1488

1489
      msFreeShape(&shape); // next
20✔
1490
    }
1491

1492
    msGMLFreeItems(items); // clean up
13✔
1493
    msGMLFreeConstants(constants);
13✔
1494
  }
1495

1496
  // extend the response a bit for templating (HERE)
1497
  if (format == OGCAPIFormat::HTML) {
13✔
1498
    const char *title = getCollectionTitle(layer);
1499
    const char *id = layer->name;
2✔
1500
    response["collection"] = {{"id", id}, {"title", title ? title : ""}};
18✔
1501
  }
1502

1503
  if (featureId) {
13✔
1504
    std::string api_root = getApiRootUrl(map);
1✔
1505
    const char *id = layer->name;
1✔
1506
    char *id_encoded = msEncodeUrl(id); // free after use
1✔
1507

1508
    response["links"] = {
1✔
1509
        {{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
1✔
1510
         {"type", OGCAPI_MIMETYPE_GEOJSON},
1511
         {"title", "This document as GeoJSON"},
1512
         {"href", api_root + "/collections/" + std::string(id_encoded) +
2✔
1513
                      "/items/" + featureId + "?f=json"}},
2✔
1514
        {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
2✔
1515
         {"type", OGCAPI_MIMETYPE_HTML},
1516
         {"title", "This document as HTML"},
1517
         {"href", api_root + "/collections/" + std::string(id_encoded) +
2✔
1518
                      "/items/" + featureId + "?f=html"}},
2✔
1519
        {{"rel", "collection"},
1520
         {"type", OGCAPI_MIMETYPE_JSON},
1521
         {"title", "This collection as JSON"},
1522
         {"href",
1523
          api_root + "/collections/" + std::string(id_encoded) + "?f=json"}},
2✔
1524
        {{"rel", "collection"},
1525
         {"type", OGCAPI_MIMETYPE_HTML},
1526
         {"title", "This collection as HTML"},
1527
         {"href",
1528
          api_root + "/collections/" + std::string(id_encoded) + "?f=html"}}};
72✔
1529

1530
    msFree(id_encoded);
1✔
1531

1532
    outputResponse(
2✔
1533
        map, request,
1534
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
1535
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEM, response, extraHeaders);
1536
  } else {
1537
    outputResponse(
22✔
1538
        map, request,
1539
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
1540
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEMS, response, extraHeaders);
1541
  }
1542
  return MS_SUCCESS;
1543
}
1544

1545
static int processCollectionRequest(mapObj *map, cgiRequestObj *request,
2✔
1546
                                    const char *collectionId,
1547
                                    OGCAPIFormat format) {
1548
  json response;
1549
  int l;
1550

1551
  for (l = 0; l < map->numlayers; l++) {
2✔
1552
    if (strcmp(map->layers[l]->name, collectionId) == 0)
2✔
1553
      break; // match
1554
  }
1555

1556
  if (l == map->numlayers) { // invalid collectionId
2✔
1557
    outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1558
    return MS_SUCCESS;
×
1559
  }
1560

1561
  try {
1562
    response = getCollection(map, map->layers[l], format);
2✔
1563
    if (response.is_null()) { // same as not found
2✔
1564
      outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1565
      return MS_SUCCESS;
×
1566
    }
1567
  } catch (const std::runtime_error &e) {
×
1568
    outputError(OGCAPI_CONFIG_ERROR,
×
1569
                "Error getting collection. " + std::string(e.what()));
×
1570
    return MS_SUCCESS;
1571
  }
1572

1573
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTION,
2✔
1574
                 response);
1575
  return MS_SUCCESS;
2✔
1576
}
1577

1578
static int processCollectionsRequest(mapObj *map, cgiRequestObj *request,
1✔
1579
                                     OGCAPIFormat format) {
1580
  json response;
1581
  int i;
1582

1583
  // define api root url
1584
  std::string api_root = getApiRootUrl(map);
1✔
1585

1586
  // build response object
1587
  response = {{"links",
1588
               {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
1✔
1589
                 {"type", OGCAPI_MIMETYPE_JSON},
1590
                 {"title", "This document as JSON"},
1591
                 {"href", api_root + "/collections?f=json"}},
2✔
1592
                {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
2✔
1593
                 {"type", OGCAPI_MIMETYPE_HTML},
1594
                 {"title", "This document as HTML"},
1595
                 {"href", api_root + "/collections?f=html"}}}},
2✔
1596
              {"collections", json::array()}};
44✔
1597

1598
  for (i = 0; i < map->numlayers; i++) {
4✔
1599
    try {
1600
      json collection = getCollection(map, map->layers[i], format);
3✔
1601
      if (!collection.is_null())
3✔
1602
        response["collections"].push_back(collection);
3✔
1603
    } catch (const std::runtime_error &e) {
×
1604
      outputError(OGCAPI_CONFIG_ERROR,
×
1605
                  "Error getting collection." + std::string(e.what()));
×
1606
      return MS_SUCCESS;
1607
    }
1608
  }
1609

1610
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTIONS,
1✔
1611
                 response);
1612
  return MS_SUCCESS;
1✔
1613
}
1614

1615
static int processApiRequest(mapObj *map, cgiRequestObj *request,
1✔
1616
                             OGCAPIFormat format) {
1617
  // Strongly inspired from
1618
  // https://github.com/geopython/pygeoapi/blob/master/pygeoapi/openapi.py
1619

1620
  json response;
1621

1622
  response = {
1623
      {"openapi", "3.0.2"},
1624
      {"tags", json::array()},
2✔
1625
  };
10✔
1626

1627
  response["info"] = {
1✔
1628
      {"title", getTitle(map)},
1✔
1629
      {"version", getWebMetadata(map, "A", "version", "1.0.0")},
1✔
1630
  };
9✔
1631

1632
  for (const char *item : {"description", "termsOfService"}) {
3✔
1633
    const char *value = getWebMetadata(map, "AO", item, nullptr);
2✔
1634
    if (value) {
2✔
1635
      response["info"][item] = value;
4✔
1636
    }
1637
  }
1638

1639
  for (const auto &pair : {
3✔
1640
           std::make_pair("name", "contactperson"),
1641
           std::make_pair("url", "contacturl"),
1642
           std::make_pair("email", "contactelectronicmailaddress"),
1643
       }) {
4✔
1644
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
3✔
1645
    if (value) {
3✔
1646
      response["info"]["contact"][pair.first] = value;
6✔
1647
    }
1648
  }
1649

1650
  for (const auto &pair : {
2✔
1651
           std::make_pair("name", "licensename"),
1652
           std::make_pair("url", "licenseurl"),
1653
       }) {
3✔
1654
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
2✔
1655
    if (value) {
2✔
1656
      response["info"]["license"][pair.first] = value;
×
1657
    }
1658
  }
1659

1660
  {
1661
    const char *value = getWebMetadata(map, "AO", "keywords", nullptr);
1✔
1662
    if (value) {
1✔
1663
      response["info"]["x-keywords"] = value;
2✔
1664
    }
1665
  }
1666

1667
  json server;
1668
  server["url"] = getApiRootUrl(map);
3✔
1669
  {
1670
    const char *value =
1671
        getWebMetadata(map, "AO", "server_description", nullptr);
1✔
1672
    if (value) {
1✔
1673
      server["description"] = value;
2✔
1674
    }
1675
  }
1676
  response["servers"].push_back(server);
1✔
1677

1678
  const std::string oapif_schema_base_url = msOWSGetSchemasLocation(map);
1✔
1679
  const std::string oapif_yaml_url = oapif_schema_base_url +
1680
                                     "/ogcapi/features/part1/1.0/openapi/"
1681
                                     "ogcapi-features-1.yaml";
1✔
1682
  const std::string oapif_part2_yaml_url = oapif_schema_base_url +
1683
                                           "/ogcapi/features/part2/1.0/openapi/"
1684
                                           "ogcapi-features-2.yaml";
1✔
1685

1686
  json paths;
1687

1688
  paths["/"]["get"] = {
1✔
1689
      {"summary", "Landing page"},
1690
      {"description", "Landing page"},
1691
      {"tags", {"server"}},
1692
      {"operationId", "getLandingPage"},
1693
      {"parameters",
1694
       {
1695
           {{"$ref", "#/components/parameters/f"}},
1696
       }},
1697
      {"responses",
1698
       {{"200",
1699
         {{"$ref", oapif_yaml_url + "#/components/responses/LandingPage"}}},
2✔
1700
        {"400",
1701
         {{"$ref",
1702
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
2✔
1703
        {"500",
1704
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
53✔
1705

1706
  paths["/api"]["get"] = {
1✔
1707
      {"summary", "API documentation"},
1708
      {"description", "API documentation"},
1709
      {"tags", {"server"}},
1710
      {"operationId", "getOpenapi"},
1711
      {"parameters",
1712
       {
1713
           {{"$ref", "#/components/parameters/f"}},
1714
       }},
1715
      {"responses",
1716
       {{"200", {{"$ref", "#/components/responses/200"}}},
1717
        {"400",
1718
         {{"$ref",
1719
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
2✔
1720
        {"default", {{"$ref", "#/components/responses/default"}}}}}};
51✔
1721

1722
  paths["/conformance"]["get"] = {
1✔
1723
      {"summary", "API conformance definition"},
1724
      {"description", "API conformance definition"},
1725
      {"tags", {"server"}},
1726
      {"operationId", "getConformanceDeclaration"},
1727
      {"parameters",
1728
       {
1729
           {{"$ref", "#/components/parameters/f"}},
1730
       }},
1731
      {"responses",
1732
       {{"200",
1733
         {{"$ref",
1734
           oapif_yaml_url + "#/components/responses/ConformanceDeclaration"}}},
2✔
1735
        {"400",
1736
         {{"$ref",
1737
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
2✔
1738
        {"500",
1739
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
53✔
1740

1741
  paths["/collections"]["get"] = {
1✔
1742
      {"summary", "Collections"},
1743
      {"description", "Collections"},
1744
      {"tags", {"server"}},
1745
      {"operationId", "getCollections"},
1746
      {"parameters",
1747
       {
1748
           {{"$ref", "#/components/parameters/f"}},
1749
       }},
1750
      {"responses",
1751
       {{"200",
1752
         {{"$ref", oapif_yaml_url + "#/components/responses/Collections"}}},
2✔
1753
        {"400",
1754
         {{"$ref",
1755
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
2✔
1756
        {"500",
1757
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
53✔
1758

1759
  for (int i = 0; i < map->numlayers; i++) {
4✔
1760
    layerObj *layer = map->layers[i];
3✔
1761
    if (!includeLayer(map, layer)) {
3✔
1762
      continue;
×
1763
    }
1764

1765
    json collection_get = {
1766
        {"summary",
1767
         std::string("Get ") + getCollectionTitle(layer) + " metadata"},
6✔
1768
        {"description", getCollectionDescription(layer)},
3✔
1769
        {"tags", {layer->name}},
3✔
1770
        {"operationId", "describe" + std::string(layer->name) + "Collection"},
6✔
1771
        {"parameters",
1772
         {
1773
             {{"$ref", "#/components/parameters/f"}},
1774
         }},
1775
        {"responses",
1776
         {{"200",
1777
           {{"$ref", oapif_yaml_url + "#/components/responses/Collection"}}},
6✔
1778
          {"400",
1779
           {{"$ref",
1780
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
6✔
1781
          {"500",
1782
           {{"$ref",
1783
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
90✔
1784

1785
    std::string collectionNamePath("/collections/");
3✔
1786
    collectionNamePath += layer->name;
3✔
1787
    paths[collectionNamePath]["get"] = std::move(collection_get);
3✔
1788

1789
    // check metadata, layer then map
1790
    const char *max_limit_str =
1791
        msOWSLookupMetadata(&(layer->metadata), "A", "max_limit");
3✔
1792
    if (max_limit_str == nullptr)
3✔
1793
      max_limit_str =
1794
          msOWSLookupMetadata(&(map->web.metadata), "A", "max_limit");
3✔
1795
    const int max_limit =
1796
        max_limit_str ? atoi(max_limit_str) : OGCAPI_MAX_LIMIT;
3✔
1797
    const int default_limit = getDefaultLimit(map, layer);
3✔
1798

1799
    json items_get = {
1800
        {"summary", std::string("Get ") + getCollectionTitle(layer) + " items"},
6✔
1801
        {"description", getCollectionDescription(layer)},
3✔
1802
        {"tags", {layer->name}},
1803
        {"operationId", "get" + std::string(layer->name) + "Features"},
6✔
1804
        {"parameters",
1805
         {
1806
             {{"$ref", "#/components/parameters/f"}},
1807
             {{"$ref", oapif_yaml_url + "#/components/parameters/bbox"}},
6✔
1808
             {{"$ref", oapif_yaml_url + "#/components/parameters/datetime"}},
6✔
1809
             {{"$ref",
1810
               oapif_part2_yaml_url + "#/components/parameters/bbox-crs"}},
6✔
1811
             {{"$ref", oapif_part2_yaml_url + "#/components/parameters/crs"}},
6✔
1812
             {{"$ref", "#/components/parameters/offset"}},
1813
         }},
1814
        {"responses",
1815
         {{"200",
1816
           {{"$ref", oapif_yaml_url + "#/components/responses/Features"}}},
6✔
1817
          {"400",
1818
           {{"$ref",
1819
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
6✔
1820
          {"500",
1821
           {{"$ref",
1822
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
135✔
1823

1824
    json param_limit = {
1825
        {"name", "limit"},
1826
        {"in", "query"},
1827
        {"description", "The optional limit parameter limits the number of "
1828
                        "items that are presented in the response document."},
1829
        {"required", false},
1830
        {"schema",
1831
         {
1832
             {"type", "integer"},
1833
             {"minimum", 1},
1834
             {"maximum", max_limit},
1835
             {"default", default_limit},
1836
         }},
1837
        {"style", "form"},
1838
        {"explode", false},
1839
    };
66✔
1840
    items_get["parameters"].emplace_back(param_limit);
3✔
1841

1842
    std::string itemsPath(collectionNamePath + "/items");
3✔
1843
    paths[itemsPath]["get"] = std::move(items_get);
6✔
1844

1845
    json feature_id_get = {
1846
        {"summary",
1847
         std::string("Get ") + getCollectionTitle(layer) + " item by id"},
6✔
1848
        {"description", getCollectionDescription(layer)},
3✔
1849
        {"tags", {layer->name}},
1850
        {"operationId", "get" + std::string(layer->name) + "Feature"},
6✔
1851
        {"parameters",
1852
         {
1853
             {{"$ref", "#/components/parameters/f"}},
1854
             {{"$ref", oapif_yaml_url + "#/components/parameters/featureId"}},
6✔
1855
         }},
1856
        {"responses",
1857
         {{"200",
1858
           {{"$ref", oapif_yaml_url + "#/components/responses/Feature"}}},
6✔
1859
          {"400",
1860
           {{"$ref",
1861
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
6✔
1862
          {"404",
1863
           {{"$ref", oapif_yaml_url + "#/components/responses/NotFound"}}},
6✔
1864
          {"500",
1865
           {{"$ref",
1866
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
111✔
1867
    std::string itemsFeatureIdPath(collectionNamePath + "/items/{featureId}");
3✔
1868
    paths[itemsFeatureIdPath]["get"] = std::move(feature_id_get);
6✔
1869
  }
1870

1871
  response["paths"] = std::move(paths);
2✔
1872

1873
  json components;
1874
  components["responses"]["200"] = {{"description", "successful operation"}};
5✔
1875
  components["responses"]["default"] = {
1✔
1876
      {"description", "unexpected error"},
1877
      {"content",
1878
       {{"application/json",
1879
         {{"schema",
1880
           {{"$ref", "https://raw.githubusercontent.com/opengeospatial/"
1881
                     "ogcapi-processes/master/core/openapi/schemas/"
1882
                     "exception.yaml"}}}}}}}};
18✔
1883

1884
  json parameters;
1885
  parameters["f"] = {
1✔
1886
      {"name", "f"},
1887
      {"in", "query"},
1888
      {"description", "The optional f parameter indicates the output format "
1889
                      "which the server shall provide as part of the response "
1890
                      "document.  The default format is GeoJSON."},
1891
      {"required", false},
1892
      {"schema",
1893
       {{"type", "string"}, {"enum", {"json", "html"}}, {"default", "json"}}},
1894
      {"style", "form"},
1895
      {"explode", false},
1896
  };
42✔
1897

1898
  parameters["offset"] = {
1✔
1899
      {"name", "offset"},
1900
      {"in", "query"},
1901
      {"description",
1902
       "The optional offset parameter indicates the index within the result "
1903
       "set from which the server shall begin presenting results in the "
1904
       "response document.  The first element has an index of 0 (default)."},
1905
      {"required", false},
1906
      {"schema",
1907
       {
1908
           {"type", "integer"},
1909
           {"minimum", 0},
1910
           {"default", 0},
1911
       }},
1912
      {"style", "form"},
1913
      {"explode", false},
1914
  };
40✔
1915

1916
  components["parameters"] = std::move(parameters);
2✔
1917

1918
  response["components"] = std::move(components);
2✔
1919

1920
  // TODO: "tags" array ?
1921

1922
  outputResponse(map, request,
3✔
1923
                 format == OGCAPIFormat::JSON ? OGCAPIFormat::OpenAPI_V3
1924
                                              : format,
1925
                 OGCAPI_TEMPLATE_HTML_OPENAPI, response);
1926
  return MS_SUCCESS;
1✔
1927
}
1928

1929
#endif
1930

1931
int msOGCAPIDispatchRequest(mapObj *map, cgiRequestObj *request) {
28✔
1932
#ifdef USE_OGCAPI_SVR
1933

1934
  // make sure ogcapi requests are enabled for this map
1935
  int status = msOWSRequestIsEnabled(map, NULL, "AO", "OGCAPI", MS_FALSE);
28✔
1936
  if (status != MS_TRUE) {
28✔
1937
    msSetError(MS_OGCAPIERR, "OGC API requests are not enabled.",
×
1938
               "msCGIDispatchAPIRequest()");
1939
    return MS_FAILURE; // let normal error handling take over
×
1940
  }
1941

1942
  for (int i = 0; i < request->NumParams; i++) {
78✔
1943
    for (int j = i + 1; j < request->NumParams; j++) {
93✔
1944
      if (strcmp(request->ParamNames[i], request->ParamNames[j]) == 0) {
43✔
1945
        std::string errorMsg("Query parameter ");
1✔
1946
        errorMsg += request->ParamNames[i];
1✔
1947
        errorMsg += " is repeated";
1948
        outputError(OGCAPI_PARAM_ERROR, errorMsg.c_str());
2✔
1949
        return MS_SUCCESS;
1950
      }
1951
    }
1952
  }
1953

1954
  OGCAPIFormat format; // all endpoints need a format
1955
  const char *p = getRequestParameter(request, "f");
27✔
1956

1957
  // if f= query parameter is not specified, use HTTP Accept header if available
1958
  if (p == nullptr) {
27✔
1959
    const char *accept = getenv("HTTP_ACCEPT");
2✔
1960
    if (accept) {
2✔
1961
      if (strcmp(accept, "*/*") == 0)
1✔
1962
        p = OGCAPI_MIMETYPE_JSON;
1963
      else
1964
        p = accept;
1965
    }
1966
  }
1967

1968
  if (p &&
27✔
1969
      (strcmp(p, "json") == 0 || strstr(p, OGCAPI_MIMETYPE_JSON) != nullptr ||
26✔
1970
       strstr(p, OGCAPI_MIMETYPE_GEOJSON) != nullptr ||
3✔
1971
       strstr(p, OGCAPI_MIMETYPE_OPENAPI_V3) != nullptr)) {
1972
    format = OGCAPIFormat::JSON;
1973
  } else if (p && (strcmp(p, "html") == 0 ||
4✔
1974
                   strstr(p, OGCAPI_MIMETYPE_HTML) != nullptr)) {
1975
    format = OGCAPIFormat::HTML;
1976
  } else if (p && (strcmp(p, "png") == 0 ||
1✔
1977
                   strstr(p, OGCAPI_MIMETYPE_HTML) != nullptr)) {
1978
    // for OGC Maps check against a list of formats?
1979
    format = OGCAPIFormat::PNG;
1980
  } else if (p) {
1✔
1981
    std::string errorMsg("Unsupported format requested: ");
×
1982
    errorMsg += p;
1983
    outputError(OGCAPI_PARAM_ERROR, errorMsg.c_str());
×
1984
    return MS_SUCCESS; // avoid any downstream MapServer processing
1985
  } else {
1986
    format = OGCAPIFormat::HTML; // default for now
1987
  }
1988

1989
  if (request->api_path_length == 2) {
27✔
1990

1991
    return processLandingRequest(map, request, format);
3✔
1992

1993
  } else if (request->api_path_length == 3) {
24✔
1994

1995
    if (strcmp(request->api_path[2], "conformance") == 0) {
3✔
1996
      return processConformanceRequest(map, request, format);
1✔
1997
    } else if (strcmp(request->api_path[2], "conformance.html") == 0) {
2✔
1998
      return processConformanceRequest(map, request, OGCAPIFormat::HTML);
×
1999
    } else if (strcmp(request->api_path[2], "collections") == 0) {
2✔
2000
      return processCollectionsRequest(map, request, format);
1✔
2001
    } else if (strcmp(request->api_path[2], "collections.html") == 0) {
1✔
2002
      return processCollectionsRequest(map, request, OGCAPIFormat::HTML);
×
2003
    } else if (strcmp(request->api_path[2], "api") == 0) {
1✔
2004
      return processApiRequest(map, request, format);
1✔
2005
    }
2006

2007
  } else if (request->api_path_length == 4) {
21✔
2008

2009
    if (strcmp(request->api_path[2], "collections") ==
2✔
2010
        0) { // next argument (3) is collectionId
2011
      return processCollectionRequest(map, request, request->api_path[3],
2✔
2012
                                      format);
2✔
2013
    }
2014

2015
  } else if (request->api_path_length == 5) {
19✔
2016

2017
    if (strcmp(request->api_path[2], "collections") == 0 &&
17✔
2018
        strcmp(request->api_path[4], "items") ==
17✔
2019
            0) { // middle argument (3) is the collectionId
2020
      return processCollectionItemsRequest(map, request, request->api_path[3],
17✔
2021
                                           NULL, format);
17✔
2022
    }
2023

NEW
2024
    if (strcmp(request->api_path[2], "collections") == 0 &&
×
NEW
2025
        strcmp(request->api_path[4], "map") ==
×
2026
            0) { // middle argument (3) is the collectionId
2027

2028

2029
        int drawquerymap = MS_FALSE;
NEW
2030
        imageObj *img = msDrawMap(map, drawquerymap);
×
2031
        //} else if (strcasecmp(names[i], "WIDTH") == 0) {
2032
        //  widthfound = true;
2033
        //  map->width = atoi(values[i]);
2034
        //} else if (strcasecmp(names[i], "HEIGHT") == 0) {
2035
        //  heightfound = true;
2036
        //  map->height = atoi(values[i]);
2037
        //} else if (strcasecmp(names[i], "FORMAT") == 0) {
NEW
2038
        if (img == NULL) {
×
NEW
2039
          outputError(OGCAPI_NOT_FOUND_ERROR, "Cannot generate image2");
×
2040
        }
2041

2042
        /* Set the HTTP Cache-control headers if they are defined
2043
           in the map object */
2044

2045
        const char *http_max_age =
NEW
2046
            msOWSLookupMetadata(&(map->web.metadata), "MO", "http_max_age");
×
NEW
2047
        if (http_max_age) {
×
NEW
2048
          msIO_setHeader("Cache-Control", "max-age=%s", http_max_age);
×
2049
        }
2050

NEW
2051
        if (!strcmp(MS_IMAGE_MIME_TYPE(map->outputformat),
×
2052
                    "application/json")) {
NEW
2053
            msIO_setHeader("Content-Type", "application/json; charset=utf-8");
×
2054
        } else {
NEW
2055
            msOutputFormatResolveFromImage(map, img);
×
NEW
2056
            msIO_setHeader("Content-Type", "%s",
×
NEW
2057
                            MS_IMAGE_MIME_TYPE(map->outputformat));
×
2058
        }
2059

NEW
2060
        msIO_sendHeaders();
×
NEW
2061
        if (msSaveImage(map, img, NULL) != MS_SUCCESS) {
×
NEW
2062
            msFreeImage(img);
×
NEW
2063
            outputError(OGCAPI_NOT_FOUND_ERROR, "Cannot generate image2");
×
2064
        }
NEW
2065
        msFreeImage(img);
×
2066

NEW
2067
        return (MS_SUCCESS);
×
2068
    }
2069

2070
  } else if (request->api_path_length == 6) {
2✔
2071

2072
    if (strcmp(request->api_path[2], "collections") == 0 &&
2✔
2073
        strcmp(request->api_path[4], "items") ==
2✔
2074
            0) { // middle argument (3) is the collectionId, last argument (5)
2075
                 // is featureId
2076
      return processCollectionItemsRequest(map, request, request->api_path[3],
2✔
2077
                                           request->api_path[5], format);
2✔
2078
    }
2079
  }
2080

2081
  outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid API path.");
×
2082
  return MS_SUCCESS; // avoid any downstream MapServer processing
×
2083
#else
2084
  msSetError(MS_OGCAPIERR, "OGC API server support is not enabled.",
2085
             "msOGCAPIDispatchRequest()");
2086
  return MS_FAILURE;
2087
#endif
2088
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc