Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
"type": "cakephp-plugin",
"license": "MIT",
"require": {
"php": ">=7.4",
"cakephp/cakephp": "^4.2",
"cakephp/migrations": "^3.0",
"php": "^7.4 || ^8.0",
"cakephp/cakephp": "^3.10 || ^4.2",
"brick/geo": "^0.7.1"
},
"require-dev": {
Expand All @@ -29,7 +28,7 @@
"@test",
"@cs-check"
],
"cs-check": "phpcs --colors -p src/ tests/",
"cs-check": "phpcs --colors -p src/ tests/",
"cs-fix": "phpcbf --colors -p src/ tests/",
"stan": "phpstan analyse",
"test": "phpunit --colors=always"
Expand Down
66 changes: 12 additions & 54 deletions src/Database/Type/GeometryType.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,10 @@

namespace Chialab\Geometry\Database\Type;

use Brick\Geo\Exception\GeometryException;
use Brick\Geo\Exception\GeometryIOException;
use Brick\Geo\Geometry as BrickGeometry;
use Brick\Geo\IO\EWKBReader;
use Brick\Geo\IO\EWKTReader;
use Brick\Geo\IO\GeoJSONReader;
use Brick\Geo\IO\WKBReader;
use Cake\Database\Driver;
use Cake\Database\DriverInterface;
use Cake\Database\TypeInterface;
use Chialab\Geometry\Geometry;
use InvalidArgumentException;
use PDO;

/**
Expand Down Expand Up @@ -58,54 +50,26 @@ public function getBaseType(): string
}

/**
* Parse string or JSON into Geometry object.
* Check if the value is a null geometry.
*
* @param mixed $geometry Geometry.
* @return \Brick\Geo\Geometry
* @param mixed $value The value to check.
* @return bool
*/
protected static function parseGeometry($geometry): BrickGeometry
protected static function isNullGeometry($value): bool
{
if ($geometry instanceof Geometry) {
return $geometry->getGeometry();
}

if ($geometry instanceof BrickGeometry) {
return $geometry;
}

if (is_string($geometry)) {
try {
return (new EWKBReader())->read($geometry);
} catch (GeometryIOException $e) {
// Not a WKB string.
}

try {
return (new EWKTReader())->read($geometry);
} catch (GeometryIOException $e) {
// Not a WKT string.
}
}

try {
return (new GeoJSONReader())->read(is_string($geometry) ? $geometry : json_encode($geometry));
} catch (GeometryException $e) {
// Not a GeoJSON.
}

throw new InvalidArgumentException('Could not parse geometry object');
return $value === null || $value === '';
}

/**
* @inheritdoc
*/
public function toDatabase($value, DriverInterface $driver): ?string
{
if ($value === null) {
if (static::isNullGeometry($value)) {
return null;
}

$geometry = static::parseGeometry($value);
$geometry = Geometry::parse($value)->getGeometry();
$wkb = $geometry->asBinary();
if ($driver instanceof Driver\Mysql) {
$wkb = pack('V', $geometry->SRID()) . $wkb;
Expand All @@ -119,25 +83,19 @@ public function toDatabase($value, DriverInterface $driver): ?string
*/
public function toPHP($value, DriverInterface $driver): ?Geometry
{
if ($value === null) {
if (static::isNullGeometry($value)) {
return null;
}

[$wkb, $srid] = [$value, 0];
if ($driver instanceof Driver\Mysql) {
[$wkb, $srid] = [substr($value, 4), bindec(substr($value, 0, 4))];
}
$reader = new WKBReader();

return new Geometry($reader->read($wkb, $srid));
return Geometry::parse($value);
}

/**
* @inheritdoc
*/
public function toStatement($value, DriverInterface $driver): int
{
if ($value === null) {
if (static::isNullGeometry($value)) {
return PDO::PARAM_NULL;
}

Expand All @@ -149,11 +107,11 @@ public function toStatement($value, DriverInterface $driver): int
*/
public function marshal($value): ?Geometry
{
if ($value === null) {
if (static::isNullGeometry($value)) {
return null;
}

return new Geometry(static::parseGeometry($value));
return Geometry::parse($value);
}

/**
Expand Down
82 changes: 80 additions & 2 deletions src/Geometry.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@

namespace Chialab\Geometry;

use Brick\Geo\Exception\GeometryException;
use Brick\Geo\Exception\GeometryIOException;
use Brick\Geo\Geometry as BrickGeometry;
use Brick\Geo\IO\EWKBReader;
use Brick\Geo\IO\EWKTReader;
use Brick\Geo\IO\GeoJSONReader;
use Brick\Geo\IO\GeoJSONWriter;
use Brick\Geo\IO\WKBReader;
use Brick\Geo\IO\WKTWriter;
use InvalidArgumentException;

/**
* Serializable Geometry wrapper.
Expand All @@ -26,6 +34,55 @@ public function __construct(BrickGeometry $geometry)
$this->geometry = $geometry;
}

/**
* Parse string or JSON into Geometry object.
*
* @param mixed $geometry Geometry.
* @return static
*/
public static function parse($geometry): self
{
if ($geometry instanceof static) {
return clone $geometry;
}

if ($geometry instanceof BrickGeometry) {
return new static($geometry);
}

if (is_string($geometry)) {
try {
return new static((new EWKBReader())->read($geometry));
} catch (GeometryIOException $e) {
// Not a WKB string.
}

if (strlen($geometry) > 4) {
try {
[$wkb, $srid] = [substr($geometry, 4), bindec(substr($geometry, 0, 4))];

return new static((new WKBReader())->read($wkb, $srid));
} catch (GeometryIOException $e) {
// Not a WKB string with SRID prefix.
}
}

try {
return new static((new EWKTReader())->read($geometry));
} catch (GeometryIOException $e) {
// Not a WKT string.
}
}

try {
return new static((new GeoJSONReader())->read(is_string($geometry) ? $geometry : json_encode($geometry)));
} catch (GeometryException $e) {
// Not a GeoJSON.
}

throw new InvalidArgumentException('Could not parse geometry object');
}

/**
* Get the brick geometry instance.
*
Expand All @@ -43,8 +100,29 @@ public function getGeometry(): BrickGeometry
*/
public function jsonSerialize(): \stdClass
{
$writer = new GeoJSONWriter();
return (new GeoJSONWriter())->writeRaw($this->geometry);
}

/**
* Return debugging info.
*
* @return array
*/
public function __debugInfo()
{
return [
'type' => $this->geometry->geometryType(),
'wkt' => (new WKTWriter())->write($this->geometry),
];
}

return $writer->writeRaw($this->geometry);
/**
* Clone the geometry object.
*
* @return void
*/
public function __clone()
{
$this->geometry = clone $this->geometry;
}
}
93 changes: 93 additions & 0 deletions src/Model/Behavior/GeometryBehavior.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);

namespace Chialab\Geometry\Model\Behavior;

use Cake\Database\Expression\FunctionExpression;
use Cake\Database\Expression\QueryExpression;
use Cake\ORM\Behavior;
use Cake\ORM\Query;
use Chialab\Geometry\Geometry;

/**
* Geometry behavior
*/
class GeometryBehavior extends Behavior
{
/**
* @inheritDoc
*/
protected $_defaultConfig = [
'geometryField' => 'geometry',
'storedAs' => 'geometry',
'implementedFinders' => [
'geo' => 'findGeo',
],
];

/**
* @inheritDoc
*/
public function initialize(array $config): void
{
parent::initialize($config);

$this->_table
->getSchema()
->setColumnType($this->getConfigOrFail('geometryField'), 'geometry');
}

/**
* Find an object by its geometrical properties.
*
* @param \Cake\ORM\Query $query Query object instance.
* @param array $options Filter options.
* @return \Cake\ORM\Query
*/
public function findGeo(Query $query, array $options): Query
{
$options = array_intersect_key($options, array_flip(['intersects', 'within']));

$dbField = $this->_table->aliasField($this->getConfigOrFail('geometryField'));
$dbGeom = [$dbField => 'identifier'];
switch ($this->getConfig('storedAs')) {
case 'geometry':
break;
case 'text':
case 'wkt':
$dbGeom = [new FunctionExpression('ST_GeomFromText', $dbGeom)];
break;
case 'binary':
case 'wkb':
$dbGeom = [new FunctionExpression('ST_GeomFromWKB', $dbGeom)];
break;
default:
throw new \RuntimeException('Invalid geometry storage format');
}

foreach ($options as $op => $geom) {
switch ($op) {
case 'intersects':
$geom = Geometry::parse($geom)->getGeometry()->withSRID(0);

$query = $query->where(fn (QueryExpression $exp) => $exp
->isNotNull($dbField)
->notEq($dbField, '', 'string')
->add(new FunctionExpression('ST_Intersects', array_merge($dbGeom, ['test' => $geom]), ['test' => 'geometry'])));

break;
case 'within':
$geom = Geometry::parse($geom)->getGeometry()->withSRID(0);

$query = $query->where(fn (QueryExpression $exp) => $exp
->isNotNull($dbField)
->notEq($dbField, '', 'string')
->add(new FunctionExpression('ST_Within', array_merge($dbGeom, ['test' => $geom]), ['test' => 'geometry'])));

break;
}
}

return $query;
}
}
9 changes: 8 additions & 1 deletion src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use Cake\Core\BasePlugin;
use Cake\Core\PluginApplicationInterface;
use Cake\Database\Type;
use Cake\Database\TypeFactory;
use Chialab\Geometry\Database\Type\GeometryType;

Expand All @@ -20,6 +21,12 @@ public function bootstrap(PluginApplicationInterface $app): void
{
parent::bootstrap($app);

TypeFactory::map('geometry', GeometryType::class);
if (class_exists(TypeFactory::class)) {
// CakePHP 4.x
TypeFactory::map('geometry', GeometryType::class);
} else {
// CakePHP 3.x
Type::map('geometry', GeometryType::class);
}
}
}