Skip to content

Commit

Permalink
* Set title to “All items” for the rel=”items” endpoint in catalogs i…
Browse files Browse the repository at this point in the history
…nstead 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
  • Loading branch information
jjrom committed Jan 17, 2025
1 parent f6f4d44 commit 18dcfdc
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 55 deletions.
33 changes: 33 additions & 0 deletions admin_scripts/migrate_to_9.5.9.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/command/with-contenv php

<?php

require_once("/app/resto/core/RestoConstants.php");
require_once("/app/resto/core/RestoDatabaseDriver.php");
require_once("/app/resto/core/utils/RestoLogUtil.php");
require_once("/app/resto/core/utils/Antimeridian.php");
require_once("/app/resto/core/dbfunctions/UsersFunctions.php");

/*
* Read configuration from file...
*/
$configFile = '/etc/resto/config.php';
if ( !file_exists($configFile)) {
exit(1);
}
$config = include($configFile);
$dbDriver = new RestoDatabaseDriver($config['database'] ?? null);
$queries = [];

$antimeridian = new AntiMeridian();

$targetSchema = $dbDriver->targetSchema;

try {
$dbDriver->query('ALTER TABLE ' . $targetSchema . '.catalog_feature ADD COLUMN IF NOT EXISTS created TIMESTAMP DEFAULT now()');
} catch(Exception $e){
RestoLogUtil::httpError(500, $e->getMessage());
}
echo "Looks good\n";


2 changes: 1 addition & 1 deletion app/resto/core/RestoConstants.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class RestoConstants
// [IMPORTANT] Starting resto 7.x, default routes are defined in RestoRouter class

// resto version
const VERSION = '9.5.8';
const VERSION = '9.5.9';

/* ============================================================
* NEVER EVER TOUCH THESE VALUES
Expand Down
6 changes: 2 additions & 4 deletions app/resto/core/api/STACAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -1640,14 +1640,12 @@ private function getParentAndChilds($catalogId, $params)
$element = array(
'rel' => 'items',
'type' => RestoUtil::$contentTypes['geojson'],
'href' => $this->context->core['baseUrl'] . ( str_starts_with($catalogId, 'collections/') ? '/' : '/catalogs/') . join('/', array_map('rawurlencode', explode('/', $parentAndChilds['parent']['id']))) . '/_'
'href' => $this->context->core['baseUrl'] . ( str_starts_with($catalogId, 'collections/') ? '/' : '/catalogs/') . join('/', array_map('rawurlencode', explode('/', $parentAndChilds['parent']['id']))) . '/_',
'title' => 'All items'
);
if ( $parentAndChilds['parent']['counters']['total'] > 0 ) {
$element['matched'] = $parentAndChilds['parent']['counters']['total'];
}
if ( isset($parentAndChilds['parent']['title']) ) {
$element['title'] = $parentAndChilds['parent']['title'];
}
$parentAndChilds['childs'][] = $element;
}

Expand Down
6 changes: 3 additions & 3 deletions app/resto/core/dbfunctions/CatalogsFunctions.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ public function getCatalogItems($catalogId, $baseUrl)
* Delete (within transaction)
*/
try {
$results = $this->dbDriver->pQuery('SELECT featureid, collection, title FROM ' . $this->dbDriver->targetSchema . '.catalog_feature WHERE path=$1::ltree', array(
$results = $this->dbDriver->pQuery('SELECT featureid, collection, title FROM ' . $this->dbDriver->targetSchema . '.catalog_feature WHERE path=$1::ltree ORDER BY created DESC', array(
RestoUtil::path2ltree($catalogId)
));
} catch (Exception $e) {
Expand Down Expand Up @@ -585,8 +585,8 @@ private function storeCatalog($catalog, $user, $context, $collectionId, $feature
$properties = null;
if ( isset($catalog['rtype']) && $catalog['rtype'] === 'collection' ) {
$catalog = array_merge($catalog, [
'title' => null,
'description' => null,
/*'title' => null,
'description' => null,*/
'rtype' => 'collection'
]);
}
Expand Down
67 changes: 65 additions & 2 deletions app/resto/core/utils/AntiMeridian.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ private function fixPolygon(
bool $fix_winding = true,
bool $great_circle = true
): Polygon|MultiPolygon {


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

if (count($polygons) === 1) {
$polygon = $polygons[0];
if (!Polygon::isClockwise($polygon->getExteriorRing())) {
if (Polygon::isCCW($polygon->getExteriorRing())) {
return $polygon;
} else {
$polygon->setInteriorRings($polygon->getExteriorRing());
Expand Down Expand Up @@ -366,10 +366,53 @@ function ($coord) {
}


/**
* Segment a set of coordinates at the antimeridian.
*
* [IMPORTANT] This function differs from the original implementation in that it first test if the
* polygon split or not the antimeridian based on the bbox order. To do so, it takes the asumption that
* the first coordinates is the most western point of the polygon. From this it computes the bbox and applies
* the GeoJSON rule on antimeridian crossing (https://datatracker.ietf.org/doc/html/rfc7946#section-5.2)
*
* "Consider a set of point Features within the Fiji archipelago,
* straddling the antimeridian between 16 degrees S and 20 degrees S.
* The southwest corner of the box containing these Features is at 20
* degrees S and 177 degrees E, and the northwest corner is at 16
* degrees S and 178 degrees W. The antimeridian-spanning GeoJSON
* bounding box for this FeatureCollection is
*
* "bbox": [177.0, -20.0, -178.0, -16.0]
*
* and covers 5 degrees of longitude.
*
* The complementary bounding box for the same latitude band, not
* crossing the antimeridian, is
*
* "bbox": [-178.0, -20.0, 177.0, -16.0]
*
* and covers 355 degrees of longitude.
*
* The latitude of the northeast corner is always greater than the
* latitude of the southwest corner, but bounding boxes that cross the
* antimeridian have a northeast corner longitude that is less than the
* longitude of the southwest corner."
*
*
* @param array $coords The coordinates to segment.
* @param bool $greatCircle Whether to use great circle calculations.
*/
private function segment(array $coords, bool $greatCircle): array {
$segment = [];
$segments = [];

$westernCoords = $coords[0];
$easternCoords = $this->getEasternmostCoordinate($coords);

if ($westernCoords[0] < $easternCoords[0]) {
// No antimeridian crossing
return [];
}

for ($i = 0; $i < count($coords) - 1; $i++) {
$start = $coords[$i];
$end = $coords[$i + 1];
Expand Down Expand Up @@ -403,6 +446,26 @@ private function segment(array $coords, bool $greatCircle): array {
return $segments;
}

/**
* Returns the easternmost coordinate in an array of coordinates.
*
* @param array $coordinates The array of coordinates.
* @return array|null The easternmost coordinate.
*/
private function getEasternmostCoordinate($coordinates) {
if (empty($coordinates)) {
return null; // Return null if the array is empty
}

// Sort the array by longitude in descending order
usort($coordinates, function($a, $b) {
return $b[0] <=> $a[0]; // Compare longitude values
});

// Return the first coordinate (easternmost)
return $coordinates[0];
}

private function buildPolygons(array &$segments): array {
if (empty($segments)) {
return [];
Expand Down
4 changes: 2 additions & 2 deletions app/resto/core/utils/antimeridian/LineString.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@ public function correctOrientation(): void {
}

/**
* Check if the LineString is "clockwise".
* Check if the LineString is "counter clockwise".
* LineStrings don't have a defined clockwise or counterclockwise orientation.
* This method is here for consistency, but it will always return false for LineStrings.
*
* @return bool False for LineString, as orientation doesn't apply.
*/
public function isClockwise(): bool {
public function isCCW(): bool {
return false;
}

Expand Down
100 changes: 57 additions & 43 deletions app/resto/core/utils/antimeridian/Polygon.php
Original file line number Diff line number Diff line change
@@ -1,36 +1,38 @@
<?php

class Polygon {
class Polygon
{

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

/**
* Determine if a ring is oriented clockwise.
*
* @param array $ring The ring to check.
* @return bool True if the ring is clockwise, false otherwise.
*/
public static function isClockwise(array $ring): bool {
$sum = 0;
$n = count($ring);

for ($i = 0; $i < $n - 1; $i++) {
$p1 = $ring[$i];
$p2 = $ring[$i + 1];
$sum += ($p2[0] - $p1[0]) * ($p2[1] + $p1[1]);
}

return $sum > 0;
}

public function __construct(array $geoJsonGeometryOrSegment) {

/**
* Determine if a ring is oriented CounterClockWise.
*
* @param array $ring The ring to check.
* @return bool True if the ring is ccw, false otherwise.
*/
public static function isCCW(array $ring): bool
{
$area = 0.0;
$n = count($ring);

for ($i = 0; $i < $n - 1; $i++) {
$p1 = $ring[$i];
$p2 = $ring[$i + 1];
$area += ($p2[0] - $p1[0]) * ($p2[1] + $p1[1]);
}

return $area < 0;
}

public function __construct(array $geoJsonGeometryOrSegment)
{

if (array_is_list($geoJsonGeometryOrSegment)) {
$geoJsonGeometryOrSegment[] = $geoJsonGeometryOrSegment[0];
$this->exteriorRing = $geoJsonGeometryOrSegment;
}
else {
} else {

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

public function toGeoJSON(): array {
public function toGeoJSON(): array
{

return [
'type' => $this->getType(),
'coordinates' => $this->getCoordinates()
];
}

public function getExteriorRing(): array {
public function getExteriorRing(): array
{
return $this->exteriorRing;
}

public function getInteriorRings(): array {
public function getInteriorRings(): array
{
return $this->interiorRings;
}

public function setExteriorRing($exteriorRing) {
public function setExteriorRing($exteriorRing)
{
$this->exteriorRing = $exteriorRing;
}

public function setInteriorRings($interiorRings) {
public function setInteriorRings($interiorRings)
{
$this->interiorRings = $interiorRings;
}

private function isValidRing(array $ring): bool {
private function isValidRing(array $ring): bool
{
return count($ring) >= 4 && $ring[0] === end($ring);
}

public function getCoordinates(): array {
public function getCoordinates(): array
{
$coordinates = [
$this->exteriorRing
];
if ( !empty($this->interiorRings) ) {
if (!empty($this->interiorRings)) {
$coordinates[] = $this->interiorRings;
}
return $coordinates;
}

public function isCoincidentToAntimeridian(): bool {
public function isCoincidentToAntimeridian(): bool
{
if ($this->checkRingCoincidence($this->exteriorRing)) {
return true;
}
Expand All @@ -108,7 +118,8 @@ public function isCoincidentToAntimeridian(): bool {
return false;
}

private function checkRingCoincidence(array $ring): bool {
private function checkRingCoincidence(array $ring): bool
{
for ($i = 0; $i < count($ring) - 1; $i++) {
$start = $ring[$i];
$end = $ring[$i + 1];
Expand All @@ -125,13 +136,14 @@ private function checkRingCoincidence(array $ring): bool {
*
* @return bool True if the exterior ring is CCW and interior rings are CW.
*/
public function checkOrientation(): bool {
if (Polygon::isClockwise($this->exteriorRing)) {
public function checkOrientation(): bool
{
if (!Polygon::isCCW($this->exteriorRing)) {
return false;
}

foreach ($this->interiorRings as $ring) {
if (!Polygon::isClockwise($ring)) {
if (Polygon::isCCW($ring)) {
return false;
}
}
Expand All @@ -143,13 +155,14 @@ public function checkOrientation(): bool {
* Correct the orientation of the rings.
* Ensures the exterior ring is CCW and interior rings are CW.
*/
public function correctOrientation(): void {
if (Polygon::isClockwise($this->exteriorRing)) {
public function correctOrientation(): void
{
if (!Polygon::isCCW($this->exteriorRing)) {
$this->exteriorRing = array_reverse($this->exteriorRing);
}

foreach ($this->interiorRings as &$ring) {
if (!Polygon::isClockwise($ring)) {
if (Polygon::isCCW($ring)) {
$ring = array_reverse($ring);
}
}
Expand Down Expand Up @@ -177,7 +190,8 @@ public function contains($ring): bool
*
* @return string The geometry type.
*/
public function getType(): string {
public function getType(): string
{
return 'Polygon';
}

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

if (($yi > $y) != ($yj > $y) &&
$x < ($xj - $xi) * ($y - $yi) / ($yj - $yi) + $xi) {
$x < ($xj - $xi) * ($y - $yi) / ($yj - $yi) + $xi
) {
$inside = !$inside;
}
}

return $inside;
}

}
Loading

0 comments on commit 18dcfdc

Please sign in to comment.