diff --git a/admin_scripts/migrate_to_9.5.3.php b/admin_scripts/migrate_to_9.5.3.php new file mode 100755 index 00000000..4b2a03e0 --- /dev/null +++ b/admin_scripts/migrate_to_9.5.3.php @@ -0,0 +1,50 @@ +#!/command/with-contenv php + +targetSchema; + + try { + + $dbDriver->query('BEGIN'); + + // First populate geometry column + $dbDriver->query('UPDATE ' . $targetSchema . '.feature SET geometry = geom WHERE geometry IS NULL'); + + // Now convert all geometry so there are correctly computed with antimeridian + $results = $dbDriver->query('SELECT id, ST_AsGeoJSON(geometry) as geometry FROM ' . $targetSchema . '.feature'); + while ($result = pg_fetch_assoc($results)) { + $dbDriver->pQuery('UPDATE ' . $targetSchema . '.feature SET geom=ST_SetSRID(ST_GeomFromGeoJSON($2), 4326) WHERE id=$1', array( + $result['id'], + json_encode($antimeridian->fixGeoJSON(json_decode($result['geometry'], true)), JSON_UNESCAPED_SLASHES) + )); + } + + $dbDriver->query('COMMIT'); + + } catch(Exception $e){ + $dbDriver->query('ROLLBACK'); + RestoLogUtil::httpError(500, $e->getMessage()); + } + echo "Looks good\n"; + + \ No newline at end of file diff --git a/app/resto/core/RestoConstants.php b/app/resto/core/RestoConstants.php index b7f617de..ba61934b 100644 --- a/app/resto/core/RestoConstants.php +++ b/app/resto/core/RestoConstants.php @@ -20,7 +20,7 @@ class RestoConstants // [IMPORTANT] Starting resto 7.x, default routes are defined in RestoRouter class // resto version - const VERSION = '9.5.2'; + const VERSION = '9.5.3'; /* ============================================================ * NEVER EVER TOUCH THESE VALUES diff --git a/app/resto/core/dbfunctions/GeneralFunctions.php b/app/resto/core/dbfunctions/GeneralFunctions.php index 3b7bd329..5ba01e5f 100755 --- a/app/resto/core/dbfunctions/GeneralFunctions.php +++ b/app/resto/core/dbfunctions/GeneralFunctions.php @@ -238,17 +238,23 @@ public function getTopologyAnalysis($geometry, $params) * Convert to EPSG:4326 if input SRID differs from this projection */ $epsgCode = RestoGeometryUtil::geoJSONGeometryToSRID($geometry); - $geoJsonParser = 'ST_SetSRID(ST_GeomFromGeoJSON($1), 4326)'; - if ($epsgCode !== "4326") { - $geoJsonParser = 'ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON($1), ' . $epsgCode . '), 4326)'; + if ($epsgCode !== 4326) { + try { + $result = pg_fetch_row($this->dbDriver->query_params('SELECT ST_AsGeoJSON(ST_Force2D(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON($1), ' . $epsgCode . '), 4326))) AS geom', array( + json_encode($geometry, JSON_UNESCAPED_SLASHES) + )), 0, PGSQL_ASSOC); + $geometry = json_decode($result['geom'], true); + } catch (Exception $e) { + $error = '[GEOMETRY] ' . pg_last_error($this->dbDriver->getConnection()); + } } + $antimeridian = new AntiMeridian(); + $fixedGeometry = $antimeridian->fixGeoJSON($geometry); try { - $result = pg_fetch_row($this->dbDriver->query_params('WITH tmp AS (SELECT ST_Force2D(' . $geoJsonParser . ') AS geom, ST_Force2D(' . $this->getSplitterFunction($geoJsonParser, $params) . ') AS _geom) SELECT geom, _geom, ST_Force2D(ST_SetSRID(ST_Centroid(_geom), 4326)) AS centroid, Box2D(ST_SetSRID(_geom, 4326)) as bbox FROM tmp', array( - json_encode(array( - 'type' => $geometry['type'], - 'coordinates' => $geometry['coordinates'] - ), JSON_UNESCAPED_SLASHES) + $result = pg_fetch_row($this->dbDriver->query_params('WITH tmp AS (SELECT ST_Force2D(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326)) AS geom, ST_Force2D(ST_SetSRID(ST_GeomFromGeoJSON($2), 4326)) AS _geom) SELECT geom, _geom, ST_Force2D(ST_SetSRID(ST_Centroid(_geom), 4326)) AS centroid, Box2D(ST_SetSRID(_geom, 4326)) as bbox FROM tmp', array( + json_encode($geometry, JSON_UNESCAPED_SLASHES), + json_encode($fixedGeometry, JSON_UNESCAPED_SLASHES) )), 0, PGSQL_ASSOC); } catch (Exception $e) { $error = '[GEOMETRY] ' . pg_last_error($this->dbDriver->getConnection()); @@ -294,23 +300,4 @@ private function getIp() return $best; } - /** - * Return Split function - * - * @param string $geom - * @param array $params - */ - private function getSplitterFunction($geom, $params) - { - // Specifically no split required ! - if (isset($params['_splitGeom']) && !$params['_splitGeom']) { - return $geom; - } - - if (!isset($params['tolerance'])) { - return 'ST_SetSRID(ST_SplitDateLine(' . $geom . '), 4326)'; - } - - return 'ST_SetSRID(ST_SimplifyPreserveTopologyWhenTooBig(ST_SplitDateLine(' . $geom . '),' . $params['tolerance'] . (isset($params['maxpoints']) ? ',' . $params['maxpoints'] : '') . '), 4326)'; - } } diff --git a/app/resto/core/utils/AntiMeridian.php b/app/resto/core/utils/AntiMeridian.php new file mode 100644 index 00000000..f3389fba --- /dev/null +++ b/app/resto/core/utils/AntiMeridian.php @@ -0,0 +1,747 @@ +fixShape( + $geometry, + $force_north_pole, + $force_south_pole, + $fix_winding, + $great_circle + ); + return $geojson; + } elseif ($type === 'FeatureCollection') { + $features = $geojson['features'] ?? null; + if ($features === null) { + throw new Exception('No features field found in GeoJSON FeatureCollection'); + } + foreach ($features as $i => $feature) { + $features[$i] = $this->fixGeoJSON( + $feature, + $force_north_pole, + $force_south_pole, + $fix_winding, + $great_circle + ); + } + $geojson['features'] = $features; + return $geojson; + } else { + return $this->fixShape( + $geojson, + $force_north_pole, + $force_south_pole, + $fix_winding, + $great_circle + ); + } + } + + /** + * Fixes a shape that crosses the antimeridian. + * + * @param array $shape A polygon, multi-polygon, line string, or multi-line string + * @param bool $force_north_pole Force joined segments to enclose the north pole + * @param bool $force_south_pole Force joined segments to enclose the south pole + * @param bool $fix_winding Reverse coordinates if the polygon is wound clockwise + * @param bool $great_circle Compute meridian crossings on the sphere + * @return array The fixed shape as an associative array + * @throws Exception If the geometry type is unsupported + */ + private function fixShape( + array $shape, + bool $force_north_pole = false, + bool $force_south_pole = false, + bool $fix_winding = true, + bool $great_circle = true + ): array { + + $geom = $shape['geometry'] ?? $shape; + switch ($geom['type']) { + case 'Polygon': + return $this->fixPolygon( + new Polygon($geom), + $force_north_pole, + $force_south_pole, + $fix_winding, + $great_circle + )->toGeoJSON(); + + case 'MultiPolygon': + return $this->fixMultiPolygon( + new MultiPolygon($geom), + $force_north_pole, + $force_south_pole, + $fix_winding, + $great_circle + )->toGeoJson(); + + case 'LineString': + return $this->fixLineString( + new LineString($geom), + $great_circle + )->toGeoJSON(); + + case 'MultiLineString': + return $this->fixMultiLineString( + new MultiLineString($geom), + $great_circle + )->toGeoJSON(); + + default: + throw new Exception('Unsupported geometry type: ' . $geom['type']); + } + + } + + /** + * Fixes a polygon geometry. + * + * @param Polygon $polygon The input polygon. + * @param bool $force_north_pole If true, force the joined segments to enclose the north pole. + * @param bool $force_south_pole If true, force the joined segments to enclose the south pole. + * @param bool $fix_winding If true, reverse the polygon's coordinates if it is wound clockwise. + * @param bool $great_circle If true, compute meridian crossings on the sphere rather than using 2D geometry. + * + * @return Polygon|MultiPolygon The fixed polygon, as a single polygon or a multi-polygon if it was split. + * @throws InvalidArgumentException If input is invalid. + */ + private function fixPolygon( + Polygon $polygon, + bool $force_north_pole = false, + bool $force_south_pole = false, + bool $fix_winding = true, + bool $great_circle = true + ): Polygon|MultiPolygon { + + + if ($force_north_pole || $force_south_pole) { + $fix_winding = false; + } + + $polygons = $this->fixPolygonToList( + $polygon, + $force_north_pole, + $force_south_pole, + $fix_winding, + $great_circle + ); + + if (count($polygons) === 1) { + $polygon = $polygons[0]; + if (!Polygon::isClockwise($polygon->getExteriorRing())) { + return $polygon; + } else { + $polygon->setInteriorRings($polygon->getExteriorRing()); + $polygon->setExteriorRing([ + [-180, 90], + [-180, -90], + [180, -90], + [180, 90] + ]); + return $polygon; + } + } else { + return new MultiPolygon($polygons); + } + } + + /** + * Fixes a MultiPolygon. + * + * @param object $multi_polygon The multi-polygon geometry object + * @param bool $force_north_pole Force joined segments to enclose the north pole + * @param bool $force_south_pole Force joined segments to enclose the south pole + * @param bool $fix_winding Reverse coordinates if the polygon is wound clockwise + * @param bool $great_circle Compute meridian crossings on the sphere + * @return object The fixed multi-polygon + * @throws Exception If input is not a valid MultiPolygon + */ + private function fixMultiPolygon( + $multi_polygon, + bool $force_north_pole = false, + bool $force_south_pole = false, + bool $fix_winding = true, + bool $great_circle = true + ) { + if ($multi_polygon->getType() !== 'MultiPolygon') { + throw new Exception('Input geometry is not a MultiPolygon'); + } + + $polygons = []; + foreach ($multi_polygon->getPolygons() as $polygon) { + $fixed_polygons = $this->fixPolygonToList( + $polygon, + $force_north_pole, + $force_south_pole, + $fix_winding, + $great_circle + ); + $polygons = array_merge($polygons, $fixed_polygons); + } + + // Assume `create_multi_polygon` is a function to create a MultiPolygon from an array of polygons + return new MultiPolygon($polygons); + } + + private function fixLineString(LineString $line_string, bool $great_circle): LineString|MultiLineString { + /** + * Fixes a LineString geometry. + * + * @param LineString $line_string The input line string. + * @param bool $great_circle Compute meridian crossings on the sphere rather than using 2D geometry. + * + * @return LineString|MultiLineString The fixed line string, either as a single line string or a multi-line string if split. + */ + $segments = $this->segment($line_string->getCoordinates(), $great_circle); + + if (empty($segments)) { + return $line_string; + } else { + $linestrings = []; + for ($i = 0, $ii = count($segments); $i < $ii; $i++) { + $linestrings[] = new LineString([ + 'type' => 'LineString', + 'coordinates' => $segments[$i] + ]); + } + return new MultiLineString($linestrings); + } + } + + private function fixMultiLineString(MultiLineString $multi_line_string, bool $great_circle): MultiLineString { + /** + * Fixes a MultiLineString geometry. + * + * @param MultiLineString $multi_line_string The input multi-line string. + * @param bool $great_circle Compute meridian crossings on the sphere rather than using 2D geometry. + * + * @return MultiLineString The fixed multi-line string. + */ + $line_strings = []; + + foreach ($multi_line_string->getLineStrings() as $line_string) { + $fixed = $this->fixLineString($line_string, $great_circle); + + if ($fixed instanceof LineString) { + $line_strings[] = $fixed; + } else { + $line_strings = array_merge($line_strings, $fixed->getLineStrings()); + } + } + + return new MultiLineString($line_strings); + } + + private function fixPolygonToList( + Polygon $polygon, + bool $forceNorthPole = false, + bool $forceSouthPole = false, + bool $fixWinding = true, + bool $greatCircle = true + ): array { + + $exterior = $this->normalize($polygon->getExteriorRing()); + $segments = $this->segment($exterior, $greatCircle); + if (empty($segments)) { + + $polygon->setExteriorRing($exterior); + if ($fixWinding && !$polygon->checkOrientation()) { + error_log('Warning: Fixing winding order because the polygon extends over both poles'); + $polygon->correctOrientation(); + } + return [$polygon]; + } + else { + $interiors = []; + $interiorRings = $polygon->getInteriorRings(); + for ($i = 0, $ii = count($interiorRings); $i < $ii; $i++) { + $interiorSegments = $this->segment($interiorRings[$i], $greatCircle); + if (!empty($interiorSegments)) { + if ($fixWinding) { + $unwrappedLinearRing = new LinearRing(array_map( + function ($coord) { + return [fmod($coord[0] + 360, 360), $coord[1]]; + }, + $interiorRings[$i] + )); + if ($unwrappedLinearRing->isCCW()) { + error_log('Warning: Fixing winding order because the polygon extends over both poles'); + $interiorSegments = $this->segment(array_reverse($interiorRings[$i]), $greatCircle); + } + } + $segments = array_merge($segments, $interiorSegments); + } else { + $interiors[] = $interiorRings[$i]; + } + } + } + + $segments = $this->extendOverPoles( + $segments, + $forceNorthPole, + $forceSouthPole, + $fixWinding + ); + + $polygons = $this->buildPolygons($segments); + assert(!empty($polygons)); + + foreach ($polygons as $i => $poly) { + foreach ($interiors as $j => $interior) { + if ($poly->contains(new LinearRing($interior))) { + unset($interiors[$j]); + $polygonInteriors = $poly->getInteriorRings(); + $polygonInteriors[] = $interior; + $polygons[$i]->setExteriorRing($poly->getExteriorRing()); + $polygons[$i]->setInteriorRings($polygonInteriors); + } + } + } + + assert(empty($interiors)); + return $polygons; + } + + + private function segment(array $coords, bool $greatCircle): array { + $segment = []; + $segments = []; + + for ($i = 0; $i < count($coords) - 1; $i++) { + $start = $coords[$i]; + $end = $coords[$i + 1]; + + $segment[] = $start; + + if (($end[0] - $start[0] > 180) && ($end[0] - $start[0] != 360)) { // Left crossing + $latitude = $this->crossingLatitude($start, $end, $greatCircle); + $segment[] = [-180, $latitude]; + $segments[] = $segment; + $segment = [[180, $latitude]]; + } elseif (($start[0] - $end[0] > 180) && ($start[0] - $end[0] != 360)) { // Right crossing + $latitude = $this->crossingLatitude($end, $start, $greatCircle); + $segment[] = [180, $latitude]; + $segments[] = $segment; + $segment = [[-180, $latitude]]; + } + } + + if (empty($segments)) { + // No antimeridian crossings + return []; + } elseif ($coords[count($coords) - 1] == $segments[0][0]) { + // Join polygons + $segments[0] = array_merge($segment, $segments[0]); + } else { + $segment[] = $coords[count($coords) - 1]; + $segments[] = $segment; + } + + return $segments; + } + + private function buildPolygons(array &$segments): array { + if (empty($segments)) { + return []; + } + + $segment = array_pop($segments); + $isRight = end($segment)[0] === 180; + $candidates = []; + + if ($this->isSelfClosing($segment)) { + // Self-closing segments might end up joining up with themselves. + $candidates[] = [null, $segment[0][1]]; + } + + foreach ($segments as $i => $s) { + // Is the start of $s on the same side as the end of $segment? + if ($s[0][0] === end($segment)[0]) { + if ( + ($isRight && $s[0][1] > end($segment)[1] && + (!$this->isSelfClosing($s) || end($s)[1] < $segment[0][1])) || + (!$isRight && $s[0][1] < end($segment)[1] && + (!$this->isSelfClosing($s) || end($s)[1] > $segment[0][1])) + ) { + $candidates[] = [$i, $s[0][1]]; + } + } + } + + // Sort the candidates by latitude + usort($candidates, function ($a, $b) use ($isRight) { + if ($isRight) { + return $b[1] <=> $a[1]; + } else { + return $a[1] <=> $b[1]; + } + }); + + $index = $candidates[0][0] ?? null; + + if ($index !== null) { + // Join the segments, then re-add them to the list and recurse. + $segment = array_merge($segment, array_splice($segments, $index, 1)[0]); + $segments[] = $segment; + return $this->buildPolygons($segments); + } else { + // Build the rest of the polygons without this segment. + $polygons = $this->buildPolygons($segments); + + // Check if all points in the segment are identical + $isIdentical = true; + foreach ($segment as $point) { + if ($point !== $segment[0]) { + $isIdentical = false; + break; + } + } + + if (!$isIdentical) { + $polygons[] = new Polygon($segment); + } + + return $polygons; + } + } + + private function isSelfClosing(array $segment): bool { + $isRight = $segment[count($segment) - 1][0] == 180; + + return $segment[0][0] == $segment[count($segment) - 1][0] && ( + ($isRight && $segment[0][1] > $segment[count($segment) - 1][1]) || + (!$isRight && $segment[0][1] < $segment[count($segment) - 1][1]) + ); + } + + private function normalize(array $coords): array { + $original = $coords; + $allAreOnAntimeridian = true; + foreach ($coords as $i => &$point) { + $longitude = $point[0]; + $latitude = $point[1]; + + // Check if the longitude is close to 180 + if ($this->isClose($longitude, 180)) { + if (abs($latitude) != 90 && $this->isClose($coords[($i - 1 + count($coords)) % count($coords)][0], -180)) { + $point[0] = -180; + } else { + $point[0] = 180; + } + } + // Check if the longitude is close to -180 + elseif ($this->isClose($longitude, -180)) { + if (abs($latitude) != 90 && $this->isClose($coords[($i - 1 + count($coords)) % count($coords)][0], 180)) { + $point[0] = 180; + } else { + $point[0] = -180; + } + } + // Normalize the longitude to be within -180 and 180 + else { + $point[0] = $this->modulo($longitude + 180, 360) - 180; + $allAreOnAntimeridian = false; + } + + // If the point has more than 2 elements, preserve additional dimensions + if (count($point) > 2) { + $point = array_merge([$point[0], $point[1]], array_slice($point, 2)); + } + } + unset($point); // Unset reference after foreach + + if ($allAreOnAntimeridian) { + return $original; + } else { + return $coords; + } + } + + /** + * Helper function to determine if two numbers are close within a small tolerance. + * + * @param float $a First number. + * @param float $b Second number. + * @param float $tol Tolerance (default is 1e-9). + * @return bool True if the numbers are close, false otherwise. + */ + private function isClose(float $a, float $b, float $tol = 1e-9): bool { + return abs($a - $b) < $tol; + } + + /** + * Convert spherical degrees to Cartesian coordinates. + * + * @param array $point An array [longitude, latitude]. + * @return array An array [x, y, z]. + */ + private function sphericalDegreesToCartesian(array $point): array { + [$lon, $lat] = array_map('deg2rad', $point); + return [ + cos($lon) * cos($lat), + sin($lon) * cos($lat), + sin($lat) + ]; + } + + /** + * Calculate the latitude of the crossing point on a great circle. + * + * @param array $start An array [longitude, latitude]. + * @param array $end An array [longitude, latitude]. + * @return float The crossing latitude. + */ + private function crossingLatitudeGreatCircle(array $start, array $end): float { + $p1 = $this->sphericalDegreesToCartesian($start); + $p2 = $this->sphericalDegreesToCartesian($end); + + // Cross product of the two vectors. + $n1 = $this->crossProduct($p1, $p2); + + // Meridian plane unit vector. + $n2 = [0, -1, 0]; + + // Intersection of both planes. + $intersection = $this->crossProduct($n1, $n2); + $norm = sqrt(array_sum(array_map(fn($x) => $x ** 2, $intersection))); + + // Normalize the intersection vector. + $intersection = array_map(fn($x) => $x / $norm, $intersection); + + // Return the latitude (arcsin of z-coordinate). + return round(rad2deg(asin($intersection[2])), $this->roundPrecision); + } + + /** + * Calculate the latitude of the crossing point using a flat approximation. + * + * @param array $start An array [longitude, latitude]. + * @param array $end An array [longitude, latitude]. + * @return float The crossing latitude. + */ + private function crossingLatitudeFlat(array $start, array $end): float { + $latitudeDelta = $end[1] - $start[1]; + + if ($end[0] > 0) { + return round( + $start[1] + (180.0 - $start[0]) * $latitudeDelta / ($end[0] + 360.0 - $start[0]), + $this->roundPrecision + ); + } else { + return round( + $start[1] + ($start[0] + 180.0) * $latitudeDelta / ($start[0] + 360.0 - $end[0]), + $this->roundPrecision + ); + } + } + + /** + * Calculate the crossing latitude for a given start and end point. + * + * @param array $start An array [longitude, latitude]. + * @param array $end An array [longitude, latitude]. + * @param bool $greatCircle Whether to use great circle calculations. + * @return float The crossing latitude. + */ + private function crossingLatitude(array $start, array $end, bool $greatCircle): float { + if (abs($start[0]) == 180) { + return $start[1]; + } elseif (abs($end[0]) == 180) { + return $end[1]; + } + + if ($greatCircle) { + return $this->crossingLatitudeGreatCircle($start, $end); + } + return $this->crossingLatitudeFlat($start, $end); + } + + /** + * Calculate the cross product of two 3D vectors. + * + * @param array $v1 The first vector [x, y, z]. + * @param array $v2 The second vector [x, y, z]. + * @return array The cross product [x, y, z]. + */ + private function crossProduct(array $v1, array $v2): array { + return [ + $v1[1] * $v2[2] - $v1[2] * $v2[1], + $v1[2] * $v2[0] - $v1[0] * $v2[2], + $v1[0] * $v2[1] - $v1[1] * $v2[0] + ]; + } + + private function extendOverPoles(array $segments, bool $forceNorthPole, bool $forceSouthPole, bool $fixWinding): array { + $leftStart = null; + $rightStart = null; + $leftEnd = null; + $rightEnd = null; + + foreach ($segments as $i => $segment) { + if ($segment[0][0] === -180 && + ($leftStart === null || $segment[0][1] < $leftStart->latitude)) { + $leftStart = new IndexAndLatitude($i, $segment[0][1]); + } elseif ($segment[0][0] === 180 && + ($rightStart === null || $segment[0][1] > $rightStart->latitude)) { + $rightStart = new IndexAndLatitude($i, $segment[0][1]); + } + if ($segment[count($segment) - 1][0] === -180 && + ($leftEnd === null || $segment[count($segment) - 1][1] < $leftEnd->latitude)) { + $leftEnd = new IndexAndLatitude($i, $segment[count($segment) - 1][1]); + } elseif ($segment[count($segment) - 1][0] === 180 && + ($rightEnd === null || $segment[count($segment) - 1][1] > $rightEnd->latitude)) { + $rightEnd = new IndexAndLatitude($i, $segment[count($segment) - 1][1]); + } + } + + $isOverNorthPole = false; + $isOverSouthPole = false; + $originalSegments = json_decode(json_encode($segments), true); // Deep copy + + if ($leftEnd !== null) { + if ( + ($forceNorthPole && !$forceSouthPole) && + $rightEnd === null && + ($leftStart === null || $leftEnd->latitude > $leftStart->latitude) + ) { + $isOverNorthPole = true; + $segments[$leftEnd->index][] = [-180, 90]; + $segments[$leftEnd->index][] = [180, 90]; + $segments[$leftEnd->index] = array_reverse($segments[$leftEnd->index]); + } elseif ( + $forceSouthPole || + $leftStart === null || + $leftEnd->latitude < $leftStart->latitude + ) { + $isOverSouthPole = true; + $segments[$leftEnd->index][] = [-180, -90]; + $segments[$leftEnd->index][] = [180, -90]; + } + } + + if ($rightEnd !== null) { + if ( + ($forceSouthPole && !$forceNorthPole) && + ($rightStart === null || $rightEnd->latitude < $rightStart->latitude) + ) { + $isOverSouthPole = true; + $segments[$rightEnd->index][] = [180, -90]; + $segments[$rightEnd->index][] = [-180, -90]; + $segments[$rightEnd->index] = array_reverse($segments[$rightEnd->index]); + } elseif ( + $forceNorthPole || + $rightStart === null || + $rightEnd->latitude > $rightStart->latitude + ) { + $isOverNorthPole = true; + $segments[$rightEnd->index][] = [180, 90]; + $segments[$rightEnd->index][] = [-180, 90]; + } + } + + if ($fixWinding && $isOverNorthPole && $isOverSouthPole) { + if ($forceNorthPole || $forceSouthPole) { + throw new Exception('Invalid state: Both poles are forced while fixing winding is enabled'); + } + + error_log('Warning: Fixing winding order because the polygon extends over both poles'); + foreach ($originalSegments as &$segment) { + $segment = array_reverse($segment); + } + return $originalSegments; + } + + return $segments; + } + + /** + * Modulo function that retains decimal precision. + * + * @param float $dividend The number to be divided. + * @param float $divisor The number by which the dividend is divided. + * @return float The remainder after division. + */ + function modulo(float $dividend, float $divisor): float + { + if ($divisor == 0.0) { + throw new InvalidArgumentException("Divisor cannot be zero."); + } + + $remainder = fmod($dividend, $divisor); + + // To ensure the remainder is always positive: + if ($remainder < 0) { + $remainder += abs($divisor); + } + + return $remainder; + } + + +} + + + diff --git a/app/resto/core/utils/RestoWMSUtil.php b/app/resto/core/utils/RestoWMSUtil.php index 5b637303..0a0d0d92 100755 --- a/app/resto/core/utils/RestoWMSUtil.php +++ b/app/resto/core/utils/RestoWMSUtil.php @@ -148,16 +148,16 @@ private function getLowResolutionUrl($wms) */ private function stream($url) { - header('Pragma: public'); - header('Cache-Control: public, must-revalidate, post-check=0, pre-check=0'); - header('Expires: 0'); - header('Content-Type: image/png'); - - $curl = new Curl(); + $curl = new Curly(); + $curl->setHeaders(array( + 'Pragma: public', + 'Cache-Control: public, must-revalidate, post-check=0, pre-check=0', + 'Expires: 0', + 'Content-Type: image/png' + )); $curl->setOptions(array( CURLOPT_RETURNTRANSFER => false, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_HTTPHEADER => array() + CURLOPT_FOLLOWLOCATION => true )); $curl->get(urldecode($url)); $curl->close(); diff --git a/app/resto/core/utils/antimeridian/IndexAndLatitude.php b/app/resto/core/utils/antimeridian/IndexAndLatitude.php new file mode 100644 index 00000000..53eb522e --- /dev/null +++ b/app/resto/core/utils/antimeridian/IndexAndLatitude.php @@ -0,0 +1,11 @@ +index = $index; + $this->latitude = $latitude; + } +} diff --git a/app/resto/core/utils/antimeridian/LineString.php b/app/resto/core/utils/antimeridian/LineString.php new file mode 100644 index 00000000..f9bd37c5 --- /dev/null +++ b/app/resto/core/utils/antimeridian/LineString.php @@ -0,0 +1,100 @@ +coordinates = $geoJsonGeometry['coordinates']; + + // A valid LineString must have at least two coordinates + if (count($this->coordinates) < 2) { + throw new Exception('Invalid GeoJSON: LineString must have at least two coordinates'); + } + } + + public function toGeoJSON(): array { + return [ + 'type' => $this->getType(), + 'coordinates' => $this->getCoordinates() + ]; + } + + /** + * Get the coordinates of the LineString. + * + * @return array An array of coordinates representing the line. + */ + public function getCoordinates(): array { + return $this->coordinates; + } + + /** + * Check if the LineString crosses the antimeridian. + * + * @return bool True if any segment of the LineString crosses the antimeridian. + */ + public function isCoincidentToAntimeridian(): bool { + for ($i = 0; $i < count($this->coordinates) - 1; $i++) { + $start = $this->coordinates[$i]; + $end = $this->coordinates[$i + 1]; + + if (abs($start[0]) == 180 && $start[0] == $end[0]) { + return true; + } + } + + return false; + } + + /** + * Check the orientation of the LineString. + * + * @return bool Always returns true since LineStrings have no "orientation" like polygons. + */ + public function checkOrientation(): bool { + return true; // LineStrings don't have the concept of orientation (clockwise or counterclockwise) + } + + /** + * Correct the orientation of the LineString. + * Since LineStrings do not have orientation to correct, this method does nothing. + */ + public function correctOrientation(): void { + // LineStrings don't have orientation like Polygons, so there's nothing to correct + } + + /** + * Check if the LineString is "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 { + return false; + } + + /** + * Get the type of the geometry (Polygon, MultiPolygon, LineString, MultiLineString). + * + * @return string The geometry type. + */ + public function getType(): string { + return 'LineString'; + } + +} diff --git a/app/resto/core/utils/antimeridian/LinearRing.php b/app/resto/core/utils/antimeridian/LinearRing.php new file mode 100644 index 00000000..001bc48d --- /dev/null +++ b/app/resto/core/utils/antimeridian/LinearRing.php @@ -0,0 +1,116 @@ +isValidLinearRing($coordinates)) { + throw new InvalidArgumentException('Invalid LinearRing: must be closed and have at least 4 points.'); + } + $this->coordinates = $coordinates; + } + + /** + * Get the coordinates of the LinearRing. + * + * @return array The list of coordinates. + */ + public function getCoordinates(): array + { + return $this->coordinates; + } + + /** + * Check if the LinearRing is closed (first and last points are the same). + * + * @return bool True if closed, otherwise false. + */ + public function isClosed(): bool + { + return $this->coordinates[0] === end($this->coordinates); + } + + /** + * Check if the LinearRing has a counter-clockwise orientation. + * + * @return bool True if counter-clockwise, otherwise false. + */ + public function isCCW(): bool + { + $area = 0.0; + $count = count($this->coordinates); + + for ($i = 0; $i < $count - 1; $i++) { + $p1 = $this->coordinates[$i]; + $p2 = $this->coordinates[$i + 1]; + $area += ($p2[0] - $p1[0]) * ($p2[1] + $p1[1]); + } + + return $area < 0; // Counter-clockwise if area is negative. + } + + /** + * Reverse the coordinates of the LinearRing. + * + * @return void + */ + public function reverse(): void + { + $this->coordinates = array_reverse($this->coordinates); + } + + /** + * Validate that the coordinates form a valid LinearRing. + * + * @param array $coordinates The coordinates to validate. + * @return bool True if valid, otherwise false. + */ + private function isValidLinearRing(array $coordinates): bool + { + if (count($coordinates) < 4) { + return false; + } + + return $coordinates[0] === end($coordinates); // Must be closed. + } + + /** + * Normalize the coordinates of the LinearRing to ensure longitudes are within [-180, 180]. + * + * @return void + */ + public function normalize(): void + { + foreach ($this->coordinates as &$point) { + $point[0] = (($point[0] + 180) % 360) - 180; // Normalize longitude. + } + } + + /** + * Get the bounding box of the LinearRing. + * + * @return array Bounding box as [minX, minY, maxX, maxY]. + */ + public function getBoundingBox(): array + { + $minX = $minY = PHP_FLOAT_MAX; + $maxX = $maxY = PHP_FLOAT_MIN; + + foreach ($this->coordinates as $point) { + $minX = min($minX, $point[0]); + $minY = min($minY, $point[1]); + $maxX = max($maxX, $point[0]); + $maxY = max($maxY, $point[1]); + } + + return [$minX, $minY, $maxX, $maxY]; + } +} diff --git a/app/resto/core/utils/antimeridian/MultiLineString.php b/app/resto/core/utils/antimeridian/MultiLineString.php new file mode 100644 index 00000000..607bfc1a --- /dev/null +++ b/app/resto/core/utils/antimeridian/MultiLineString.php @@ -0,0 +1,104 @@ +lineStrings = $geoJsonGeometryOrLineStringsArray; + } + else { + + if (!isset($geoJsonGeometryOrLineStringsArray['type']) || $geoJsonGeometryOrLineStringsArray['type'] !== 'MultiLineString') { + throw new Exception('Invalid GeoJSON: Must be of type MultiLineString'); + } + + if (!isset($geoJsonGeometryOrLineStringsArray['coordinates']) || !is_array($geoJsonGeometryOrLineStringsArray['coordinates'])) { + throw new Exception('Invalid GeoJSON: Missing or invalid coordinates field'); + } + + $this->lineStrings = []; + + foreach ($geoJsonGeometryOrLineStringsArray['coordinates'] as $coordinates) { + $lineStringGeoJson = [ + 'type' => 'LineString', + 'coordinates' => $coordinates + ]; + $this->lineStrings[] = new LineString($lineStringGeoJson); + } + } + } + + public function toGeoJSON(): array { + $coordinates = []; + for ($i = 0, $ii = count($this->lineStrings); $i < $ii; $i++) { + $coordinates[] = $this->lineStrings[$i]->getCoordinates(); + } + return [ + 'type' => $this->getType(), + 'coordinates' => $coordinates + ]; + } + + /** + * Get all LineStrings in the MultiLineString. + * + * @return array An array of LineString objects. + */ + public function getLineStrings(): array { + return $this->lineStrings; + } + + /** + * Check if any line in the MultiLineString crosses the antimeridian. + * + * @return bool True if any line crosses the antimeridian. + */ + public function isCoincidentToAntimeridian(): bool { + foreach ($this->lineStrings as $lineString) { + if ($lineString->isCoincidentToAntimeridian()) { + return true; + } + } + return false; + } + + /** + * Correct the orientation of all LineStrings in the MultiLineString. + * LineStrings don't have an orientation to correct, so this method does nothing. + */ + public function correctOrientation(): void { + // LineStrings do not have an orientation like Polygons, so nothing to correct + } + + /** + * Check the orientation of all LineStrings in the MultiLineString. + * Since LineStrings do not have orientation, this always returns true. + * + * @return bool Always true for MultiLineString, as LineStrings do not have orientation. + */ + public function checkOrientation(): bool { + return true; // LineStrings don't have orientation like Polygons + } + + /** + * Get the type of the geometry (Polygon, MultiPolygon, LineString, MultiLineString). + * + * @return string The geometry type. + */ + public function getType(): string { + return 'MultiLineString'; + } +} diff --git a/app/resto/core/utils/antimeridian/MultiPolygon.php b/app/resto/core/utils/antimeridian/MultiPolygon.php new file mode 100644 index 00000000..76395c98 --- /dev/null +++ b/app/resto/core/utils/antimeridian/MultiPolygon.php @@ -0,0 +1,109 @@ +polygons = $geoJsonGeometryOrPolygonsArray; + } + else { + if (!isset($geoJsonGeometryOrPolygonsArray['type']) || $geoJsonGeometryOrPolygonsArray['type'] !== 'MultiPolygon') { + throw new Exception('Invalid GeoJSON: Must be of type MultiPolygon'); + } + + if (!isset($geoJsonGeometryOrPolygonsArray['coordinates']) || !is_array($geoJsonGeometryOrPolygonsArray['coordinates'])) { + throw new Exception('Invalid GeoJSON: Missing or invalid coordinates field'); + } + + $this->polygons = []; + + foreach ($geoJsonGeometryOrPolygonsArray['coordinates'] as $coordinates) { + $polygonGeoJson = [ + 'type' => 'Polygon', + 'coordinates' => $coordinates + ]; + $this->polygons[] = new Polygon($polygonGeoJson); + } + } + + } + + public function toGeoJSON(): array { + $coordinates = []; + for ($i = 0, $ii = count($this->polygons); $i < $ii; $i++) { + $coordinates[] = $this->polygons[$i]->getCoordinates(); + } + return [ + 'type' => $this->getType(), + 'coordinates' => $coordinates + ]; + } + + /** + * Get all polygons in the MultiPolygon. + * + * @return array An array of Polygon objects. + */ + public function getPolygons(): array { + return $this->polygons; + } + + /** + * Check if any polygon in the MultiPolygon crosses the antimeridian. + * + * @return bool True if any polygon crosses the antimeridian. + */ + public function isCoincidentToAntimeridian(): bool { + foreach ($this->polygons as $polygon) { + if ($polygon->isCoincidentToAntimeridian()) { + return true; + } + } + return false; + } + + /** + * Correct the orientation of all polygons in the MultiPolygon. + */ + public function correctOrientation(): void { + foreach ($this->polygons as $polygon) { + $polygon->correctOrientation(); + } + } + + /** + * Check the orientation of all polygons in the MultiPolygon. + * + * @return bool True if all polygons are correctly oriented. + */ + public function checkOrientation(): bool { + foreach ($this->polygons as $polygon) { + if (!$polygon->checkOrientation()) { + return false; + } + } + return true; + } + + /** + * Get the type of the geometry (Polygon, MultiPolygon, LineString, MultiLineString). + * + * @return string The geometry type. + */ + public function getType(): string { + return 'MultiPolygon'; + } +} diff --git a/app/resto/core/utils/antimeridian/Polygon.php b/app/resto/core/utils/antimeridian/Polygon.php new file mode 100644 index 00000000..02222198 --- /dev/null +++ b/app/resto/core/utils/antimeridian/Polygon.php @@ -0,0 +1,214 @@ + 0; + } + + public function __construct(array $geoJsonGeometryOrSegment) { + + if (array_is_list($geoJsonGeometryOrSegment)) { + $geoJsonGeometryOrSegment[] = $geoJsonGeometryOrSegment[0]; + $this->exteriorRing = $geoJsonGeometryOrSegment; + } + else { + + if (!isset($geoJsonGeometryOrSegment['type']) || $geoJsonGeometryOrSegment['type'] !== 'Polygon') { + throw new Exception('Invalid GeoJSON: Must be of type Polygon'); + } + + if (!isset($geoJsonGeometryOrSegment['coordinates']) || !is_array($geoJsonGeometryOrSegment['coordinates'])) { + throw new Exception('Invalid GeoJSON: Missing or invalid coordinates field'); + } + + $coordinates = $geoJsonGeometryOrSegment['coordinates']; + + if (empty($coordinates[0]) || !$this->isValidRing($coordinates[0])) { + throw new Exception('Invalid GeoJSON: Exterior ring must be a closed linear ring with at least 4 coordinates'); + } + $this->exteriorRing = $coordinates[0]; + + for ($i = 1; $i < count($coordinates); $i++) { + if (!$this->isValidRing($coordinates[$i])) { + throw new Exception('Invalid GeoJSON: Interior ring at index $i must be a closed linear ring with at least 4 coordinates'); + } + $this->interiorRings[] = $coordinates[$i]; + } + } + } + + public function toGeoJSON(): array { + + return [ + 'type' => $this->getType(), + 'coordinates' => $this->getCoordinates() + ]; + } + + public function getExteriorRing(): array { + return $this->exteriorRing; + } + + public function getInteriorRings(): array { + return $this->interiorRings; + } + + public function setExteriorRing($exteriorRing) { + $this->exteriorRing = $exteriorRing; + } + + public function setInteriorRings($interiorRings) { + $this->interiorRings = $interiorRings; + } + + private function isValidRing(array $ring): bool { + return count($ring) >= 4 && $ring[0] === end($ring); + } + + public function getCoordinates(): array { + $coordinates = [ + $this->exteriorRing + ]; + if ( !empty($this->interiorRings) ) { + $coordinates[] = $this->interiorRings; + } + return $coordinates; + } + + public function isCoincidentToAntimeridian(): bool { + if ($this->checkRingCoincidence($this->exteriorRing)) { + return true; + } + + foreach ($this->interiorRings as $ring) { + if ($this->checkRingCoincidence($ring)) { + return true; + } + } + + return false; + } + + private function checkRingCoincidence(array $ring): bool { + for ($i = 0; $i < count($ring) - 1; $i++) { + $start = $ring[$i]; + $end = $ring[$i + 1]; + + if (abs($start[0]) == 180 && $start[0] == $end[0]) { + return true; + } + } + return false; + } + + /** + * Check if the orientation of the rings is valid. + * + * @return bool True if the exterior ring is CCW and interior rings are CW. + */ + public function checkOrientation(): bool { + if (Polygon::isClockwise($this->exteriorRing)) { + return false; + } + + foreach ($this->interiorRings as $ring) { + if (!Polygon::isClockwise($ring)) { + return false; + } + } + + return true; + } + + /** + * 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)) { + $this->exteriorRing = array_reverse($this->exteriorRing); + } + + foreach ($this->interiorRings as &$ring) { + if (!Polygon::isClockwise($ring)) { + $ring = array_reverse($ring); + } + } + } + + /** + * Checks if a given ring is contained within the polygon's exterior. + * + * @param LinearRing $ring The ring to check for containment. + * @return bool True if the ring is contained within the polygon's exterior, false otherwise. + */ + public function contains($ring): bool + { + // We assume the $ring is a LinearRing object and we check if all its coordinates are within the exterior ring. + foreach ($ring->getCoordinates() as $point) { + if (!$this->isPointInsideExterior($point)) { + return false; // Return false if any point of the interior ring is outside the exterior. + } + } + return true; // Return true if all points of the interior ring are inside the exterior. + } + + /** + * Get the type of the geometry (Polygon, MultiPolygon, LineString, MultiLineString). + * + * @return string The geometry type. + */ + public function getType(): string { + return 'Polygon'; + } + + /** + * Helper function to check if a point is inside the polygon's exterior. + * This is a basic implementation of point-in-polygon. + * + * @param array $point The point to check, given as an array [longitude, latitude]. + * @return bool True if the point is inside the exterior, false otherwise. + */ + private function isPointInsideExterior(array $point): bool + { + // Using a simple ray-casting algorithm for point-in-polygon test + $coords = $this->getExteriorRing(); + $x = $point[0]; + $y = $point[1]; + $inside = false; + + for ($i = 0, $j = count($coords) - 1; $i < count($coords); $j = $i++) { + $xi = $coords[$i][0]; + $yi = $coords[$i][1]; + $xj = $coords[$j][0]; + $yj = $coords[$j][1]; + + if (($yi > $y) != ($yj > $y) && + $x < ($xj - $xi) * ($y - $yi) / ($yj - $yi) + $xi) { + $inside = !$inside; + } + } + + return $inside; + } + +} diff --git a/examples/antimeridian/examples/almost-180.json b/examples/antimeridian/examples/almost-180.json new file mode 100644 index 00000000..2d797e21 --- /dev/null +++ b/examples/antimeridian/examples/almost-180.json @@ -0,0 +1,27 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + -176.78115373, + -18.58572933 + ], + [ + -173.71026732, + -14.73835555 + ], + [ + -176.88248512, + -12.82925277 + ], + [ + 179.99999999999997, + -15.7250416 + ], + [ + -176.78115373, + -18.58572933 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/both-poles.json b/examples/antimeridian/examples/both-poles.json new file mode 100644 index 00000000..f5e34e00 --- /dev/null +++ b/examples/antimeridian/examples/both-poles.json @@ -0,0 +1,51 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 100, + 60 + ], + [ + 175, + 65 + ], + [ + -175, + 65 + ], + [ + -120, + 0 + ], + [ + -170, + -85 + ], + [ + 175, + -85 + ], + [ + 0, + -68 + ], + [ + -90, + 0 + ], + [ + -90, + 80 + ], + [ + 0, + 85 + ], + [ + 100, + 60 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/complex-split.json b/examples/antimeridian/examples/complex-split.json new file mode 100644 index 00000000..354aba31 --- /dev/null +++ b/examples/antimeridian/examples/complex-split.json @@ -0,0 +1,59 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 120, + -40 + ], + [ + -120, + -40 + ], + [ + -120, + -20 + ], + [ + 150, + -20 + ], + [ + 150, + 0 + ], + [ + -120, + 0 + ], + [ + -120, + 60 + ], + [ + 120, + 60 + ], + [ + 120, + 40 + ], + [ + -150, + 40 + ], + [ + -150, + 20 + ], + [ + 120, + 20 + ], + [ + 120, + -40 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/crossing-latitude.json b/examples/antimeridian/examples/crossing-latitude.json new file mode 100644 index 00000000..4f8c08ed --- /dev/null +++ b/examples/antimeridian/examples/crossing-latitude.json @@ -0,0 +1,27 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 170, + 40 + ], + [ + -170, + 50 + ], + [ + -170, + 60 + ], + [ + 170, + 50 + ], + [ + 170, + 40 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/cw-only.json b/examples/antimeridian/examples/cw-only.json new file mode 100644 index 00000000..38e217a6 --- /dev/null +++ b/examples/antimeridian/examples/cw-only.json @@ -0,0 +1,27 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 100, + 40 + ], + [ + 90, + 40 + ], + [ + 90, + 50 + ], + [ + 100, + 50 + ], + [ + 100, + 40 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/cw-split.json b/examples/antimeridian/examples/cw-split.json new file mode 100644 index 00000000..983fe5fd --- /dev/null +++ b/examples/antimeridian/examples/cw-split.json @@ -0,0 +1,27 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 170, + 40 + ], + [ + 170, + 50 + ], + [ + -170, + 50 + ], + [ + -170, + 40 + ], + [ + 170, + 40 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/extra-crossing.json b/examples/antimeridian/examples/extra-crossing.json new file mode 100644 index 00000000..9da4989f --- /dev/null +++ b/examples/antimeridian/examples/extra-crossing.json @@ -0,0 +1,47 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 40, + 45 + ], + [ + 175, + 0 + ], + [ + -175, + -5 + ], + [ + -170, + -50 + ], + [ + -175, + -75 + ], + [ + 175, + -76 + ], + [ + -175, + -77 + ], + [ + -150, + -30 + ], + [ + -30, + 20 + ], + [ + 40, + 45 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/force-north-pole.json b/examples/antimeridian/examples/force-north-pole.json new file mode 100644 index 00000000..0a66a63f --- /dev/null +++ b/examples/antimeridian/examples/force-north-pole.json @@ -0,0 +1,1283 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 10.3685, + 89.6098 + ], + [ + 109.862, + 89.8078 + ], + [ + 151.934, + 89.4194 + ], + [ + 158.515, + 88.9727 + ], + [ + 160.866, + 88.5124 + ], + [ + 162.377, + 88.0578 + ], + [ + 163.225, + 87.6061 + ], + [ + 163.85, + 87.1534 + ], + [ + 164.331, + 86.695 + ], + [ + 164.679, + 86.2412 + ], + [ + 164.861, + 85.781 + ], + [ + 165.121, + 85.3232 + ], + [ + 165.328, + 84.869 + ], + [ + 165.476, + 84.4169 + ], + [ + 165.594, + 83.9604 + ], + [ + 165.693, + 83.497 + ], + [ + 165.779, + 83.0419 + ], + [ + 165.844, + 82.584 + ], + [ + 165.91, + 82.1325 + ], + [ + 165.966, + 81.6751 + ], + [ + 166.012, + 81.2187 + ], + [ + 166.048, + 80.7615 + ], + [ + 166.141, + 80.3077 + ], + [ + 166.163, + 79.8453 + ], + [ + 166.178, + 79.3915 + ], + [ + 166.253, + 78.9376 + ], + [ + 166.246, + 78.4734 + ], + [ + 166.284, + 78.0155 + ], + [ + 166.338, + 77.5632 + ], + [ + 166.346, + 77.1046 + ], + [ + 166.376, + 76.9314 + ], + [ + 174.18, + 76.8361 + ], + [ + -175.321, + 76.3227 + ], + [ + -111.726, + 37.2888 + ], + [ + -110.816, + 34.9036 + ], + [ + -109.957, + 32.5118 + ], + [ + -109.143, + 30.1141 + ], + [ + -108.369, + 27.7113 + ], + [ + -107.63, + 25.3039 + ], + [ + -106.921, + 22.8925 + ], + [ + -106.24, + 20.4776 + ], + [ + -105.583, + 18.0596 + ], + [ + -104.948, + 15.6389 + ], + [ + -104.332, + 13.216 + ], + [ + -103.734, + 10.791 + ], + [ + -103.15, + 8.36442 + ], + [ + -102.58, + 5.93647 + ], + [ + -102.021, + 3.50744 + ], + [ + -101.472, + 1.07126 + ], + [ + -100.932, + -1.35914 + ], + [ + -100.4, + -3.78986 + ], + [ + -99.875, + -6.22067 + ], + [ + -99.3547, + -8.65138 + ], + [ + -98.8384, + -11.0818 + ], + [ + -98.3248, + -13.5116 + ], + [ + -97.8129, + -15.9408 + ], + [ + -97.3014, + -18.3692 + ], + [ + -96.7891, + -20.7965 + ], + [ + -96.2748, + -23.2226 + ], + [ + -95.757, + -25.6473 + ], + [ + -95.2344, + -28.0707 + ], + [ + -94.7051, + -30.4923 + ], + [ + -94.1676, + -32.9123 + ], + [ + -93.6196, + -35.3304 + ], + [ + -93.059, + -37.7465 + ], + [ + -92.483, + -40.1604 + ], + [ + -91.8884, + -42.5721 + ], + [ + -91.2715, + -44.9814 + ], + [ + -90.6278, + -47.3882 + ], + [ + -89.9517, + -49.7922 + ], + [ + -89.2363, + -52.1932 + ], + [ + -88.473, + -54.591 + ], + [ + -87.6509, + -56.9852 + ], + [ + -86.7554, + -59.3753 + ], + [ + -85.7675, + -61.7607 + ], + [ + -84.6613, + -64.1405 + ], + [ + -83.4004, + -66.5136 + ], + [ + -81.9328, + -68.878 + ], + [ + -80.1809, + -71.231 + ], + [ + -78.0245, + -73.5683 + ], + [ + -75.2669, + -75.8826 + ], + [ + -71.567, + -78.1609 + ], + [ + -66.2885, + -80.3783 + ], + [ + -58.1511, + -82.4813 + ], + [ + -44.4644, + -84.3415 + ], + [ + -20.6712, + -85.6372 + ], + [ + 12.1746, + -85.808 + ], + [ + 38.9875, + -84.7422 + ], + [ + 54.7661, + -82.9827 + ], + [ + 63.9727, + -80.9242 + ], + [ + 69.815, + -78.7288 + ], + [ + 73.8374, + -76.4628 + ], + [ + 76.7931, + -74.1559 + ], + [ + 79.0789, + -71.8235 + ], + [ + 80.9194, + -69.4738 + ], + [ + 82.4503, + -67.1118 + ], + [ + 83.7578, + -64.7406 + ], + [ + 84.8993, + -62.3621 + ], + [ + 85.9143, + -59.9778 + ], + [ + 86.8311, + -57.5886 + ], + [ + 87.6703, + -55.1951 + ], + [ + 88.4473, + -52.798 + ], + [ + 89.1739, + -50.3975 + ], + [ + 89.8593, + -47.9939 + ], + [ + 90.5106, + -45.5876 + ], + [ + 91.1339, + -43.1786 + ], + [ + 91.7338, + -40.7672 + ], + [ + 92.3142, + -38.3534 + ], + [ + 92.8785, + -35.9375 + ], + [ + 93.4294, + -33.5196 + ], + [ + 93.9694, + -31.0998 + ], + [ + 94.5006, + -28.6781 + ], + [ + 95.0248, + -26.2548 + ], + [ + 95.5437, + -23.83 + ], + [ + 96.0588, + -21.4039 + ], + [ + 96.5716, + -18.9765 + ], + [ + 97.0832, + -16.548 + ], + [ + 97.595, + -14.1186 + ], + [ + 98.1081, + -11.6885 + ], + [ + 98.6237, + -9.25779 + ], + [ + 99.1429, + -6.82674 + ], + [ + 99.667, + -4.39552 + ], + [ + 100.197, + -1.96435 + ], + [ + 100.735, + 0.466557 + ], + [ + 101.281, + 2.89696 + ], + [ + 101.836, + 5.32662 + ], + [ + 102.404, + 7.75526 + ], + [ + 102.984, + 10.1826 + ], + [ + 103.579, + 12.6084 + ], + [ + 104.19, + 15.0323 + ], + [ + 104.821, + 17.4539 + ], + [ + 105.472, + 19.873 + ], + [ + 106.147, + 22.2891 + ], + [ + 106.848, + 24.7018 + ], + [ + 107.58, + 27.1106 + ], + [ + 108.345, + 29.5149 + ], + [ + 109.149, + 31.9143 + ], + [ + 109.996, + 34.308 + ], + [ + 110.892, + 36.6952 + ], + [ + 111.845, + 39.0751 + ], + [ + 112.863, + 41.4466 + ], + [ + 113.956, + 43.8085 + ], + [ + 115.136, + 46.1592 + ], + [ + 116.417, + 48.4971 + ], + [ + 117.817, + 50.8199 + ], + [ + 119.358, + 53.1248 + ], + [ + 121.066, + 55.4085 + ], + [ + 122.976, + 57.6667 + ], + [ + 125.129, + 59.8937 + ], + [ + 127.58, + 62.0823 + ], + [ + 130.396, + 64.2229 + ], + [ + 133.667, + 66.3028 + ], + [ + 137.502, + 68.3046 + ], + [ + 142.041, + 70.205 + ], + [ + 147.451, + 71.972 + ], + [ + 153.915, + 73.5629 + ], + [ + 161.597, + 74.9218 + ], + [ + 170.568, + 75.9813 + ], + [ + -179.309, + 76.6701 + ], + [ + -168.463, + 76.9296 + ], + [ + -168.474, + 77.1088 + ], + [ + -168.502, + 77.5655 + ], + [ + -168.574, + 78.0221 + ], + [ + -168.593, + 78.4706 + ], + [ + -168.612, + 78.936 + ], + [ + -168.667, + 79.3898 + ], + [ + -168.686, + 79.8442 + ], + [ + -168.768, + 80.3003 + ], + [ + -168.809, + 80.7608 + ], + [ + -168.858, + 81.2183 + ], + [ + -168.909, + 81.6745 + ], + [ + -168.962, + 82.1323 + ], + [ + -169.039, + 82.5888 + ], + [ + -169.188, + 83.0411 + ], + [ + -169.29, + 83.496 + ], + [ + -169.339, + 83.9583 + ], + [ + -169.472, + 84.4147 + ], + [ + -169.641, + 84.8651 + ], + [ + -169.815, + 85.3274 + ], + [ + -170.118, + 85.7809 + ], + [ + -170.406, + 86.2337 + ], + [ + -170.682, + 86.6958 + ], + [ + -171.155, + 87.1517 + ], + [ + -171.811, + 87.6072 + ], + [ + -172.815, + 88.0602 + ], + [ + -174.412, + 88.516 + ], + [ + -177.431, + 88.9674 + ], + [ + 174.732, + 89.4068 + ], + [ + 135.146, + 89.7923 + ], + [ + 37.9912, + 89.6023 + ], + [ + 94.0619, + 87.418 + ], + [ + 97.492, + 85.0435 + ], + [ + 98.4672, + 82.6609 + ], + [ + 98.7894, + 80.2758 + ], + [ + 98.8438, + 77.8894 + ], + [ + 98.7621, + 75.5019 + ], + [ + 98.6015, + 73.1136 + ], + [ + 98.3906, + 70.7243 + ], + [ + 98.1455, + 68.334 + ], + [ + 97.8757, + 65.9427 + ], + [ + 97.5873, + 63.5503 + ], + [ + 97.2843, + 61.1567 + ], + [ + 96.9694, + 58.762 + ], + [ + 96.6443, + 56.366 + ], + [ + 96.3105, + 53.9687 + ], + [ + 95.9687, + 51.5701 + ], + [ + 95.6196, + 49.1702 + ], + [ + 95.2637, + 46.769 + ], + [ + 94.9012, + 44.3664 + ], + [ + 94.5322, + 41.9626 + ], + [ + 94.1568, + 39.5574 + ], + [ + 93.7749, + 37.1511 + ], + [ + 93.3864, + 34.7436 + ], + [ + 92.9911, + 32.335 + ], + [ + 92.5888, + 29.9254 + ], + [ + 92.1792, + 27.5149 + ], + [ + 91.7619, + 25.1036 + ], + [ + 91.3366, + 22.6916 + ], + [ + 90.9028, + 20.2791 + ], + [ + 90.46, + 17.8662 + ], + [ + 90.0076, + 15.4532 + ], + [ + 89.545, + 13.0402 + ], + [ + 89.0715, + 10.6273 + ], + [ + 88.5863, + 8.21494 + ], + [ + 88.0886, + 5.80321 + ], + [ + 87.5773, + 3.39241 + ], + [ + 87.0516, + 0.98282 + ], + [ + 86.5101, + -1.42527 + ], + [ + 85.9516, + -3.83154 + ], + [ + 85.3747, + -6.23564 + ], + [ + 84.7776, + -8.63721 + ], + [ + 84.1587, + -11.0358 + ], + [ + 83.5159, + -13.4311 + ], + [ + 82.847, + -15.8225 + ], + [ + 82.1492, + -18.2096 + ], + [ + 81.4198, + -20.5917 + ], + [ + 80.6555, + -22.9683 + ], + [ + 79.8524, + -25.3387 + ], + [ + 79.0062, + -27.702 + ], + [ + 78.1122, + -30.0573 + ], + [ + 77.1644, + -32.4038 + ], + [ + 76.1564, + -34.7402 + ], + [ + 75.0804, + -37.0651 + ], + [ + 73.9275, + -39.377 + ], + [ + 72.6873, + -41.6742 + ], + [ + 71.3472, + -43.9544 + ], + [ + 69.8927, + -46.215 + ], + [ + 68.3062, + -48.453 + ], + [ + 66.567, + -50.6645 + ], + [ + 64.6501, + -52.8451 + ], + [ + 62.5255, + -54.989 + ], + [ + 60.1574, + -57.0891 + ], + [ + 57.5025, + -59.1369 + ], + [ + 54.5097, + -61.1212 + ], + [ + 51.1188, + -63.0282 + ], + [ + 47.2611, + -64.8405 + ], + [ + 42.861, + -66.5365 + ], + [ + 37.8422, + -68.0891 + ], + [ + 32.139, + -69.466 + ], + [ + 25.7161, + -70.6298 + ], + [ + 18.5948, + -71.5405 + ], + [ + 10.8801, + -72.1592 + ], + [ + 2.77311, + -72.4552 + ], + [ + -5.44774, + -72.412 + ], + [ + -13.4793, + -72.0322 + ], + [ + -21.0591, + -71.3363 + ], + [ + -28.0099, + -70.3578 + ], + [ + -34.25, + -69.1364 + ], + [ + -39.7748, + -67.7117 + ], + [ + -44.63, + -66.1201 + ], + [ + -48.8856, + -64.3925 + ], + [ + -52.6182, + -62.5545 + ], + [ + -55.9022, + -60.6266 + ], + [ + -58.8041, + -58.6253 + ], + [ + -61.3819, + -56.5635 + ], + [ + -63.6846, + -54.4517 + ], + [ + -65.7534, + -52.2982 + ], + [ + -67.6228, + -50.1095 + ], + [ + -69.3212, + -47.8911 + ], + [ + -70.8724, + -45.6474 + ], + [ + -72.2964, + -43.3818 + ], + [ + -73.6099, + -41.0974 + ], + [ + -74.827, + -38.7966 + ], + [ + -75.9595, + -36.4815 + ], + [ + -77.0174, + -34.1539 + ], + [ + -78.0095, + -31.8152 + ], + [ + -78.9431, + -29.4667 + ], + [ + -79.8246, + -27.1096 + ], + [ + -80.6594, + -24.7448 + ], + [ + -81.4523, + -22.3732 + ], + [ + -82.2075, + -19.9955 + ], + [ + -82.9285, + -17.6125 + ], + [ + -83.6187, + -15.2246 + ], + [ + -84.2809, + -12.8326 + ], + [ + -84.9174, + -10.4369 + ], + [ + -85.5306, + -8.03787 + ], + [ + -86.1224, + -5.63604 + ], + [ + -86.6945, + -3.23178 + ], + [ + -87.2485, + -0.825447 + ], + [ + -87.7859, + 1.58262 + ], + [ + -88.3078, + 3.99212 + ], + [ + -88.8168, + 6.40905 + ], + [ + -89.3112, + 8.82055 + ], + [ + -89.7933, + 11.2327 + ], + [ + -90.264, + 13.6451 + ], + [ + -90.7239, + 16.0578 + ], + [ + -91.1737, + 18.4704 + ], + [ + -91.6141, + 20.8828 + ], + [ + -92.0457, + 23.2948 + ], + [ + -92.4688, + 25.7062 + ], + [ + -92.884, + 28.1169 + ], + [ + -93.2917, + 30.5269 + ], + [ + -93.6921, + 32.9359 + ], + [ + -94.0856, + 35.3439 + ], + [ + -94.4723, + 37.7507 + ], + [ + -94.8525, + 40.1565 + ], + [ + -97.1809, + 86.0477 + ], + [ + -89.7948, + 88.4093 + ], + [ + 10.3685, + 89.6098 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/great-circle.json b/examples/antimeridian/examples/great-circle.json new file mode 100644 index 00000000..ca9734fe --- /dev/null +++ b/examples/antimeridian/examples/great-circle.json @@ -0,0 +1,27 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 179.45, + -65.5 + ], + [ + 178.3, + -66 + ], + [ + 179.75, + -66.5 + ], + [ + -179.1, + -66 + ], + [ + 179.45, + -65.5 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/issues-124.json b/examples/antimeridian/examples/issues-124.json new file mode 100644 index 00000000..774ff889 --- /dev/null +++ b/examples/antimeridian/examples/issues-124.json @@ -0,0 +1,811 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 123.901846, + -29.596765 + ], + [ + 125.905655, + -29.482468 + ], + [ + 127.90284, + -29.33837 + ], + [ + 129.892531, + -29.164777 + ], + [ + 131.873905, + -28.962029 + ], + [ + 133.846196, + -28.730497 + ], + [ + 135.808693, + -28.470586 + ], + [ + 137.760745, + -28.182725 + ], + [ + 139.701766, + -27.867371 + ], + [ + 141.631233, + -27.525003 + ], + [ + 143.548689, + -27.156122 + ], + [ + 145.453744, + -26.761247 + ], + [ + 147.346071, + -26.340915 + ], + [ + 149.225413, + -25.895676 + ], + [ + 151.091573, + -25.426095 + ], + [ + 152.94442, + -24.932748 + ], + [ + 154.783882, + -24.416219 + ], + [ + 156.609948, + -23.877103 + ], + [ + 158.42266, + -23.316001 + ], + [ + 160.222119, + -22.733521 + ], + [ + 162.008472, + -22.130275 + ], + [ + 163.781918, + -21.506881 + ], + [ + 165.5427, + -20.863961 + ], + [ + 167.291103, + -20.202141 + ], + [ + 169.027453, + -19.52205 + ], + [ + 170.752112, + -18.824321 + ], + [ + 172.465476, + -18.10959 + ], + [ + 174.167973, + -17.378496 + ], + [ + 175.860059, + -16.631684 + ], + [ + 177.542215, + -15.869801 + ], + [ + 179.214949, + -15.093498 + ], + [ + -179.121213, + -14.303433 + ], + [ + -177.465723, + -13.500268 + ], + [ + -175.818017, + -12.68467 + ], + [ + -174.177514, + -11.857314 + ], + [ + -172.543622, + -11.01888 + ], + [ + -170.915733, + -10.170058 + ], + [ + -169.293233, + -9.311543 + ], + [ + -167.675494, + -8.44404 + ], + [ + -166.061883, + -7.568263 + ], + [ + -164.451761, + -6.684935 + ], + [ + -162.844481, + -5.794788 + ], + [ + -161.239394, + -4.898567 + ], + [ + -159.635847, + -3.997022 + ], + [ + -158.033185, + -3.09092 + ], + [ + -156.430751, + -2.181032 + ], + [ + -154.827888, + -1.268146 + ], + [ + -153.223941, + -0.353057 + ], + [ + -151.618256, + 0.563429 + ], + [ + -150.010182, + 1.480493 + ], + [ + -148.39907, + 2.39731 + ], + [ + -146.784279, + 3.313042 + ], + [ + -145.165171, + 4.226841 + ], + [ + -143.541118, + 5.137852 + ], + [ + -141.911499, + 6.045211 + ], + [ + -140.275702, + 6.948045 + ], + [ + -138.633128, + 7.845477 + ], + [ + -136.98319, + 8.736619 + ], + [ + -135.325315, + 9.620583 + ], + [ + -133.658947, + 10.496472 + ], + [ + -131.983548, + 11.363389 + ], + [ + -130.298601, + 12.220432 + ], + [ + -128.603608, + 13.066701 + ], + [ + -126.898101, + 13.901294 + ], + [ + -125.181634, + 14.723309 + ], + [ + -123.453794, + 15.531851 + ], + [ + -121.714197, + 16.326024 + ], + [ + -119.962496, + 17.104942 + ], + [ + -118.198382, + 17.867724 + ], + [ + -116.421584, + 18.613496 + ], + [ + -114.631877, + 19.341398 + ], + [ + -112.82908, + 20.050579 + ], + [ + -111.013063, + 20.740203 + ], + [ + -109.183745, + 21.409449 + ], + [ + -107.341102, + 22.057513 + ], + [ + -105.485167, + 22.683612 + ], + [ + -103.61603, + 23.286982 + ], + [ + -101.733844, + 23.866882 + ], + [ + -99.838825, + 24.422599 + ], + [ + -97.931251, + 24.953444 + ], + [ + -96.011468, + 25.458758 + ], + [ + -94.079886, + 25.937915 + ], + [ + -92.136981, + 26.390319 + ], + [ + -90.183293, + 26.815412 + ], + [ + -88.219426, + 27.212671 + ], + [ + -86.246047, + 27.581612 + ], + [ + -84.263878, + 27.921791 + ], + [ + -82.273701, + 28.232809 + ], + [ + -80.276348, + 28.514305 + ], + [ + -78.272698, + 28.765967 + ], + [ + -76.263673, + 28.987525 + ], + [ + -74.25023, + 29.178759 + ], + [ + -72.233359, + 29.339491 + ], + [ + -70.214071, + 29.469595 + ], + [ + -68.193396, + 29.568988 + ], + [ + -66.172372, + 29.637637 + ], + [ + -64.152041, + 29.675554 + ], + [ + -62.13344, + 29.682796 + ], + [ + -60.117593, + 29.659465 + ], + [ + -58.105504, + 29.605708 + ], + [ + -56.098154, + 29.521712 + ], + [ + -54.096489, + 29.407707 + ], + [ + -52.101418, + 29.263957 + ], + [ + -50.113805, + 29.090767 + ], + [ + -48.134465, + 28.888475 + ], + [ + -46.164162, + 28.657449 + ], + [ + -44.203601, + 28.398091 + ], + [ + -42.253427, + 28.110828 + ], + [ + -40.314224, + 27.796113 + ], + [ + -38.386512, + 27.454422 + ], + [ + -36.470745, + 27.086251 + ], + [ + -34.567312, + 26.692118 + ], + [ + -32.676536, + 26.272555 + ], + [ + -30.798677, + 25.828108 + ], + [ + -28.933928, + 25.359339 + ], + [ + -27.082423, + 24.866819 + ], + [ + -25.244232, + 24.35113 + ], + [ + -23.419371, + 23.812863 + ], + [ + -21.607794, + 23.252614 + ], + [ + -19.809407, + 22.670989 + ], + [ + -18.024062, + 22.068596 + ], + [ + -16.251564, + 21.44605 + ], + [ + -14.491672, + 20.803969 + ], + [ + -12.744103, + 20.142976 + ], + [ + -11.008535, + 19.463698 + ], + [ + -9.284608, + 18.766764 + ], + [ + -7.571931, + 18.052808 + ], + [ + -5.870077, + 17.322467 + ], + [ + -4.178596, + 16.576382 + ], + [ + -2.497007, + 15.815198 + ], + [ + -0.824807, + 15.039565 + ], + [ + 0.838527, + 14.250138 + ], + [ + 2.49354, + 13.447577 + ], + [ + 4.140794, + 12.632548 + ], + [ + 5.780866, + 11.805723 + ], + [ + 7.414347, + 10.967783 + ], + [ + 9.041841, + 10.119414 + ], + [ + 10.663961, + 9.261312 + ], + [ + 12.281332, + 8.394181 + ], + [ + 13.894585, + 7.518732 + ], + [ + 15.50436, + 6.635689 + ], + [ + 17.1113, + 5.745783 + ], + [ + 18.716053, + 4.849757 + ], + [ + 20.319274, + 3.948364 + ], + [ + 21.921615, + 3.042366 + ], + [ + 23.523733, + 2.132539 + ], + [ + 25.126286, + 1.219666 + ], + [ + 26.729929, + 0.304545 + ], + [ + 28.335316, + -0.612019 + ], + [ + 29.943101, + -1.529207 + ], + [ + 31.553931, + -2.446193 + ], + [ + 33.16845, + -3.362138 + ], + [ + 34.787298, + -4.276195 + ], + [ + 36.411104, + -5.187508 + ], + [ + 38.040493, + -6.095211 + ], + [ + 39.676077, + -6.998433 + ], + [ + 41.318459, + -7.896293 + ], + [ + 42.968228, + -8.787904 + ], + [ + 44.62596, + -9.672376 + ], + [ + 46.292214, + -10.548811 + ], + [ + 47.967532, + -11.41631 + ], + [ + 49.652434, + -12.273971 + ], + [ + 51.34742, + -13.12089 + ], + [ + 53.052964, + -13.956165 + ], + [ + 54.769514, + -14.778892 + ], + [ + 56.497488, + -15.588172 + ], + [ + 58.237273, + -16.383109 + ], + [ + 59.989218, + -17.162813 + ], + [ + 61.753638, + -17.9264 + ], + [ + 63.530807, + -18.672995 + ], + [ + 65.320953, + -19.401734 + ], + [ + 67.12426, + -20.111762 + ], + [ + 68.940862, + -20.802242 + ], + [ + 70.770841, + -21.472348 + ], + [ + 72.614226, + -22.121273 + ], + [ + 74.470984, + -22.748229 + ], + [ + 76.341029, + -23.352451 + ], + [ + 78.224208, + -23.933193 + ], + [ + 80.120307, + -24.489736 + ], + [ + 82.029047, + -25.021391 + ], + [ + 83.950084, + -25.527493 + ], + [ + 85.883006, + -26.007412 + ], + [ + 87.827336, + -26.460549 + ], + [ + 89.782534, + -26.886341 + ], + [ + 91.747992, + -27.284263 + ], + [ + 93.723041, + -27.653826 + ], + [ + 95.706955, + -27.994585 + ], + [ + 97.698948, + -28.306134 + ], + [ + 99.698183, + -28.588113 + ], + [ + 101.703777, + -28.840204 + ], + [ + 103.714801, + -29.062138 + ], + [ + 105.730292, + -29.253689 + ], + [ + 107.749255, + -29.414681 + ], + [ + 109.770669, + -29.544984 + ], + [ + 111.793499, + -29.644516 + ], + [ + 113.816699, + -29.713241 + ], + [ + 115.839219, + -29.751171 + ], + [ + 117.860015, + -29.758364 + ], + [ + 119.878056, + -29.734922 + ], + [ + 121.892328, + -29.680993 + ], + [ + 123.901846, + -29.596765 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/issues-81.json b/examples/antimeridian/examples/issues-81.json new file mode 100644 index 00000000..73da2cb9 --- /dev/null +++ b/examples/antimeridian/examples/issues-81.json @@ -0,0 +1,27 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 166, + 77 + ], + [ + 180, + 78 + ], + [ + -180, + 81 + ], + [ + 162, + 81 + ], + [ + 166, + 77 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/latitude-band.json b/examples/antimeridian/examples/latitude-band.json new file mode 100644 index 00000000..126dd6c0 --- /dev/null +++ b/examples/antimeridian/examples/latitude-band.json @@ -0,0 +1,27 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + -180, + 40 + ], + [ + 180, + 40 + ], + [ + 180, + 50 + ], + [ + -180, + 50 + ], + [ + -180, + 40 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/line.json b/examples/antimeridian/examples/line.json new file mode 100644 index 00000000..941dd745 --- /dev/null +++ b/examples/antimeridian/examples/line.json @@ -0,0 +1,13 @@ +{ + "type": "LineString", + "coordinates": [ + [ + 170, + -10 + ], + [ + -170, + 10 + ] + ] +} diff --git a/examples/antimeridian/examples/multi-line.json b/examples/antimeridian/examples/multi-line.json new file mode 100644 index 00000000..ae5b487b --- /dev/null +++ b/examples/antimeridian/examples/multi-line.json @@ -0,0 +1,25 @@ +{ + "type": "MultiLineString", + "coordinates": [ + [ + [ + 170, + -10 + ], + [ + -170, + 10 + ] + ], + [ + [ + 0, + 0 + ], + [ + 1, + 1 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/multi-no-antimeridian.json b/examples/antimeridian/examples/multi-no-antimeridian.json new file mode 100644 index 00000000..641d845a --- /dev/null +++ b/examples/antimeridian/examples/multi-no-antimeridian.json @@ -0,0 +1,53 @@ +{ + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 100, + 40 + ], + [ + 100, + 50 + ], + [ + 90, + 50 + ], + [ + 90, + 40 + ], + [ + 100, + 40 + ] + ] + ], + [ + [ + [ + 100, + 10 + ], + [ + 100, + 20 + ], + [ + 90, + 20 + ], + [ + 90, + 10 + ], + [ + 100, + 10 + ] + ] + ] + ] +} diff --git a/examples/antimeridian/examples/multi-split.json b/examples/antimeridian/examples/multi-split.json new file mode 100644 index 00000000..317320f5 --- /dev/null +++ b/examples/antimeridian/examples/multi-split.json @@ -0,0 +1,53 @@ +{ + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 170, + 40 + ], + [ + -170, + 40 + ], + [ + -170, + 50 + ], + [ + 170, + 50 + ], + [ + 170, + 40 + ] + ] + ], + [ + [ + [ + 170, + -40 + ], + [ + 170, + -50 + ], + [ + -170, + -50 + ], + [ + -170, + -40 + ], + [ + 170, + -40 + ] + ] + ] + ] +} diff --git a/examples/antimeridian/examples/north-pole.json b/examples/antimeridian/examples/north-pole.json new file mode 100644 index 00000000..4dc4b791 --- /dev/null +++ b/examples/antimeridian/examples/north-pole.json @@ -0,0 +1,27 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 45, + 40 + ], + [ + 135, + 40 + ], + [ + -135, + 40 + ], + [ + -45, + 40 + ], + [ + 45, + 40 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/one-ccw-hole.json b/examples/antimeridian/examples/one-ccw-hole.json new file mode 100644 index 00000000..f86436cb --- /dev/null +++ b/examples/antimeridian/examples/one-ccw-hole.json @@ -0,0 +1,49 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 170, + 40 + ], + [ + -170, + 40 + ], + [ + -170, + 60 + ], + [ + 170, + 60 + ], + [ + 170, + 40 + ] + ], + [ + [ + 175, + 45 + ], + [ + -175, + 45 + ], + [ + -175, + 55 + ], + [ + 175, + 55 + ], + [ + 175, + 45 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/one-hole.json b/examples/antimeridian/examples/one-hole.json new file mode 100644 index 00000000..d3f0f9c6 --- /dev/null +++ b/examples/antimeridian/examples/one-hole.json @@ -0,0 +1,49 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 170, + 40 + ], + [ + -170, + 40 + ], + [ + -170, + 60 + ], + [ + 170, + 60 + ], + [ + 170, + 40 + ] + ], + [ + [ + 175, + 45 + ], + [ + 175, + 55 + ], + [ + -175, + 55 + ], + [ + -175, + 45 + ], + [ + 175, + 45 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/over-180.json b/examples/antimeridian/examples/over-180.json new file mode 100644 index 00000000..2092ba08 --- /dev/null +++ b/examples/antimeridian/examples/over-180.json @@ -0,0 +1,27 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 170, + 40 + ], + [ + 190, + 40 + ], + [ + 190, + 50 + ], + [ + 170, + 50 + ], + [ + 170, + 40 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/overlap.json b/examples/antimeridian/examples/overlap.json new file mode 100644 index 00000000..1bd03148 --- /dev/null +++ b/examples/antimeridian/examples/overlap.json @@ -0,0 +1,1283 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 162.681, + -72.4829 + ], + [ + 162.648, + -72.9666 + ], + [ + 162.667, + -73.424 + ], + [ + 162.647, + -73.8931 + ], + [ + 162.713, + -74.3108 + ], + [ + 162.722, + -74.7677 + ], + [ + 162.732, + -75.2246 + ], + [ + 162.726, + -75.687 + ], + [ + 162.753, + -76.1384 + ], + [ + 162.746, + -76.5968 + ], + [ + 162.739, + -77.0641 + ], + [ + 162.774, + -77.5224 + ], + [ + 162.763, + -77.9775 + ], + [ + 162.821, + -78.4224 + ], + [ + 162.838, + -78.8792 + ], + [ + 162.83, + -79.3407 + ], + [ + 162.856, + -79.7943 + ], + [ + 162.869, + -80.2516 + ], + [ + 162.879, + -80.712 + ], + [ + 162.894, + -81.1693 + ], + [ + 162.954, + -81.6255 + ], + [ + 162.974, + -82.0819 + ], + [ + 163.022, + -82.5335 + ], + [ + 163.033, + -82.9874 + ], + [ + 163.108, + -83.4468 + ], + [ + 163.165, + -83.8991 + ], + [ + 163.169, + -84.3602 + ], + [ + 163.322, + -84.8158 + ], + [ + 163.342, + -85.261 + ], + [ + 163.439, + -85.723 + ], + [ + 163.563, + -85.9079 + ], + [ + -173.272, + -85.5187 + ], + [ + -148.82, + -83.8664 + ], + [ + -135.841, + -81.622 + ], + [ + -128.385, + -79.1461 + ], + [ + -123.599, + -76.5659 + ], + [ + -120.246, + -73.9309 + ], + [ + -117.735, + -71.2637 + ], + [ + -115.756, + -68.5759 + ], + [ + -114.134, + -65.8737 + ], + [ + -112.76, + -63.161 + ], + [ + -111.567, + -60.4402 + ], + [ + -110.508, + -57.7127 + ], + [ + -109.552, + -54.9798 + ], + [ + -108.675, + -52.242 + ], + [ + -107.861, + -49.5 + ], + [ + -107.097, + -46.7542 + ], + [ + -106.373, + -44.0048 + ], + [ + -105.682, + -41.2522 + ], + [ + -105.017, + -38.4965 + ], + [ + -104.373, + -35.738 + ], + [ + -103.746, + -32.9769 + ], + [ + -103.133, + -30.2134 + ], + [ + -102.53, + -27.4476 + ], + [ + -101.936, + -24.6797 + ], + [ + -101.347, + -21.91 + ], + [ + -100.761, + -19.1387 + ], + [ + -100.177, + -16.3659 + ], + [ + -99.5929, + -13.592 + ], + [ + -99.0065, + -10.8172 + ], + [ + -98.4164, + -8.04175 + ], + [ + -97.8207, + -5.26596 + ], + [ + -97.2177, + -2.49013 + ], + [ + -96.6055, + 0.285399 + ], + [ + -95.9824, + 3.0603 + ], + [ + -95.3462, + 5.83419 + ], + [ + -94.6947, + 8.60668 + ], + [ + -94.0256, + 11.3774 + ], + [ + -93.3362, + 14.1458 + ], + [ + -92.6234, + 16.9115 + ], + [ + -91.8839, + 19.6739 + ], + [ + -91.1139, + 22.4325 + ], + [ + -90.309, + 25.1866 + ], + [ + -89.464, + 27.9355 + ], + [ + -88.573, + 30.6783 + ], + [ + -87.629, + 33.4143 + ], + [ + -86.6235, + 36.1422 + ], + [ + -85.5466, + 38.8609 + ], + [ + -84.3861, + 41.5687 + ], + [ + -83.1272, + 44.264 + ], + [ + -81.7516, + 46.9443 + ], + [ + -80.2367, + 49.6069 + ], + [ + -78.5538, + 52.2482 + ], + [ + -76.6666, + 54.8637 + ], + [ + -74.5284, + 57.4473 + ], + [ + -72.0782, + 59.9911 + ], + [ + -69.2365, + 62.4844 + ], + [ + -65.8978, + 64.9123 + ], + [ + -61.9232, + 67.2542 + ], + [ + -57.1311, + 69.4812 + ], + [ + -51.2908, + 71.5516 + ], + [ + -44.136, + 73.4074 + ], + [ + -35.4141, + 74.969 + ], + [ + -25.0188, + 76.1369 + ], + [ + -13.2009, + 76.8064 + ], + [ + -0.701322, + 76.9008 + ], + [ + 11.4357, + 76.4083 + ], + [ + 22.3333, + 75.3881 + ], + [ + 31.5874, + 73.94 + ], + [ + 39.2165, + 72.1681 + ], + [ + 45.4446, + 70.1586 + ], + [ + 50.5826, + 67.9567 + ], + [ + 54.7882, + 65.6468 + ], + [ + 58.3069, + 63.243 + ], + [ + 61.2895, + 60.7682 + ], + [ + 63.8512, + 58.2388 + ], + [ + 66.0787, + 55.6666 + ], + [ + 68.0379, + 53.0603 + ], + [ + 69.7796, + 50.4265 + ], + [ + 71.3431, + 47.7701 + ], + [ + 72.759, + 45.095 + ], + [ + 74.0518, + 42.4042 + ], + [ + 75.241, + 39.7002 + ], + [ + 76.3424, + 36.9848 + ], + [ + 77.3688, + 34.2597 + ], + [ + 78.3308, + 31.5262 + ], + [ + 79.2374, + 28.7855 + ], + [ + 80.0959, + 26.0385 + ], + [ + 80.9126, + 23.2861 + ], + [ + 81.6929, + 20.5289 + ], + [ + 82.4414, + 17.7678 + ], + [ + 83.1621, + 15.0032 + ], + [ + 83.8584, + 12.2357 + ], + [ + 84.5335, + 9.46581 + ], + [ + 85.1902, + 6.69397 + ], + [ + 85.8309, + 3.92062 + ], + [ + 86.4578, + 1.14614 + ], + [ + 87.0732, + -1.62907 + ], + [ + 87.6788, + -4.40469 + ], + [ + 88.2766, + -7.18036 + ], + [ + 88.8683, + -9.95579 + ], + [ + 89.4557, + -12.7307 + ], + [ + 90.0404, + -15.5047 + ], + [ + 90.6243, + -18.2777 + ], + [ + 91.2092, + -21.0493 + ], + [ + 91.7969, + -23.8194 + ], + [ + 92.3896, + -26.5877 + ], + [ + 92.9894, + -29.354 + ], + [ + 93.599, + -32.1181 + ], + [ + 94.2212, + -34.8798 + ], + [ + 94.8594, + -37.639 + ], + [ + 95.5175, + -40.3954 + ], + [ + 96.2002, + -43.1488 + ], + [ + 96.9133, + -45.8991 + ], + [ + 97.664, + -48.6459 + ], + [ + 98.4613, + -51.389 + ], + [ + 99.317, + -54.128 + ], + [ + 100.247, + -56.8624 + ], + [ + 101.271, + -59.5915 + ], + [ + 102.418, + -62.3144 + ], + [ + 103.729, + -65.0297 + ], + [ + 105.265, + -67.7354 + ], + [ + 107.119, + -70.4281 + ], + [ + 109.439, + -73.1025 + ], + [ + 112.484, + -75.7491 + ], + [ + 116.733, + -78.3498 + ], + [ + 123.157, + -80.8663 + ], + [ + 133.941, + -83.2055 + ], + [ + 154.033, + -85.1068 + ], + [ + -171.529, + -85.9077 + ], + [ + -171.655, + -85.7132 + ], + [ + -171.638, + -85.2721 + ], + [ + -171.7, + -84.8155 + ], + [ + -171.833, + -84.3588 + ], + [ + -171.796, + -83.9023 + ], + [ + -171.836, + -83.4528 + ], + [ + -171.867, + -82.989 + ], + [ + -171.896, + -82.5323 + ], + [ + -171.953, + -82.0823 + ], + [ + -171.945, + -81.619 + ], + [ + -172.007, + -81.1686 + ], + [ + -172.004, + -80.711 + ], + [ + -172.008, + -80.2503 + ], + [ + -172.025, + -79.7944 + ], + [ + -172.047, + -79.3407 + ], + [ + -172.072, + -78.8864 + ], + [ + -172.055, + -78.4219 + ], + [ + -172.066, + -77.9651 + ], + [ + -172.076, + -77.5083 + ], + [ + -172.101, + -77.056 + ], + [ + -172.094, + -76.5946 + ], + [ + -172.103, + -76.1378 + ], + [ + -172.11, + -75.6809 + ], + [ + -172.131, + -75.2314 + ], + [ + -172.125, + -74.7671 + ], + [ + -172.139, + -74.3156 + ], + [ + -172.171, + -73.8539 + ], + [ + -172.143, + -73.3962 + ], + [ + -172.178, + -72.9409 + ], + [ + -172.154, + -72.4823 + ], + [ + 178.512, + -72.2699 + ], + [ + 169.601, + -71.6298 + ], + [ + 161.438, + -70.6039 + ], + [ + 154.185, + -69.2493 + ], + [ + 147.865, + -67.6256 + ], + [ + 142.413, + -65.7864 + ], + [ + 137.724, + -63.7771 + ], + [ + 133.682, + -61.6335 + ], + [ + 130.183, + -59.3836 + ], + [ + 127.133, + -57.0489 + ], + [ + 124.456, + -54.646 + ], + [ + 122.088, + -52.1878 + ], + [ + 119.979, + -49.6842 + ], + [ + 118.087, + -47.1429 + ], + [ + 116.377, + -44.5703 + ], + [ + 114.822, + -41.9711 + ], + [ + 113.4, + -39.3494 + ], + [ + 112.091, + -36.7084 + ], + [ + 110.881, + -34.0508 + ], + [ + 109.756, + -31.379 + ], + [ + 108.706, + -28.6946 + ], + [ + 107.722, + -25.9994 + ], + [ + 106.795, + -23.2948 + ], + [ + 105.92, + -20.5819 + ], + [ + 105.09, + -17.8617 + ], + [ + 104.301, + -15.1353 + ], + [ + 103.548, + -12.4034 + ], + [ + 102.829, + -9.66677 + ], + [ + 102.138, + -6.92609 + ], + [ + 101.475, + -4.18197 + ], + [ + 100.836, + -1.43498 + ], + [ + 100.219, + 1.31439 + ], + [ + 99.6226, + 4.06565 + ], + [ + 99.0446, + 6.81838 + ], + [ + 98.4837, + 9.57217 + ], + [ + 97.9385, + 12.3267 + ], + [ + 97.4078, + 15.0815 + ], + [ + 96.8906, + 17.8364 + ], + [ + 96.3858, + 20.591 + ], + [ + 95.8928, + 23.3452 + ], + [ + 95.4106, + 26.0986 + ], + [ + 94.9388, + 28.8511 + ], + [ + 94.4767, + 31.6025 + ], + [ + 94.0238, + 34.3526 + ], + [ + 93.5799, + 37.1013 + ], + [ + 93.1446, + 39.8485 + ], + [ + 92.7178, + 42.5941 + ], + [ + 92.2993, + 45.338 + ], + [ + 91.8895, + 48.0803 + ], + [ + 91.4884, + 50.8208 + ], + [ + 91.0968, + 53.5596 + ], + [ + 90.7153, + 56.2967 + ], + [ + 90.3455, + 59.0322 + ], + [ + 89.9892, + 61.766 + ], + [ + 89.6494, + 64.4984 + ], + [ + 89.3309, + 67.2292 + ], + [ + 89.0408, + 69.9587 + ], + [ + 88.7913, + 72.6869 + ], + [ + 88.6026, + 75.4375 + ], + [ + 88.5197, + 78.1633 + ], + [ + 88.6321, + 80.8877 + ], + [ + 89.1876, + 83.6101 + ], + [ + 91.1695, + 86.3275 + ], + [ + 106.237, + 89.0015 + ], + [ + -106.531, + 88.1678 + ], + [ + -100.186, + 85.4649 + ], + [ + -98.8889, + 82.7452 + ], + [ + -98.5216, + 80.022 + ], + [ + -98.4868, + 77.2971 + ], + [ + -98.6093, + 74.5709 + ], + [ + -98.8191, + 71.8435 + ], + [ + -99.0829, + 69.1148 + ], + [ + -99.3827, + 66.3848 + ], + [ + -99.7085, + 63.6535 + ], + [ + -100.054, + 60.9206 + ], + [ + -100.415, + 58.1862 + ], + [ + -100.788, + 55.4502 + ], + [ + -101.173, + 52.7125 + ], + [ + -101.567, + 49.973 + ], + [ + -101.971, + 47.2319 + ], + [ + -102.384, + 44.489 + ], + [ + -102.805, + 41.7444 + ], + [ + -103.234, + 38.9982 + ], + [ + -103.672, + 36.2505 + ], + [ + -104.119, + 33.5012 + ], + [ + -104.575, + 30.7505 + ], + [ + -105.04, + 27.9987 + ], + [ + -105.515, + 25.2457 + ], + [ + -106, + 22.4919 + ], + [ + -106.497, + 19.7375 + ], + [ + -107.005, + 16.9826 + ], + [ + -107.527, + 14.2275 + ], + [ + -108.062, + 11.4726 + ], + [ + -108.612, + 8.71809 + ], + [ + -109.178, + 5.96442 + ], + [ + -109.761, + 3.21194 + ], + [ + -110.364, + 0.461038 + ], + [ + -110.987, + -2.28782 + ], + [ + -111.634, + -5.03415 + ], + [ + -112.305, + -7.77744 + ], + [ + -113.004, + -10.5171 + ], + [ + -113.734, + -13.2525 + ], + [ + -114.497, + -15.983 + ], + [ + -115.299, + -18.7077 + ], + [ + -116.142, + -21.4259 + ], + [ + -117.033, + -24.1365 + ], + [ + -117.977, + -26.8385 + ], + [ + -118.981, + -29.5307 + ], + [ + -120.053, + -32.2115 + ], + [ + -121.203, + -34.8793 + ], + [ + -122.442, + -37.5321 + ], + [ + -123.784, + -40.1675 + ], + [ + -125.245, + -42.7827 + ], + [ + -126.846, + -45.3743 + ], + [ + -128.609, + -47.9379 + ], + [ + -130.565, + -50.4683 + ], + [ + -132.75, + -52.9588 + ], + [ + -135.208, + -55.4011 + ], + [ + -137.994, + -57.7844 + ], + [ + -141.175, + -60.0948 + ], + [ + -144.832, + -62.3143 + ], + [ + -149.064, + -64.4196 + ], + [ + -153.979, + -66.3802 + ], + [ + -159.691, + -68.1576 + ], + [ + -166.298, + -69.7037 + ], + [ + -173.843, + -70.9625 + ], + [ + 177.735, + -71.8738 + ], + [ + 168.65, + -72.3841 + ], + [ + 162.681, + -72.4829 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/point-on-antimeridian.json b/examples/antimeridian/examples/point-on-antimeridian.json new file mode 100644 index 00000000..4312c59a --- /dev/null +++ b/examples/antimeridian/examples/point-on-antimeridian.json @@ -0,0 +1,27 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + -176.78115373, + -18.58572933 + ], + [ + -173.71026732, + -14.73835555 + ], + [ + -176.88248512, + -12.82925277 + ], + [ + 180.0, + -15.7250416 + ], + [ + -176.78115373, + -18.58572933 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/simple-with-ccw-hole.json b/examples/antimeridian/examples/simple-with-ccw-hole.json new file mode 100644 index 00000000..9044c7e2 --- /dev/null +++ b/examples/antimeridian/examples/simple-with-ccw-hole.json @@ -0,0 +1,49 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 100, + 40 + ], + [ + 100, + 50 + ], + [ + 90, + 50 + ], + [ + 90, + 40 + ], + [ + 100, + 40 + ] + ], + [ + [ + 92, + 42 + ], + [ + 98, + 42 + ], + [ + 98, + 48 + ], + [ + 92, + 48 + ], + [ + 92, + 42 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/simple.json b/examples/antimeridian/examples/simple.json new file mode 100644 index 00000000..a2b53d6e --- /dev/null +++ b/examples/antimeridian/examples/simple.json @@ -0,0 +1,27 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 100, + 40 + ], + [ + 100, + 50 + ], + [ + 90, + 50 + ], + [ + 90, + 40 + ], + [ + 100, + 40 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/south-pole.json b/examples/antimeridian/examples/south-pole.json new file mode 100644 index 00000000..0e6106e2 --- /dev/null +++ b/examples/antimeridian/examples/south-pole.json @@ -0,0 +1,27 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 45, + -80 + ], + [ + -45, + -80 + ], + [ + -135, + -80 + ], + [ + 135, + -80 + ], + [ + 45, + -80 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/split.json b/examples/antimeridian/examples/split.json new file mode 100644 index 00000000..71366405 --- /dev/null +++ b/examples/antimeridian/examples/split.json @@ -0,0 +1,27 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 170, + 40 + ], + [ + -170, + 40 + ], + [ + -170, + 50 + ], + [ + 170, + 50 + ], + [ + 170, + 40 + ] + ] + ] +} diff --git a/examples/antimeridian/examples/two-holes.json b/examples/antimeridian/examples/two-holes.json new file mode 100644 index 00000000..9b7ed389 --- /dev/null +++ b/examples/antimeridian/examples/two-holes.json @@ -0,0 +1,71 @@ +{ + "type": "Polygon", + "coordinates": [ + [ + [ + 160, + 40 + ], + [ + -170, + 40 + ], + [ + -170, + 60 + ], + [ + 160, + 60 + ], + [ + 160, + 40 + ] + ], + [ + [ + 175, + 45 + ], + [ + 175, + 55 + ], + [ + -175, + 55 + ], + [ + -175, + 45 + ], + [ + 175, + 45 + ] + ], + [ + [ + 165, + 45 + ], + [ + 165, + 55 + ], + [ + 170, + 55 + ], + [ + 170, + 45 + ], + [ + 165, + 45 + ] + ] + ] +} diff --git a/examples/antimeridian/test.php b/examples/antimeridian/test.php new file mode 100644 index 00000000..dd96d918 --- /dev/null +++ b/examples/antimeridian/test.php @@ -0,0 +1,348 @@ + 'Feature', + 'geometry' => [ + "type" => "MultiPolygon", + "coordinates" => [ + [ + [[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]], // First polygon + ], + [ + [[-10, -10], [-5, -10], [-5, -5], [-10, -5], [-10, -10]], // Second polygon + ] + ] + ] + ]; + + $geojsonS5 = [ + 'type' => 'Feature', + 'properties' => [], + 'geometry' => [ + 'type' => 'Polygon', + 'coordinates'=> [ + [ + [ -56.1922950, 53.5909730], + [ -54.1918070, 55.1427400], + [ -52.0226300, 56.6554100], + [ -49.6661380, 58.1234550], + [ -47.1019200, 59.5404240], + [ -44.3087430, 60.8987350], + [ -41.2659260, 62.1902240], + [ -37.9514050, 63.4047800], + [ -34.3478240, 64.5318300], + [ -30.4416640, 65.5591660], + [ -26.2261900, 66.4734950], + [ -21.7077220, 67.2615900], + [ -16.9058110, 67.9096600], + [ -11.8584610, 68.4052900], + [ -6.6206280, 68.7379800], + [ -1.2637311, 68.8997400], + [ 4.1296535, 68.8868800], + [ 9.4738180, 68.6995700], + [ 14.6873660, 68.3424800], + [ 19.7013740, 67.8236850], + [ 24.4630500, 67.1545000], + [ 28.9372900, 66.3471100], + [ 33.1068840, 65.4155960], + [ 36.9680000, 64.3732500], + [ 40.5275570, 63.2330600], + [ 43.8003650, 62.0069300], + [ 46.8054580, 60.7056920], + [ 49.5631300, 59.3386570], + [ 52.0956760, 57.9144400], + [ 54.4237600, 56.4401660], + [ 56.5675620, 54.9222600], + [ 58.5453070, 53.3658640], + [ 60.3738000, 51.7759060], + [ 62.0682100, 50.1562160], + [ 63.6419940, 48.5102540], + [ 65.1071850, 46.8410030], + [ 66.4752350, 45.1513560], + [ 67.7547800, 43.4431570], + [ 68.9545700, 41.7184500], + [ 70.0821600, 39.9789470], + [ 71.1442640, 38.2262200], + [ 72.1467700, 36.4616700], + [ 73.0946400, 34.6861100], + [ 73.9928300, 32.9007300], + [ 74.8456500, 31.1067370], + [ 75.6564400, 29.3045650], + [ 76.4287950, 27.4950870], + [ 77.1656800, 25.6790390], + [ 77.8697000, 23.8569180], + [ 78.5431400, 22.0292030], + [ 79.1884300, 20.1965560], + [ 79.8074700, 18.3594590], + [ 80.4018200, 16.5181000], + [ 80.9731750, 14.6729630], + [ 81.5231900, 12.8245930], + [ 82.0527500, 10.9729960], + [ 82.5634300, 9.1187610], + [ 83.0560900, 7.2620287], + [ 83.5316540, 5.4031250], + [ 83.9910200, 3.5422018], + [ 84.4350100, 1.6796422], + [ 84.8643340, -0.1844159], + [ 85.2795800, -2.0496845], + [ 85.6812500, -3.9160523], + [ 86.0699900, -5.7832017], + [ 86.4461700, -7.6510277], + [ 86.8100500, -9.5193820], + [ 87.1622700, -11.3879880], + [ 87.5026800, -13.2568150], + [ 87.8316650, -15.1256430], + [ 88.1495900, -16.9942280], + [ 88.4561900, -18.8626200], + [ 88.7517400, -20.7305770], + [ 89.0361700, -22.5979270], + [ 89.3095400, -24.4646590], + [ 89.5713650, -26.3305990], + [ 89.8217600, -28.1956560], + [ 90.0603900, -30.0596310], + [ 90.2867800, -31.9225440], + [ 90.5001900, -33.7842830], + [ 90.7006600, -35.6446500], + [ 90.8867600, -37.5036740], + [ 91.0586700, -39.3611200], + [ 91.2143100, -41.2171200], + [ 91.3531300, -43.0713400], + [ 91.4731000, -44.9238430], + [ 91.5730900, -46.7744560], + [ 91.6503200, -48.6231600], + [ 91.7029650, -50.4697270], + [ 91.7274860, -52.3141980], + [ 91.7201700, -54.1563500], + [ 91.6771400, -55.9959900], + [ 91.5920700, -57.8329120], + [ 91.4582000, -59.6670300], + [ 91.2669700, -61.4979320], + [ 91.0069000, -63.3252700], + [ 90.6639900, -65.1485100], + [ 90.2185600, -66.9671700], + [ 89.6463500, -68.7802300], + [ 88.9113900, -70.5865500], + [ 87.9666100, -72.3843840], + [ 86.7414900, -74.1713300], + [ 85.1324900, -75.9436400], + [ 82.9810100, -77.6953300], + [ 80.0279900, -79.4167250], + [ 75.8415100, -81.0908200], + [ 69.6492540, -82.6854200], + [ 60.0685120, -84.1360900], + [ 44.8626900, -85.3077200], + [ 41.6165120, -85.4600800], + [ 44.6215360, -85.7326500], + [ 79.8345700, -87.0896100], + [ 122.8751300, -86.7606050], + [ 139.1886000, -85.9911400], + [ 150.5562400, -84.9400560], + [ 155.6530300, -84.2117500], + [ 160.3266000, -83.3234000], + [ 162.9363600, -82.7068600], + [ 165.7203500, -81.9316200], + [ 167.4692500, -81.3732000], + [ 169.5095000, -80.6432800], + [ 170.8918300, -80.0964500], + [ 172.6115000, -79.3527100], + [ 173.8473500, -78.7723100], + [ 175.4710800, -77.9479450], + [ 176.7008700, -77.2738950], + [ 178.4008800, -76.2641600], + [ 179.7544400, -75.3864000], + [-178.2763800, -73.9663240], + [-176.6203500, -72.6050700], + [-174.0369700, -70.0530400], + [-173.0306900, -68.8665000], + [-173.0306900, -68.8665000], + [-178.2936400, -68.5645200], + [ 176.6216700, -68.0989800], + [ 171.7739400, -67.4797500], + [ 167.2030000, -66.7194300], + [ 162.9322700, -65.8308900], + [ 158.9693000, -64.8278800], + [ 155.3103500, -63.7233470], + [ 151.9431300, -62.5294840], + [ 148.8496100, -61.2573620], + [ 146.0095800, -59.9168930], + [ 143.4017500, -58.5166700], + [ 141.0044100, -57.0642900], + [ 138.7977100, -55.5662570], + [ 136.7627900, -54.0282670], + [ 134.8822300, -52.4551000], + [ 133.1403800, -50.8509750], + [ 131.5229800, -49.2196120], + [ 130.0179700, -47.5639760], + [ 128.6137000, -45.8867870], + [ 127.3007000, -44.1904600], + [ 126.0696640, -42.4768640], + [ 124.9134750, -40.7479130], + [ 123.8248700, -39.0050960], + [ 122.7974800, -37.2498440], + [ 121.8261600, -35.4832800], + [ 120.9060800, -33.7064500], + [ 120.0326460, -31.9204440], + [ 119.2020100, -30.1260300], + [ 118.4109800, -28.3239440], + [ 117.6563640, -26.5148330], + [ 116.9351600, -24.6993940], + [ 116.2450700, -22.8781800], + [ 115.5837860, -21.0516760], + [ 114.9493260, -19.2203400], + [ 114.3397750, -17.3846020], + [ 113.7535400, -15.5448520], + [ 113.1890700, -13.7014930], + [ 112.6450700, -11.8548070], + [ 112.1201600, -10.0051880], + [ 111.6134950, -8.1528010], + [ 111.1238400, -6.2980150], + [ 110.6502000, -4.4410880], + [ 110.1920200, -2.5822449], + [ 109.7483500, -0.7217003], + [ 109.3187400, 1.1404055], + [ 108.9022500, 3.0037248], + [ 108.4985900, 4.8681264], + [ 108.1072000, 6.7334810], + [ 107.7275700, 8.5995655], + [ 107.3594360, 10.4662640], + [ 107.0025250, 12.3333650], + [ 106.6562900, 14.2007475], + [ 106.3207860, 16.0682740], + [ 105.9959100, 17.9357870], + [ 105.6810000, 19.8032600], + [ 105.3765200, 21.6705110], + [ 105.0822600, 23.5373330], + [ 104.7980100, 25.4037670], + [ 104.5246050, 27.2696150], + [ 104.2615050, 29.1349850], + [ 104.0090100, 30.9995400], + [ 103.7677300, 32.8632900], + [ 103.5379400, 34.7261660], + [ 103.3200200, 36.5881270], + [ 103.1147500, 38.4490800], + [ 102.9228740, 40.3089200], + [ 102.7453160, 42.1676560], + [ 102.5827300, 44.0251600], + [ 102.4371800, 45.8814000], + [ 102.3098700, 47.7363360], + [ 102.2023700, 49.5899050], + [ 102.1175000, 51.4419100], + [ 102.0579450, 53.2924350], + [ 102.0273200, 55.1412960], + [ 102.0293900, 56.9884100], + [ 102.0700900, 58.8336500], + [ 102.1555300, 60.6768040], + [ 102.2943700, 62.5176660], + [ 102.4973300, 64.3560100], + [ 102.7790500, 66.1913760], + [ 103.1581600, 68.0232800], + [ 103.6610950, 69.8509750], + [ 104.3231660, 71.6734850], + [ 105.1963000, 73.4893340], + [ 106.3566100, 75.2962500], + [ 107.9219700, 77.0907500], + [ 110.0833200, 78.8670500], + [ 113.1659400, 80.6150500], + [ 117.7637100, 82.3157500], + [ 125.0423600, 83.9293700], + [ 137.3609200, 85.3644300], + [ 158.6695600, 86.4004060], + [-171.0506900, 86.6447000], + [-144.4548300, 85.9513600], + [-128.1873300, 84.6745900], + [-118.8037950, 83.1321640], + [-113.0730600, 81.4666500], + [-109.3359800, 79.7376100], + [-106.7693700, 77.9721500], + [-104.9379400, 76.1839400], + [-103.5947650, 74.3805900], + [-102.5909100, 72.5664800], + [-101.8325040, 70.7445400], + [-101.2566800, 68.9164600], + [-100.8209460, 67.0835400], + [-100.4946100, 65.2466350], + [-100.2557600, 63.4063720], + [-100.0874940, 61.5631800], + [-100.0653300, 61.2558300], + [ -99.4083800, 61.2076230], + [ -94.7393800, 60.7733460], + [ -90.5492200, 60.2530500], + [ -88.2155900, 59.9159360], + [ -85.7231140, 59.5255850], + [ -84.1564500, 59.2677800], + [ -82.3316040, 58.9587360], + [ -81.1003650, 58.7463000], + [ -79.5803100, 58.4807900], + [ -78.5003300, 58.2903500], + [ -77.1036600, 58.0421140], + [ -76.0666960, 57.8564700], + [ -74.6671900, 57.6041760], + [ -73.5826950, 57.4073000], + [ -72.0526200, 57.1272430], + [ -70.8100500, 56.8973050], + [ -68.9634550, 56.5495000], + [ -67.3740200, 56.2416200], + [ -64.8388400, 55.7263900], + [ -62.4570240, 55.2052700], + [ -58.1353260, 54.1355000], + [ -56.1922950, 53.5909730], + [ -56.1922950, 53.5909730] + ] + ] + ] + ]; + + $geojsons = [ + 'examples/almost-180.json', + 'examples/both-poles.json', + 'examples/complex-split.json', + 'examples/crossing-latitude.json', + 'examples/cw-only.json', + 'examples/cw-split.json', + 'examples/extra-crossing.json', + 'examples/force-north-pole.json', + 'examples/great-circle.json', + 'examples/issues-124.json', + 'examples/issues-81.json', + 'examples/latitude-band.json', + 'examples/line.json', + 'examples/multi-line.json', + 'examples/multi-no-antimeridian.json', + 'examples/multi-split.json', + 'examples/north-pole.json', + 'examples/one-ccw-hole.json', + 'examples/one-hole.json', + 'examples/over-180.json', + 'examples/overlap.json', + 'examples/point-on-antimeridian.json', + 'examples/simple-with-ccw-hole.json', + 'examples/simple.json', + 'examples/south-pole.json', + 'examples/split.json', + 'examples/two-holes.json' + ]; + + $antimeridian = new AntiMeridian(); + + for ($i = 0, $ii = count($geojsons); $i < $ii; $i++) { + $json = file_get_contents($geojsons[$i]); + // Check if the file was read successfully + if ($json === false) { + throw new Exception('Error reading file ' . $geojsons[$i]); + } + $fixedGeoJSON = $antimeridian->fixGeoJSON(json_decode($json, true)); + $fixedGeoJSON['properties'] = [ + 'FileName'=> $geojsons[$i] + ]; + echo json_encode($fixedGeoJSON, true) . "\n\n"; + } + +} catch (Exception $e) { + echo 'Error: ' . $e->getMessage(); +} diff --git a/examples/items/dummyComplex.json b/examples/items/dummyComplex.json new file mode 100644 index 00000000..5c16bc53 --- /dev/null +++ b/examples/items/dummyComplex.json @@ -0,0 +1,67 @@ +{ + "id": "Dummy3", + "type": "Feature", + "properties": { + "datetime": "2024-06-21T16:27:00Z", + "description": "Dummy3" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 120, + -40 + ], + [ + -120, + -40 + ], + [ + -120, + -20 + ], + [ + 150, + -20 + ], + [ + 150, + 0 + ], + [ + -120, + 0 + ], + [ + -120, + 60 + ], + [ + 120, + 60 + ], + [ + 120, + 40 + ], + [ + -150, + 40 + ], + [ + -150, + 20 + ], + [ + 120, + 20 + ], + [ + 120, + -40 + ] + ] + ] + } +} \ No newline at end of file diff --git a/resto-database-model/01_tamn.sql b/resto-database-model/01_tamn.sql deleted file mode 100644 index 0db5dbbb..00000000 --- a/resto-database-model/01_tamn.sql +++ /dev/null @@ -1,529 +0,0 @@ - - --- Clean old functions -DROP FUNCTION IF EXISTS public.ST_DistanceToNorthPole(geometry); -DROP FUNCTION IF EXISTS public.ST_DistanceToSouthPole(geometry); - -DROP FUNCTION IF EXISTS public.ST_SplitNorthPole(geometry, integer); -DROP FUNCTION IF EXISTS public.ST_SplitNorthPole(geometry, integer, integer); -DROP FUNCTION IF EXISTS public.ST_SplitNorthPole(geometry, integer, integer, integer); - -DROP FUNCTION IF EXISTS public.ST_SplitSouthPole(geometry, integer); -DROP FUNCTION IF EXISTS public.ST_SplitSouthPole(geometry, integer, integer); -DROP FUNCTION IF EXISTS public.ST_SplitSouthPole(geometry, integer, integer, integer); - -DROP FUNCTION IF EXISTS public.ST_SplitAntimeridian(geometry); -DROP FUNCTION IF EXISTS public.ST_IntersectsAntimeridian(geometry); - -DROP FUNCTION IF EXISTS public.ST_SplitDateLine(geometry, integer); -DROP FUNCTION IF EXISTS public.ST_SplitDateLine(geometry, integer, integer); - --- --- Insert North_Pole_Azimuthal_Equidistant and South_Pole_Azimuthal_Equidistant special projections --- -INSERT INTO spatial_ref_sys (srid, auth_name, auth_srid, proj4text, srtext) VALUES ( 102016, 'ESRI', 102016, '+proj=aeqd +lat_0=90 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs ', 'PROJCS["North_Pole_Azimuthal_Equidistant",GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Azimuthal_Equidistant"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],PARAMETER["Latitude_Of_Origin",90],UNIT["Meter",1],AUTHORITY["EPSG","102016"]]') ON CONFLICT DO NOTHING; -INSERT INTO spatial_ref_sys (srid, auth_name, auth_srid, proj4text, srtext) VALUES ( 102019, 'ESRI', 102019, '+proj=aeqd +lat_0=-90 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs ', 'PROJCS["South_Pole_Azimuthal_Equidistant",GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Azimuthal_Equidistant"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],PARAMETER["Latitude_Of_Origin",-90],UNIT["Meter",1],AUTHORITY["EPSG","102019"]]') ON CONFLICT DO NOTHING; - --- --- Copyright (C) 2018 Jerome Gasperi --- --- SYNOPSYS: --- ST_DistanceToNorthPole(geom_in) --- --- DESCRIPTION: --- Returns distance in meters to South Pole - if error occurs returns -1 --- --- USAGE: --- SELECT ST_DistanceToNorthPole(geom_in geometry); --- -CREATE OR REPLACE FUNCTION ST_DistanceToNorthPole(geom_in geometry) - RETURNS numeric AS $$ -DECLARE - north_pole geography := ST_SetSrid(ST_MakePoint(0, 90), 4326)::geography; - text_var1 text := ''; - text_var2 text := ''; - text_var3 text := ''; -BEGIN - - -- [IMPORTANT] Computation is done in geographical coordinates - RETURN ST_Distance(geom_in::geography, north_pole, true); - - -- If any of above failed, revert to use original polygon - -- This prevents ingestion error, but may potentially lead to incorrect spatial query. - EXCEPTION WHEN OTHERS THEN - GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT, - text_var2 = PG_EXCEPTION_DETAIL, - text_var3 = PG_EXCEPTION_HINT; - raise WARNING 'ST_DistanceToNorthPole: exception occured: Msg: %, detail: %, hint: %', text_var1, text_var1, text_var3; - RETURN -1; - -END -$$ LANGUAGE 'plpgsql' IMMUTABLE; - --- --- Copyright (C) 2018 Jerome Gasperi --- --- SYNOPSYS: --- ST_DistanceToSouthPole(geom_in) --- --- DESCRIPTION: --- Returns distance in meters to South Pole - if error occurs returns -1 --- --- USAGE: --- SELECT ST_DistanceToSouthPole(geom_in geometry); --- -CREATE OR REPLACE FUNCTION ST_DistanceToSouthPole(geom_in geometry) - RETURNS numeric AS $$ -DECLARE - south_pole geography := ST_SetSrid(ST_MakePoint(0, -90), 4326)::geography; - text_var1 text := ''; - text_var2 text := ''; - text_var3 text := ''; -BEGIN - - -- [IMPORTANT] Computation is done in geographical coordinates - RETURN ST_Distance(geom_in::geography, south_pole, true); - - -- If any of above failed, revert to use original polygon - -- This prevents ingestion error, but may potentially lead to incorrect spatial query. - EXCEPTION WHEN OTHERS THEN - GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT, - text_var2 = PG_EXCEPTION_DETAIL, - text_var3 = PG_EXCEPTION_HINT; - raise WARNING 'ST_DistanceToSouthPole: exception occured: Msg: %, detail: %, hint: %', text_var1, text_var1, text_var3; - RETURN -1; - -END -$$ LANGUAGE 'plpgsql' IMMUTABLE; - --- --- Copyright (C) 2019 Jerome Gasperi --- --- SYNOPSYS: --- ST_IntersectsAntimeridian(polygon) --- --- DESCRIPTION: --- This function returns TRUE if input geometry intersects the antimeridian, FALSE otherwise --- --- USAGE: --- SELECT ST_IntersectsAntimeridian(geom_in geometry); --- -CREATE OR REPLACE FUNCTION ST_IntersectsAntimeridian(geom_in geometry) - RETURNS INTEGER AS $$ -DECLARE - part RECORD; - blade geography := ST_SetSrid(ST_MakeLine(ARRAY[ST_MakePoint(180, -90), ST_MakePoint(180, 0), ST_MakePoint(180, 90)]), 4326)::geography; - text_var1 text := ''; - text_var2 text := ''; - text_var3 text := ''; -BEGIN - - -- Detect MultiPolygon - IF GeometryType(geom_in) = 'MULTIPOLYGON' THEN - - FOR part IN SELECT (ST_Dump(geom_in)).geom LOOP - - -- AntiMeridian is crossed for sure - IF ST_IsPolygonCW(part.geom) AND abs(ST_XMax(part.geom) - ST_XMin(part.geom)) > 360 THEN - --RAISE NOTICE 'Polygon is CW and abs > 360'; - RETURN 1; - -- AntiMeridian is perhaps crossed - ELSIF ST_Intersects(part.geom::geography, blade) THEN - --RAISE NOTICE 'Polygon intersects blade'; - RETURN 1; - END IF; - - END LOOP; - - ELSE - - -- AntiMeridian is crossed - IF ST_IsPolygonCW(geom_in) AND abs(ST_XMax(geom_in) - ST_XMin(geom_in)) > 360 THEN - --RAISE NOTICE 'Polygon is CW and abs > 360'; - RETURN 1; - ELSIF ST_Intersects(geom_in::geography, blade) THEN - --RAISE NOTICE 'Polygon intersects blade'; - RETURN 1; - END IF; - - END IF; - - RETURN 0; - - -- If any of above failed, raise error and return -1 - EXCEPTION WHEN OTHERS THEN - GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT, - text_var2 = PG_EXCEPTION_DETAIL, - text_var3 = PG_EXCEPTION_HINT; - raise WARNING 'ST_IntersectsAntimeridian: exception occured: Msg: %, detail: %, hint: %', text_var1, text_var1, text_var3; - RETURN -1; - -END -$$ LANGUAGE 'plpgsql' IMMUTABLE; - --- --- Copyright (C) 2018 Jerome Gasperi --- --- SYNOPSYS: --- ST_SplitSouthPole(polygon, radius, pole_distance, trans_buffer) --- --- DESCRIPTION: --- Splits input geometry that crosses the South Pole. If radius is specified, then remove a circle of radius meters around the South Pole from input geometry --- --- [WARNING] Only work for EPSG:4326 geometry that CROSS the South Pole with latitude length STRICTLY LOWER than 90 degrees --- --- USAGE: --- SELECT ST_SplitSouthPole(geom_in geometry); --- or --- SELECT ST_SplitSouthPole(geom_in geometry, radius integer, pole_distance integer, trans_buffer); --- --- -CREATE OR REPLACE FUNCTION ST_SplitSouthPole(geom_in geometry, radius integer default NULL, pole_distance integer DEFAULT 500000, trans_buffer integer DEFAULT -1) - RETURNS geometry AS $$ -DECLARE - pole_split geometry; - pole_geom geometry; - pole_blade geometry; - epsg_code integer; - distance_to_north numeric; - force_epsg3031 boolean := FALSE; - text_var1 text := ''; - text_var2 text := ''; - text_var3 text := ''; -BEGIN - - -- Convert geometry to WGS 84 / Arctic Polar Stereographic - if force_epsg3031 IS TRUE OR ST_Ymax(geom_in) < -60 THEN - --RAISE NOTICE 'Using EPSG:3031 - latitudes are lower than 60 degrees'; - epsg_code := 3031; - -- (0, -89)deg -> (0, -90)deg -> (-180, 89.9)deg - pole_blade := ST_SetSrid(ST_MakeLine(ARRAY[ST_MakePoint(0, 108655.09), ST_MakePoint(0, 0), ST_MakePoint(0, -14077221718.36)]), epsg_code); - - -- Convert geometry to North Pole Azimuthal Equidistant - ELSE - --RAISE NOTICE 'Using EPSG:102019'; - epsg_code := 102019; - -- (0, 90)deg -> (0, -90)deg -> (180, 90)deg - pole_blade := ST_SetSrid(ST_MakeLine(ARRAY[ST_MakePoint(0, 20003931.45862726), ST_MakePoint(0, 0), ST_MakePoint(0, -20003931.45862726)]), epsg_code); - END IF; - - -- - -- Always apply a Buffer(0) to make output geometry valid - -- - -- [NOTE] Densify over pole to avoid issue in ST_Difference - distance_to_north := ST_DistanceTonorthPole(geom_in); - IF distance_to_north > -1 AND distance_to_north <= pole_distance THEN - --RAISE NOTICE 'ST_SplitSouthPole : Segmentize and simplify topology'; - pole_geom := ST_Buffer(ST_Transform(ST_Segmentize(geom_in::geography, 50000)::geometry, epsg_code), 0); - ELSE - pole_geom := ST_Buffer(ST_Transform(geom_in, epsg_code), 0); - END IF; - - -- Convert polar geometry to epsg_code and optionaly remove a radius hole centered on North Pole. - IF radius IS NOT NULL THEN - pole_split:= ST_Difference(pole_geom, ST_Buffer(ST_SetSrid(ST_MakePoint(0, 0), epsg_code), radius)); - ELSE - pole_split := pole_geom; - END IF; - - -- Split polygon to avoid -180/180 crossing issue. - -- Note: applying negative buffer ensure valid multipolygons that don't share a common edge - IF distance_to_north > -1 AND distance_to_north <= pole_distance THEN - pole_split := ST_SimplifyPreserveTopology(ST_Buffer(ST_Transform(ST_Buffer(ST_Split(pole_split, pole_blade), trans_buffer), 4326), 0), 0.01); - ELSE - pole_split := ST_Buffer(ST_Transform(ST_Buffer(ST_Split(pole_split, pole_blade), trans_buffer), 4326), 0); - END IF; - - RETURN pole_split; - - -- If any of above failed, revert to use original polygon - -- This prevents ingestion error, but may potentially lead to incorrect spatial query. - EXCEPTION WHEN OTHERS THEN - GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT, - text_var2 = PG_EXCEPTION_DETAIL, - text_var3 = PG_EXCEPTION_HINT; - raise WARNING 'ST_SplitSouthPole: exception occured: Msg: %, detail: %, hint: %', text_var1, text_var1, text_var3; - RETURN geom_in; - -END -$$ LANGUAGE 'plpgsql' IMMUTABLE; - - --- --- Copyright (C) 2018 Jerome Gasperi --- --- SYNOPSYS: --- ST_SplitNorthPole(polygon, radius integer default NULL, pole_distance integer DEFAULT 500000, trans_buffer DEFAULT -1) --- --- DESCRIPTION: --- Splits input geometry that crosses the North Pole. If radius is specified, then remove a circle of radius meters around the North Pole from input geometry --- --- --- [WARNING] Only work for EPSG:4326 geometry that CROSS the North Pole with latitude length STRICTLY LOWER than 90 degrees --- --- USAGE: --- SELECT ST_SplitNorthPole(geom_in geometry); --- or --- SELECT ST_SplitNorthPole(geom_in geometry, radius integer, pole_distance integer, trans_buffer integer); --- -CREATE OR REPLACE FUNCTION ST_SplitNorthPole(geom_in geometry, radius integer default NULL, pole_distance integer DEFAULT 500000, trans_buffer integer DEFAULT -1) - RETURNS geometry AS $$ -DECLARE - pole_geom geometry; - pole_blade geometry; - distance_to_south numeric; - epsg_code integer; - force_epsg3413 boolean := FALSE; - text_var1 text := ''; - text_var2 text := ''; - text_var3 text := ''; -BEGIN - - -- Latitudes are above 60 degrees => input geometry is converted to WGS 84 / Antarctic Polar Stereographic - IF force_epsg3413 IS TRUE OR ST_Ymin(geom_in) > 60 THEN - --RAISE NOTICE 'Using EPSG:3413 - latitudes are greater than 60 degrees'; - epsg_code := 3413; - -- (0, 90)deg -> (-180, -89.9)deg - pole_blade := ST_SetSrid(ST_MakeLine(ARRAY[ST_MakePoint(-2353926.81, 2345724.36), ST_MakePoint(0, 0), ST_MakePoint(0, 0), ST_MakePoint(-2349829.16, 2349829.16)]), epsg_code); - - -- Latitudes are below 60 degrees => input geometry is converted to North Pole Azimuthal Equidistant - ELSE - --RAISE NOTICE 'Using EPSG:102016'; - epsg_code := 102016; - -- (-180, 90)deg -> (0, 90)deg -> (-180, -89.9)deg - --pole_blade := ST_SetSrid(ST_MakeLine(ARRAY[ST_MakePoint(2.4497750631170925E-9, 2.0003931458627265E7), ST_MakePoint(1.2248875315585463E-9, 1.0001965729313632E7), ST_MakePoint(0, 0), ST_MakePoint(0, 0), ST_MakePoint(0, -10001965.72931363), ST_MakePoint(0, -20003931.45862726)]), epsg_code); - pole_blade := ST_SetSrid(ST_MakeLine(ARRAY[ST_MakePoint(0, 20003931.458627265), ST_MakePoint(0, 10001965.729313632), ST_MakePoint(0, 0), ST_MakePoint(0, 0), ST_MakePoint(0, -10001965.72931363), ST_MakePoint(0, -20003931.45862726)]), epsg_code); - END IF; - - -- - -- Always apply a Buffer(0) to make output geometry valid - -- - -- [NOTE] Densify over pole to avoid issue in ST_Difference - -- - distance_to_south := ST_DistanceToSouthPole(geom_in); - IF distance_to_south > -1 AND distance_to_south <= pole_distance THEN - --RAISE NOTICE 'ST_SplitNorthPole : Segmentize'; - pole_geom := ST_Buffer(ST_Transform(ST_Segmentize(geom_in::geography, 50000)::geometry, epsg_code), 0.0); - ELSE - pole_geom := ST_Buffer(ST_Transform(geom_in, epsg_code), 0.0); - END IF; - - -- Convert polar geometry to epsg_code and optionaly remove a radius hole centered on North Pole. - IF radius IS NOT NULL THEN - pole_geom:= ST_Difference(pole_geom, ST_Buffer(ST_SetSrid(ST_MakePoint(0, 0), epsg_code), radius)); - END IF; - - -- Split polygon to avoid -180/180 crossing issue. - -- Note: applying negative buffer ensure valid multipolygons that don't share a common edge - IF distance_to_south > -1 AND distance_to_south <= pole_distance THEN - --RAISE NOTICE 'ST_SplitNorthPole : Simplify topology'; - pole_geom := ST_SimplifyPreserveTopology(ST_Buffer(ST_Transform(ST_Buffer( ST_ForcePolygonCCW(ST_Split(pole_geom, pole_blade)) , trans_buffer), 4326), 0.0), 0.01); - ELSE - pole_geom := ST_Buffer(ST_Transform(ST_Buffer(ST_Split(pole_geom, pole_blade), trans_buffer), 4326), 0.0); - END IF; - - RETURN pole_geom; - - -- If any of above failed, revert to use original polygon - -- This prevents ingestion error, but may potentially lead to incorrect spatial query. - EXCEPTION WHEN OTHERS THEN - GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT, - text_var2 = PG_EXCEPTION_DETAIL, - text_var3 = PG_EXCEPTION_HINT; - raise WARNING 'ST_SplitNorthPole: exception occured: Msg: %, detail: %, hint: %', text_var1, text_var1, text_var3; - RETURN geom_in; - -END -$$ LANGUAGE 'plpgsql' IMMUTABLE; - --- --- Copyright (C) 2019 Jerome Gasperi --- --- SYNOPSYS: --- ST_SplitAntimeridian(polygon) --- --- DESCRIPTION: --- Splits the input polygon geometry against the -180/180 date line --- --- [WARNING] Only work for EPSG:4326 geometry that DO NOT CROSS the North Pole or the South Pole --- --- USAGE: --- SELECT ST_SplitAntimeridian(geom_in geometry); --- -CREATE OR REPLACE FUNCTION ST_SplitAntimeridian(geom_in geometry) - RETURNS geometry AS $$ -DECLARE - geom_out geometry; - is_valid boolean; - text_var1 text := ''; - text_var2 text := ''; - text_var3 text := ''; -BEGIN - - -- Shift polygon to 0-360 and cut at 180 - geom_out := ST_Buffer(ST_WrapX(ST_ShiftLongitude(geom_in), 180, -360), 0); - - -- See case test id=S2A_OPER_PRD_MSIL1C_PDMC_20160720T163945_R116_V20160714T235631_20160714T235631 - -- If output geometry still crosses antimeridian - split it again - -- This case arises if input geometry longitude is outside -180/180 bounds - IF ST_IntersectsAntimeridian(geom_out) = 1 THEN - geom_out := ST_Buffer(ST_WrapX(ST_ShiftLongitude(geom_out), 180, -360), 0); - END IF; - - RETURN geom_out; - - -- If any of above failed, revert to use original polygon - -- This prevents ingestion error, but may potentially lead to incorrect spatial query. - EXCEPTION WHEN OTHERS THEN - GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT, - text_var2 = PG_EXCEPTION_DETAIL, - text_var3 = PG_EXCEPTION_HINT; - raise WARNING 'ST_SplitAntimeridian: exception occured: Msg: %, detail: %, hint: %', text_var1, text_var1, text_var3; - RETURN geom_in; - -END -$$ LANGUAGE 'plpgsql' IMMUTABLE; - --- --- Copyright (C) 2021 Jerome Gasperi --- --- SYNOPSYS: --- ST_CutPoles(polygon) --- --- DESCRIPTION: --- Cut both poles for input polygon geometry that crosses both poles --- --- USAGE: --- SELECT ST_CutPoles(geom_in geometry); --- -CREATE OR REPLACE FUNCTION ST_CutPoles(geom_in geometry) - RETURNS geometry AS $$ -DECLARE - m geometry_dump; - cutted geometry := ST_GeomFromText('GEOMETRYCOLLECTION EMPTY', 4326); - text_var1 text := ''; - text_var2 text := ''; - text_var3 text := ''; -BEGIN - - FOR m IN SELECT (ST_Dump(ST_Split(geom_in, ST_SetSrid(ST_MakeLine(ARRAY[ST_MakePoint(-180, 0), ST_MakePoint(0, 0), ST_MakePoint(180, 0)]), 4326)))).* LOOP - - -- Keep only geometries that are not only on the poles - IF ST_YMin(m.geom) < 60 AND ST_YMax(m.geom) > -60 THEN - cutted := ST_Union(cutted, m.geom); - END IF; - - END LOOP; - - RETURN cutted; - - -- If any of above failed, revert to use original polygon - -- This prevents ingestion error, but may potentially lead to incorrect spatial query. - EXCEPTION WHEN OTHERS THEN - GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT, - text_var2 = PG_EXCEPTION_DETAIL, - text_var3 = PG_EXCEPTION_HINT; - raise WARNING 'ST_CutPoles: exception occured: Msg: %, detail: %, hint: %', text_var1, text_var1, text_var3; - RETURN geom_in; - -END -$$ LANGUAGE 'plpgsql' IMMUTABLE; - - --- --- Copyright (C) 2018 Jerome Gasperi --- --- SYNOPSYS: --- ST_SplitDateLine(geom_in geometry, radius integer DEFAULT 50000, pole_distance integer DEFAULT 500000) --- --- DESCRIPTION: --- Splits input geometry if it intersects the antimeridian longitude (i.e. -180/180) --- It supports poles crossing (i.e. geometry that crosses the North Pole or the South Pole) --- --- Non crossing input geometry are returned without modification --- --- [WARNING] Input geometry that crosses one of the pole MUST have a length in latitude strictly lower than 90 degrees --- --- USAGE: --- SELECT ST_SplitDateLine(geom_in, radius, pole_distance); --- -CREATE OR REPLACE FUNCTION ST_SplitDateLine(geom_in geometry, radius integer default 50000, pole_distance integer DEFAULT 500000) - RETURNS geometry AS $$ -DECLARE - np_distance numeric; - sp_distance numeric; - geo_type text; - text_var1 text := ''; - text_var2 text := ''; - text_var3 text := ''; -BEGIN - - -- Geometry must be a (Multi)Polygon - geo_type := GeometryType(geom_in); - - IF geo_type <> 'POLYGON' AND geo_type <> 'MULTIPOLYGON' THEN - - RAISE NOTICE 'ST_SplitDateLine: input geometry must be a POLYGON or a MULTIPOLYGON'; - RETURN geom_in; - - END IF; - - -- Compute distances to North and South poles - np_distance := ST_DistanceToNorthPole(geom_in); - sp_distance := ST_DistanceToSouthPole(geom_in); - - -- Poles crosses twice => split geometry at the equator - IF np_distance = 0 AND sp_distance = 0 THEN - geom_in := ST_CutPoles(geom_in); - -- (Re)compute distances to North and South poles - np_distance := ST_DistanceToNorthPole(geom_in); - sp_distance := ST_DistanceToSouthPole(geom_in); - END IF; - - -- Input geometry crosses North Pole - IF np_distance > -1 AND np_distance <= pole_distance THEN - - -- Input geometry is even closer to South Pole - IF sp_distance > -1 AND sp_distance < np_distance THEN - RETURN ST_SplitSouthPole(geom_in, radius, pole_distance); - ELSE - RETURN ST_SplitNorthPole(geom_in, radius, pole_distance); - END IF; - - END IF; - - -- Input geometry crosses South Pole - IF sp_distance > -1 AND sp_distance <= pole_distance THEN - - -- Input geometry is even closer to North Pole - IF np_distance > -1 AND np_distance < sp_distance THEN - RETURN ST_SplitNorthPole(geom_in, radius, pole_distance); - ELSE - RETURN ST_SplitSouthPole(geom_in, radius, pole_distance); - END IF; - - END IF; - - -- Input geometry crosses -180/180 but not the poles - IF ST_IntersectsAntimeridian(geom_in) = 1 THEN - RETURN ST_SplitAntimeridian(geom_in); - END IF; - - -- Non crossing geometry is not modified unless invalid - IF ST_isValid(geom_in, 0) IS FALSE THEN - RETURN ST_Buffer(ST_MakeValid(geom_in), 0); - END IF; - - RETURN geom_in; - - -- If any of above failed, revert to use original polygon - -- This prevents ingestion error, but may potentially lead to incorrect spatial query. - EXCEPTION WHEN OTHERS THEN - GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT, - text_var2 = PG_EXCEPTION_DETAIL, - text_var3 = PG_EXCEPTION_HINT; - raise WARNING 'ST_SplitDateLine: exception occured: Msg: %, detail: %, hint: %', text_var1, text_var1, text_var3; - RETURN geom_in; - -END -$$ LANGUAGE 'plpgsql' IMMUTABLE; diff --git a/resto-database-model/03_resto_target_model.sql b/resto-database-model/03_resto_target_model.sql index c7772dcf..bb39bedf 100644 --- a/resto-database-model/03_resto_target_model.sql +++ b/resto-database-model/03_resto_target_model.sql @@ -183,7 +183,7 @@ CREATE TABLE IF NOT EXISTS __DATABASE_TARGET_SCHEMA__.feature ( -- Centroid computed from geometry centroid GEOMETRY(POINT, 4326), - -- Result of ST_SplitDateLine(geometry) + -- Result of Antimeridian function applied to original geometry -- Guarantee a valid geometry in database even if input geometry crosses -180/180 meridian of crosses North or South pole -- If input geometry does not cross one of this case, then input geometry is not -- modified and geom equals geomety.