diff --git a/config/postgis.php b/config/postgis.php index 17e9868..9b40c57 100644 --- a/config/postgis.php +++ b/config/postgis.php @@ -2,9 +2,12 @@ // config for Clickbar/Postgis use Clickbar\Postgis\IO\Generator\Geojson\GeojsonGenerator; +use Clickbar\Postgis\IO\Generator\WKT\WKTGenerator; return [ + 'schema' => 'public', + 'eloquent' => [ 'default_postgis_type' => 'geography', 'default_srid' => 4326, @@ -12,4 +15,8 @@ 'json_generator' => GeojsonGenerator::class, + 'insert_generator' => \Clickbar\Postgis\IO\Generator\WKB\WKBGenerator::class, + + 'string_generator' => \Clickbar\Postgis\IO\Generator\Geojson\GeojsonGenerator::class, + ]; diff --git a/src/Eloquent/HasPostgisColumns.php b/src/Eloquent/HasPostgisColumns.php index 19fe2ff..6a30752 100644 --- a/src/Eloquent/HasPostgisColumns.php +++ b/src/Eloquent/HasPostgisColumns.php @@ -3,6 +3,13 @@ namespace Clickbar\Postgis\Eloquent; use Clickbar\Postgis\Exception\PostgisColumnsNotDefinedException; +use Clickbar\Postgis\Geometries\Geometry; +use Clickbar\Postgis\Geometries\GeometryCollection; +use Clickbar\Postgis\Geometries\GeometryFactory; +use Clickbar\Postgis\IO\Generator\BaseGenerator; + +use Clickbar\Postgis\IO\Parser\WKB\WKBParser; +use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Support\Arr; trait HasPostgisColumns @@ -12,7 +19,7 @@ public function getPostgisTypeAndSrid(string $key) $this->assertKeyIsInPostgisColumns($key); $default = [ - 'geomtype' => config('postgis.eloquent.default_postgis_type'), + 'type' => config('postgis.eloquent.default_postgis_type'), 'srid' => config('postgis.eloquent.default_srid'), ]; @@ -32,6 +39,87 @@ public function getPostgisColumnNames() }, array_keys($this->postgisColumns)); } + private function getGenerator(): BaseGenerator + { + $generatorClass = config('postgis.insert_generator'); + + return new $generatorClass(); + } + + protected function geomFromText(Geometry $geometry, $srid = 4326) + { + $generator = $this->getGenerator(); + $geometrySql = $generator->toPostgisGeometrySql($geometry, config('postgis.schema', 'public')); + if ($geometry->hasSrid() && $geometry->getSrid() != $srid) { + $geometrySql = 'ST_TRANSFORM(' . $geometrySql . ', ' . $srid . ')'; + } + + return $this->getConnection()->raw($geometrySql); + } + + protected function geogFromText(Geometry $geometry, $srid = 4326) + { + $generator = $this->getGenerator(); + $geometrySql = $generator->toPostgisGeographySql($geometry, config('postgis.schema', 'public')); + + if ($geometry->hasSrid() && $geometry->getSrid() != $srid) { + $geometrySql = 'ST_TRANSFORM(' . $geometrySql . ', ' . $srid . ')'; + } + + return $this->getConnection()->raw($geometrySql); + } + + public function getGeometryAsInsertable(Geometry $geometry, array $columnConfig) + { + return match (strtoupper($columnConfig['type'])) { + 'GEOMETRY' => $this->geomFromText($geometry, $columnConfig['srid']), + default => $this->geogFromText($geometry, $columnConfig['srid']), + }; + } + + protected function performInsert(EloquentBuilder $query, array $options = []) + { + $geometryCache = []; + + foreach ($this->attributes as $key => $value) { + if ($value instanceof Geometry) { + $geometryCache[$key] = $value; //Preserve the geometry objects prior to the insert + if ($value instanceof GeometryCollection) { + // --> Only insertable into geometry column types + $this->attributes[$key] = $this->geomFromText($value); + } else { + $this->attributes[$key] = $this->geomFromText($value); + $columnConfig = $this->getPostgisTypeAndSrid($key); + $this->attributes[$key] = $this->getGeometryAsInsertable($value, $columnConfig); + } + } + } + + $insert = parent::performInsert($query, $options); + + foreach ($geometryCache as $key => $value) { + $this->attributes[$key] = $value; //Retrieve the geometry objects so they can be used in the model + } + + return $insert; //Return the result of the parent insert + } + + public function setRawAttributes(array $attributes, $sync = false) + { + $pgfields = $this->getPostgisColumnNames(); + + // postgis always returns the geometry as a WKB string, so we need to convert it to a Geometry object + $parser = new WKBParser(new GeometryFactory()); + + foreach ($attributes as $key => &$value) { + if (in_array($key, $pgfields) && is_string($value)) { + $value = $parser->parse($value); + } + } + + return parent::setRawAttributes($attributes, $sync); + } + protected function assertPostgisColumnsNotEmpty() { if (! property_exists($this, 'postgisColumns')) { diff --git a/src/Geometries/Geometry.php b/src/Geometries/Geometry.php index 134ec82..40f4bff 100644 --- a/src/Geometries/Geometry.php +++ b/src/Geometries/Geometry.php @@ -5,7 +5,7 @@ use Clickbar\Postgis\IO\Dimension; use JsonSerializable; -abstract class Geometry implements GeometryInterface, JsonSerializable +abstract class Geometry implements GeometryInterface, JsonSerializable, \Stringable { public function __construct( protected Dimension $dimension, @@ -29,6 +29,11 @@ public function getSrid(): ?int return $this->srid; } + public function hasSrid(): bool + { + return $this->srid !== null && $this->srid !== 0; + } + public function jsonSerialize(): mixed { $generatorClass = config('postgis.json_generator'); @@ -36,4 +41,17 @@ public function jsonSerialize(): mixed return json_encode($generator->generate($this)); } + + public function __toString(): string + { + $generatorClass = config('postgis.string_generator'); + $generator = new $generatorClass(); + + $generated = $generator->generate($this); + if (! is_string($generated)) { + return json_encode($generated); + } + + return $generated; + } } diff --git a/src/IO/Generator/BaseGenerator.php b/src/IO/Generator/BaseGenerator.php index aca37b4..98b3acd 100644 --- a/src/IO/Generator/BaseGenerator.php +++ b/src/IO/Generator/BaseGenerator.php @@ -59,4 +59,8 @@ abstract public function generateMultiPolygon(MultiPolygon $multiPolygon): mixed abstract public function generateMultiPoint(MultiPoint $multiPoint): mixed; abstract public function generateGeometryCollection(GeometryCollection $geometryCollection): mixed; + + abstract public function toPostgisGeometrySql(Geometry $geometry, string $schema): mixed; + + abstract public function toPostgisGeographySql(Geometry $geometry, string $schema): mixed; } diff --git a/src/IO/Generator/Geojson/GeojsonGenerator.php b/src/IO/Generator/Geojson/GeojsonGenerator.php index 4c3920a..4242dd5 100644 --- a/src/IO/Generator/Geojson/GeojsonGenerator.php +++ b/src/IO/Generator/Geojson/GeojsonGenerator.php @@ -99,4 +99,14 @@ public function generateGeometryCollection(GeometryCollection $geometryCollectio 'geometries' => array_map(fn (Geometry $geometry) => $this->generate($geometry), $geometryCollection->getGeometries()), ]; } + + public function toPostgisGeometrySql(Geometry $geometry, string $schema): mixed + { + return sprintf("%s.st_geomfromgeojson('%s')", $schema, json_encode($this->generate($geometry))); + } + + public function toPostgisGeographySql(Geometry $geometry, string $schema): mixed + { + return sprintf("%s.st_geomfromgeojson('%s')::geography", $schema, json_encode($this->generate($geometry))); + } } diff --git a/src/IO/Generator/WKB/WKBGenerator.php b/src/IO/Generator/WKB/WKBGenerator.php index 1d10195..3083c22 100644 --- a/src/IO/Generator/WKB/WKBGenerator.php +++ b/src/IO/Generator/WKB/WKBGenerator.php @@ -155,4 +155,14 @@ public function generateGeometryCollection(GeometryCollection $geometryCollectio return $this->byteStringBuilder->toByteString(true); } + + public function toPostgisGeometrySql(Geometry $geometry, string $schema): mixed + { + return sprintf("'%s'::geometry", $this->generate($geometry)); + } + + public function toPostgisGeographySql(Geometry $geometry, string $schema): mixed + { + return sprintf("'%s'::geography", $this->generate($geometry)); + } } diff --git a/src/IO/Generator/WKT/WKTGenerator.php b/src/IO/Generator/WKT/WKTGenerator.php index 0842180..32c8b0d 100644 --- a/src/IO/Generator/WKT/WKTGenerator.php +++ b/src/IO/Generator/WKT/WKTGenerator.php @@ -55,6 +55,17 @@ private function apply3dIfNeeded(string $type, Geometry $geometry): string return $type; } + public function generate(Geometry $geometry) + { + $wktWithoutSrid = parent::generate($geometry); + + if (! $geometry->hasSrid()) { + return $wktWithoutSrid; + } + + return sprintf('SRID=%d;%s', $geometry->getSrid(), $wktWithoutSrid); + } + public function generatePoint(Point $point): mixed { $wktType = $this->apply3dIfNeeded('POINT', $point); @@ -105,11 +116,21 @@ public function generateGeometryCollection(GeometryCollection $geometryCollectio { $geometryWktStrings = implode(',', array_map( function (Geometry $geometry) { - return $this->generate($geometry); + return parent::generate($geometry); }, $geometryCollection->getGeometries() )); return sprintf('GEOMETRYCOLLECTION(%s)', $geometryWktStrings); } + + public function toPostgisGeometrySql(Geometry $geometry, string $schema): mixed + { + return sprintf("%s.ST_GeomFromEWKT('%s')", $schema, $this->generate($geometry)); + } + + public function toPostgisGeographySql(Geometry $geometry, string $schema): mixed + { + return sprintf("%s.ST_GeogFromText('%s')", $schema, $this->generate($geometry)); + } } diff --git a/src/IO/Parser/WKB/WKBParser.php b/src/IO/Parser/WKB/WKBParser.php index fef24c9..3274cd0 100644 --- a/src/IO/Parser/WKB/WKBParser.php +++ b/src/IO/Parser/WKB/WKBParser.php @@ -24,6 +24,9 @@ public function parse($input): Geometry { $this->scanner = new Scanner($input); + $this->dimension = null; + $this->srid = null; + return $this->parseWkbSegment(); } diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php index 9fda505..72bbc67 100644 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -159,9 +159,11 @@ public function geometry($column, int|null $srid = 4326, string $postgisType = ' * @param bool $typmod * @return Fluent */ - public function geometrycollection($column, $srid = null, $dimensions = 2, $typmod = true) + public function geometrycollection($column, $srid = null) { - return $this->addCommand('geometrycollection', compact('column', 'srid', 'dimensions', 'typmod')); + $postgisType = 'GEOMETRY'; + + return $this->addColumn('geometrycollection', $column, compact('postgisType', 'srid')); } /** diff --git a/src/Schema/Grammars/PostgisGrammar.php b/src/Schema/Grammars/PostgisGrammar.php index c1982c3..b4b3b1b 100644 --- a/src/Schema/Grammars/PostgisGrammar.php +++ b/src/Schema/Grammars/PostgisGrammar.php @@ -3,6 +3,7 @@ namespace Clickbar\Postgis\Schema\Grammars; use Clickbar\Postgis\Exception\UnsupportedPostgisTypeException; +use Clickbar\Postgis\Schema\Blueprint; use Illuminate\Database\Schema\Grammars\PostgresGrammar; use Illuminate\Support\Fluent; @@ -62,18 +63,37 @@ public function typeMultilinestring(Fluent $column): string public function typeGeography(Fluent $column): string { - return 'GEOGRAPHY'; + return $this->createTypeDefinition($column, 'GEOGRAPHY'); } public function typeGeometry(Fluent $column): string { - return 'GEOMETRY'; + return $this->createTypeDefinition($column, 'GEOMETRY'); + } + + public function typeGeometryCollection(Fluent $column): string + { + return $this->createTypeDefinition($column, 'GEOMETRYCOLLECTION'); } /* * COMPILE Statements */ + /** + * Adds a statement to add a geometrycollection geometry column + * + * @param Blueprint $blueprint + * @param Fluent $command + * @return string + */ + public function compileGeometrycollection(Blueprint $blueprint, Fluent $command) + { + $command->type = 'GEOMETRYCOLLECTION'; + + return $this->compileGeometry($blueprint, $command); + } + /** * Adds a statement to create the postgis extension * @@ -131,7 +151,7 @@ protected function assertValidPostgisType(Fluent $column) throw new UnsupportedPostgisTypeException("Postgis type '$column->postgisType' is not a valid postgis type. Valid types are $implodedValidTypes"); } - if (! filter_var($column->srid, FILTER_VALIDATE_INT)) { + if (filter_var($column->srid, FILTER_VALIDATE_INT) === false) { throw new UnsupportedPostgisTypeException("The given SRID '$column->srid' is not valid. Only integers are allowed"); } @@ -144,7 +164,7 @@ private function createTypeDefinition(Fluent $column, $geometryType): string { $this->assertValidPostgisType($column); - $schema = config('postgis.schema', 'public'); // TODO: Add config + $schema = config('postgis.schema', 'public'); $type = strtoupper($column->postgisType); return $schema . '.' . $type . '(' . $geometryType . ', ' . $column->srid . ')';