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

geographika / mapserver / 17317900016

29 Aug 2025 07:43AM UTC coverage: 32.457%. First build
17317900016

push

github

geographika
Allow caching using OGC Features API

10 of 25 new or added lines in 1 file covered. (40.0%)

48578 of 149667 relevant lines covered (32.46%)

14836.84 hits per line

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

76.68
/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
#include "mapogcfilter.h"
34

35
#include "cpl_conv.h"
36

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

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

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

49
#define OGCAPI_DEFAULT_TITLE "MapServer OGC API"
50

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

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

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

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

73
#define OGCAPI_DEFAULT_GEOMETRY_PRECISION 6
74

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

80
#ifdef USE_OGCAPI_SVR
81

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

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

120
  json j = {{"code", code}, {"description", description}};
63✔
121

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

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

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

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

149
  for (i = 0; i < request->NumParams; i++) {
323✔
150
    if (strcmp(item, request->ParamNames[i]) == 0)
240✔
151
      return request->ParamValues[i];
57✔
152
  }
153

154
  return NULL;
155
}
156

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

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

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

172
  return max_limit;
22✔
173
}
174

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

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

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

190
  return default_limit;
27✔
191
}
192

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

202
  int max_limit;
203
  max_limit = getMaxLimit(map, layer);
22✔
204

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

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

222
  return MS_SUCCESS;
223
}
224

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

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

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

276
  const char *bboxParam = getRequestParameter(request, "bbox");
20✔
277
  if (!bboxParam || strlen(bboxParam) == 0) { // missing or empty extent
20✔
278
    rectObj rect;
279
    if (FLTLayerSetInvalidRectIfSupported(layer, &rect, "AO")) {
15✔
280
      bbox->minx = rect.minx;
×
281
      bbox->miny = rect.miny;
×
282
      bbox->maxx = rect.maxx;
×
283
      bbox->maxy = rect.maxy;
×
284
    } else {
285
      // assign map->extent (no projection necessary)
286
      bbox->minx = map->extent.minx;
15✔
287
      bbox->miny = map->extent.miny;
15✔
288
      bbox->maxx = map->extent.maxx;
15✔
289
      bbox->maxy = map->extent.maxy;
15✔
290
    }
291
  } else {
15✔
292
    const auto tokens = msStringSplit(bboxParam, ',');
5✔
293
    if (tokens.size() != 4) {
5✔
294
      outputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
295
      return false;
×
296
    }
297

298
    double values[4];
299
    for (int i = 0; i < 4; i++) {
25✔
300
      status = msStringToDouble(tokens[i].c_str(), &values[i]);
20✔
301
      if (status != MS_SUCCESS) {
20✔
302
        outputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
303
        return false;
×
304
      }
305
    }
306

307
    bbox->minx = values[0]; // assign
5✔
308
    bbox->miny = values[1];
5✔
309
    bbox->maxx = values[2];
5✔
310
    bbox->maxy = values[3];
5✔
311

312
    // validate bbox is well-formed (degenerate is ok)
313
    if (MS_VALID_SEARCH_EXTENT(*bbox) != MS_TRUE) {
5✔
314
      outputError(OGCAPI_PARAM_ERROR, "Bad value for bbox.");
×
315
      return false;
×
316
    }
317

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

347
    projectionObj bboxProj;
348
    msInitProjection(&bboxProj);
3✔
349
    msProjectionInheritContextFrom(&bboxProj, &(map->projection));
3✔
350
    if (msLoadProjectionString(&bboxProj, bboxCrs.c_str()) != 0) {
3✔
351
      msFreeProjection(&bboxProj);
×
352
      outputError(OGCAPI_SERVER_ERROR, "Cannot process bbox-crs.");
×
353
      return false;
×
354
    }
355

356
    status = msProjectRect(&bboxProj, outputProj, bbox);
3✔
357
    msFreeProjection(&bboxProj);
3✔
358
    if (status != MS_SUCCESS) {
3✔
359
      outputError(OGCAPI_SERVER_ERROR,
×
360
                  "Cannot reproject bbox from bbox-crs to output CRS.");
361
      return false;
×
362
    }
363
  }
364

365
  return true;
366
}
367

368
/*
369
** Returns the template directory location or NULL if it isn't set.
370
*/
371
static std::string getTemplateDirectory(mapObj *map, const char *key,
5✔
372
                                        const char *envvar) {
373
  const char *directory;
374

375
  directory = msOWSLookupMetadata(&(map->web.metadata), "A", key);
5✔
376

377
  if (directory == NULL) {
5✔
378
    directory = CPLGetConfigOption(envvar, NULL);
×
379
  }
380

381
  std::string s;
382
  if (directory != NULL) {
5✔
383
    s = directory;
384
    if (!s.empty() && (s.back() != '/' && s.back() != '\\')) {
5✔
385
      // add a trailing slash if missing
386
      std::string slash = "/";
3✔
387
#ifdef _WIN32
388
      slash = "\\";
389
#endif
390
      s += slash;
391
    }
392
  }
393

394
  return s;
5✔
395
}
396

397
/*
398
** Returns the service title from oga_{key} and/or ows_{key} or a default value
399
*if not set.
400
*/
401
static const char *getWebMetadata(mapObj *map, const char *domain,
402
                                  const char *key, const char *defaultVal) {
403
  const char *value;
404

405
  if ((value = msOWSLookupMetadata(&(map->web.metadata), domain, key)) != NULL)
11✔
406
    return value;
407
  else
408
    return defaultVal;
1✔
409
}
410

411
/*
412
** Returns the service title from oga|ows_title or a default value if not set.
413
*/
414
static const char *getTitle(mapObj *map) {
415
  return getWebMetadata(map, "OA", "title", OGCAPI_DEFAULT_TITLE);
416
}
417

418
/*
419
** Returns the API root URL from oga_onlineresource or builds a value if not
420
*set.
421
*/
422
static std::string getApiRootUrl(mapObj *map) {
31✔
423
  const char *root;
424

425
  if ((root = msOWSLookupMetadata(&(map->web.metadata), "A",
31✔
426
                                  "onlineresource")) != NULL)
427
    return std::string(root);
31✔
428
  else
429
    return "http://" + std::string(getenv("SERVER_NAME")) + ":" +
×
430
           std::string(getenv("SERVER_PORT")) +
×
431
           std::string(getenv("SCRIPT_NAME")) +
×
432
           std::string(getenv("PATH_INFO"));
×
433
}
434

435
static json getFeatureConstant(const gmlConstantObj *constant) {
×
436
  json j; // empty (null)
437

438
  if (!constant)
×
439
    throw std::runtime_error("Null constant metadata.");
×
440
  if (!constant->value)
×
441
    return j;
442

443
  // initialize
444
  j = {{constant->name, constant->value}};
×
445

446
  return j;
×
447
}
×
448

449
static json getFeatureItem(const gmlItemObj *item, const char *value) {
170✔
450
  json j; // empty (null)
451
  const char *key;
452

453
  if (!item)
170✔
454
    throw std::runtime_error("Null item metadata.");
×
455
  if (!item->visible)
170✔
456
    return j;
457

458
  if (item->alias)
108✔
459
    key = item->alias;
68✔
460
  else
461
    key = item->name;
40✔
462

463
  // initialize
464
  j = {{key, value}};
432✔
465

466
  if (item->type &&
172✔
467
      (EQUAL(item->type, "Date") || EQUAL(item->type, "DateTime") ||
64✔
468
       EQUAL(item->type, "Time"))) {
48✔
469
    struct tm tm;
470
    if (msParseTime(value, &tm) == MS_TRUE) {
16✔
471
      char tmpValue[64];
472
      if (EQUAL(item->type, "Date"))
16✔
473
        snprintf(tmpValue, sizeof(tmpValue), "%04d-%02d-%02d",
×
474
                 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
×
475
      else if (EQUAL(item->type, "Time"))
16✔
476
        snprintf(tmpValue, sizeof(tmpValue), "%02d:%02d:%02dZ", tm.tm_hour,
×
477
                 tm.tm_min, tm.tm_sec);
478
      else
479
        snprintf(tmpValue, sizeof(tmpValue), "%04d-%02d-%02dT%02d:%02d:%02dZ",
16✔
480
                 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour,
16✔
481
                 tm.tm_min, tm.tm_sec);
482

483
      j = {{key, tmpValue}};
64✔
484
    }
485
  } else if (item->type &&
108✔
486
             (EQUAL(item->type, "Integer") || EQUAL(item->type, "Long"))) {
48✔
487
    try {
488
      j = {{key, std::stoll(value)}};
80✔
489
    } catch (const std::exception &) {
×
490
    }
×
491
  } else if (item->type && EQUAL(item->type, "Real")) {
76✔
492
    try {
493
      j = {{key, std::stod(value)}};
160✔
494
    } catch (const std::exception &) {
×
495
    }
×
496
  } else if (item->type && EQUAL(item->type, "Boolean")) {
44✔
497
    if (EQUAL(value, "0") || EQUAL(value, "false")) {
×
498
      j = {{key, false}};
×
499
    } else {
500
      j = {{key, true}};
×
501
    }
502
  }
503

504
  return j;
505
}
736✔
506

507
static double round_down(double value, int decimal_places) {
10✔
508
  const double multiplier = std::pow(10.0, decimal_places);
509
  return std::floor(value * multiplier) / multiplier;
10✔
510
}
511
// https://stackoverflow.com/questions/25925290/c-round-a-double-up-to-2-decimal-places
512
static double round_up(double value, int decimal_places) {
30,614✔
513
  const double multiplier = std::pow(10.0, decimal_places);
514
  return std::ceil(value * multiplier) / multiplier;
30,614✔
515
}
516

517
static json getFeatureGeometry(shapeObj *shape, int precision,
23✔
518
                               bool outputCrsAxisInverted) {
519
  json geometry; // empty (null)
520
  int *outerList = NULL, numOuterRings = 0;
521

522
  if (!shape)
23✔
523
    throw std::runtime_error("Null shape.");
×
524

525
  switch (shape->type) {
23✔
526
  case (MS_SHAPE_POINT):
1✔
527
    if (shape->numlines == 0 ||
1✔
528
        shape->line[0].numpoints == 0) // not enough info for a point
1✔
529
      return geometry;
530

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

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

590
    outerList = msGetOuterList(shape);
22✔
591
    if (outerList == NULL)
22✔
592
      throw std::runtime_error("Unable to allocate list of outer rings.");
×
593
    for (int k = 0; k < shape->numlines; k++) {
50✔
594
      if (outerList[k] == MS_TRUE)
28✔
595
        numOuterRings++;
24✔
596
    }
597

598
    if (numOuterRings == 1) {
22✔
599
      geometry["type"] = "Polygon";
40✔
600
      geometry["coordinates"] = json::array();
20✔
601
      for (int i = 0; i < shape->numlines; i++) {
40✔
602
        json part = json::array();
20✔
603
        for (int j = 0; j < shape->line[i].numpoints; j++) {
15,275✔
604
          double x = shape->line[i].point[j].x;
15,255✔
605
          double y = shape->line[i].point[j].y;
15,255✔
606
          if (outputCrsAxisInverted)
15,255✔
607
            std::swap(x, y);
608
          part.push_back({round_up(x, precision), round_up(y, precision)});
91,530✔
609
        }
610
        geometry["coordinates"].push_back(part);
20✔
611
      }
612
    } else {
613
      geometry["type"] = "MultiPolygon";
4✔
614
      geometry["coordinates"] = json::array();
2✔
615

616
      for (int k = 0; k < shape->numlines; k++) {
10✔
617
        if (outerList[k] ==
8✔
618
            MS_TRUE) { // outer ring: generate polygon and add to coordinates
619
          int *innerList = msGetInnerList(shape, k, outerList);
4✔
620
          if (innerList == NULL) {
4✔
621
            msFree(outerList);
×
622
            throw std::runtime_error("Unable to allocate list of inner rings.");
×
623
          }
624

625
          json polygon = json::array();
4✔
626
          for (int i = 0; i < shape->numlines; i++) {
20✔
627
            if (i == k ||
16✔
628
                innerList[i] ==
12✔
629
                    MS_TRUE) { // add outer ring (k) and any inner rings
630
              json part = json::array();
8✔
631
              for (int j = 0; j < shape->line[i].numpoints; j++) {
54✔
632
                double x = shape->line[i].point[j].x;
46✔
633
                double y = shape->line[i].point[j].y;
46✔
634
                if (outputCrsAxisInverted)
46✔
635
                  std::swap(x, y);
636
                part.push_back(
184✔
637
                    {round_up(x, precision), round_up(y, precision)});
92✔
638
              }
639
              polygon.push_back(part);
8✔
640
            }
641
          }
642

643
          msFree(innerList);
4✔
644
          geometry["coordinates"].push_back(polygon);
4✔
645
        }
646
      }
647
    }
648
    msFree(outerList);
22✔
649
    break;
22✔
650
  default:
×
651
    throw std::runtime_error("Invalid shape type.");
×
652
    break;
653
  }
654

655
  return geometry;
656
}
657

658
/*
659
** Return a GeoJSON representation of a shape.
660
*/
661
static json getFeature(layerObj *layer, shapeObj *shape, gmlItemListObj *items,
23✔
662
                       gmlConstantListObj *constants, int geometry_precision,
663
                       bool outputCrsAxisInverted) {
664
  int i;
665
  json feature; // empty (null)
666

667
  if (!layer || !shape)
23✔
668
    throw std::runtime_error("Null arguments.");
×
669

670
  // initialize
671
  feature = {{"type", "Feature"}, {"properties", json::object()}};
184✔
672

673
  // id
674
  const char *featureIdItem =
675
      msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
23✔
676
  if (featureIdItem == NULL)
23✔
677
    throw std::runtime_error(
678
        "Missing required featureid metadata."); // should have been trapped
×
679
                                                 // earlier
680
  for (i = 0; i < items->numitems; i++) {
95✔
681
    if (strcasecmp(featureIdItem, items->items[i].name) == 0) {
95✔
682
      feature["id"] = shape->values[i];
46✔
683
      break;
23✔
684
    }
685
  }
686

687
  if (i == items->numitems)
23✔
688
    throw std::runtime_error("Feature id not found.");
×
689

690
  // properties - build from items and constants, no group support for now
691

692
  for (int i = 0; i < items->numitems; i++) {
193✔
693
    try {
694
      json item = getFeatureItem(&(items->items[i]), shape->values[i]);
170✔
695
      if (!item.is_null())
170✔
696
        feature["properties"].insert(item.begin(), item.end());
108✔
697
    } catch (const std::runtime_error &) {
×
698
      throw std::runtime_error("Error fetching item.");
×
699
    }
×
700
  }
701

702
  for (int i = 0; i < constants->numconstants; i++) {
23✔
703
    try {
704
      json constant = getFeatureConstant(&(constants->constants[i]));
×
705
      if (!constant.is_null())
×
706
        feature["properties"].insert(constant.begin(), constant.end());
×
707
    } catch (const std::runtime_error &) {
×
708
      throw std::runtime_error("Error fetching constant.");
×
709
    }
×
710
  }
711

712
  // geometry
713
  try {
714
    json geometry =
715
        getFeatureGeometry(shape, geometry_precision, outputCrsAxisInverted);
23✔
716
    if (!geometry.is_null())
23✔
717
      feature["geometry"] = std::move(geometry);
46✔
718
  } catch (const std::runtime_error &) {
×
719
    throw std::runtime_error("Error fetching geometry.");
×
720
  }
×
721

722
  return feature;
23✔
723
}
184✔
724

725
static json getLink(hashTableObj *metadata, const std::string &name) {
11✔
726
  json link;
727

728
  const char *href =
729
      msOWSLookupMetadata(metadata, "A", (name + "_href").c_str());
11✔
730
  if (!href)
11✔
731
    throw std::runtime_error("Missing required link href property.");
×
732

733
  const char *title =
734
      msOWSLookupMetadata(metadata, "A", (name + "_title").c_str());
11✔
735
  const char *type =
736
      msOWSLookupMetadata(metadata, "A", (name + "_type").c_str());
22✔
737

738
  link = {{"href", href},
739
          {"title", title ? title : href},
740
          {"type", type ? type : "text/html"}};
132✔
741

742
  return link;
11✔
743
}
110✔
744

745
static const char *getCollectionDescription(layerObj *layer) {
14✔
746
  const char *description =
747
      msOWSLookupMetadata(&(layer->metadata), "A", "description");
14✔
748
  if (!description)
14✔
749
    description = msOWSLookupMetadata(&(layer->metadata), "OF",
×
750
                                      "abstract"); // fallback on abstract
751
  if (!description)
×
752
    description =
753
        "<!-- Warning: unable to set the collection description. -->"; // finally
754
                                                                       // a
755
                                                                       // warning...
756
  return description;
14✔
757
}
758

759
static const char *getCollectionTitle(layerObj *layer) {
760
  const char *title = msOWSLookupMetadata(&(layer->metadata), "AOF", "title");
12✔
761
  if (!title)
17✔
762
    title = layer->name; // revert to layer name if no title found
×
763
  return title;
764
}
765

766
static int getGeometryPrecision(mapObj *map, layerObj *layer) {
21✔
767
  int geometry_precision = OGCAPI_DEFAULT_GEOMETRY_PRECISION;
768
  if (msOWSLookupMetadata(&(layer->metadata), "AF", "geometry_precision")) {
21✔
769
    geometry_precision = atoi(
×
770
        msOWSLookupMetadata(&(layer->metadata), "AF", "geometry_precision"));
771
  } else if (msOWSLookupMetadata(&map->web.metadata, "AF",
21✔
772
                                 "geometry_precision")) {
773
    geometry_precision = atoi(
21✔
774
        msOWSLookupMetadata(&map->web.metadata, "AF", "geometry_precision"));
775
  }
776
  return geometry_precision;
21✔
777
}
778

779
static json getCollection(mapObj *map, layerObj *layer, OGCAPIFormat format) {
5✔
780
  json collection; // empty (null)
781
  rectObj bbox;
782

783
  if (!map || !layer)
5✔
784
    return collection;
785

786
  if (!includeLayer(map, layer))
5✔
787
    return collection;
788

789
  // initialize some things
790
  std::string api_root = getApiRootUrl(map);
5✔
791

792
  if (msOWSGetLayerExtent(map, layer, "AOF", &bbox) == MS_SUCCESS) {
5✔
793
    if (layer->projection.numargs > 0)
5✔
794
      msOWSProjectToWGS84(&layer->projection, &bbox);
5✔
795
    else if (map->projection.numargs > 0)
×
796
      msOWSProjectToWGS84(&map->projection, &bbox);
×
797
    else
798
      throw std::runtime_error(
799
          "Unable to transform bounding box, no projection defined.");
×
800
  } else {
801
    throw std::runtime_error(
802
        "Unable to get collection bounding box."); // might be too harsh since
×
803
                                                   // extent is optional
804
  }
805

806
  const char *description = getCollectionDescription(layer);
5✔
807
  const char *title = getCollectionTitle(layer);
5✔
808

809
  const char *id = layer->name;
5✔
810
  char *id_encoded = msEncodeUrl(id); // free after use
5✔
811

812
  const int geometry_precision = getGeometryPrecision(map, layer);
5✔
813

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

848
                 }},
849
                {"itemType", "feature"}};
425✔
850

851
  msFree(id_encoded); // done
5✔
852

853
  // handle optional configuration (keywords and links)
854
  const char *value = msOWSLookupMetadata(&(layer->metadata), "A", "keywords");
5✔
855
  if (!value)
5✔
856
    value = msOWSLookupMetadata(&(layer->metadata), "OF",
×
857
                                "keywordlist"); // fallback on keywordlist
858
  if (value) {
×
859
    std::vector<std::string> keywords = msStringSplit(value, ',');
5✔
860
    for (const std::string &keyword : keywords) {
24✔
861
      collection["keywords"].push_back(keyword);
57✔
862
    }
863
  }
864

865
  value = msOWSLookupMetadata(&(layer->metadata), "A", "links");
5✔
866
  if (value) {
5✔
867
    std::vector<std::string> names = msStringSplit(value, ',');
5✔
868
    for (const std::string &name : names) {
10✔
869
      try {
870
        json link = getLink(&(layer->metadata), name);
5✔
871
        collection["links"].push_back(link);
5✔
872
      } catch (const std::runtime_error &e) {
×
873
        throw e;
×
874
      }
×
875
    }
876
  }
877

878
  // Part 2 - CRS support
879
  // Inspect metadata to set the "crs": [] member and "storageCrs" member
880

881
  json jCrsList = getCrsList(map, layer);
5✔
882
  if (!jCrsList.empty()) {
5✔
883
    collection["crs"] = std::move(jCrsList);
5✔
884

885
    std::string storageCrs = getStorageCrs(layer);
5✔
886
    if (!storageCrs.empty()) {
5✔
887
      collection["storageCrs"] = std::move(storageCrs);
10✔
888
    }
889
  }
890

891
  return collection;
892
}
525✔
893

894
/*
895
** Output stuff...
896
*/
897

898
static void outputJson(const json &j, const char *mimetype,
19✔
899
                       const std::map<std::string, std::string> &extraHeaders) {
900
  std::string js;
901

902
  try {
903
    js = j.dump();
19✔
904
  } catch (...) {
1✔
905
    outputError(OGCAPI_CONFIG_ERROR, "Invalid UTF-8 data, check encoding.");
1✔
906
    return;
907
  }
1✔
908

909
  msIO_setHeader("Content-Type", "%s", mimetype);
18✔
910
  for (const auto &kvp : extraHeaders) {
30✔
911
    msIO_setHeader(kvp.first.c_str(), "%s", kvp.second.c_str());
12✔
912
  }
913
  msIO_sendHeaders();
18✔
914
  msIO_printf("%s\n", js.c_str());
18✔
915
}
916

917
static void outputTemplate(const char *directory, const char *filename,
5✔
918
                           const json &j, const char *mimetype) {
919
  std::string _directory(directory);
5✔
920
  std::string _filename(filename);
5✔
921
  Environment env{_directory}; // catch
5✔
922

923
  // ERB-style instead of Mustache (we'll see)
924
  // env.set_expression("<%=", "%>");
925
  // env.set_statement("<%", "%>");
926

927
  // callbacks, need:
928
  //   - match (regex)
929
  //   - contains (substring)
930
  //   - URL encode
931

932
  try {
933
    std::string js = j.dump();
5✔
934
  } catch (...) {
1✔
935
    outputError(OGCAPI_CONFIG_ERROR, "Invalid UTF-8 data, check encoding.");
1✔
936
    return;
937
  }
1✔
938

939
  try {
940
    Template t = env.parse_template(_filename); // catch
4✔
941
    std::string result = env.render(t, j);
4✔
942

943
    msIO_setHeader("Content-Type", "%s", mimetype);
4✔
944
    msIO_sendHeaders();
4✔
945
    msIO_printf("%s\n", result.c_str());
4✔
946
  } catch (const inja::RenderError &e) {
4✔
947
    outputError(OGCAPI_CONFIG_ERROR, "Template rendering error. " +
×
948
                                         std::string(e.what()) + " (" +
×
949
                                         std::string(filename) + ").");
×
950
    return;
951
  } catch (const inja::InjaError &e) {
×
952
    outputError(OGCAPI_CONFIG_ERROR, "InjaError error. " +
×
953
                                         std::string(e.what()) + " (" +
×
954
                                         std::string(filename) + ")." + " (" +
×
955
                                         std::string(directory) + ").");
×
956
    return;
957
  } catch (...) {
×
958
    outputError(OGCAPI_SERVER_ERROR, "General template handling error.");
×
959
    return;
960
  }
×
961
}
5✔
962

963
static void msWFSSetShapeCache(mapObj *map) {
13✔
964
  const char *pszFeaturesCacheCount =
965
      msOWSLookupMetadata(&(map->web.metadata), "F", "features_cache_count");
13✔
966
  const char *pszFeaturesCacheSize =
967
      msOWSLookupMetadata(&(map->web.metadata), "F", "features_cache_size");
13✔
968
  if (pszFeaturesCacheCount) {
13✔
NEW
969
    map->query.cache_shapes = MS_TRUE;
×
NEW
970
    map->query.max_cached_shape_count = atoi(pszFeaturesCacheCount);
×
NEW
971
    if (map->debug >= MS_DEBUGLEVEL_V) {
×
NEW
972
      msDebug("Caching up to %d shapes\n", map->query.max_cached_shape_count);
×
973
    }
974
  }
975

976
  if (pszFeaturesCacheSize) {
13✔
NEW
977
    map->query.cache_shapes = MS_TRUE;
×
NEW
978
    map->query.max_cached_shape_ram_amount = atoi(pszFeaturesCacheSize);
×
NEW
979
    if (strstr(pszFeaturesCacheSize, "mb") ||
×
980
        strstr(pszFeaturesCacheSize, "MB"))
NEW
981
      map->query.max_cached_shape_ram_amount *= 1024 * 1024;
×
NEW
982
    if (map->debug >= MS_DEBUGLEVEL_V) {
×
NEW
983
      msDebug("Caching up to %d bytes of shapes\n",
×
984
              map->query.max_cached_shape_ram_amount);
985
    }
986
  }
987
}
13✔
988

989

990
/*
991
** Generic response output.
992
*/
993
static void
994
outputResponse(mapObj *map, cgiRequestObj *request, OGCAPIFormat format,
24✔
995
               const char *filename, const json &response,
996
               const std::map<std::string, std::string> &extraHeaders =
997
                   std::map<std::string, std::string>()) {
998
  std::string path;
999
  char fullpath[MS_MAXPATHLEN];
1000

1001
  if (format == OGCAPIFormat::JSON) {
24✔
1002
    outputJson(response, OGCAPI_MIMETYPE_JSON, extraHeaders);
5✔
1003
  } else if (format == OGCAPIFormat::GeoJSON) {
19✔
1004
    outputJson(response, OGCAPI_MIMETYPE_GEOJSON, extraHeaders);
13✔
1005
  } else if (format == OGCAPIFormat::OpenAPI_V3) {
6✔
1006
    outputJson(response, OGCAPI_MIMETYPE_OPENAPI_V3, extraHeaders);
1✔
1007
  } else if (format == OGCAPIFormat::HTML) {
5✔
1008
    path = getTemplateDirectory(map, "html_template_directory",
10✔
1009
                                "OGCAPI_HTML_TEMPLATE_DIRECTORY");
5✔
1010
    if (path.empty()) {
5✔
1011
      outputError(OGCAPI_CONFIG_ERROR, "Template directory not set.");
×
1012
      return; // bail
×
1013
    }
1014
    msBuildPath(fullpath, map->mappath, path.c_str());
5✔
1015

1016
    json j;
1017

1018
    j["response"] = response; // nest the response so we could write the whole
10✔
1019
                              // object in the template
1020

1021
    // extend the JSON with a few things that we need for templating
1022
    j["template"] = {{"path", json::array()},
10✔
1023
                     {"params", json::object()},
5✔
1024
                     {"api_root", getApiRootUrl(map)},
5✔
1025
                     {"title", getTitle(map)},
5✔
1026
                     {"tags", json::object()}};
85✔
1027

1028
    // api path
1029
    for (int i = 0; i < request->api_path_length; i++)
26✔
1030
      j["template"]["path"].push_back(request->api_path[i]);
63✔
1031

1032
    // parameters (optional)
1033
    for (int i = 0; i < request->NumParams; i++) {
9✔
1034
      if (request->ParamValues[i] &&
4✔
1035
          strlen(request->ParamValues[i]) > 0) { // skip empty params
4✔
1036
        j["template"]["params"].update(
24✔
1037
            {{request->ParamNames[i], request->ParamValues[i]}});
4✔
1038
      }
1039
    }
1040

1041
    // add custom tags (optional)
1042
    const char *tags =
1043
        msOWSLookupMetadata(&(map->web.metadata), "A", "html_tags");
5✔
1044
    if (tags) {
5✔
1045
      std::vector<std::string> names = msStringSplit(tags, ',');
2✔
1046
      for (std::string name : names) {
6✔
1047
        const char *value = msOWSLookupMetadata(&(map->web.metadata), "A",
4✔
1048
                                                ("tag_" + name).c_str());
8✔
1049
        if (value) {
4✔
1050
          j["template"]["tags"].update({{name, value}}); // add object
24✔
1051
        }
1052
      }
1053
    }
1054

1055
    outputTemplate(fullpath, filename, j, OGCAPI_MIMETYPE_HTML);
5✔
1056
  } else {
1057
    outputError(OGCAPI_PARAM_ERROR, "Unsupported format requested.");
×
1058
  }
1059
}
132✔
1060

1061
/*
1062
** Process stuff...
1063
*/
1064
static int processLandingRequest(mapObj *map, cgiRequestObj *request,
3✔
1065
                                 OGCAPIFormat format) {
1066
  json response;
1067

1068
  // define ambiguous elements
1069
  const char *description =
1070
      msOWSLookupMetadata(&(map->web.metadata), "A", "description");
3✔
1071
  if (!description)
3✔
1072
    description =
1073
        msOWSLookupMetadata(&(map->web.metadata), "OF",
×
1074
                            "abstract"); // fallback on abstract if necessary
1075

1076
  // define api root url
1077
  std::string api_root = getApiRootUrl(map);
3✔
1078

1079
  // build response object
1080
  //   - consider conditionally excluding links for HTML format
1081
  response = {
1082
      {"title", getTitle(map)},
3✔
1083
      {"description", description ? description : ""},
3✔
1084
      {"links",
1085
       {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
4✔
1086
         {"type", OGCAPI_MIMETYPE_JSON},
1087
         {"title", "This document as JSON"},
1088
         {"href", api_root + "?f=json"}},
3✔
1089
        {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
5✔
1090
         {"type", OGCAPI_MIMETYPE_HTML},
1091
         {"title", "This document as HTML"},
1092
         {"href", api_root + "?f=html"}},
3✔
1093
        {{"rel", "conformance"},
1094
         {"type", OGCAPI_MIMETYPE_JSON},
1095
         {"title",
1096
          "OCG API conformance classes implemented by this server (JSON)"},
1097
         {"href", api_root + "/conformance?f=json"}},
3✔
1098
        {{"rel", "conformance"},
1099
         {"type", OGCAPI_MIMETYPE_HTML},
1100
         {"title", "OCG API conformance classes implemented by this server"},
1101
         {"href", api_root + "/conformance?f=html"}},
3✔
1102
        {{"rel", "data"},
1103
         {"type", OGCAPI_MIMETYPE_JSON},
1104
         {"title", "Information about feature collections available from this "
1105
                   "server (JSON)"},
1106
         {"href", api_root + "/collections?f=json"}},
3✔
1107
        {{"rel", "data"},
1108
         {"type", OGCAPI_MIMETYPE_HTML},
1109
         {"title",
1110
          "Information about feature collections available from this server"},
1111
         {"href", api_root + "/collections?f=html"}},
3✔
1112
        {{"rel", "service-desc"},
1113
         {"type", OGCAPI_MIMETYPE_OPENAPI_V3},
1114
         {"title", "OpenAPI document"},
1115
         {"href", api_root + "/api?f=json"}},
3✔
1116
        {{"rel", "service-doc"},
1117
         {"type", OGCAPI_MIMETYPE_HTML},
1118
         {"title", "API documentation"},
1119
         {"href", api_root + "/api?f=html"}}}}};
345✔
1120

1121
  // handle custom links (optional)
1122
  const char *links = msOWSLookupMetadata(&(map->web.metadata), "A", "links");
3✔
1123
  if (links) {
3✔
1124
    std::vector<std::string> names = msStringSplit(links, ',');
3✔
1125
    for (std::string name : names) {
9✔
1126
      try {
1127
        json link = getLink(&(map->web.metadata), name);
6✔
1128
        response["links"].push_back(link);
6✔
1129
      } catch (const std::runtime_error &e) {
×
1130
        outputError(OGCAPI_CONFIG_ERROR, std::string(e.what()));
×
1131
        return MS_SUCCESS;
1132
      }
×
1133
    }
1134
  }
1135

1136
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_LANDING, response);
3✔
1137
  return MS_SUCCESS;
3✔
1138
}
459✔
1139

1140
static int processConformanceRequest(mapObj *map, cgiRequestObj *request,
1✔
1141
                                     OGCAPIFormat format) {
1142
  json response;
1143

1144
  // build response object
1145
  response = {
1146
      {"conformsTo",
1147
       {
1148
           "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core",
1149
           "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections",
1150
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core",
1151
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30",
1152
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/html",
1153
           "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson",
1154
           "http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs",
1155
       }}};
11✔
1156

1157
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_CONFORMANCE,
2✔
1158
                 response);
1159
  return MS_SUCCESS;
1✔
1160
}
12✔
1161

1162
static int processCollectionItemsRequest(mapObj *map, cgiRequestObj *request,
22✔
1163
                                         const char *collectionId,
1164
                                         const char *featureId,
1165
                                         OGCAPIFormat format) {
1166
  json response;
1167
  int i;
1168
  layerObj *layer;
1169

1170
  int limit;
1171
  rectObj bbox;
1172

1173
  int numberMatched = 0;
1174

1175
  // find the right layer
1176
  for (i = 0; i < map->numlayers; i++) {
26✔
1177
    if (strcmp(map->layers[i]->name, collectionId) == 0)
26✔
1178
      break; // match
1179
  }
1180

1181
  if (i == map->numlayers) { // invalid collectionId
22✔
1182
    outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1183
    return MS_SUCCESS;
×
1184
  }
1185

1186
  layer = map->layers[i]; // for convenience
22✔
1187
  layer->status = MS_ON;  // force on (do we need to save and reset?)
22✔
1188

1189
  if (!includeLayer(map, layer)) {
22✔
1190
    outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1191
    return MS_SUCCESS;
×
1192
  }
1193

1194
  //
1195
  // handle parameters specific to this endpoint
1196
  //
1197
  if (getLimit(map, request, layer, &limit) != MS_SUCCESS) {
22✔
1198
    outputError(OGCAPI_PARAM_ERROR, "Bad value for limit.");
×
1199
    return MS_SUCCESS;
×
1200
  }
1201

1202
  const char *crs = getRequestParameter(request, "crs");
22✔
1203

1204
  std::string outputCrs = "EPSG:4326";
22✔
1205
  bool outputCrsAxisInverted =
1206
      false; // because above EPSG:4326 is meant to be OGC:CRS84 actually
1207
  std::map<std::string, std::string> extraHeaders;
1208
  if (crs) {
22✔
1209
    bool isExpectedCrs = false;
1210
    for (const auto &crsItem : getCrsList(map, layer)) {
26✔
1211
      if (crs == crsItem.get<std::string>()) {
22✔
1212
        isExpectedCrs = true;
1213
        break;
1214
      }
1215
    }
1216
    if (!isExpectedCrs) {
4✔
1217
      outputError(OGCAPI_PARAM_ERROR, "Bad value for crs.");
2✔
1218
      return MS_SUCCESS;
2✔
1219
    }
1220
    extraHeaders["Content-Crs"] = '<' + std::string(crs) + '>';
6✔
1221
    if (std::string(crs) != CRS84_URL) {
4✔
1222
      if (std::string(crs).find(EPSG_PREFIX_URL) == 0) {
4✔
1223
        const char *code = crs + strlen(EPSG_PREFIX_URL);
2✔
1224
        outputCrs = std::string("EPSG:") + code;
6✔
1225
        outputCrsAxisInverted = msIsAxisInverted(atoi(code));
2✔
1226
      }
1227
    }
1228
  } else {
1229
    extraHeaders["Content-Crs"] = '<' + std::string(CRS84_URL) + '>';
72✔
1230
  }
1231

1232
  struct ReprojectionObjects {
1233
    reprojectionObj *reprojector = NULL;
1234
    projectionObj proj;
1235

1236
    ReprojectionObjects() { msInitProjection(&proj); }
20✔
1237

1238
    ~ReprojectionObjects() {
1239
      msProjectDestroyReprojector(reprojector);
20✔
1240
      msFreeProjection(&proj);
20✔
1241
    }
20✔
1242

1243
    int executeQuery(mapObj *map) {
29✔
1244
      projectionObj backupMapProjection = map->projection;
29✔
1245
      map->projection = proj;
29✔
1246
      int ret = msExecuteQuery(map);
29✔
1247
      map->projection = backupMapProjection;
29✔
1248
      return ret;
29✔
1249
    }
1250
  };
1251
  ReprojectionObjects reprObjs;
1252

1253
  msProjectionInheritContextFrom(&reprObjs.proj, &(map->projection));
20✔
1254
  if (msLoadProjectionString(&reprObjs.proj, outputCrs.c_str()) != 0) {
20✔
1255
    outputError(OGCAPI_SERVER_ERROR, "Cannot instantiate output CRS.");
×
1256
    return MS_SUCCESS;
×
1257
  }
1258

1259
  if (layer->projection.numargs > 0) {
20✔
1260
    if (msProjectionsDiffer(&(layer->projection), &reprObjs.proj)) {
20✔
1261
      reprObjs.reprojector =
13✔
1262
          msProjectCreateReprojector(&(layer->projection), &reprObjs.proj);
13✔
1263
      if (reprObjs.reprojector == NULL) {
13✔
1264
        outputError(OGCAPI_SERVER_ERROR, "Error creating re-projector.");
×
1265
        return MS_SUCCESS;
×
1266
      }
1267
    }
1268
  } else if (map->projection.numargs > 0) {
×
1269
    if (msProjectionsDiffer(&(map->projection), &reprObjs.proj)) {
×
1270
      reprObjs.reprojector =
×
1271
          msProjectCreateReprojector(&(map->projection), &reprObjs.proj);
×
1272
      if (reprObjs.reprojector == NULL) {
×
1273
        outputError(OGCAPI_SERVER_ERROR, "Error creating re-projector.");
×
1274
        return MS_SUCCESS;
×
1275
      }
1276
    }
1277
  } else {
1278
    outputError(OGCAPI_CONFIG_ERROR,
×
1279
                "Unable to transform geometries, no projection defined.");
1280
    return MS_SUCCESS;
×
1281
  }
1282

1283
  if (map->projection.numargs > 0) {
20✔
1284
    msProjectRect(&(map->projection), &reprObjs.proj, &map->extent);
20✔
1285
  }
1286

1287
  if (!getBbox(map, layer, request, &bbox, &reprObjs.proj)) {
20✔
1288
    return MS_SUCCESS;
1289
  }
1290

1291
  int offset = 0;
18✔
1292
  if (featureId) {
18✔
1293
    const char *featureIdItem =
1294
        msOWSLookupMetadata(&(layer->metadata), "AGFO", "featureid");
3✔
1295
    if (featureIdItem == NULL) {
3✔
1296
      outputError(OGCAPI_CONFIG_ERROR, "Missing required featureid metadata.");
×
1297
      return MS_SUCCESS;
×
1298
    }
1299

1300
    // TODO: does featureIdItem exist in the data?
1301

1302
    // optional validation
1303
    const char *featureIdValidation =
1304
        msLookupHashTable(&(layer->validation), featureIdItem);
3✔
1305
    if (featureIdValidation &&
6✔
1306
        msValidateParameter(featureId, featureIdValidation, NULL, NULL, NULL) !=
3✔
1307
            MS_SUCCESS) {
1308
      outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid feature id.");
1✔
1309
      return MS_SUCCESS;
1✔
1310
    }
1311

1312
    map->query.type = MS_QUERY_BY_FILTER;
2✔
1313
    map->query.mode = MS_QUERY_SINGLE;
2✔
1314
    map->query.layer = i;
2✔
1315
    map->query.rect = bbox;
2✔
1316
    map->query.filteritem = strdup(featureIdItem);
2✔
1317

1318
    msInitExpression(&map->query.filter);
2✔
1319
    map->query.filter.type = MS_STRING;
2✔
1320
    map->query.filter.string = strdup(featureId);
2✔
1321

1322
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
2✔
1323
      outputError(OGCAPI_NOT_FOUND_ERROR, "Collection items id query failed.");
×
1324
      return MS_SUCCESS;
×
1325
    }
1326

1327
    if (!layer->resultcache || layer->resultcache->numresults != 1) {
2✔
1328
      outputError(OGCAPI_NOT_FOUND_ERROR, "Collection items id query failed.");
×
1329
      return MS_SUCCESS;
×
1330
    }
1331
  } else { // bbox query
1332

1333
    const char *compliance_mode =
1334
        msOWSLookupMetadata(&(map->web.metadata), "A", "compliance_mode");
15✔
1335
    if (compliance_mode != NULL && strcasecmp(compliance_mode, "true") == 0) {
15✔
1336
      for (int j = 0; j < request->NumParams; j++) {
44✔
1337
        const char *paramName = request->ParamNames[j];
30✔
1338
        if (strcmp(paramName, "f") == 0 || strcmp(paramName, "bbox") == 0 ||
30✔
1339
            strcmp(paramName, "bbox-crs") == 0 ||
12✔
1340
            strcmp(paramName, "datetime") == 0 ||
10✔
1341
            strcmp(paramName, "limit") == 0 ||
10✔
1342
            strcmp(paramName, "offset") == 0 || strcmp(paramName, "crs") == 0) {
4✔
1343
          // ok
1344
        } else {
1345
          outputError(
1✔
1346
              OGCAPI_PARAM_ERROR,
1347
              (std::string("Unknown query parameter: ") + paramName).c_str());
1✔
1348
          return MS_SUCCESS;
1✔
1349
        }
1350
      }
1351
    }
1352

1353
    map->query.type = MS_QUERY_BY_RECT;
14✔
1354
    map->query.mode = MS_QUERY_MULTIPLE;
14✔
1355
    map->query.layer = i;
14✔
1356
    map->query.rect = bbox;
14✔
1357
    map->query.only_cache_result_count = MS_TRUE;
14✔
1358

1359
    // get number matched
1360
    if (reprObjs.executeQuery(map) != MS_SUCCESS) {
14✔
1361
      outputError(OGCAPI_NOT_FOUND_ERROR, "Collection items query failed.");
×
1362
      return MS_SUCCESS;
×
1363
    }
1364

1365
    if (!layer->resultcache) {
14✔
1366
      outputError(OGCAPI_NOT_FOUND_ERROR, "Collection items query failed.");
×
1367
      return MS_SUCCESS;
×
1368
    }
1369

1370
    numberMatched = layer->resultcache->numresults;
14✔
1371

1372
    if (numberMatched > 0) {
14✔
1373
      map->query.only_cache_result_count = MS_FALSE;
13✔
1374
      map->query.maxfeatures = limit;
13✔
1375

1376
      msWFSSetShapeCache(map);
13✔
1377

1378
      const char *offsetStr = getRequestParameter(request, "offset");
13✔
1379
      if (offsetStr) {
13✔
1380
        if (msStringToInt(offsetStr, &offset, 10) != MS_SUCCESS) {
1✔
1381
          outputError(OGCAPI_PARAM_ERROR, "Bad value for offset.");
×
1382
          return MS_SUCCESS;
×
1383
        }
1384

1385
        if (offset < 0 || offset >= numberMatched) {
1✔
1386
          outputError(OGCAPI_PARAM_ERROR, "Offset out of range.");
×
1387
          return MS_SUCCESS;
×
1388
        }
1389

1390
        // msExecuteQuery() use a 1-based offset convention, whereas the API
1391
        // uses a 0-based offset convention.
1392
        map->query.startindex = 1 + offset;
1✔
1393
        layer->startindex = 1 + offset;
1✔
1394
      }
1395

1396
      if (reprObjs.executeQuery(map) != MS_SUCCESS || !layer->resultcache) {
13✔
1397
        outputError(OGCAPI_NOT_FOUND_ERROR, "Collection items query failed.");
×
1398
        return MS_SUCCESS;
×
1399
      }
1400
    }
1401
  }
1402

1403
  // build response object
1404
  if (!featureId) {
16✔
1405
    std::string api_root = getApiRootUrl(map);
14✔
1406
    const char *id = layer->name;
14✔
1407
    char *id_encoded = msEncodeUrl(id); // free after use
14✔
1408

1409
    std::string extra_kvp = "&limit=" + std::to_string(limit);
14✔
1410
    extra_kvp += "&offset=" + std::to_string(offset);
28✔
1411

1412
    std::string other_extra_kvp;
1413
    if (crs)
14✔
1414
      other_extra_kvp += "&crs=" + std::string(crs);
4✔
1415
    const char *bbox = getRequestParameter(request, "bbox");
14✔
1416
    if (bbox)
14✔
1417
      other_extra_kvp += "&bbox=" + std::string(bbox);
6✔
1418
    const char *bboxCrs = getRequestParameter(request, "bbox-crs");
14✔
1419
    if (bboxCrs)
14✔
1420
      other_extra_kvp += "&bbox-crs=" + std::string(bboxCrs);
4✔
1421

1422
    response = {
1423
        {"type", "FeatureCollection"},
1424
        {"numberMatched", numberMatched},
1425
        {"numberReturned", layer->resultcache->numresults},
14✔
1426
        {"features", json::array()},
14✔
1427
        {"links",
1428
         {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
17✔
1429
           {"type", OGCAPI_MIMETYPE_GEOJSON},
1430
           {"title", "Items for this collection as GeoJSON"},
1431
           {"href", api_root + "/collections/" + std::string(id_encoded) +
28✔
1432
                        "/items?f=json" + extra_kvp + other_extra_kvp}},
14✔
1433
          {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
25✔
1434
           {"type", OGCAPI_MIMETYPE_HTML},
1435
           {"title", "Items for this collection as HTML"},
1436
           {"href", api_root + "/collections/" + std::string(id_encoded) +
28✔
1437
                        "/items?f=html" + extra_kvp + other_extra_kvp}}}}};
588✔
1438

1439
    if (offset + layer->resultcache->numresults < numberMatched) {
14✔
1440
      response["links"].push_back(
98✔
1441
          {{"rel", "next"},
1442
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
7✔
1443
                                                 : OGCAPI_MIMETYPE_HTML},
1444
           {"title", "next page"},
1445
           {"href",
1446
            api_root + "/collections/" + std::string(id_encoded) +
14✔
1447
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
14✔
1448
                "&limit=" + std::to_string(limit) + "&offset=" +
28✔
1449
                std::to_string(offset + limit) + other_extra_kvp}});
7✔
1450
    }
1451

1452
    if (offset > 0) {
14✔
1453
      response["links"].push_back(
14✔
1454
          {{"rel", "prev"},
1455
           {"type", format == OGCAPIFormat::JSON ? OGCAPI_MIMETYPE_GEOJSON
1✔
1456
                                                 : OGCAPI_MIMETYPE_HTML},
1457
           {"title", "previous page"},
1458
           {"href",
1459
            api_root + "/collections/" + std::string(id_encoded) +
2✔
1460
                "/items?f=" + (format == OGCAPIFormat::JSON ? "json" : "html") +
2✔
1461
                "&limit=" + std::to_string(limit) +
3✔
1462
                "&offset=" + std::to_string(MS_MAX(0, (offset - limit))) +
2✔
1463
                other_extra_kvp}});
1464
    }
1465

1466
    msFree(id_encoded); // done
14✔
1467
  }
1468

1469
  // features (items)
1470
  {
1471
    shapeObj shape;
1472
    msInitShape(&shape);
16✔
1473

1474
    // we piggyback on GML configuration
1475
    gmlItemListObj *items = msGMLGetItems(layer, "AG");
16✔
1476
    gmlConstantListObj *constants = msGMLGetConstants(layer, "AG");
16✔
1477

1478
    if (!items || !constants) {
16✔
1479
      msGMLFreeItems(items);
×
1480
      msGMLFreeConstants(constants);
×
1481
      outputError(OGCAPI_SERVER_ERROR,
×
1482
                  "Error fetching layer attribute metadata.");
1483
      return MS_SUCCESS;
×
1484
    }
1485

1486
    const int geometry_precision = getGeometryPrecision(map, layer);
16✔
1487
    int status = 0;
1488

1489
    for (i = 0; i < layer->resultcache->numresults; i++) {
39✔
1490
        if (layer->resultcache->results[i].shape) {
23✔
NEW
1491
          msCopyShape(layer->resultcache->results[i].shape, &shape);
×
1492
        }
1493
      else {
1494
          status =
1495
              msLayerGetShape(layer, &shape, &(layer->resultcache->results[i]));
23✔
1496
          if (status != MS_SUCCESS) {
23✔
NEW
1497
            msGMLFreeItems(items);
×
NEW
1498
            msGMLFreeConstants(constants);
×
NEW
1499
            outputError(OGCAPI_SERVER_ERROR, "Error fetching feature.");
×
NEW
1500
            return MS_SUCCESS;
×
1501
          }
1502
      }
1503
      // check - already reprojected?
1504
      if (reprObjs.reprojector) {
23✔
1505
        status = msProjectShapeEx(reprObjs.reprojector, &shape);
16✔
1506
        if (status != MS_SUCCESS) {
16✔
1507
          msGMLFreeItems(items);
×
1508
          msGMLFreeConstants(constants);
×
1509
          msFreeShape(&shape);
×
1510
          outputError(OGCAPI_SERVER_ERROR, "Error reprojecting feature.");
×
1511
          return MS_SUCCESS;
×
1512
        }
1513
      }
1514

1515
      try {
1516
        json feature = getFeature(layer, &shape, items, constants,
1517
                                  geometry_precision, outputCrsAxisInverted);
23✔
1518
        if (featureId) {
23✔
1519
          response = std::move(feature);
2✔
1520
        } else {
1521
          response["features"].emplace_back(std::move(feature));
21✔
1522
        }
1523
      } catch (const std::runtime_error &e) {
×
1524
        msGMLFreeItems(items);
×
1525
        msGMLFreeConstants(constants);
×
1526
        msFreeShape(&shape);
×
1527
        outputError(OGCAPI_SERVER_ERROR,
×
1528
                    "Error getting feature. " + std::string(e.what()));
×
1529
        return MS_SUCCESS;
1530
      }
×
1531

1532
      msFreeShape(&shape); // next
23✔
1533
    }
1534

1535
    msGMLFreeItems(items); // clean up
16✔
1536
    msGMLFreeConstants(constants);
16✔
1537
  }
1538

1539
  // extend the response a bit for templating (HERE)
1540
  if (format == OGCAPIFormat::HTML) {
16✔
1541
    const char *title = getCollectionTitle(layer);
1542
    const char *id = layer->name;
3✔
1543
    response["collection"] = {{"id", id}, {"title", title ? title : ""}};
27✔
1544
  }
1545

1546
  if (featureId) {
16✔
1547
    std::string api_root = getApiRootUrl(map);
2✔
1548
    const char *id = layer->name;
2✔
1549
    char *id_encoded = msEncodeUrl(id); // free after use
2✔
1550

1551
    response["links"] = {
2✔
1552
        {{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
2✔
1553
         {"type", OGCAPI_MIMETYPE_GEOJSON},
1554
         {"title", "This document as GeoJSON"},
1555
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
1556
                      "/items/" + featureId + "?f=json"}},
2✔
1557
        {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
4✔
1558
         {"type", OGCAPI_MIMETYPE_HTML},
1559
         {"title", "This document as HTML"},
1560
         {"href", api_root + "/collections/" + std::string(id_encoded) +
4✔
1561
                      "/items/" + featureId + "?f=html"}},
2✔
1562
        {{"rel", "collection"},
1563
         {"type", OGCAPI_MIMETYPE_JSON},
1564
         {"title", "This collection as JSON"},
1565
         {"href",
1566
          api_root + "/collections/" + std::string(id_encoded) + "?f=json"}},
4✔
1567
        {{"rel", "collection"},
1568
         {"type", OGCAPI_MIMETYPE_HTML},
1569
         {"title", "This collection as HTML"},
1570
         {"href",
1571
          api_root + "/collections/" + std::string(id_encoded) + "?f=html"}}};
110✔
1572

1573
    msFree(id_encoded);
2✔
1574

1575
    outputResponse(
2✔
1576
        map, request,
1577
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
1578
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEM, response, extraHeaders);
1579
  } else {
1580
    outputResponse(
25✔
1581
        map, request,
1582
        format == OGCAPIFormat::JSON ? OGCAPIFormat::GeoJSON : format,
1583
        OGCAPI_TEMPLATE_HTML_COLLECTION_ITEMS, response, extraHeaders);
1584
  }
1585
  return MS_SUCCESS;
1586
}
999✔
1587

1588
static int processCollectionRequest(mapObj *map, cgiRequestObj *request,
2✔
1589
                                    const char *collectionId,
1590
                                    OGCAPIFormat format) {
1591
  json response;
1592
  int l;
1593

1594
  for (l = 0; l < map->numlayers; l++) {
2✔
1595
    if (strcmp(map->layers[l]->name, collectionId) == 0)
2✔
1596
      break; // match
1597
  }
1598

1599
  if (l == map->numlayers) { // invalid collectionId
2✔
1600
    outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1601
    return MS_SUCCESS;
×
1602
  }
1603

1604
  try {
1605
    response = getCollection(map, map->layers[l], format);
2✔
1606
    if (response.is_null()) { // same as not found
2✔
1607
      outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid collection.");
×
1608
      return MS_SUCCESS;
×
1609
    }
1610
  } catch (const std::runtime_error &e) {
×
1611
    outputError(OGCAPI_CONFIG_ERROR,
×
1612
                "Error getting collection. " + std::string(e.what()));
×
1613
    return MS_SUCCESS;
1614
  }
×
1615

1616
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTION,
2✔
1617
                 response);
1618
  return MS_SUCCESS;
2✔
1619
}
1620

1621
static int processCollectionsRequest(mapObj *map, cgiRequestObj *request,
1✔
1622
                                     OGCAPIFormat format) {
1623
  json response;
1624
  int i;
1625

1626
  // define api root url
1627
  std::string api_root = getApiRootUrl(map);
1✔
1628

1629
  // build response object
1630
  response = {{"links",
1631
               {{{"rel", format == OGCAPIFormat::JSON ? "self" : "alternate"},
1✔
1632
                 {"type", OGCAPI_MIMETYPE_JSON},
1633
                 {"title", "This document as JSON"},
1634
                 {"href", api_root + "/collections?f=json"}},
1✔
1635
                {{"rel", format == OGCAPIFormat::HTML ? "self" : "alternate"},
2✔
1636
                 {"type", OGCAPI_MIMETYPE_HTML},
1637
                 {"title", "This document as HTML"},
1638
                 {"href", api_root + "/collections?f=html"}}}},
1✔
1639
              {"collections", json::array()}};
34✔
1640

1641
  for (i = 0; i < map->numlayers; i++) {
4✔
1642
    try {
1643
      json collection = getCollection(map, map->layers[i], format);
3✔
1644
      if (!collection.is_null())
3✔
1645
        response["collections"].push_back(collection);
3✔
1646
    } catch (const std::runtime_error &e) {
×
1647
      outputError(OGCAPI_CONFIG_ERROR,
×
1648
                  "Error getting collection." + std::string(e.what()));
×
1649
      return MS_SUCCESS;
1650
    }
×
1651
  }
1652

1653
  outputResponse(map, request, format, OGCAPI_TEMPLATE_HTML_COLLECTIONS,
1✔
1654
                 response);
1655
  return MS_SUCCESS;
1✔
1656
}
43✔
1657

1658
static int processApiRequest(mapObj *map, cgiRequestObj *request,
1✔
1659
                             OGCAPIFormat format) {
1660
  // Strongly inspired from
1661
  // https://github.com/geopython/pygeoapi/blob/master/pygeoapi/openapi.py
1662

1663
  json response;
1664

1665
  response = {
1666
      {"openapi", "3.0.2"},
1667
      {"tags", json::array()},
1✔
1668
  };
7✔
1669

1670
  response["info"] = {
1✔
1671
      {"title", getTitle(map)},
1✔
1672
      {"version", getWebMetadata(map, "A", "version", "1.0.0")},
1✔
1673
  };
7✔
1674

1675
  for (const char *item : {"description", "termsOfService"}) {
3✔
1676
    const char *value = getWebMetadata(map, "AO", item, nullptr);
2✔
1677
    if (value) {
2✔
1678
      response["info"][item] = value;
4✔
1679
    }
1680
  }
1681

1682
  for (const auto &pair : {
3✔
1683
           std::make_pair("name", "contactperson"),
1684
           std::make_pair("url", "contacturl"),
1685
           std::make_pair("email", "contactelectronicmailaddress"),
1686
       }) {
4✔
1687
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
3✔
1688
    if (value) {
3✔
1689
      response["info"]["contact"][pair.first] = value;
6✔
1690
    }
1691
  }
1692

1693
  for (const auto &pair : {
2✔
1694
           std::make_pair("name", "licensename"),
1695
           std::make_pair("url", "licenseurl"),
1696
       }) {
3✔
1697
    const char *value = getWebMetadata(map, "AO", pair.second, nullptr);
2✔
1698
    if (value) {
2✔
1699
      response["info"]["license"][pair.first] = value;
×
1700
    }
1701
  }
1702

1703
  {
1704
    const char *value = getWebMetadata(map, "AO", "keywords", nullptr);
1✔
1705
    if (value) {
1✔
1706
      response["info"]["x-keywords"] = value;
2✔
1707
    }
1708
  }
1709

1710
  json server;
1711
  server["url"] = getApiRootUrl(map);
3✔
1712
  {
1713
    const char *value =
1714
        getWebMetadata(map, "AO", "server_description", nullptr);
1✔
1715
    if (value) {
1✔
1716
      server["description"] = value;
2✔
1717
    }
1718
  }
1719
  response["servers"].push_back(server);
1✔
1720

1721
  const std::string oapif_schema_base_url = msOWSGetSchemasLocation(map);
1✔
1722
  const std::string oapif_yaml_url = oapif_schema_base_url +
1723
                                     "/ogcapi/features/part1/1.0/openapi/"
1724
                                     "ogcapi-features-1.yaml";
1✔
1725
  const std::string oapif_part2_yaml_url = oapif_schema_base_url +
1726
                                           "/ogcapi/features/part2/1.0/openapi/"
1727
                                           "ogcapi-features-2.yaml";
1✔
1728

1729
  json paths;
1730

1731
  paths["/"]["get"] = {
1✔
1732
      {"summary", "Landing page"},
1733
      {"description", "Landing page"},
1734
      {"tags", {"server"}},
1735
      {"operationId", "getLandingPage"},
1736
      {"parameters",
1737
       {
1738
           {{"$ref", "#/components/parameters/f"}},
1739
       }},
1740
      {"responses",
1741
       {{"200",
1742
         {{"$ref", oapif_yaml_url + "#/components/responses/LandingPage"}}},
1✔
1743
        {"400",
1744
         {{"$ref",
1745
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1746
        {"500",
1747
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
1748

1749
  paths["/api"]["get"] = {
1✔
1750
      {"summary", "API documentation"},
1751
      {"description", "API documentation"},
1752
      {"tags", {"server"}},
1753
      {"operationId", "getOpenapi"},
1754
      {"parameters",
1755
       {
1756
           {{"$ref", "#/components/parameters/f"}},
1757
       }},
1758
      {"responses",
1759
       {{"200", {{"$ref", "#/components/responses/200"}}},
1760
        {"400",
1761
         {{"$ref",
1762
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1763
        {"default", {{"$ref", "#/components/responses/default"}}}}}};
42✔
1764

1765
  paths["/conformance"]["get"] = {
1✔
1766
      {"summary", "API conformance definition"},
1767
      {"description", "API conformance definition"},
1768
      {"tags", {"server"}},
1769
      {"operationId", "getConformanceDeclaration"},
1770
      {"parameters",
1771
       {
1772
           {{"$ref", "#/components/parameters/f"}},
1773
       }},
1774
      {"responses",
1775
       {{"200",
1776
         {{"$ref",
1777
           oapif_yaml_url + "#/components/responses/ConformanceDeclaration"}}},
1✔
1778
        {"400",
1779
         {{"$ref",
1780
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1781
        {"500",
1782
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
1783

1784
  paths["/collections"]["get"] = {
1✔
1785
      {"summary", "Collections"},
1786
      {"description", "Collections"},
1787
      {"tags", {"server"}},
1788
      {"operationId", "getCollections"},
1789
      {"parameters",
1790
       {
1791
           {{"$ref", "#/components/parameters/f"}},
1792
       }},
1793
      {"responses",
1794
       {{"200",
1795
         {{"$ref", oapif_yaml_url + "#/components/responses/Collections"}}},
1✔
1796
        {"400",
1797
         {{"$ref",
1798
           oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
1✔
1799
        {"500",
1800
         {{"$ref", oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
43✔
1801

1802
  for (int i = 0; i < map->numlayers; i++) {
4✔
1803
    layerObj *layer = map->layers[i];
3✔
1804
    if (!includeLayer(map, layer)) {
3✔
1805
      continue;
×
1806
    }
1807

1808
    json collection_get = {
1809
        {"summary",
1810
         std::string("Get ") + getCollectionTitle(layer) + " metadata"},
3✔
1811
        {"description", getCollectionDescription(layer)},
3✔
1812
        {"tags", {layer->name}},
3✔
1813
        {"operationId", "describe" + std::string(layer->name) + "Collection"},
3✔
1814
        {"parameters",
1815
         {
1816
             {{"$ref", "#/components/parameters/f"}},
1817
         }},
1818
        {"responses",
1819
         {{"200",
1820
           {{"$ref", oapif_yaml_url + "#/components/responses/Collection"}}},
3✔
1821
          {"400",
1822
           {{"$ref",
1823
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
1824
          {"500",
1825
           {{"$ref",
1826
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
129✔
1827

1828
    std::string collectionNamePath("/collections/");
×
1829
    collectionNamePath += layer->name;
3✔
1830
    paths[collectionNamePath]["get"] = std::move(collection_get);
3✔
1831

1832
    // check metadata, layer then map
1833
    const char *max_limit_str =
1834
        msOWSLookupMetadata(&(layer->metadata), "A", "max_limit");
3✔
1835
    if (max_limit_str == nullptr)
3✔
1836
      max_limit_str =
1837
          msOWSLookupMetadata(&(map->web.metadata), "A", "max_limit");
3✔
1838
    const int max_limit =
1839
        max_limit_str ? atoi(max_limit_str) : OGCAPI_MAX_LIMIT;
3✔
1840
    const int default_limit = getDefaultLimit(map, layer);
3✔
1841

1842
    json items_get = {
1843
        {"summary", std::string("Get ") + getCollectionTitle(layer) + " items"},
3✔
1844
        {"description", getCollectionDescription(layer)},
3✔
1845
        {"tags", {layer->name}},
1846
        {"operationId", "get" + std::string(layer->name) + "Features"},
3✔
1847
        {"parameters",
1848
         {
1849
             {{"$ref", "#/components/parameters/f"}},
1850
             {{"$ref", oapif_yaml_url + "#/components/parameters/bbox"}},
3✔
1851
             {{"$ref", oapif_yaml_url + "#/components/parameters/datetime"}},
3✔
1852
             {{"$ref",
1853
               oapif_part2_yaml_url + "#/components/parameters/bbox-crs"}},
3✔
1854
             {{"$ref", oapif_part2_yaml_url + "#/components/parameters/crs"}},
3✔
1855
             {{"$ref", "#/components/parameters/offset"}},
1856
         }},
1857
        {"responses",
1858
         {{"200",
1859
           {{"$ref", oapif_yaml_url + "#/components/responses/Features"}}},
3✔
1860
          {"400",
1861
           {{"$ref",
1862
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
1863
          {"500",
1864
           {{"$ref",
1865
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
189✔
1866

1867
    json param_limit = {
1868
        {"name", "limit"},
1869
        {"in", "query"},
1870
        {"description", "The optional limit parameter limits the number of "
1871
                        "items that are presented in the response document."},
1872
        {"required", false},
1873
        {"schema",
1874
         {
1875
             {"type", "integer"},
1876
             {"minimum", 1},
1877
             {"maximum", max_limit},
1878
             {"default", default_limit},
1879
         }},
1880
        {"style", "form"},
1881
        {"explode", false},
1882
    };
102✔
1883
    items_get["parameters"].emplace_back(param_limit);
3✔
1884

1885
    std::string itemsPath(collectionNamePath + "/items");
3✔
1886
    paths[itemsPath]["get"] = std::move(items_get);
6✔
1887

1888
    json feature_id_get = {
1889
        {"summary",
1890
         std::string("Get ") + getCollectionTitle(layer) + " item by id"},
3✔
1891
        {"description", getCollectionDescription(layer)},
3✔
1892
        {"tags", {layer->name}},
1893
        {"operationId", "get" + std::string(layer->name) + "Feature"},
3✔
1894
        {"parameters",
1895
         {
1896
             {{"$ref", "#/components/parameters/f"}},
1897
             {{"$ref", oapif_yaml_url + "#/components/parameters/featureId"}},
3✔
1898
         }},
1899
        {"responses",
1900
         {{"200",
1901
           {{"$ref", oapif_yaml_url + "#/components/responses/Feature"}}},
3✔
1902
          {"400",
1903
           {{"$ref",
1904
             oapif_yaml_url + "#/components/responses/InvalidParameter"}}},
3✔
1905
          {"404",
1906
           {{"$ref", oapif_yaml_url + "#/components/responses/NotFound"}}},
3✔
1907
          {"500",
1908
           {{"$ref",
1909
             oapif_yaml_url + "#/components/responses/ServerError"}}}}}};
159✔
1910
    std::string itemsFeatureIdPath(collectionNamePath + "/items/{featureId}");
3✔
1911
    paths[itemsFeatureIdPath]["get"] = std::move(feature_id_get);
6✔
1912
  }
1913

1914
  response["paths"] = std::move(paths);
2✔
1915

1916
  json components;
1917
  components["responses"]["200"] = {{"description", "successful operation"}};
5✔
1918
  components["responses"]["default"] = {
1✔
1919
      {"description", "unexpected error"},
1920
      {"content",
1921
       {{"application/json",
1922
         {{"schema",
1923
           {{"$ref", "https://schemas.opengis.net/ogcapi/common/part1/1.0/"
1924
                     "openapi/schemas/exception.yaml"}}}}}}}};
16✔
1925

1926
  json parameters;
1927
  parameters["f"] = {
1✔
1928
      {"name", "f"},
1929
      {"in", "query"},
1930
      {"description", "The optional f parameter indicates the output format "
1931
                      "which the server shall provide as part of the response "
1932
                      "document.  The default format is GeoJSON."},
1933
      {"required", false},
1934
      {"schema",
1935
       {{"type", "string"}, {"enum", {"json", "html"}}, {"default", "json"}}},
1936
      {"style", "form"},
1937
      {"explode", false},
1938
  };
33✔
1939

1940
  parameters["offset"] = {
1✔
1941
      {"name", "offset"},
1942
      {"in", "query"},
1943
      {"description",
1944
       "The optional offset parameter indicates the index within the result "
1945
       "set from which the server shall begin presenting results in the "
1946
       "response document.  The first element has an index of 0 (default)."},
1947
      {"required", false},
1948
      {"schema",
1949
       {
1950
           {"type", "integer"},
1951
           {"minimum", 0},
1952
           {"default", 0},
1953
       }},
1954
      {"style", "form"},
1955
      {"explode", false},
1956
  };
31✔
1957

1958
  components["parameters"] = std::move(parameters);
2✔
1959

1960
  response["components"] = std::move(components);
2✔
1961

1962
  // TODO: "tags" array ?
1963

1964
  outputResponse(map, request,
3✔
1965
                 format == OGCAPIFormat::JSON ? OGCAPIFormat::OpenAPI_V3
1966
                                              : format,
1967
                 OGCAPI_TEMPLATE_HTML_OPENAPI, response);
1968
  return MS_SUCCESS;
1✔
1969
}
1,167✔
1970

1971
#endif
1972

1973
int msOGCAPIDispatchRequest(mapObj *map, cgiRequestObj *request) {
31✔
1974
#ifdef USE_OGCAPI_SVR
1975

1976
  // make sure ogcapi requests are enabled for this map
1977
  int status = msOWSRequestIsEnabled(map, NULL, "AO", "OGCAPI", MS_FALSE);
31✔
1978
  if (status != MS_TRUE) {
31✔
1979
    msSetError(MS_OGCAPIERR, "OGC API requests are not enabled.",
×
1980
               "msCGIDispatchAPIRequest()");
1981
    return MS_FAILURE; // let normal error handling take over
×
1982
  }
1983

1984
  for (int i = 0; i < request->NumParams; i++) {
84✔
1985
    for (int j = i + 1; j < request->NumParams; j++) {
96✔
1986
      if (strcmp(request->ParamNames[i], request->ParamNames[j]) == 0) {
43✔
1987
        std::string errorMsg("Query parameter ");
1✔
1988
        errorMsg += request->ParamNames[i];
1✔
1989
        errorMsg += " is repeated";
1990
        outputError(OGCAPI_PARAM_ERROR, errorMsg.c_str());
2✔
1991
        return MS_SUCCESS;
1992
      }
1993
    }
1994
  }
1995

1996
  OGCAPIFormat format; // all endpoints need a format
1997
  const char *p = getRequestParameter(request, "f");
30✔
1998

1999
  // if f= query parameter is not specified, use HTTP Accept header if available
2000
  if (p == nullptr) {
30✔
2001
    const char *accept = getenv("HTTP_ACCEPT");
2✔
2002
    if (accept) {
2✔
2003
      if (strcmp(accept, "*/*") == 0)
1✔
2004
        p = OGCAPI_MIMETYPE_JSON;
2005
      else
2006
        p = accept;
2007
    }
2008
  }
2009

2010
  if (p &&
29✔
2011
      (strcmp(p, "json") == 0 || strstr(p, OGCAPI_MIMETYPE_JSON) != nullptr ||
29✔
2012
       strstr(p, OGCAPI_MIMETYPE_GEOJSON) != nullptr ||
4✔
2013
       strstr(p, OGCAPI_MIMETYPE_OPENAPI_V3) != nullptr)) {
2014
    format = OGCAPIFormat::JSON;
2015
  } else if (p && (strcmp(p, "html") == 0 ||
4✔
2016
                   strstr(p, OGCAPI_MIMETYPE_HTML) != nullptr)) {
2017
    format = OGCAPIFormat::HTML;
2018
  } else if (p) {
2019
    std::string errorMsg("Unsupported format requested: ");
×
2020
    errorMsg += p;
2021
    outputError(OGCAPI_PARAM_ERROR, errorMsg.c_str());
×
2022
    return MS_SUCCESS; // avoid any downstream MapServer processing
2023
  } else {
2024
    format = OGCAPIFormat::HTML; // default for now
2025
  }
2026

2027
  if (request->api_path_length == 2) {
30✔
2028

2029
    return processLandingRequest(map, request, format);
3✔
2030

2031
  } else if (request->api_path_length == 3) {
2032

2033
    if (strcmp(request->api_path[2], "conformance") == 0) {
3✔
2034
      return processConformanceRequest(map, request, format);
1✔
2035
    } else if (strcmp(request->api_path[2], "conformance.html") == 0) {
2✔
2036
      return processConformanceRequest(map, request, OGCAPIFormat::HTML);
×
2037
    } else if (strcmp(request->api_path[2], "collections") == 0) {
2✔
2038
      return processCollectionsRequest(map, request, format);
1✔
2039
    } else if (strcmp(request->api_path[2], "collections.html") == 0) {
1✔
2040
      return processCollectionsRequest(map, request, OGCAPIFormat::HTML);
×
2041
    } else if (strcmp(request->api_path[2], "api") == 0) {
1✔
2042
      return processApiRequest(map, request, format);
1✔
2043
    }
2044

2045
  } else if (request->api_path_length == 4) {
2046

2047
    if (strcmp(request->api_path[2], "collections") ==
2✔
2048
        0) { // next argument (3) is collectionId
2049
      return processCollectionRequest(map, request, request->api_path[3],
2✔
2050
                                      format);
2✔
2051
    }
2052

2053
  } else if (request->api_path_length == 5) {
2054

2055
    if (strcmp(request->api_path[2], "collections") == 0 &&
19✔
2056
        strcmp(request->api_path[4], "items") ==
19✔
2057
            0) { // middle argument (3) is the collectionId
2058
      return processCollectionItemsRequest(map, request, request->api_path[3],
19✔
2059
                                           NULL, format);
19✔
2060
    }
2061

2062
  } else if (request->api_path_length == 6) {
2063

2064
    if (strcmp(request->api_path[2], "collections") == 0 &&
3✔
2065
        strcmp(request->api_path[4], "items") ==
3✔
2066
            0) { // middle argument (3) is the collectionId, last argument (5)
2067
                 // is featureId
2068
      return processCollectionItemsRequest(map, request, request->api_path[3],
3✔
2069
                                           request->api_path[5], format);
3✔
2070
    }
2071
  }
2072

2073
  outputError(OGCAPI_NOT_FOUND_ERROR, "Invalid API path.");
×
2074
  return MS_SUCCESS; // avoid any downstream MapServer processing
×
2075
#else
2076
  msSetError(MS_OGCAPIERR, "OGC API server support is not enabled.",
2077
             "msOGCAPIDispatchRequest()");
2078
  return MS_FAILURE;
2079
#endif
2080
}
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

© 2026 Coveralls, Inc