diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7a1a68a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,46 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +`longitude-one/spatial-core` provides shared PHP primitives for LongitudeOne spatial libraries. +PSR-4 maps `LongitudeOne\Core\` to `lib/`, which contains `Enum/`, `Exception/`, and `Diagnostic/`. +Tests live in `tests/Unit/` and `tests/Contract/` under `LongitudeOne\Core\Tests\`. +Quality configurations and isolated tool dependencies live in `quality/`; CI lives in `.github/workflows/`. +Consult `README.md` for public behavior and `CONTRIBUTING.md` for the full contribution checklist. + +## Build, Test, and Development Commands + +Use PHP 8.3+ with `mbstring` and Composer. This library has no application server or asset build. + +- `composer install`: install library development dependencies. +- `composer upgrade-tools`: install or update project and quality-tool dependencies; inspect resulting changes. +- `composer validate --strict`: validate package metadata. +- `composer test`: run PHPUnit. +- `composer test-coverage`: generate coverage reports under `.phpunit.cache/`; requires a coverage driver. +- `composer quality`: run PHP-CS-Fixer checks, PHPStan level 9, PHPMD, and PHP_CodeSniffer. +- `composer quality-fix`: apply PHP-CS-Fixer formatting; rerun quality checks afterward. +- `composer markdownlint`: lint Markdown; install Node.js and `markdownlint-cli2` first. +- `git diff --check`: detect whitespace errors before committing. + +## Coding Style & Naming Conventions + +Use four-space indentation, LF endings, strict types, explicit types, and the existing copyright header. +Follow the configured Symfony/PHP-CS-Fixer and PSR-2-based rules. +Use PascalCase class names, camelCase methods and variables, and uppercase enum cases. +Match filenames to symbols, such as `GeometryTypeEnum.php`; preserve established enum backing values. + +## Testing Guidelines + +Use PHPUnit 12.5 with `*Test.php` classes and descriptive `test...` methods. +Mirror source organization in unit tests; place public-contract checks in `tests/Contract/`. +Run focused tests with `composer test -- --filter GeometryTypeEnumTest`. +Cover boundary cases, exceptions, and regressions. When extending an enum, verify every method for the added cases. +Generate coverage for executable changes; no numeric coverage minimum is configured in PHPUnit. + +## Commit & Pull Request Guidelines + +Follow Conventional Commits, as in history: `feat: ...`, `fix: ...`, or `chore: ...`. +Use optional scopes, for example `fix(range): report the correct axis error`. +Link the issue, explain behavioral changes, and report validation results in the PR. +All required checks must pass before pushing. Obtain independent review and Alexandre’s merge decision. +Do not mark work Done before merge. Escalate unresolved public API, compatibility, architecture, or normative decisions to Alexandre. diff --git a/CHANGELOG.md b/CHANGELOG.md index aa380cb..740a14c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ All notable changes to this project will be documented in this file. See [commit ### ✨ New Features -* Implement DiagnosticValueFormatter ([#4](https://github.com/longitude-one/spatial-core/issues/4)) ([db6f205](https://github.com/longitude-one/spatial-core/commit/db6f205768ace5ae3c9bbc1a3736f4926681799b)), closes [#3](https://github.com/longitude-one/spatial-core/issues/3) +* Implement DiagnosticValueFormatter ([#4](https://github.com/longitude-one/spatial-core/issues/4)) ([db6f205](https://github.com/longitude-one/spatial-core/commit/db6f205768ace5ae3c9bbc1a3736f4926681799b)) ### 🔧 Maintenance diff --git a/README.md b/README.md index 8168dec..e001270 100644 --- a/README.md +++ b/README.md @@ -149,13 +149,20 @@ assert(!$dimension->isCurve()); ### `GeometryTypeEnum` -`GeometryTypeEnum` names the concrete shape of a spatial value, such as a point, line string, polygon, -or geometry collection. It exposes a type's homogeneous component and topological dimension, making -it useful for creating and validating multipart geometries. Its `isInstantiable()` method returns -`false` for the abstract `SURFACE` type and `true` for every other enum case, including `MULTICURVE` -and `MULTISURFACE`. The classification considers OGC Simple Feature Access 1.2.1 sections 6.1.2, -6.1.10, and 6.1.13, and ISO/IEC CD 13249-3:201x(E) sections 4.2, 4.2.8, 4.2.16, and 4.2.18; the ISO -source is a Committee Draft, not a published ISO edition. +`GeometryTypeEnum` identifies geometry types and exposes their homogeneous component and topological +dimension. Its `isInstantiable()` method returns `false` for `GEOMETRY`, `CURVE`, `SOLID`, and `SURFACE`, +and `true` for every other case, including `MULTICURVE` and `MULTISURFACE`. + +The additional types `BREPSOLID`, `CIRCLE`, `CLOTHOID`, `COMPOUNDSURFACE`, `ELLIPTICALCURVE`, +`GEODESICSTRING`, `NURBSCURVE`, and `SPIRALCURVE` are instantiable. `BREPSOLID` has volume dimension, +`COMPOUNDSURFACE` has surface dimension, and the remaining types have curve dimension. None is a +multi-geometry or a geometry collection. `CIRCLE` and `GEODESICSTRING` have `POINT` components. +The other additions have no single concrete component type exposed by this enum: compound surfaces +and solid shells can use different surface subtypes, while NURBS curves use weighted control points +and knots, and the other curves use constructive parameters. + +These additions follow ISO/IEC DIS 13249-3, fifth-edition ballot text N2593 dated 2015-01-25, +clauses 4.2.7–4.2.12, 4.2.20, 4.2.22, 7.4.1–7.9.1, 8.7.1, and 9.2.1. Enum membership does not imply concrete geometry classes or support for encoding or decoding these types. ```php use LongitudeOne\Core\Enum\GeometryTypeEnum; diff --git a/lib/Enum/GeometryTypeEnum.php b/lib/Enum/GeometryTypeEnum.php index 63ba88c..99eb109 100644 --- a/lib/Enum/GeometryTypeEnum.php +++ b/lib/Enum/GeometryTypeEnum.php @@ -18,18 +18,36 @@ */ enum GeometryTypeEnum: string { + /** A solid bounded by exterior and interior surface shells. */ + case BREPSOLID = 'BRepSolid'; + + /** A simple closed circular curve. */ + case CIRCLE = 'Circle'; + /** A route formed from circular arcs. */ case CIRCULARSTRING = 'CircularString'; + /** A curve with clothoid interpolation. */ + case CLOTHOID = 'Clothoid'; + /** A route made of linear and circular segments. */ case COMPOUNDCURVE = 'CompoundCurve'; + /** A surface formed from connected surface patches. */ + case COMPOUNDSURFACE = 'CompoundSurface'; + /** An abstract curve geometry type. */ case CURVE = 'Curve'; /** An area bounded by curves. */ case CURVEPOLYGON = 'CurvePolygon'; + /** A curve with elliptical interpolation. */ + case ELLIPTICALCURVE = 'EllipticalCurve'; + + /** A route formed from geodesic segments. */ + case GEODESICSTRING = 'GeodesicString'; + /** The generic geometry type. */ case GEOMETRY = 'Geometry'; @@ -54,6 +72,9 @@ enum GeometryTypeEnum: string /** A collection of surfaces. */ case MULTISURFACE = 'MultiSurface'; + /** A non-uniform rational B-spline curve. */ + case NURBSCURVE = 'NURBSCurve'; + /** A location. */ case POINT = 'Point'; @@ -66,6 +87,9 @@ enum GeometryTypeEnum: string /** A solid geometry type. */ case SOLID = 'Solid'; + /** A curve with spiral interpolation. */ + case SPIRALCURVE = 'SpiralCurve'; + /** An abstract surface geometry type. */ case SURFACE = 'Surface'; @@ -81,7 +105,7 @@ enum GeometryTypeEnum: string public function componentType(): ?self { return match ($this) { - self::CIRCULARSTRING, self::LINESTRING, self::MULTIPOINT => self::POINT, + self::CIRCLE, self::CIRCULARSTRING, self::GEODESICSTRING, self::LINESTRING, self::MULTIPOINT => self::POINT, self::POLYGON, self::MULTILINESTRING, self::TRIANGLE => self::LINESTRING, self::MULTIPOLYGON, self::POLYHEDRALSURFACE => self::POLYGON, self::TIN => self::TRIANGLE, @@ -135,12 +159,19 @@ public function topologicalDimension(): ?TopologicalDimensionEnum { return match ($this) { self::POINT, self::MULTIPOINT => TopologicalDimensionEnum::POINT, + self::CIRCLE, self::CIRCULARSTRING, + self::CLOTHOID, self::COMPOUNDCURVE, self::CURVE, + self::ELLIPTICALCURVE, + self::GEODESICSTRING, self::LINESTRING, self::MULTICURVE, - self::MULTILINESTRING => TopologicalDimensionEnum::CURVE, + self::MULTILINESTRING, + self::NURBSCURVE, + self::SPIRALCURVE => TopologicalDimensionEnum::CURVE, + self::COMPOUNDSURFACE, self::CURVEPOLYGON, self::MULTIPOLYGON, self::MULTISURFACE, @@ -149,7 +180,7 @@ public function topologicalDimension(): ?TopologicalDimensionEnum self::SURFACE, self::TIN, self::TRIANGLE => TopologicalDimensionEnum::SURFACE, - self::SOLID => TopologicalDimensionEnum::VOLUME, + self::BREPSOLID, self::SOLID => TopologicalDimensionEnum::VOLUME, default => null, }; } diff --git a/quality/markdownlint/.markdownlint-cli2.json b/quality/markdownlint/.markdownlint-cli2.json index 9be23e6..5b42e27 100644 --- a/quality/markdownlint/.markdownlint-cli2.json +++ b/quality/markdownlint/.markdownlint-cli2.json @@ -2,7 +2,8 @@ "config": { "MD013": { "line_length": 200 - } + }, + "MD024": false }, "globs": [ "**/*.md" diff --git a/tests/Unit/Enum/GeometryTypeEnumTest.php b/tests/Unit/Enum/GeometryTypeEnumTest.php index 7b4808f..c675d05 100644 --- a/tests/Unit/Enum/GeometryTypeEnumTest.php +++ b/tests/Unit/Enum/GeometryTypeEnumTest.php @@ -26,6 +26,37 @@ */ final class GeometryTypeEnumTest extends TestCase { + /** + * Additional ISO geometry types retain their identity and complete classification. + */ + public function testAdditionalGeometryTypes(): void + { + $expected = [ + ['BREPSOLID', 'BRepSolid', TopologicalDimensionEnum::VOLUME, null], + ['CIRCLE', 'Circle', TopologicalDimensionEnum::CURVE, GeometryTypeEnum::POINT], + ['CLOTHOID', 'Clothoid', TopologicalDimensionEnum::CURVE, null], + ['COMPOUNDSURFACE', 'CompoundSurface', TopologicalDimensionEnum::SURFACE, null], + ['ELLIPTICALCURVE', 'EllipticalCurve', TopologicalDimensionEnum::CURVE, null], + ['GEODESICSTRING', 'GeodesicString', TopologicalDimensionEnum::CURVE, GeometryTypeEnum::POINT], + ['NURBSCURVE', 'NURBSCurve', TopologicalDimensionEnum::CURVE, null], + ['SPIRALCURVE', 'SpiralCurve', TopologicalDimensionEnum::CURVE, null], + ]; + + foreach ($expected as [$name, $value, $dimension, $component]) { + $type = GeometryTypeEnum::from($value); + + self::assertSame($name, $type->name); + self::assertSame($value, $type->value); + self::assertSame($type, GeometryTypeEnum::tryFrom($value)); + self::assertContains($type, GeometryTypeEnum::cases()); + self::assertSame($dimension, $type->topologicalDimension()); + self::assertSame($component, $type->componentType()); + self::assertTrue($type->isInstantiable()); + self::assertFalse($type->isCollection()); + self::assertFalse($type->isMulti()); + } + } + /** * A circular route remains a curve made from point locations. */ @@ -112,6 +143,14 @@ public function testGenericMultipartGeometries(): void public function testInstantiabilityClassification(): void { $expected = [ + GeometryTypeEnum::SPIRALCURVE->name => true, + GeometryTypeEnum::NURBSCURVE->name => true, + GeometryTypeEnum::GEODESICSTRING->name => true, + GeometryTypeEnum::ELLIPTICALCURVE->name => true, + GeometryTypeEnum::COMPOUNDSURFACE->name => true, + GeometryTypeEnum::CLOTHOID->name => true, + GeometryTypeEnum::CIRCLE->name => true, + GeometryTypeEnum::BREPSOLID->name => true, GeometryTypeEnum::CIRCULARSTRING->name => true, GeometryTypeEnum::COMPOUNDCURVE->name => true, GeometryTypeEnum::CURVE->name => false,