Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IO GeoJSON Support #14

Merged
merged 45 commits into from
Jan 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
3acb675
Add Required Classes & Functions for GeoJSON POINT type.
Jan 19, 2019
bc07326
Add GeoJSON Polygon support
Jan 19, 2019
6994bd5
Use GeometryIOException::unsupportedGeoJSONType() helper function whe…
Jan 19, 2019
8279cbc
Refactor GeoJson Reader
Jan 20, 2019
e3c4417
Add GeoJSON Geometry Types: MULTIPOINT, MULTILINESTRING, MULTIPOLYGON
Jan 20, 2019
76a71c9
Update GeoJSON Reader doc-blocks to account for all @throws
Jan 20, 2019
878f2c1
Add FeatureCollection Tests
Jan 20, 2019
ac50e13
Add GeoJSON Writer Cupporting Point & MultiPoint
Jan 20, 2019
579d60c
Add LineString & MultiLineString to GeoJSONWriter
Jan 20, 2019
267471c
Add Polygon & Multi Polygon to GeoJSON Write
Jan 20, 2019
66c6ead
Add FeatureCollection to GeoJSON writer
Jan 20, 2019
45562d8
Add GeoJson Z-axis support
Jan 21, 2019
48bd2fb
Refactor Checking of GeoJSON FeatureCollection.Fetures array
Jan 21, 2019
0aeb6a4
Add GeoJSON genPoint ...$coods type as Float
Jan 21, 2019
7ee7faa
Change snake_case Param $geojson_array to camelCase $geojsonArray to …
Jan 21, 2019
f58518c
Return TypeHint GeoJSONWriter Private Functions
Jan 21, 2019
52cf6bf
Add GeoJSON json_decode checks in GeoJSONReader::read()
Jan 21, 2019
651a5a4
Merge GeoJSON Abstract Reader into Reader class
Jan 21, 2019
9cfd97f
Refactor GeoJSON Writer
Jan 21, 2019
a3eda38
Add string $context to GeometryIOException::invalidGeoJSON(...)
Jan 21, 2019
526ed7f
Remove GeoJSON spatial reference systems (SRIDs)
Jan 21, 2019
150043e
Fix error when writing GeoJSON it "Multi"-Class Geometry is passed.
Jan 21, 2019
b306873
Fix GeometryIOException::invalidGeoJSON(...) message part `GeoJson` t…
Jan 22, 2019
b69760d
Update IO GeoJson doc-blocks to fit standards
Jan 22, 2019
05017b4
Update GeoJSON SRID to 4326
Jan 22, 2019
54059b9
Remove Extra lines after ForEach statements in GeoJSON files
Jan 22, 2019
cd36118
Fix tests for GeoJSON SRID = 4326
Jan 22, 2019
6499d55
Remove GeoJSON isEmpty check when reading
Jan 22, 2019
41ad7b0
Simplify GeoJSON is3D return conditional
Jan 22, 2019
99140e1
Refactor GeometryCollection Verification in GeoJSONWriter
Jan 22, 2019
88b1fa1
Remove empty lines after GeoJSONReader cases
Jan 22, 2019
0ac1c49
Fix GeoJSONWriterTest function names to say "Write" not "Read"
Jan 22, 2019
1fe43e0
Simplify GeoJSONWriter::formatGeoJSONGeometry(...) function logic
Jan 22, 2019
a8f07f1
Remove unneeded GeoJSON GeometryCollection is_empty check
Jan 22, 2019
5c024b2
Type hint all GeoJSON read{Grometry} functions with ...$coords as array
Jan 22, 2019
3161c38
Update GeoJSONReader @throw blocks
Jan 23, 2019
151006b
Refactor GeoJSONWriter::FormatGeoJSONGeometry() to use a string array…
Jan 23, 2019
297af8d
Require CaseSensitive GeoJSON keys and types
Jan 23, 2019
6263978
Refactor IO GeoJSON array_key_exists to isset calls
Jan 23, 2019
86180cf
Refactor GeoJSONReader Gen* Functions to not Pack/Unpack aray parameters
Jan 23, 2019
79a4a7f
Fixes/Formatting for GeoJSONReader
Jan 24, 2019
d570ca9
Simplify GeoJSONReader @throws
Jan 24, 2019
e554f97
Add comment to GeoJSONWriter to note GeometryCollection Filtering
Jan 24, 2019
e591185
Remove isM from all tests
Jan 24, 2019
ab9b2bf
Remove Extra Lines
Jan 24, 2019
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
24 changes: 24 additions & 0 deletions src/Exception/GeometryIOException.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ public static function invalidEWKT() : GeometryIOException
return new self('Invalid EWKT.');
}

/**
* @param string $context
*
* @return GeometryIOException
*/
public static function invalidGeoJSON(string $context) : GeometryIOException
{
$message = sprintf('Invalid GeoJSON: %s.', $context);

return new static($message);
}

/**
* @param int $wkbType
*
Expand All @@ -47,6 +59,18 @@ public static function unsupportedWKBType(int $wkbType) : GeometryIOException
return new self($message);
}

/**
* @param string $geojsonType
*
* @return GeometryIOException
*/
public static function unsupportedGeoJSONType(string $geojsonType) : GeometryIOException
{
$message = sprintf('Unsupported GeoJSON type: %s.', $geojsonType);

return new static($message);
}

/**
* @param string $geometryType
*
Expand Down
309 changes: 309 additions & 0 deletions src/IO/GeoJSONReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
<?php

declare(strict_types = 1);

namespace Brick\Geo\IO;

use Brick\Geo\CoordinateSystem;
use Brick\Geo\Exception\GeometryException;
use Brick\Geo\Exception\GeometryIOException;
use Brick\Geo\Geometry;
use Brick\Geo\GeometryCollection;
use Brick\Geo\LineString;
use Brick\Geo\MultiLineString;
use Brick\Geo\MultiPoint;
use Brick\Geo\MultiPolygon;
use Brick\Geo\Point;
use Brick\Geo\Polygon;

/**
* Builds geometries out of GeoJSON Text strings.
*/
class GeoJSONReader
{
/**
* @param string $geojson The GeoJSON to read.
*
* @return Geometry
*
* @throws GeometryException If the GeoJSON file is invalid.
*/
public function read(string $geojson) : Geometry
{
$geojsonArray = json_decode($geojson, true);

if (json_last_error() !== JSON_ERROR_NONE) {
throw new GeometryIOException(json_last_error_msg(), json_last_error());
}

if (! is_array($geojsonArray)) {
throw GeometryIOException::invalidGeoJSON('Unable to parse GeoJSON String.');
}

$geometry = $this->readGeoJSON($geojsonArray);

return $geometry;
}

/**
* @param array $geojson
*
* @return Geometry
*
* @throws GeometryException If the GeoJSON file is invalid.
*/
private function readGeoJSON(array $geojson) : Geometry
{
if (! isset($geojson['type']) || ! is_string($geojson['type'])) {
throw GeometryIOException::invalidGeoJSON('Missing or Malformed "type" attribute.');
}

switch ($geojson['type']) {
case 'Feature':
return $this->readFeature($geojson);

case 'FeatureCollection':
if (! isset($geojson['features']) || ! is_array($geojson['features'])) {
throw GeometryIOException::invalidGeoJSON('Missing or Malformed "FeatureCollection.features" attribute.');
}

$geometries = [];

foreach ($geojson['features'] as $feature) {
$geometries[] = $this->readFeature($feature);
}

return GeometryCollection::of(...$geometries);

case 'Point':
case 'MultiPoint':
case 'LineString':
case 'MultiLineString':
case 'Polygon':
case 'MultiPolygon':
return $this->readGeometry($geojson);

default:
throw GeometryIOException::unsupportedGeoJSONType($geojson['type']);
}
}

/**
* @param array $feature
*
* @return Geometry
*
* @throws GeometryException If the GeoJSON file is invalid.
*/
private function readFeature(array $feature) : Geometry
{
// Verify Type 'Feature'
if (! isset($feature['type']) || 'Feature' !== $feature['type']) {
throw GeometryIOException::invalidGeoJSON('Missing or Malformed "Feature.type" attribute.');
}

// Verify Geometry exists and is array
if (! isset($feature['geometry']) || ! is_array($feature['geometry'])) {
throw GeometryIOException::invalidGeoJSON('Missing or Malformed "Feature.geometry" attribute.');
}

return $this->readGeometry($feature['geometry']);
}

/**
* @param array $geometry
*
* @return Geometry
*
* @throws GeometryException If the GeoJSON file is invalid.
*/
private function readGeometry(array $geometry) : Geometry
{
// Verify Geometry `type`
if (! isset($geometry['type']) || ! is_string($geometry['type'])) {
throw GeometryIOException::invalidGeoJSON('Missing or Malformed "Geometry.type" attribute.');
}

$geoType = $geometry['type'];

// Verify Geometry `coordinates`
if (! isset($geometry['coordinates']) || ! array($geometry['coordinates'])) {
throw GeometryIOException::invalidGeoJSON('Missing or Malformed "Geometry.coordinates" attribute.');
}

$geoCoords = $geometry['coordinates'];

$hasZ = $this->hasZ($geoCoords);
$hasM = false;
$srid = 4326;

$cs = new CoordinateSystem($hasZ, $hasM, $srid);

switch ($geoType) {
case 'Point':
return $this->genPoint($cs, $geoCoords);

case 'MultiPoint':
return $this->genMultiPoint($cs, $geoCoords);

case 'LineString':
return $this->genLineString($cs, $geoCoords);

case 'MultiLineString':
return $this->genMultiLineString($cs, $geoCoords);

case 'Polygon':
return $this->genPolygon($cs, $geoCoords);

case 'MultiPolygon':
return $this->genMultiPolygon($cs, $geoCoords);
}

throw GeometryIOException::unsupportedGeoJSONType($geoType);
}

/**
* [x, y]
*
* @param CoordinateSystem $cs
* @param array $coords
*
* @return Point
*
* @throws GeometryException If the GeoJSON file is invalid.
*/
private function genPoint(CoordinateSystem $cs, array $coords) : Point
{
return new Point($cs, ...$coords);
}

/**
* [[x, y], ...]
*
* @param CoordinateSystem $cs
* @param array $coords
*
* @return MultiPoint
*
* @throws GeometryException If the GeoJSON file is invalid.
*/
private function genMultiPoint(CoordinateSystem $cs, array $coords) : MultiPoint
{
$points = [];

foreach ($coords as $pointCoords) {
$points[] = $this->genPoint($cs, $pointCoords);
}

return new MultiPoint($cs, ...$points);
}

/**
* [[x, y], ...]
*
* @param CoordinateSystem $cs
* @param array $coords
*
* @return LineString
*
* @throws GeometryException If the GeoJSON file is invalid.
*/
private function genLineString(CoordinateSystem $cs, array $coords) : LineString
{
$points = [];

foreach ($coords as $pointCoords) {
$points[] = $this->genPoint($cs, $pointCoords);
}

return new LineString($cs, ...$points);
}

/**
* [[[x, y], ...], ...]
*
* @param CoordinateSystem $cs
* @param array $coords
*
* @return MultiLineString
*
* @throws GeometryException If the GeoJSON file is invalid.
*/
private function genMultiLineString(CoordinateSystem $cs, array $coords) : MultiLineString
{
$lineStrings = [];

foreach ($coords as $lineStringCoords) {
$lineStrings[] = $this->genLineString($cs, $lineStringCoords);
}

return new MultiLineString($cs, ...$lineStrings);
}

/**
* [[[x, y], ...], ...]
*
* @param CoordinateSystem $cs
* @param array $coords
*
* @return Polygon
*
* @throws GeometryException If the GeoJSON file is invalid.
*/
private function genPolygon(CoordinateSystem $cs, array $coords) : Polygon
{
$lineStrings = [];

foreach ($coords as $lineStringCoords) {
$lineStrings[] = $this->genLineString($cs, $lineStringCoords);
}

return new Polygon($cs, ...$lineStrings);
}

/**
* [[[x, y], ...], ...]
*
* @param CoordinateSystem $cs
* @param array $coords
*
* @return MultiPolygon
*
* @throws GeometryException If the GeoJSON file is invalid.
*/
private function genMultiPolygon(CoordinateSystem $cs, array $coords) : MultiPolygon
{
$polygons = [];

foreach ($coords as $polygonCoords) {
$polygons[] = $this->genPolygon($cs, $polygonCoords);
}

return new MultiPolygon($cs, ...$polygons);
}

/**
* @param $coords
*
* @return bool
michaelcurry marked this conversation as resolved.
Show resolved Hide resolved
*/
private function hasZ(array $coords)
{
if (empty($coords)) {
return false;
}

// At least one Geometry hasZ
if (! is_array($coords[0])) {
return 3 === count($coords);
}

foreach ($coords as $coord) {
if ($this->hasZ($coord)) {
return true;
}
}

return false;
}
}
Loading