diff --git a/CHANGELOG.md b/CHANGELOG.md index 489b94f..fe90b9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 25.06.2023 2.1.3 + +- BugFix: + - Fehler in der Namespace-Unterstützung (Dateistruktur, Cronjob-Registrierung) bereinigt. + ## 25.06.2023 2.1.2 - BugFix: diff --git a/boot.php b/boot.php index 9b4a47e..de4e9a9 100644 --- a/boot.php +++ b/boot.php @@ -61,7 +61,7 @@ } // Start of Cronjob -rex_cronjob_manager::registerType('FriendsOfRedaxo\\Geolocation\\Cronjob'); +rex_cronjob_manager::registerType(Cronjob::class); // if BE: activate JS and CSS if (rex::isBackend()) { diff --git a/lib/yform/dataset/Layer.php b/lib/Layer.php similarity index 100% rename from lib/yform/dataset/Layer.php rename to lib/Layer.php diff --git a/lib/yform/dataset/Mapset.php b/lib/Mapset.php similarity index 100% rename from lib/yform/dataset/Mapset.php rename to lib/Mapset.php diff --git a/lib/calc/Box.php b/lib/calc/Box.php index dbc4246..1d71712 100644 --- a/lib/calc/Box.php +++ b/lib/calc/Box.php @@ -1,595 +1,595 @@ - $points - * @throws InvalidBoxParameter - */ - public static function factory(array $points): ?self - { - if (0 === count($points)) { - return null; - } - $lat = []; - $lng = []; - foreach ($points as $point) { - if (!($point instanceof Point)) { - throw new InvalidBoxParameter(InvalidBoxParameter::NOT_A_POINT, [gettype($point)]); - } - $lat[] = $point->lat(); - $lng[] = $point->lng(); - } - $south = min($lat); - $north = max($lat); - $west = min($lng); - $east = max($lng); - - if (180 <= ($east - $west)) { - throw new InvalidBoxParameter(InvalidBoxParameter::BOX2WIDE, [$east, $west, $east - $west]); - } - - $self = new self(); - $self->nw = Point::byLatLng([$north, $west]); - $self->se = Point::byLatLng([$south, $east]); - $self->bounds = new Bounds($self->nw->coord(), $self->se->coord()); - return $self; - } - - /** - * Factory-Methode zum Anlegen der Box aus zwei gegenüberliegenden Koordinaten-Punkten. - * - * @api - * @throws InvalidBoxParameter - */ - public static function byCorner(Point $cornerA, Point $cornerB): ?self - { - return self::factory([$cornerA, $cornerB]); - } - - /** - * Factory-Methode zum Anlegen der quadratischen Box aus Mittelpunkt - * und Innenradius in Meter. - * - * Die Arbeit macht self::bySize. Aus dem Radius wird (mal 2) werden Höhe und Breite gesetzt. - * - * @api - * @throws InvalidBoxParameter - */ - public static function byInnerCircle(Point $center, int|float $radius): ?self - { - $height = $width = $radius * 2; - return self::bySize($center, $height, $width); - } - - /** - * Factory-Methode zum Anlegen der quadratischen Box aus Mittelpunkt - * und Außenradius in Meter. - * - * Die Arbeit macht self::bySize. Aus dem Radius wird (mal 2) Höhe und Breite gesetzt. - * - * @api - * @throws InvalidBoxParameter - */ - public static function byOuterCircle(Point $center, int|float $radius): ?self - { - $height = $width = $radius / sqrt(2) * 2; - return self::bySize($center, $height, $width); - } - - /** - * Factory-Methode zum Anlegen der Box aus Mittelpunkt, Höhe und Breite (in Meter). - * - * Geprüft wird, ob der Platz auch reicht. Die Box muss weit genug vom Pol entfernt sein, - * damit die Breite der Box auf dem polnäheren Breitengrad nicht über die Datumsgrente führt. - * Die maximale Boxbreite muss unter 180° sein. - * - * @api - * @throws InvalidBoxParameter - */ - public static function bySize(Point $center, int|float $width, int|float $height): ?self - { - $width = $width / 2; - $height = $height / 2; - - // In Pole-Nähe gibt es sehr seltsame Ergebnisse; erst mal prüfen - // Abbruch wenn ab Center der Radius über die Pole reicht. - $pole = (0 <= $center->lat()) ? 90 : -90; - $poleDistance = $center->distanceTo(Point::byLatLng([$pole, $center->lat()])); - if ($poleDistance <= $height) { - throw new InvalidBoxParameter(InvalidBoxParameter::POLE); - } - - // Polenahe Kante - $pointSN = $center->moveBy(abs($pole - 90), $height); - - // Am Datumsgenzen-Meridian gibt es sehr seltsame Ergebnisse; erst mal prüfen - // Abbruch wenn als Breite der pol-näheren Kante der Radius über den 180°-Meridian reicht. - $dateline = (0 <= $center->lng()) ? 180 : -180; - $datelineDistance = $pointSN->distanceTo(Point::byLatLng([$pointSN->lat(), $dateline])); - if ($datelineDistance <= $width) { - throw new InvalidBoxParameter(InvalidBoxParameter::DATELINE); - } - - // Dateline-nahe Kante - $pointWE = $center->moveBy(abs($dateline - 90), $width); - - // Die Box passt. Nun die tatsächlichen Grenzen berechnen - // Die Breite muss untr 180° liegen. - $W = $pointWE->lng(); - $E = $center->lng() * 2 - $W; - if (180 <= abs($E - $W)) { - throw new InvalidBoxParameter(InvalidBoxParameter::BOX2WIDE, [$E, $W, $E - $W]); - } - $S = $pointSN->lat(); - $N = $center->lat() * 2 - $S; - - return self::factory([ - Point::byLatLng([$N, $W]), - Point::byLatLng([$S, $E]), - ]); - } - - // Es folgen Methoden zur Ausgabe einzelner Daten ---------------------------------------------- - - /** - * Nördlicher Breitengrad der Box. - * - * @api - */ - // REVIEW: Warum wird hier $precision nicht genutzt? - public function north(): float - { - return $this->bounds->getNorth(); - } - - /** - * Südlicher Breitengrad der Box. - * - * @api - */ - // REVIEW: Warum wird hier $precision nicht genutzt? - public function south(): float - { - return $this->bounds->getSouth(); - } - - /** - * Westlicher Längengrad der Box. - * - * @api - */ - public function west(): float - { - // REVIEW: Warum wird hier $precision nicht genutzt? - return $this->bounds->getWest(); - } - - /** - * Östlicher Längengrad der Box. - * - * @api - */ - // REVIEW: Warum wird hier $precision nicht genutzt? - public function east(): float - { - return $this->bounds->getEast(); - } - - /** - * NordWestliche Ecke der Box. - * - * Faktisch ist die Rückgabe des internen NordWest-Punktes - * Wenn die Box per Extend vergrößert wird, wird auch ein neuer NW-Punkt angelegt. - * Eine extern als Zugriffshilfe zwischengespeicherte Instanz ist dann nicht mehr aktuell. - * - * @api - */ - public function northWest(): Point - { - return $this->nw; - } - - /** - * Südöstliche Ecke der Box. - * - * Faktisch ist die Rückgabe des internen SüdWest-Punktes - * Wenn die Box per Extend vergrößert wird, wird auch ein neuer NW-Punkt angelegt. - * Eine extern als Zugriffshilfe zwischengespeicherte Instanz ist dann nicht mehr aktuell. - * - * @api - */ - public function southEast(): Point - { - return $this->se; - } - - /** - * Nordöstliche Ecke der Box. - * - * Das es hierfür keinen existenten Point gibt, wird eine neu angelegte Instanz - * Übergeben. Hat den Nachteil, dass bei jedem Abruf eine neue Instanz angelegt wird. - * - * @api - */ - public function northEast(): Point - { - return Point::byLatLng([$this->north(), $this->east()]); - } - - /** - * Südwestliche Ecke der Box. - * - * Das es hierfür keinen existenten Point gibt, wird eine neu angelegte Instanz - * Übergeben. Hat den Nachteil, dass bei jedem Abruf eine neue Instanz angelegt wird. - * - * @api - */ - public function southWest(): Point - { - return Point::byLatLng([$this->south(), $this->west()]); - } - - /** - * Zentrum der Box. - * - * Das es hierfür keinen existenten Point gibt, wird eine neu angelegte Instanz - * Übergeben. Hat den Nachteil, dass bei jedem Abruf eine neue Instanz angelegt wird. - * - * @api - */ - public function center(): Point - { - return Point::byCoordinate($this->bounds->getCenter()); - } - - /** - * Koordinaten in der Reihenfolge lat/lng (LeafletJS-Reihenfolge). - * $precision: Anzahl Nachkommastellen; default: alle. - * - * Liefert ein Array mit den Eck-Koordinaten der Box - * [ 0 => [0=>latNW,1=lngNW], 1=>[0=>latSE,lngSE] ] - * - * @api - * @return array{0:array{0:float,1:float},1:array{0:float,1:float}} - */ - public function latLng(?int $precision = null): array - { - return [ - 0 => $this->northWest()->latLng($precision), - 1 => $this->southEast()->latLng($precision), - ]; - } - - /** - * Koordinaten in der Reihenfolge lng/lat (geoJSON-Reihenfolge). - * $precision: Anzahl Nachkommastellen; default: alle. - * - * Liefert ein Array mit den Eck-Koordinaten der Box - * [ 0 => [0=>lngNW,1=latNW], 1=>[0=>lmgSE,latSE] ] - * - * @api - * @return array{0:array{0:float,1:float},1:array{0:float,1:float}} - */ - public function lngLat(?int $precision = null): array - { - return [ - 0 => $this->northWest()->lngLat($precision), - 1 => $this->southEast()->lngLat($precision), - ]; - } - - /** - * Die Breite der Box als Abstand der Längengrade. - * $precision: Anzahl Nachkommastellen; default: alle. - * - * @api - */ - // REVIEW: Warum wird hier $precision nicht genutzt? - public function width(?int $precision = null): float - { - return abs($this->east() - $this->west()); - } - - /** - * Die Höhe der Box als Abstand der Breitengrade. - * $precision: Anzahl Nachkommastellen; default: alle. - * - * @api - */ - // REVIEW: Warum wird hier $precision nicht genutzt? - public function height(?int $precision = null): float - { - return abs($this->north() - $this->south()); - } - - /** - * Abschnitt für einen geoJSON-Datensatz: als zwei Punkte. - * $precision: Anzahl Nachkommastellen; default: alle. - * - * @api - * @return array{type:string,coordinates:array{0:array{0:float,1:float},1:array{0:float,1:float}}} - */ - public function geoJSONMultipoint(?int $precision = null): array - { - return [ - 'type' => 'MultiPoint', - 'coordinates' => $this->lngLat($precision), - ]; - } - - /** - * Abschnitt für einen geoJSON-Datensatz: als geschlossenes Polygon. - * $precision: Anzahl Nachkommastellen; default: alle. - * - * @api - * @return array{type:string,coordinates:list>} - */ - public function geoJSONPolygon(?int $precision = null): array - { - return [ - 'type' => 'Polygon', - 'coordinates' => [[ - $this->northWest()->lngLat($precision), - $this->northEast()->lngLat($precision), - $this->southEast()->lngLat($precision), - $this->southWest()->lngLat($precision), - $this->northWest()->lngLat($precision), - ]], - ]; - } - - // Es folgen Methoden für Berechnungen --------------------------------------------------------- - - /** - * Außenradius, also Kreis um den Mittelpunkt mit den Radius "halbe Diagonale" - * Funktioniert auch bei nicht-quadratischen Boxen ohne Einschränkung. - * - * @api - */ - public function outerRadius(): float - { - return $this->nw->distanceTo($this->se) / 2; - } - - /** - * Innenradius, also Kreis um den Mittelpunkt mit den Radius "halbe Distanz Nord/Süd" oder - * "halbe Distanz Ost/West". Es wird der kürzere Weg herangezogen, da es sonst bei nicht- - * quadratischen Boxen schwierig wird. - * - * @api - */ - public function innerRadius(): float - { - $sw = $this->southWest(); - $distanceSN = $this->nw->distanceTo($sw); - $distanceWE = $this->se->distanceTo($sw); - return min($distanceSN, $distanceWE) / 2; - } - - /** - * Prüft ob der angegeben Point innerhalb der Box liegt. - * - * @api - */ - public function contains(Point $point): bool - { - if ($point->lat() > $this->north()) { - return false; - } - if ($point->lat() < $this->south()) { - return false; - } - if ($point->lng() < $this->west()) { - return false; - } - if ($point->lng() > $this->east()) { - return false; - } - return true; - } - - /** - * Erweitert die Box so, dass der angegebene Punkt innerhalb der Box liegt. - * - * Sofern der neue Punkt dazu führt, dass die Box 180° oder mehr breit werden würde, - * bricht die Methode mit einer Exception ab. - * Im Erfolgsfall sind $this->nw, $this->se und $this->bounds anschließend neu gesetzt. - * - * @api - * @param array|Point $data Entweder ein einzelner Punkt oder ein Array - */ - public function extendBy(array|Point $data): self - { - if (!is_array($data)) { - $data = [$data]; - } - - $south = $north = $west = $east = []; - - foreach ($data as $point) { - if (!($point instanceof Point)) { - throw new InvalidBoxParameter(InvalidBoxParameter::BOXEXTEND); - } - if (!$this->contains($point)) { - $south[] = $point->lat(); - $north[] = $point->lat(); - $west[] = $point->lng(); - $east[] = $point->lng(); - } - } - - if (0 < count($south)) { // es reicht aus, nur ein Array zu prüfen, alle sind gleich groß - $south[] = $this->south(); - $north[] = $this->north(); - $west[] = $this->west(); - $east[] = $this->east(); - - $south = min($south); - $north = max($north); - $west = min($west); - $east = max($east); - - $this->nw = Point::byLatLng([$north, $west]); - $this->se = Point::byLatLng([$south, $east]); - $this->bounds = new Bounds($this->nw->coord(), $this->se->coord()); - } - return $this; - } - - /** - * Erweitert die Box so, dass der angegebene Punkt innerhalb der Box liegt. - * - * Sofern der neue Punkt dazu führt, dass die Box 180° oder mehr breit werden würde, - * bricht die Methode mit einer Exception ab. - * Im Erfolgsfall sind $this->nw, $this->se und $this->bounds anschließend neu gesetzt. - * - * @api - * @param float $factorLat Umrechnungsfaktor für Höhe (und ggf. Länge) - * @param null|float $factorLng Umrechnungsfaktor für Breite (oder null für Übernahme Höhenfaktor) - * @param int $reference Referenzpunkt (Box::HOOK_CE, .._NE, .._SE, .._SW, ..NW) - */ - public function resizeBy(float $factorLat, ?float $factorLng = null, int $reference = self::HOOK_CE): self - { - if (null === $factorLng) { - $factorLng = $factorLat; - } - - // Faktoren müssen > 0 sein. - if ($factorLat <= 0) { - throw new InvalidBoxParameter(InvalidBoxParameter::BOXRESIZELAT, [$factorLat]); - } - - if ($factorLng <= 0) { - throw new InvalidBoxParameter(InvalidBoxParameter::BOXRESIZELNG, [$factorLng]); - } - - // Abkürzung: wenn Faktor 1 dann unverändert - if (abs($factorLat - 1) < 0.0000001 && abs($factorLng - 1) < 0.0000001) { - return $this; - } - - $deltaLat = $this->height() * $factorLat; - $deltaLng = $this->width() * $factorLng; - - switch ($reference) { - case self::HOOK_NW: - $north = $this->north(); - $west = $this->west(); - $south = $north - $deltaLat; - $east = $west + $deltaLng; - break; - case self::HOOK_NE: - $north = $this->north(); - $east = $this->east(); - $south = $north - $deltaLat; - $west = $east - $deltaLng; - break; - case self::HOOK_SE: - $south = $this->south(); - $east = $this->east(); - $north = $south + $deltaLat; - $west = $east - $deltaLng; - break; - case self::HOOK_SW: - $south = $this->south(); - $west = $this->west(); - $north = $south + $deltaLat; - $east = $west + $deltaLng; - break; - case self::HOOK_CE: - $deltaLat = $deltaLat / 2; - $deltaLng = $deltaLng / 2; - $center = $this->center(); - $south = $center->lat() - $deltaLat; - $north = $center->lat() + $deltaLat; - $west = $center->lng() - $deltaLng; - $east = $center->lng() + $deltaLng; - break; - default: - throw new InvalidBoxParameter(InvalidBoxParameter::BOXRESIZE, [$reference]); - } - - // passen die neuen Grenzen? - // Lat nur normalisieren, Lng als Fehler melden - if (180 <= ($east - $west)) { - throw new InvalidBoxParameter(InvalidBoxParameter::BOX2WIDE, [$east, $west, $east - $west]); - } - $north = Math::normalizeLatitude($north); - $south = Math::normalizeLatitude($south); - - // neue Koordinaten übernehmen - - $this->nw = Point::byLatLng([$north, $west]); - $this->se = Point::byLatLng([$south, $east]); - $this->bounds = new Bounds($this->nw->coord(), $this->se->coord()); - - return $this; - } - - /** - * Erzeugt eine Kopie, bei der auch die internen Objekte geklont werden. - */ - public function __clone() - { - $this->nw = Point::byLatLng($this->nw->latLng()); - $this->se = Point::byLatLng($this->se->latLng()); - $this->bounds = new Bounds($this->nw->coord(), $this->se->coord()); - } -} + $points + * @throws InvalidBoxParameter + */ + public static function factory(array $points): ?self + { + if (0 === count($points)) { + return null; + } + $lat = []; + $lng = []; + foreach ($points as $point) { + if (!($point instanceof Point)) { + throw new InvalidBoxParameter(InvalidBoxParameter::NOT_A_POINT, [gettype($point)]); + } + $lat[] = $point->lat(); + $lng[] = $point->lng(); + } + $south = min($lat); + $north = max($lat); + $west = min($lng); + $east = max($lng); + + if (180 <= ($east - $west)) { + throw new InvalidBoxParameter(InvalidBoxParameter::BOX2WIDE, [$east, $west, $east - $west]); + } + + $self = new self(); + $self->nw = Point::byLatLng([$north, $west]); + $self->se = Point::byLatLng([$south, $east]); + $self->bounds = new Bounds($self->nw->coord(), $self->se->coord()); + return $self; + } + + /** + * Factory-Methode zum Anlegen der Box aus zwei gegenüberliegenden Koordinaten-Punkten. + * + * @api + * @throws InvalidBoxParameter + */ + public static function byCorner(Point $cornerA, Point $cornerB): ?self + { + return self::factory([$cornerA, $cornerB]); + } + + /** + * Factory-Methode zum Anlegen der quadratischen Box aus Mittelpunkt + * und Innenradius in Meter. + * + * Die Arbeit macht self::bySize. Aus dem Radius wird (mal 2) werden Höhe und Breite gesetzt. + * + * @api + * @throws InvalidBoxParameter + */ + public static function byInnerCircle(Point $center, int|float $radius): ?self + { + $height = $width = $radius * 2; + return self::bySize($center, $height, $width); + } + + /** + * Factory-Methode zum Anlegen der quadratischen Box aus Mittelpunkt + * und Außenradius in Meter. + * + * Die Arbeit macht self::bySize. Aus dem Radius wird (mal 2) Höhe und Breite gesetzt. + * + * @api + * @throws InvalidBoxParameter + */ + public static function byOuterCircle(Point $center, int|float $radius): ?self + { + $height = $width = $radius / sqrt(2) * 2; + return self::bySize($center, $height, $width); + } + + /** + * Factory-Methode zum Anlegen der Box aus Mittelpunkt, Höhe und Breite (in Meter). + * + * Geprüft wird, ob der Platz auch reicht. Die Box muss weit genug vom Pol entfernt sein, + * damit die Breite der Box auf dem polnäheren Breitengrad nicht über die Datumsgrente führt. + * Die maximale Boxbreite muss unter 180° sein. + * + * @api + * @throws InvalidBoxParameter + */ + public static function bySize(Point $center, int|float $width, int|float $height): ?self + { + $width = $width / 2; + $height = $height / 2; + + // In Pole-Nähe gibt es sehr seltsame Ergebnisse; erst mal prüfen + // Abbruch wenn ab Center der Radius über die Pole reicht. + $pole = (0 <= $center->lat()) ? 90 : -90; + $poleDistance = $center->distanceTo(Point::byLatLng([$pole, $center->lat()])); + if ($poleDistance <= $height) { + throw new InvalidBoxParameter(InvalidBoxParameter::POLE); + } + + // Polenahe Kante + $pointSN = $center->moveBy(abs($pole - 90), $height); + + // Am Datumsgenzen-Meridian gibt es sehr seltsame Ergebnisse; erst mal prüfen + // Abbruch wenn als Breite der pol-näheren Kante der Radius über den 180°-Meridian reicht. + $dateline = (0 <= $center->lng()) ? 180 : -180; + $datelineDistance = $pointSN->distanceTo(Point::byLatLng([$pointSN->lat(), $dateline])); + if ($datelineDistance <= $width) { + throw new InvalidBoxParameter(InvalidBoxParameter::DATELINE); + } + + // Dateline-nahe Kante + $pointWE = $center->moveBy(abs($dateline - 90), $width); + + // Die Box passt. Nun die tatsächlichen Grenzen berechnen + // Die Breite muss untr 180° liegen. + $W = $pointWE->lng(); + $E = $center->lng() * 2 - $W; + if (180 <= abs($E - $W)) { + throw new InvalidBoxParameter(InvalidBoxParameter::BOX2WIDE, [$E, $W, $E - $W]); + } + $S = $pointSN->lat(); + $N = $center->lat() * 2 - $S; + + return self::factory([ + Point::byLatLng([$N, $W]), + Point::byLatLng([$S, $E]), + ]); + } + + // Es folgen Methoden zur Ausgabe einzelner Daten ---------------------------------------------- + + /** + * Nördlicher Breitengrad der Box. + * + * @api + */ + // REVIEW: Warum wird hier $precision nicht genutzt? + public function north(): float + { + return $this->bounds->getNorth(); + } + + /** + * Südlicher Breitengrad der Box. + * + * @api + */ + // REVIEW: Warum wird hier $precision nicht genutzt? + public function south(): float + { + return $this->bounds->getSouth(); + } + + /** + * Westlicher Längengrad der Box. + * + * @api + */ + public function west(): float + { + // REVIEW: Warum wird hier $precision nicht genutzt? + return $this->bounds->getWest(); + } + + /** + * Östlicher Längengrad der Box. + * + * @api + */ + // REVIEW: Warum wird hier $precision nicht genutzt? + public function east(): float + { + return $this->bounds->getEast(); + } + + /** + * NordWestliche Ecke der Box. + * + * Faktisch ist die Rückgabe des internen NordWest-Punktes + * Wenn die Box per Extend vergrößert wird, wird auch ein neuer NW-Punkt angelegt. + * Eine extern als Zugriffshilfe zwischengespeicherte Instanz ist dann nicht mehr aktuell. + * + * @api + */ + public function northWest(): Point + { + return $this->nw; + } + + /** + * Südöstliche Ecke der Box. + * + * Faktisch ist die Rückgabe des internen SüdWest-Punktes + * Wenn die Box per Extend vergrößert wird, wird auch ein neuer NW-Punkt angelegt. + * Eine extern als Zugriffshilfe zwischengespeicherte Instanz ist dann nicht mehr aktuell. + * + * @api + */ + public function southEast(): Point + { + return $this->se; + } + + /** + * Nordöstliche Ecke der Box. + * + * Das es hierfür keinen existenten Point gibt, wird eine neu angelegte Instanz + * Übergeben. Hat den Nachteil, dass bei jedem Abruf eine neue Instanz angelegt wird. + * + * @api + */ + public function northEast(): Point + { + return Point::byLatLng([$this->north(), $this->east()]); + } + + /** + * Südwestliche Ecke der Box. + * + * Das es hierfür keinen existenten Point gibt, wird eine neu angelegte Instanz + * Übergeben. Hat den Nachteil, dass bei jedem Abruf eine neue Instanz angelegt wird. + * + * @api + */ + public function southWest(): Point + { + return Point::byLatLng([$this->south(), $this->west()]); + } + + /** + * Zentrum der Box. + * + * Das es hierfür keinen existenten Point gibt, wird eine neu angelegte Instanz + * Übergeben. Hat den Nachteil, dass bei jedem Abruf eine neue Instanz angelegt wird. + * + * @api + */ + public function center(): Point + { + return Point::byCoordinate($this->bounds->getCenter()); + } + + /** + * Koordinaten in der Reihenfolge lat/lng (LeafletJS-Reihenfolge). + * $precision: Anzahl Nachkommastellen; default: alle. + * + * Liefert ein Array mit den Eck-Koordinaten der Box + * [ 0 => [0=>latNW,1=lngNW], 1=>[0=>latSE,lngSE] ] + * + * @api + * @return array{0:array{0:float,1:float},1:array{0:float,1:float}} + */ + public function latLng(?int $precision = null): array + { + return [ + 0 => $this->northWest()->latLng($precision), + 1 => $this->southEast()->latLng($precision), + ]; + } + + /** + * Koordinaten in der Reihenfolge lng/lat (geoJSON-Reihenfolge). + * $precision: Anzahl Nachkommastellen; default: alle. + * + * Liefert ein Array mit den Eck-Koordinaten der Box + * [ 0 => [0=>lngNW,1=latNW], 1=>[0=>lmgSE,latSE] ] + * + * @api + * @return array{0:array{0:float,1:float},1:array{0:float,1:float}} + */ + public function lngLat(?int $precision = null): array + { + return [ + 0 => $this->northWest()->lngLat($precision), + 1 => $this->southEast()->lngLat($precision), + ]; + } + + /** + * Die Breite der Box als Abstand der Längengrade. + * $precision: Anzahl Nachkommastellen; default: alle. + * + * @api + */ + // REVIEW: Warum wird hier $precision nicht genutzt? + public function width(?int $precision = null): float + { + return abs($this->east() - $this->west()); + } + + /** + * Die Höhe der Box als Abstand der Breitengrade. + * $precision: Anzahl Nachkommastellen; default: alle. + * + * @api + */ + // REVIEW: Warum wird hier $precision nicht genutzt? + public function height(?int $precision = null): float + { + return abs($this->north() - $this->south()); + } + + /** + * Abschnitt für einen geoJSON-Datensatz: als zwei Punkte. + * $precision: Anzahl Nachkommastellen; default: alle. + * + * @api + * @return array{type:string,coordinates:array{0:array{0:float,1:float},1:array{0:float,1:float}}} + */ + public function geoJSONMultipoint(?int $precision = null): array + { + return [ + 'type' => 'MultiPoint', + 'coordinates' => $this->lngLat($precision), + ]; + } + + /** + * Abschnitt für einen geoJSON-Datensatz: als geschlossenes Polygon. + * $precision: Anzahl Nachkommastellen; default: alle. + * + * @api + * @return array{type:string,coordinates:list>} + */ + public function geoJSONPolygon(?int $precision = null): array + { + return [ + 'type' => 'Polygon', + 'coordinates' => [[ + $this->northWest()->lngLat($precision), + $this->northEast()->lngLat($precision), + $this->southEast()->lngLat($precision), + $this->southWest()->lngLat($precision), + $this->northWest()->lngLat($precision), + ]], + ]; + } + + // Es folgen Methoden für Berechnungen --------------------------------------------------------- + + /** + * Außenradius, also Kreis um den Mittelpunkt mit den Radius "halbe Diagonale" + * Funktioniert auch bei nicht-quadratischen Boxen ohne Einschränkung. + * + * @api + */ + public function outerRadius(): float + { + return $this->nw->distanceTo($this->se) / 2; + } + + /** + * Innenradius, also Kreis um den Mittelpunkt mit den Radius "halbe Distanz Nord/Süd" oder + * "halbe Distanz Ost/West". Es wird der kürzere Weg herangezogen, da es sonst bei nicht- + * quadratischen Boxen schwierig wird. + * + * @api + */ + public function innerRadius(): float + { + $sw = $this->southWest(); + $distanceSN = $this->nw->distanceTo($sw); + $distanceWE = $this->se->distanceTo($sw); + return min($distanceSN, $distanceWE) / 2; + } + + /** + * Prüft ob der angegeben Point innerhalb der Box liegt. + * + * @api + */ + public function contains(Point $point): bool + { + if ($point->lat() > $this->north()) { + return false; + } + if ($point->lat() < $this->south()) { + return false; + } + if ($point->lng() < $this->west()) { + return false; + } + if ($point->lng() > $this->east()) { + return false; + } + return true; + } + + /** + * Erweitert die Box so, dass der angegebene Punkt innerhalb der Box liegt. + * + * Sofern der neue Punkt dazu führt, dass die Box 180° oder mehr breit werden würde, + * bricht die Methode mit einer Exception ab. + * Im Erfolgsfall sind $this->nw, $this->se und $this->bounds anschließend neu gesetzt. + * + * @api + * @param array|Point $data Entweder ein einzelner Punkt oder ein Array + */ + public function extendBy(array|Point $data): self + { + if (!is_array($data)) { + $data = [$data]; + } + + $south = $north = $west = $east = []; + + foreach ($data as $point) { + if (!($point instanceof Point)) { + throw new InvalidBoxParameter(InvalidBoxParameter::BOXEXTEND); + } + if (!$this->contains($point)) { + $south[] = $point->lat(); + $north[] = $point->lat(); + $west[] = $point->lng(); + $east[] = $point->lng(); + } + } + + if (0 < count($south)) { // es reicht aus, nur ein Array zu prüfen, alle sind gleich groß + $south[] = $this->south(); + $north[] = $this->north(); + $west[] = $this->west(); + $east[] = $this->east(); + + $south = min($south); + $north = max($north); + $west = min($west); + $east = max($east); + + $this->nw = Point::byLatLng([$north, $west]); + $this->se = Point::byLatLng([$south, $east]); + $this->bounds = new Bounds($this->nw->coord(), $this->se->coord()); + } + return $this; + } + + /** + * Erweitert die Box so, dass der angegebene Punkt innerhalb der Box liegt. + * + * Sofern der neue Punkt dazu führt, dass die Box 180° oder mehr breit werden würde, + * bricht die Methode mit einer Exception ab. + * Im Erfolgsfall sind $this->nw, $this->se und $this->bounds anschließend neu gesetzt. + * + * @api + * @param float $factorLat Umrechnungsfaktor für Höhe (und ggf. Länge) + * @param null|float $factorLng Umrechnungsfaktor für Breite (oder null für Übernahme Höhenfaktor) + * @param int $reference Referenzpunkt (Box::HOOK_CE, .._NE, .._SE, .._SW, ..NW) + */ + public function resizeBy(float $factorLat, ?float $factorLng = null, int $reference = self::HOOK_CE): self + { + if (null === $factorLng) { + $factorLng = $factorLat; + } + + // Faktoren müssen > 0 sein. + if ($factorLat <= 0) { + throw new InvalidBoxParameter(InvalidBoxParameter::BOXRESIZELAT, [$factorLat]); + } + + if ($factorLng <= 0) { + throw new InvalidBoxParameter(InvalidBoxParameter::BOXRESIZELNG, [$factorLng]); + } + + // Abkürzung: wenn Faktor 1 dann unverändert + if (abs($factorLat - 1) < 0.0000001 && abs($factorLng - 1) < 0.0000001) { + return $this; + } + + $deltaLat = $this->height() * $factorLat; + $deltaLng = $this->width() * $factorLng; + + switch ($reference) { + case self::HOOK_NW: + $north = $this->north(); + $west = $this->west(); + $south = $north - $deltaLat; + $east = $west + $deltaLng; + break; + case self::HOOK_NE: + $north = $this->north(); + $east = $this->east(); + $south = $north - $deltaLat; + $west = $east - $deltaLng; + break; + case self::HOOK_SE: + $south = $this->south(); + $east = $this->east(); + $north = $south + $deltaLat; + $west = $east - $deltaLng; + break; + case self::HOOK_SW: + $south = $this->south(); + $west = $this->west(); + $north = $south + $deltaLat; + $east = $west + $deltaLng; + break; + case self::HOOK_CE: + $deltaLat = $deltaLat / 2; + $deltaLng = $deltaLng / 2; + $center = $this->center(); + $south = $center->lat() - $deltaLat; + $north = $center->lat() + $deltaLat; + $west = $center->lng() - $deltaLng; + $east = $center->lng() + $deltaLng; + break; + default: + throw new InvalidBoxParameter(InvalidBoxParameter::BOXRESIZE, [$reference]); + } + + // passen die neuen Grenzen? + // Lat nur normalisieren, Lng als Fehler melden + if (180 <= ($east - $west)) { + throw new InvalidBoxParameter(InvalidBoxParameter::BOX2WIDE, [$east, $west, $east - $west]); + } + $north = Math::normalizeLatitude($north); + $south = Math::normalizeLatitude($south); + + // neue Koordinaten übernehmen + + $this->nw = Point::byLatLng([$north, $west]); + $this->se = Point::byLatLng([$south, $east]); + $this->bounds = new Bounds($this->nw->coord(), $this->se->coord()); + + return $this; + } + + /** + * Erzeugt eine Kopie, bei der auch die internen Objekte geklont werden. + */ + public function __clone() + { + $this->nw = Point::byLatLng($this->nw->latLng()); + $this->se = Point::byLatLng($this->se->latLng()); + $this->bounds = new Bounds($this->nw->coord(), $this->se->coord()); + } +} diff --git a/lib/calc/Math.php b/lib/calc/Math.php index e26d5b3..749b694 100644 --- a/lib/calc/Math.php +++ b/lib/calc/Math.php @@ -1,222 +1,222 @@ -calculateBearing($from->coord(), $to->coord()); - } - - /** - * Berechnet die Kompass-Richtung am Ziel (0...360°) aus Richtung Start kommend. - * - * @api - */ - public static function bearingFrom(Point $from, Point $to): float - { - return self::bearingCalculator()->calculateFinalBearing($from->coord(), $to->coord()); - } - - /** - * Berechnet die kürzeste Distanz zwischen Start und Ziel, geht also über den Großkreis - * Das Ergebnis ist die Diszanz in Meter. - * - * @api - */ - public static function distance(Point $from, Point $to): float - { - return self::distanceCalculator()->getDistance($from->coord(), $to->coord()); - } - - /** - * Berechnet den Zielpunkt über Startpunkt, Richtung (bearingTo) und Distanz - * Richtung in Grad (0°...360°), Distanz in Meter. - * - * @api - */ - public static function goBearingDistance(Point $from, float $bearing, float $distance): Point - { - $bearing = self::normalizeBearing($bearing); - $target = self::bearingCalculator()->calculateDestination($from->coord(), $bearing, $distance); - return Point::byCoordinate($target); - } - - /** - * Umrechnung der dezimalen Koordinaten in die Elemente Grad/Minute. - * - * [ - * 'degree' => Grad mit Vorzeichen (- entspricht westl Länge bzw. südliche Breite) - * 'minute' => Bogenminuten mit Nachkommastellen - * 'min' => Bogenminuten ohne Nachkommastellen - * ] - * - * @api - * @return array{degree:int,minute:float,min:int} - */ - public static function dd2dm(float $degree, ?int $precision = null): array - { - $deg = (int) $degree; - $minute = abs($degree - $deg) * 60; - return [ - 'degree' => $deg, - 'minute' => null === $precision ? $minute : round($minute, max(0, $precision)), - 'min' => (int) round($minute), - ]; - } - - /** - * Umrechnung der dezimalen Koordinaten in die Elemente Grad/Minute/Sekunde. - * - * [ - * 'degree' => Grad mit Vorzeichen (- entspricht westl Länge bzw. südliche Breite) - * 'minute' => Bogenminuten mit Nachkommastellen - * 'second' => Bogensekunde mit Nachkommastellen - * 'sec' => Bogensekunde ohne Nachkommastellen - * ] - * - * @api - * @return array{degree:int,minute:int,second:float,sec:int} - */ - public static function dd2dms(float $degree, ?int $precision = null): array - { - $deg = (int) $degree; - $minute = abs($degree - $deg) * 60; - $min = (int) $minute; - $second = abs($minute - $min) * 60; - return [ - 'degree' => $deg, - 'minute' => $min, - 'second' => null === $precision ? $second : round($second, max(0, $precision)), - 'sec' => (int) round($second), - ]; - } - - /** - * Rechnet positive und negative Längengrade um in Werte -180...+180 - * Optional: clippen statt umrechnen. - * - * -200° => 160° - * 200° => -160° - * - * @api - */ - public static function normalizeLongitude(float $longitude, bool $clip = false): float - { - if ($clip) { - return max(-180, min($longitude, 180)); - } - $longitude = fmod($longitude, 360); - if ($longitude < -180) { - return $longitude + 360; - } - - if ($longitude > 180) { - return $longitude - 360; - } - return $longitude; - } - - /** - * Kappt Beitengrade an den Polen. Überlauf ist hier Quatsch. - * - * 91° => 90° - * -91° => -90° - * - * @api - */ - public static function normalizeLatitude(float $latitude): float - { - return max(-90, min(90, $latitude)); - } - - /** - * Rechnet positive und negative Kompasskurse um in Werte 0...360 - * - 450° => 90° - * -90° => 270°. - * - * @api - */ - public static function normalizeBearing(float $bearing): float - { - return fmod($bearing, 360) + ($bearing < 0 ? 360 : 0); - } -} +calculateBearing($from->coord(), $to->coord()); + } + + /** + * Berechnet die Kompass-Richtung am Ziel (0...360°) aus Richtung Start kommend. + * + * @api + */ + public static function bearingFrom(Point $from, Point $to): float + { + return self::bearingCalculator()->calculateFinalBearing($from->coord(), $to->coord()); + } + + /** + * Berechnet die kürzeste Distanz zwischen Start und Ziel, geht also über den Großkreis + * Das Ergebnis ist die Diszanz in Meter. + * + * @api + */ + public static function distance(Point $from, Point $to): float + { + return self::distanceCalculator()->getDistance($from->coord(), $to->coord()); + } + + /** + * Berechnet den Zielpunkt über Startpunkt, Richtung (bearingTo) und Distanz + * Richtung in Grad (0°...360°), Distanz in Meter. + * + * @api + */ + public static function goBearingDistance(Point $from, float $bearing, float $distance): Point + { + $bearing = self::normalizeBearing($bearing); + $target = self::bearingCalculator()->calculateDestination($from->coord(), $bearing, $distance); + return Point::byCoordinate($target); + } + + /** + * Umrechnung der dezimalen Koordinaten in die Elemente Grad/Minute. + * + * [ + * 'degree' => Grad mit Vorzeichen (- entspricht westl Länge bzw. südliche Breite) + * 'minute' => Bogenminuten mit Nachkommastellen + * 'min' => Bogenminuten ohne Nachkommastellen + * ] + * + * @api + * @return array{degree:int,minute:float,min:int} + */ + public static function dd2dm(float $degree, ?int $precision = null): array + { + $deg = (int) $degree; + $minute = abs($degree - $deg) * 60; + return [ + 'degree' => $deg, + 'minute' => null === $precision ? $minute : round($minute, max(0, $precision)), + 'min' => (int) round($minute), + ]; + } + + /** + * Umrechnung der dezimalen Koordinaten in die Elemente Grad/Minute/Sekunde. + * + * [ + * 'degree' => Grad mit Vorzeichen (- entspricht westl Länge bzw. südliche Breite) + * 'minute' => Bogenminuten mit Nachkommastellen + * 'second' => Bogensekunde mit Nachkommastellen + * 'sec' => Bogensekunde ohne Nachkommastellen + * ] + * + * @api + * @return array{degree:int,minute:int,second:float,sec:int} + */ + public static function dd2dms(float $degree, ?int $precision = null): array + { + $deg = (int) $degree; + $minute = abs($degree - $deg) * 60; + $min = (int) $minute; + $second = abs($minute - $min) * 60; + return [ + 'degree' => $deg, + 'minute' => $min, + 'second' => null === $precision ? $second : round($second, max(0, $precision)), + 'sec' => (int) round($second), + ]; + } + + /** + * Rechnet positive und negative Längengrade um in Werte -180...+180 + * Optional: clippen statt umrechnen. + * + * -200° => 160° + * 200° => -160° + * + * @api + */ + public static function normalizeLongitude(float $longitude, bool $clip = false): float + { + if ($clip) { + return max(-180, min($longitude, 180)); + } + $longitude = fmod($longitude, 360); + if ($longitude < -180) { + return $longitude + 360; + } + + if ($longitude > 180) { + return $longitude - 360; + } + return $longitude; + } + + /** + * Kappt Beitengrade an den Polen. Überlauf ist hier Quatsch. + * + * 91° => 90° + * -91° => -90° + * + * @api + */ + public static function normalizeLatitude(float $latitude): float + { + return max(-90, min(90, $latitude)); + } + + /** + * Rechnet positive und negative Kompasskurse um in Werte 0...360 + * - 450° => 90° + * -90° => 270°. + * + * @api + */ + public static function normalizeBearing(float $bearing): float + { + return fmod($bearing, 360) + ($bearing < 0 ? 360 : 0); + } +} diff --git a/lib/calc/Point.php b/lib/calc/Point.php index e5ad988..1393a71 100644 --- a/lib/calc/Point.php +++ b/lib/calc/Point.php @@ -1,379 +1,379 @@ -coord; - } - - // Es folgen Factory-Methoden zum Anlegen eines Punktes ---------------------------------------- - - /** - * Allgemeine Methode zum Anlegen des Koordinaten-Punktes. - * - * Die Koordinaten [$lat => latitude,$lng=>longitude] müssen existieren und numerisch sein. - * Die Einhaltung der Grenzen (-90...+90,-180...+180) wird von \Location\Coordinate geprüft - * - * Damit können auch Punkte aus Arrays mit Text-Keys angelegt werden - * $daten = ['lng'=>12.34,'lat'=>-1234]; - * $point = Point::factory( $daten, 'lat','lng'); - * - * @api - * @param array $point Koordinaten als Array mit zwei Werten - * @param int|string $keyLat Array-Key für das Element mit Breitangrad - * @param int|string $keyLng Array-Key für das Element mit Längengrad - * @throws InvalidPointParameter - */ - public static function factory(array $point, int|string $keyLat, int|string $keyLng): self - { - if ($keyLat === $keyLng) { - throw new InvalidPointParameter(InvalidPointParameter::KEY_LAT_LNG, [$keyLat, $keyLng]); - } - if (!isset($point[$keyLat])) { - throw new InvalidPointParameter(InvalidPointParameter::LAT_MISSING, [$keyLat]); - } - if (!isset($point[$keyLng])) { - throw new InvalidPointParameter(InvalidPointParameter::LNG_MISSING, [$keyLng]); - } - if (!is_numeric($point[$keyLat])) { - throw new InvalidPointParameter(InvalidPointParameter::LAT_RANGE, [$point[$keyLat]]); - } - if (!is_numeric($point[$keyLng])) { - throw new InvalidPointParameter(InvalidPointParameter::LNG_RANGE, [$point[$keyLng]]); - } - $latitude = Math::normalizeLatitude($point[$keyLat]); - $longitude = Math::normalizeLongitude($point[$keyLng]); - - return self::byCoordinate(new Coordinate($latitude, $longitude)); - } - - /** - * Koordinaten-Punkt auf Basis einer \Location\Coordinate anlegen. - * - * @api - */ - public static function byCoordinate(Coordinate $point): self - { - $self = new self(); - $self->coord = $point; - return $self; - } - - /** - * Koordinaten-Punkt aus einem numerischen Array [0=>latitude,1=>longitude] anlegen. - * Das ist die übliche Reihenfolge für LeafletJS. - * - * @api - * @param array $point - */ - public static function byLatLng(array $point): self - { - return self::factory($point, 0, 1); - } - - /** - * Koordinaten-Punkt aus einem numerischen Array [0=>longitude,1=>latitude] anlegen. - * Das ist die übliche Reihenfolge für geoJSON-Datensätze. - * - * @api - * @param array $point - */ - public static function byLngLat(array $point): self - { - return self::factory($point, 1, 0); - } - - /** - * Koordinaten-Punkt aus einem Textfeld anlegen. - * - * Die String-Auswertung durch \Location\Factory\CoordinateFactory nach Best Guess. Also wird - * nicht jeder String erkannt. CoordinateFactory hadert mit Punkt vs. Komma, also vor dem - * Aufruf die Locale korrigieren - * - * @api - * @see https://github.com/mjaschen/phpgeo/blob/master/docs/700_Parsing_and_Input/110_Coordinates_Parser.md - * @throws InvalidPointParameter - */ - public static function byText(string $point): self - { - try { - $loc = (string) setlocale(LC_NUMERIC, '0'); - setlocale(LC_NUMERIC, 'en_US.UTF-8'); - $coordinate = CoordinateFactory::fromString($point); - setlocale(LC_NUMERIC, $loc); - } catch (Exception $e) { - throw new InvalidPointParameter(InvalidPointParameter::STRING2DD, [$point], $e); - } - - return self::byCoordinate($coordinate); - } - - // Es folgen Methoden zur Ausgabe der Punkt-Daten ---------------------------------------------- - - /** - * Breitengrad. - * $precision: Anzahl Nachkommastellen; default: alle. - * - * @api - */ - public function lat(?int $precision = null): float - { - if (null === $precision) { - return $this->coord->getLat(); - } - return round($this->coord->getLat(), max(0, $precision)); - } - - /** - * Längengrad. - * $precision: Anzahl Nachkommastellen; default: alle. - * - * @api - */ - public function lng(?int $precision = null): float - { - if (null === $precision) { - return $this->coord->getLng(); - } - return round($this->coord->getLng(), max(0, $precision)); - } - - /** - * Gegenstück zu byLatLng: [0=>latitude,1=>longitude]. - * $precision: Anzahl Nachkommastellen; default: alle. - * - * @api - * @return array{0:float,1:float} - */ - public function latLng(?int $precision = null): array - { - return [$this->lat($precision), $this->lng($precision)]; - } - - /** - * Gegenstück zu byLngLat: [0=>longitude,1=>latitude]. - * $precision: Anzahl Nachkommastellen; default: alle. - * - * @api - * @return array{0:float,1:float} - */ - public function lngLat(?int $precision = null): array - { - return [$this->lng($precision), $this->lat($precision)]; - } - - /** - * Gegenstück zu factory: [$lat=>latitude,$lng=>longitude]. - * $precision: Anzahl Nachkommastellen; default: alle. - * - * @api - * @throws InvalidPointParameter - * @return array - */ - public function pos(int|string $keyLat, int|string $keyLng, ?int $precision = null): array - { - if ($keyLat === $keyLng) { - throw new InvalidPointParameter(InvalidPointParameter::KEY_LAT_LNG, [$keyLat, $keyLng]); - } - return [ - $keyLat => $this->lat($precision), - $keyLng => $this->lng($precision), - ]; - } - - /** - * Gegenstück zu byText: "latitude longitude". - * - * Die konkrete Ausgabe kann durch den gewählten Formatierer gesteuert werden: - * DD: als Grad/Dezimalstellen - * DM: als Grad und Minute/Dezimalstellen - * DMS: als Grad Minute Sekunde/Dezimalstellen - * - * Die Formatierer hadern mit Punkt vs. Komma, also vor dem Aufruf die Locale korrigieren - * - * @api - * @param int $formatter Formatierer: Point::DD, Point::DM, Point::DMS - * @param string $delimiter Trennzeichen zwischen lat/Lng; mindestens 1 Zeichen; default="," - * @param ?int $precision Anzahl Nachkommastellen; default: alle - * @throws InvalidPointParameter - */ - public function text(int $formatter = self::DD, string $delimiter = ',', ?int $precision = null): string - { - if ('' === $delimiter || str_contains($delimiter, '.')) { - throw new InvalidPointParameter(InvalidPointParameter::DELIMITER, [$delimiter]); - } - if (null !== $precision && 0 > $precision) { - throw new InvalidPointParameter(InvalidPointParameter::PRECISION, [$precision]); - } - - $loc = (string) setlocale(LC_NUMERIC, '0'); - setlocale(LC_NUMERIC, 'en_US.UTF-8'); - - if (self::DM === $formatter) { - $precision = $precision ?? max( - strlen((string) abs($this->lat() - (int) $this->lat())) - 2, - strlen((string) abs($this->lng() - (int) $this->lng())) - 2, - ); - $str = (new DecimalMinutes($delimiter)) - ->setDigits($precision) - ->setDecimalPoint('.') - ->format($this->coord); - } elseif (self::DMS === $formatter) { - $str = (new DMS2($delimiter)) - ->setDigits($precision) - ->setDecimalPoint('.') - ->format($this->coord); - } else { - $str = $this->lat($precision) . - $delimiter . - $this->lng($precision); - } - - setlocale(LC_NUMERIC, $loc); - return $str; - } - - /** - * Liefert für diesen Punkt ein Punkt-Element für einen geoJSON-Datensatz. - * $precision: Anzahl Nachkommastellen; default: alle. - * - * @api - * @return array{type:string,coordinates:array{0:float,1:float}} - */ - public function geoJSON(?int $precision = null): array - { - return [ - 'type' => 'Point', - 'coordinates' => $this->lngLat($precision), - ]; - } - - // Es folgen Methoden für Berechnungen ausgehend von diesem Punkt ------------------------------ - - /** - * Berechnet die kürzeste Distanz in Meter zwischen diesem und dem Zielpunkt auf dem Großkreis. - * siehe Doku: Die kürzeste Distanz kann auch über die Pole gehen oder über die Dataumsgrenze. - * - * @api - */ - public function distanceTo(self $point): float - { - return Math::distance($this, $point); - } - - /** - * Berechnet die Kompassrichtung (0°…360°) am Ausgangspunkt zwischen diesem und dem Zielpunkt - * auf dem Großkreis. - * - * @api - */ - public function bearingTo(self $point): float - { - return Math::bearingTo($this, $point); - } - - /** - * Berechnet die Kompassrichtung (0°…360°) am Zielpunkt zwischen diesem und dem Zielpunkt - * auf dem Großkreis. - * - * @api - */ - public function bearingAt(self $point): float - { - return Math::bearingFrom($this, $point); - } - - /** - * Berechnet den Zielpunkt ab diesem Punkt über Richtung in Grad (0°…360°, $bearing) - * und Distanz in Meter ($distance). - * - * Das Ergebnis ist der Zielpunkt - * - * @api - */ - public function moveBy(float $bearing, float $distance): self - { - return Math::goBearingDistance($this, $bearing, $distance); - } - - /** - * Sind zwei Punkte "identisch"? - * - * Die Funktion überprüft, ob dieser Pnkt und der als Parameter angegebene zweite - * Punkt ($point) innerhalb des erlaubsten Abstands ($allowedDistance) liegen. - * - * Je nach Zoomfaktor der Karte kann man hiermit zu nah beieinander liegende Punkte aussortieren. - * - * @api - */ - public function equals(self $point, float $allowedDistance = 0.1): bool - { - return $this->coord->hasSameLocation($point->coord, $allowedDistance); - } - - /** - * Liegt dieser Punkt außerhalb der Box, wird sie so erweitert, - * das der Punkt Teil der Box wird. - * Vorher isInBox aufzurufen ist nicht nötig. - * - * @api - */ - public function extendBox(Box $box): self - { - $box->extendBy($this); - return $this; - } - - /** - * Prüft ab, ob dieser Punkt innerhalb der Box liegt. - * - * @api - */ - public function isInBox(Box $box): bool - { - return $box->contains($this); - } -} +coord; + } + + // Es folgen Factory-Methoden zum Anlegen eines Punktes ---------------------------------------- + + /** + * Allgemeine Methode zum Anlegen des Koordinaten-Punktes. + * + * Die Koordinaten [$lat => latitude,$lng=>longitude] müssen existieren und numerisch sein. + * Die Einhaltung der Grenzen (-90...+90,-180...+180) wird von \Location\Coordinate geprüft + * + * Damit können auch Punkte aus Arrays mit Text-Keys angelegt werden + * $daten = ['lng'=>12.34,'lat'=>-1234]; + * $point = Point::factory( $daten, 'lat','lng'); + * + * @api + * @param array $point Koordinaten als Array mit zwei Werten + * @param int|string $keyLat Array-Key für das Element mit Breitangrad + * @param int|string $keyLng Array-Key für das Element mit Längengrad + * @throws InvalidPointParameter + */ + public static function factory(array $point, int|string $keyLat, int|string $keyLng): self + { + if ($keyLat === $keyLng) { + throw new InvalidPointParameter(InvalidPointParameter::KEY_LAT_LNG, [$keyLat, $keyLng]); + } + if (!isset($point[$keyLat])) { + throw new InvalidPointParameter(InvalidPointParameter::LAT_MISSING, [$keyLat]); + } + if (!isset($point[$keyLng])) { + throw new InvalidPointParameter(InvalidPointParameter::LNG_MISSING, [$keyLng]); + } + if (!is_numeric($point[$keyLat])) { + throw new InvalidPointParameter(InvalidPointParameter::LAT_RANGE, [$point[$keyLat]]); + } + if (!is_numeric($point[$keyLng])) { + throw new InvalidPointParameter(InvalidPointParameter::LNG_RANGE, [$point[$keyLng]]); + } + $latitude = Math::normalizeLatitude($point[$keyLat]); + $longitude = Math::normalizeLongitude($point[$keyLng]); + + return self::byCoordinate(new Coordinate($latitude, $longitude)); + } + + /** + * Koordinaten-Punkt auf Basis einer \Location\Coordinate anlegen. + * + * @api + */ + public static function byCoordinate(Coordinate $point): self + { + $self = new self(); + $self->coord = $point; + return $self; + } + + /** + * Koordinaten-Punkt aus einem numerischen Array [0=>latitude,1=>longitude] anlegen. + * Das ist die übliche Reihenfolge für LeafletJS. + * + * @api + * @param array $point + */ + public static function byLatLng(array $point): self + { + return self::factory($point, 0, 1); + } + + /** + * Koordinaten-Punkt aus einem numerischen Array [0=>longitude,1=>latitude] anlegen. + * Das ist die übliche Reihenfolge für geoJSON-Datensätze. + * + * @api + * @param array $point + */ + public static function byLngLat(array $point): self + { + return self::factory($point, 1, 0); + } + + /** + * Koordinaten-Punkt aus einem Textfeld anlegen. + * + * Die String-Auswertung durch \Location\Factory\CoordinateFactory nach Best Guess. Also wird + * nicht jeder String erkannt. CoordinateFactory hadert mit Punkt vs. Komma, also vor dem + * Aufruf die Locale korrigieren + * + * @api + * @see https://github.com/mjaschen/phpgeo/blob/master/docs/700_Parsing_and_Input/110_Coordinates_Parser.md + * @throws InvalidPointParameter + */ + public static function byText(string $point): self + { + try { + $loc = (string) setlocale(LC_NUMERIC, '0'); + setlocale(LC_NUMERIC, 'en_US.UTF-8'); + $coordinate = CoordinateFactory::fromString($point); + setlocale(LC_NUMERIC, $loc); + } catch (Exception $e) { + throw new InvalidPointParameter(InvalidPointParameter::STRING2DD, [$point], $e); + } + + return self::byCoordinate($coordinate); + } + + // Es folgen Methoden zur Ausgabe der Punkt-Daten ---------------------------------------------- + + /** + * Breitengrad. + * $precision: Anzahl Nachkommastellen; default: alle. + * + * @api + */ + public function lat(?int $precision = null): float + { + if (null === $precision) { + return $this->coord->getLat(); + } + return round($this->coord->getLat(), max(0, $precision)); + } + + /** + * Längengrad. + * $precision: Anzahl Nachkommastellen; default: alle. + * + * @api + */ + public function lng(?int $precision = null): float + { + if (null === $precision) { + return $this->coord->getLng(); + } + return round($this->coord->getLng(), max(0, $precision)); + } + + /** + * Gegenstück zu byLatLng: [0=>latitude,1=>longitude]. + * $precision: Anzahl Nachkommastellen; default: alle. + * + * @api + * @return array{0:float,1:float} + */ + public function latLng(?int $precision = null): array + { + return [$this->lat($precision), $this->lng($precision)]; + } + + /** + * Gegenstück zu byLngLat: [0=>longitude,1=>latitude]. + * $precision: Anzahl Nachkommastellen; default: alle. + * + * @api + * @return array{0:float,1:float} + */ + public function lngLat(?int $precision = null): array + { + return [$this->lng($precision), $this->lat($precision)]; + } + + /** + * Gegenstück zu factory: [$lat=>latitude,$lng=>longitude]. + * $precision: Anzahl Nachkommastellen; default: alle. + * + * @api + * @throws InvalidPointParameter + * @return array + */ + public function pos(int|string $keyLat, int|string $keyLng, ?int $precision = null): array + { + if ($keyLat === $keyLng) { + throw new InvalidPointParameter(InvalidPointParameter::KEY_LAT_LNG, [$keyLat, $keyLng]); + } + return [ + $keyLat => $this->lat($precision), + $keyLng => $this->lng($precision), + ]; + } + + /** + * Gegenstück zu byText: "latitude longitude". + * + * Die konkrete Ausgabe kann durch den gewählten Formatierer gesteuert werden: + * DD: als Grad/Dezimalstellen + * DM: als Grad und Minute/Dezimalstellen + * DMS: als Grad Minute Sekunde/Dezimalstellen + * + * Die Formatierer hadern mit Punkt vs. Komma, also vor dem Aufruf die Locale korrigieren + * + * @api + * @param int $formatter Formatierer: Point::DD, Point::DM, Point::DMS + * @param string $delimiter Trennzeichen zwischen lat/Lng; mindestens 1 Zeichen; default="," + * @param ?int $precision Anzahl Nachkommastellen; default: alle + * @throws InvalidPointParameter + */ + public function text(int $formatter = self::DD, string $delimiter = ',', ?int $precision = null): string + { + if ('' === $delimiter || str_contains($delimiter, '.')) { + throw new InvalidPointParameter(InvalidPointParameter::DELIMITER, [$delimiter]); + } + if (null !== $precision && 0 > $precision) { + throw new InvalidPointParameter(InvalidPointParameter::PRECISION, [$precision]); + } + + $loc = (string) setlocale(LC_NUMERIC, '0'); + setlocale(LC_NUMERIC, 'en_US.UTF-8'); + + if (self::DM === $formatter) { + $precision = $precision ?? max( + strlen((string) abs($this->lat() - (int) $this->lat())) - 2, + strlen((string) abs($this->lng() - (int) $this->lng())) - 2, + ); + $str = (new DecimalMinutes($delimiter)) + ->setDigits($precision) + ->setDecimalPoint('.') + ->format($this->coord); + } elseif (self::DMS === $formatter) { + $str = (new DMS2($delimiter)) + ->setDigits($precision) + ->setDecimalPoint('.') + ->format($this->coord); + } else { + $str = $this->lat($precision) . + $delimiter . + $this->lng($precision); + } + + setlocale(LC_NUMERIC, $loc); + return $str; + } + + /** + * Liefert für diesen Punkt ein Punkt-Element für einen geoJSON-Datensatz. + * $precision: Anzahl Nachkommastellen; default: alle. + * + * @api + * @return array{type:string,coordinates:array{0:float,1:float}} + */ + public function geoJSON(?int $precision = null): array + { + return [ + 'type' => 'Point', + 'coordinates' => $this->lngLat($precision), + ]; + } + + // Es folgen Methoden für Berechnungen ausgehend von diesem Punkt ------------------------------ + + /** + * Berechnet die kürzeste Distanz in Meter zwischen diesem und dem Zielpunkt auf dem Großkreis. + * siehe Doku: Die kürzeste Distanz kann auch über die Pole gehen oder über die Dataumsgrenze. + * + * @api + */ + public function distanceTo(self $point): float + { + return Math::distance($this, $point); + } + + /** + * Berechnet die Kompassrichtung (0°…360°) am Ausgangspunkt zwischen diesem und dem Zielpunkt + * auf dem Großkreis. + * + * @api + */ + public function bearingTo(self $point): float + { + return Math::bearingTo($this, $point); + } + + /** + * Berechnet die Kompassrichtung (0°…360°) am Zielpunkt zwischen diesem und dem Zielpunkt + * auf dem Großkreis. + * + * @api + */ + public function bearingAt(self $point): float + { + return Math::bearingFrom($this, $point); + } + + /** + * Berechnet den Zielpunkt ab diesem Punkt über Richtung in Grad (0°…360°, $bearing) + * und Distanz in Meter ($distance). + * + * Das Ergebnis ist der Zielpunkt + * + * @api + */ + public function moveBy(float $bearing, float $distance): self + { + return Math::goBearingDistance($this, $bearing, $distance); + } + + /** + * Sind zwei Punkte "identisch"? + * + * Die Funktion überprüft, ob dieser Pnkt und der als Parameter angegebene zweite + * Punkt ($point) innerhalb des erlaubsten Abstands ($allowedDistance) liegen. + * + * Je nach Zoomfaktor der Karte kann man hiermit zu nah beieinander liegende Punkte aussortieren. + * + * @api + */ + public function equals(self $point, float $allowedDistance = 0.1): bool + { + return $this->coord->hasSameLocation($point->coord, $allowedDistance); + } + + /** + * Liegt dieser Punkt außerhalb der Box, wird sie so erweitert, + * das der Punkt Teil der Box wird. + * Vorher isInBox aufzurufen ist nicht nötig. + * + * @api + */ + public function extendBox(Box $box): self + { + $box->extendBy($this); + return $this; + } + + /** + * Prüft ab, ob dieser Punkt innerhalb der Box liegt. + * + * @api + */ + public function isInBox(Box $box): bool + { + return $box->contains($this); + } +} diff --git a/package.yml b/package.yml index dfd8ee4..18f835c 100644 --- a/package.yml +++ b/package.yml @@ -1,5 +1,5 @@ package: geolocation -version: '2.1.2' +version: '2.1.3' author: Friends Of REDAXO supportpage: https://github.com/FriendsOfREDAXO/geolocation