Skip to content

Commit 18dcfdc

Browse files
committed
* Set title to “All items” for the rel=”items” endpoint in catalogs instead of repeating the parent title
* Add a “created” column in catalog_feature table to order feature catalog by last inserted, first displayed * The PHP antimeridian computation to split polygon that replaced the SQL code incorrectly split polygons that span more than 180 degrees in longitude. This does not work for global product. Hence, the hypothesis for split/no split is no more based on longitude length asumption but on the order of westernmost vs easternmost coordinates with the asumption that the first coordinates in the GeoJSON is always the westernmost coordinate (follows GeoJSON convention) * Title and description of collection are now duplicated within the catalog table so a search in rocket correctly displayed it
1 parent f6f4d44 commit 18dcfdc

File tree

8 files changed

+198
-55
lines changed

8 files changed

+198
-55
lines changed

admin_scripts/migrate_to_9.5.9.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/command/with-contenv php
2+
3+
<?php
4+
5+
require_once("/app/resto/core/RestoConstants.php");
6+
require_once("/app/resto/core/RestoDatabaseDriver.php");
7+
require_once("/app/resto/core/utils/RestoLogUtil.php");
8+
require_once("/app/resto/core/utils/Antimeridian.php");
9+
require_once("/app/resto/core/dbfunctions/UsersFunctions.php");
10+
11+
/*
12+
* Read configuration from file...
13+
*/
14+
$configFile = '/etc/resto/config.php';
15+
if ( !file_exists($configFile)) {
16+
exit(1);
17+
}
18+
$config = include($configFile);
19+
$dbDriver = new RestoDatabaseDriver($config['database'] ?? null);
20+
$queries = [];
21+
22+
$antimeridian = new AntiMeridian();
23+
24+
$targetSchema = $dbDriver->targetSchema;
25+
26+
try {
27+
$dbDriver->query('ALTER TABLE ' . $targetSchema . '.catalog_feature ADD COLUMN IF NOT EXISTS created TIMESTAMP DEFAULT now()');
28+
} catch(Exception $e){
29+
RestoLogUtil::httpError(500, $e->getMessage());
30+
}
31+
echo "Looks good\n";
32+
33+

app/resto/core/RestoConstants.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class RestoConstants
2020
// [IMPORTANT] Starting resto 7.x, default routes are defined in RestoRouter class
2121

2222
// resto version
23-
const VERSION = '9.5.8';
23+
const VERSION = '9.5.9';
2424

2525
/* ============================================================
2626
* NEVER EVER TOUCH THESE VALUES

app/resto/core/api/STACAPI.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,14 +1640,12 @@ private function getParentAndChilds($catalogId, $params)
16401640
$element = array(
16411641
'rel' => 'items',
16421642
'type' => RestoUtil::$contentTypes['geojson'],
1643-
'href' => $this->context->core['baseUrl'] . ( str_starts_with($catalogId, 'collections/') ? '/' : '/catalogs/') . join('/', array_map('rawurlencode', explode('/', $parentAndChilds['parent']['id']))) . '/_'
1643+
'href' => $this->context->core['baseUrl'] . ( str_starts_with($catalogId, 'collections/') ? '/' : '/catalogs/') . join('/', array_map('rawurlencode', explode('/', $parentAndChilds['parent']['id']))) . '/_',
1644+
'title' => 'All items'
16441645
);
16451646
if ( $parentAndChilds['parent']['counters']['total'] > 0 ) {
16461647
$element['matched'] = $parentAndChilds['parent']['counters']['total'];
16471648
}
1648-
if ( isset($parentAndChilds['parent']['title']) ) {
1649-
$element['title'] = $parentAndChilds['parent']['title'];
1650-
}
16511649
$parentAndChilds['childs'][] = $element;
16521650
}
16531651

app/resto/core/dbfunctions/CatalogsFunctions.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ public function getCatalogItems($catalogId, $baseUrl)
202202
* Delete (within transaction)
203203
*/
204204
try {
205-
$results = $this->dbDriver->pQuery('SELECT featureid, collection, title FROM ' . $this->dbDriver->targetSchema . '.catalog_feature WHERE path=$1::ltree', array(
205+
$results = $this->dbDriver->pQuery('SELECT featureid, collection, title FROM ' . $this->dbDriver->targetSchema . '.catalog_feature WHERE path=$1::ltree ORDER BY created DESC', array(
206206
RestoUtil::path2ltree($catalogId)
207207
));
208208
} catch (Exception $e) {
@@ -585,8 +585,8 @@ private function storeCatalog($catalog, $user, $context, $collectionId, $feature
585585
$properties = null;
586586
if ( isset($catalog['rtype']) && $catalog['rtype'] === 'collection' ) {
587587
$catalog = array_merge($catalog, [
588-
'title' => null,
589-
'description' => null,
588+
/*'title' => null,
589+
'description' => null,*/
590590
'rtype' => 'collection'
591591
]);
592592
}

app/resto/core/utils/AntiMeridian.php

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ private function fixPolygon(
175175
bool $fix_winding = true,
176176
bool $great_circle = true
177177
): Polygon|MultiPolygon {
178-
178+
179179

180180
if ($force_north_pole || $force_south_pole) {
181181
$fix_winding = false;
@@ -191,7 +191,7 @@ private function fixPolygon(
191191

192192
if (count($polygons) === 1) {
193193
$polygon = $polygons[0];
194-
if (!Polygon::isClockwise($polygon->getExteriorRing())) {
194+
if (Polygon::isCCW($polygon->getExteriorRing())) {
195195
return $polygon;
196196
} else {
197197
$polygon->setInteriorRings($polygon->getExteriorRing());
@@ -366,10 +366,53 @@ function ($coord) {
366366
}
367367

368368

369+
/**
370+
* Segment a set of coordinates at the antimeridian.
371+
*
372+
* [IMPORTANT] This function differs from the original implementation in that it first test if the
373+
* polygon split or not the antimeridian based on the bbox order. To do so, it takes the asumption that
374+
* the first coordinates is the most western point of the polygon. From this it computes the bbox and applies
375+
* the GeoJSON rule on antimeridian crossing (https://datatracker.ietf.org/doc/html/rfc7946#section-5.2)
376+
*
377+
* "Consider a set of point Features within the Fiji archipelago,
378+
* straddling the antimeridian between 16 degrees S and 20 degrees S.
379+
* The southwest corner of the box containing these Features is at 20
380+
* degrees S and 177 degrees E, and the northwest corner is at 16
381+
* degrees S and 178 degrees W. The antimeridian-spanning GeoJSON
382+
* bounding box for this FeatureCollection is
383+
*
384+
* "bbox": [177.0, -20.0, -178.0, -16.0]
385+
*
386+
* and covers 5 degrees of longitude.
387+
*
388+
* The complementary bounding box for the same latitude band, not
389+
* crossing the antimeridian, is
390+
*
391+
* "bbox": [-178.0, -20.0, 177.0, -16.0]
392+
*
393+
* and covers 355 degrees of longitude.
394+
*
395+
* The latitude of the northeast corner is always greater than the
396+
* latitude of the southwest corner, but bounding boxes that cross the
397+
* antimeridian have a northeast corner longitude that is less than the
398+
* longitude of the southwest corner."
399+
*
400+
*
401+
* @param array $coords The coordinates to segment.
402+
* @param bool $greatCircle Whether to use great circle calculations.
403+
*/
369404
private function segment(array $coords, bool $greatCircle): array {
370405
$segment = [];
371406
$segments = [];
372407

408+
$westernCoords = $coords[0];
409+
$easternCoords = $this->getEasternmostCoordinate($coords);
410+
411+
if ($westernCoords[0] < $easternCoords[0]) {
412+
// No antimeridian crossing
413+
return [];
414+
}
415+
373416
for ($i = 0; $i < count($coords) - 1; $i++) {
374417
$start = $coords[$i];
375418
$end = $coords[$i + 1];
@@ -403,6 +446,26 @@ private function segment(array $coords, bool $greatCircle): array {
403446
return $segments;
404447
}
405448

449+
/**
450+
* Returns the easternmost coordinate in an array of coordinates.
451+
*
452+
* @param array $coordinates The array of coordinates.
453+
* @return array|null The easternmost coordinate.
454+
*/
455+
private function getEasternmostCoordinate($coordinates) {
456+
if (empty($coordinates)) {
457+
return null; // Return null if the array is empty
458+
}
459+
460+
// Sort the array by longitude in descending order
461+
usort($coordinates, function($a, $b) {
462+
return $b[0] <=> $a[0]; // Compare longitude values
463+
});
464+
465+
// Return the first coordinate (easternmost)
466+
return $coordinates[0];
467+
}
468+
406469
private function buildPolygons(array &$segments): array {
407470
if (empty($segments)) {
408471
return [];

app/resto/core/utils/antimeridian/LineString.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,13 @@ public function correctOrientation(): void {
7878
}
7979

8080
/**
81-
* Check if the LineString is "clockwise".
81+
* Check if the LineString is "counter clockwise".
8282
* LineStrings don't have a defined clockwise or counterclockwise orientation.
8383
* This method is here for consistency, but it will always return false for LineStrings.
8484
*
8585
* @return bool False for LineString, as orientation doesn't apply.
8686
*/
87-
public function isClockwise(): bool {
87+
public function isCCW(): bool {
8888
return false;
8989
}
9090

app/resto/core/utils/antimeridian/Polygon.php

Lines changed: 57 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,38 @@
11
<?php
22

3-
class Polygon {
3+
class Polygon
4+
{
45

56
private $exteriorRing; // Array of coordinates representing the exterior ring
67
private $interiorRings = []; // Array of arrays, each representing an interior ring
78

8-
/**
9-
* Determine if a ring is oriented clockwise.
10-
*
11-
* @param array $ring The ring to check.
12-
* @return bool True if the ring is clockwise, false otherwise.
13-
*/
14-
public static function isClockwise(array $ring): bool {
15-
$sum = 0;
16-
$n = count($ring);
17-
18-
for ($i = 0; $i < $n - 1; $i++) {
19-
$p1 = $ring[$i];
20-
$p2 = $ring[$i + 1];
21-
$sum += ($p2[0] - $p1[0]) * ($p2[1] + $p1[1]);
22-
}
23-
24-
return $sum > 0;
25-
}
26-
27-
public function __construct(array $geoJsonGeometryOrSegment) {
28-
9+
/**
10+
* Determine if a ring is oriented CounterClockWise.
11+
*
12+
* @param array $ring The ring to check.
13+
* @return bool True if the ring is ccw, false otherwise.
14+
*/
15+
public static function isCCW(array $ring): bool
16+
{
17+
$area = 0.0;
18+
$n = count($ring);
19+
20+
for ($i = 0; $i < $n - 1; $i++) {
21+
$p1 = $ring[$i];
22+
$p2 = $ring[$i + 1];
23+
$area += ($p2[0] - $p1[0]) * ($p2[1] + $p1[1]);
24+
}
25+
26+
return $area < 0;
27+
}
28+
29+
public function __construct(array $geoJsonGeometryOrSegment)
30+
{
31+
2932
if (array_is_list($geoJsonGeometryOrSegment)) {
3033
$geoJsonGeometryOrSegment[] = $geoJsonGeometryOrSegment[0];
3134
$this->exteriorRing = $geoJsonGeometryOrSegment;
32-
}
33-
else {
35+
} else {
3436

3537
if (!isset($geoJsonGeometryOrSegment['type']) || $geoJsonGeometryOrSegment['type'] !== 'Polygon') {
3638
throw new Exception('Invalid GeoJSON: Must be of type Polygon');
@@ -56,45 +58,53 @@ public function __construct(array $geoJsonGeometryOrSegment) {
5658
}
5759
}
5860

59-
public function toGeoJSON(): array {
61+
public function toGeoJSON(): array
62+
{
6063

6164
return [
6265
'type' => $this->getType(),
6366
'coordinates' => $this->getCoordinates()
6467
];
6568
}
6669

67-
public function getExteriorRing(): array {
70+
public function getExteriorRing(): array
71+
{
6872
return $this->exteriorRing;
6973
}
7074

71-
public function getInteriorRings(): array {
75+
public function getInteriorRings(): array
76+
{
7277
return $this->interiorRings;
7378
}
7479

75-
public function setExteriorRing($exteriorRing) {
80+
public function setExteriorRing($exteriorRing)
81+
{
7682
$this->exteriorRing = $exteriorRing;
7783
}
7884

79-
public function setInteriorRings($interiorRings) {
85+
public function setInteriorRings($interiorRings)
86+
{
8087
$this->interiorRings = $interiorRings;
8188
}
8289

83-
private function isValidRing(array $ring): bool {
90+
private function isValidRing(array $ring): bool
91+
{
8492
return count($ring) >= 4 && $ring[0] === end($ring);
8593
}
8694

87-
public function getCoordinates(): array {
95+
public function getCoordinates(): array
96+
{
8897
$coordinates = [
8998
$this->exteriorRing
9099
];
91-
if ( !empty($this->interiorRings) ) {
100+
if (!empty($this->interiorRings)) {
92101
$coordinates[] = $this->interiorRings;
93102
}
94103
return $coordinates;
95104
}
96105

97-
public function isCoincidentToAntimeridian(): bool {
106+
public function isCoincidentToAntimeridian(): bool
107+
{
98108
if ($this->checkRingCoincidence($this->exteriorRing)) {
99109
return true;
100110
}
@@ -108,7 +118,8 @@ public function isCoincidentToAntimeridian(): bool {
108118
return false;
109119
}
110120

111-
private function checkRingCoincidence(array $ring): bool {
121+
private function checkRingCoincidence(array $ring): bool
122+
{
112123
for ($i = 0; $i < count($ring) - 1; $i++) {
113124
$start = $ring[$i];
114125
$end = $ring[$i + 1];
@@ -125,13 +136,14 @@ private function checkRingCoincidence(array $ring): bool {
125136
*
126137
* @return bool True if the exterior ring is CCW and interior rings are CW.
127138
*/
128-
public function checkOrientation(): bool {
129-
if (Polygon::isClockwise($this->exteriorRing)) {
139+
public function checkOrientation(): bool
140+
{
141+
if (!Polygon::isCCW($this->exteriorRing)) {
130142
return false;
131143
}
132144

133145
foreach ($this->interiorRings as $ring) {
134-
if (!Polygon::isClockwise($ring)) {
146+
if (Polygon::isCCW($ring)) {
135147
return false;
136148
}
137149
}
@@ -143,13 +155,14 @@ public function checkOrientation(): bool {
143155
* Correct the orientation of the rings.
144156
* Ensures the exterior ring is CCW and interior rings are CW.
145157
*/
146-
public function correctOrientation(): void {
147-
if (Polygon::isClockwise($this->exteriorRing)) {
158+
public function correctOrientation(): void
159+
{
160+
if (!Polygon::isCCW($this->exteriorRing)) {
148161
$this->exteriorRing = array_reverse($this->exteriorRing);
149162
}
150163

151164
foreach ($this->interiorRings as &$ring) {
152-
if (!Polygon::isClockwise($ring)) {
165+
if (Polygon::isCCW($ring)) {
153166
$ring = array_reverse($ring);
154167
}
155168
}
@@ -177,7 +190,8 @@ public function contains($ring): bool
177190
*
178191
* @return string The geometry type.
179192
*/
180-
public function getType(): string {
193+
public function getType(): string
194+
{
181195
return 'Polygon';
182196
}
183197

@@ -203,12 +217,12 @@ private function isPointInsideExterior(array $point): bool
203217
$yj = $coords[$j][1];
204218

205219
if (($yi > $y) != ($yj > $y) &&
206-
$x < ($xj - $xi) * ($y - $yi) / ($yj - $yi) + $xi) {
220+
$x < ($xj - $xi) * ($y - $yi) / ($yj - $yi) + $xi
221+
) {
207222
$inside = !$inside;
208223
}
209224
}
210225

211226
return $inside;
212227
}
213-
214228
}

0 commit comments

Comments
 (0)