From 6c11a2ea68b41cb9031e7c6d763e452095f6270d Mon Sep 17 00:00:00 2001 From: Jeyaraman-Esakki Date: Thu, 27 Jun 2019 21:14:48 +0530 Subject: [PATCH] Implementation of Spatial LRS and Util Functions --- .../DefaultTemplate.11.1.xaml | 543 ------- BuildProcessTemplates/DefaultTemplate.xaml | 602 -------- .../LabDefaultTemplate.11.xaml | 208 --- BuildProcessTemplates/UpgradeTemplate.xaml | 76 - Functions.cs | 421 ------ KMLProcessor/Constants.cs | 56 - KMLProcessor/FilterSetSridGeographySink.cs | 79 - KMLProcessor/GeographyContext.cs | 55 - KMLProcessor/Import/AltitudeMode.cs | 18 - KMLProcessor/Import/Geography.cs | 225 --- KMLProcessor/Import/GroundOverlay.cs | 107 -- KMLProcessor/Import/KMLParser.cs | 1211 --------------- KMLProcessor/Import/KMLProcessor.cs | 163 -- KMLProcessor/Import/LatLonAltBox.cs | 93 -- KMLProcessor/Import/LatLonBox.cs | 33 - KMLProcessor/Import/LatLonBoxBase.cs | 217 --- KMLProcessor/Import/LatLonQuad.cs | 84 -- KMLProcessor/Import/LineString.cs | 128 -- KMLProcessor/Import/LinearRing.cs | 63 - KMLProcessor/Import/Model.cs | 140 -- KMLProcessor/Import/MultiGeometry.cs | 80 - KMLProcessor/Import/Placemark.cs | 116 -- KMLProcessor/Import/Point.cs | 219 --- KMLProcessor/Import/Polygon.cs | 135 -- KMLProcessor/Import/Region.cs | 61 - KMLProcessor/KmlFunctions.cs | 275 ---- KMLProcessor/Utilities.cs | 43 - Projections/AlbersEqualAreaProjection.cs | 85 -- .../LambertConformalConicProjection.cs | 77 - Projections/ObliqueMercatorProjection.cs | 88 -- SQL Scripts/Register.sql | 96 -- SQL Scripts/Unregister.sql | 36 - SQLSpatialTools.sln | 43 +- SQLSpatialTools.sln.DotSettings | 46 + SQLSpatialTools.suo | Bin 43008 -> 0 bytes Sinks/GeographyFilters.cs | 345 ----- Sinks/GeometryFilters.cs | 339 ----- Sinks/Projector.cs | 54 - Sinks/Unprojector.cs | 54 - Sinks/VacuousGeographyToGeometrySink.cs | 58 - Sinks/VacuousGeometryToGeographySink.cs | 58 - SpatialTools.pfx | Bin 0 -> 1764 bytes Types/AffineTransform.cs | 127 -- Types/SqlProjection.cs | 284 ---- .../Aggregates}/GeographyUnionAggregate.cs | 614 ++++---- .../Aggregates}/GeometryEnvelopeAggregate.cs | 205 +-- src/Functions/General/Geography.cs | 346 +++++ src/Functions/General/Geometry.cs | 209 +++ src/Functions/LRS/Geometry.cs | 1120 ++++++++++++++ src/Functions/Util/Geometry.cs | 202 +++ src/KMLProcessor/Constants.cs | 32 + .../KMLProcessor}/Export/ExportContext.cs | 269 ++-- .../Export/KeyholeMarkupLanguageBase.cs | 239 ++- .../Export/KeyholeMarkupLanguageGeography.cs | 486 +++--- .../Export/KeyholeMarkupLanguageGeometry.cs | 340 +++-- .../FilterSetSridGeographySink.cs | 80 + src/KMLProcessor/GeographyContext.cs | 31 + src/KMLProcessor/Import/AltitudeMode.cs | 18 + src/KMLProcessor/Import/Geography.cs | 180 +++ src/KMLProcessor/Import/GroundOverlay.cs | 80 + src/KMLProcessor/Import/KMLParser.cs | 1128 ++++++++++++++ src/KMLProcessor/Import/KMLProcessor.cs | 122 ++ src/KMLProcessor/Import/LatLonAltBox.cs | 84 ++ src/KMLProcessor/Import/LatLonBox.cs | 21 + src/KMLProcessor/Import/LatLonBoxBase.cs | 154 ++ src/KMLProcessor/Import/LatLonQuad.cs | 79 + src/KMLProcessor/Import/LineString.cs | 116 ++ src/KMLProcessor/Import/LinearRing.cs | 57 + src/KMLProcessor/Import/Model.cs | 79 + src/KMLProcessor/Import/MultiGeometry.cs | 72 + src/KMLProcessor/Import/Placemark.cs | 71 + src/KMLProcessor/Import/Point.cs | 163 ++ src/KMLProcessor/Import/Polygon.cs | 119 ++ src/KMLProcessor/Import/Region.cs | 48 + .../KMLProcessor}/KMLException.cs | 73 +- src/KMLProcessor/KmlFunctions.cs | 287 ++++ src/KMLProcessor/Utilities.cs | 44 + src/Projections/AlbersEqualAreaProjection.cs | 85 ++ .../Projections}/EquirectangularProjection.cs | 91 +- .../Projections}/GnomonicProjection.cs | 172 +-- .../LambertConformalConicProjection.cs | 78 + {Projections => src/Projections}/MathX.cs | 184 +-- .../Projections}/MercatorProjection.cs | 94 +- src/Projections/ObliqueMercatorProjection.cs | 89 ++ .../Projections}/Projection.cs | 201 ++- .../TransverseMercatorProjection.cs | 171 +-- src/Properties/AssemblyInfo.cs | 43 + .../Resource/Resource.Designer.cs | 236 +-- Resource.resx => src/Resource/Resource.resx | 274 ++-- src/SQL Scripts/Register.sql | 401 +++++ src/SQL Scripts/RegisterAndValidate.sql | 25 + src/SQL Scripts/Unregister.sql | 150 ++ .../SQL Scripts}/aggregate_example.sql | 12 +- src/SQL Scripts/lrs_geometry_example.sql | 176 +++ .../SQL Scripts}/makevalid_example.sql | 14 +- .../SQL Scripts}/projection_example.sql | 12 +- .../SQL Scripts}/transform_example.sql | 20 +- src/SQL Scripts/util_geometry_example.sql | 36 + .../SQLSpatialTools.csproj | 426 +++--- .../Sinks/Geography}/DensifyGeographySink.cs | 246 +-- src/Sinks/Geography/GeographyFilters.cs | 367 +++++ .../Geography}/LocateAlongGeographySink.cs | 208 +-- src/Sinks/Geography/Projector.cs | 62 + .../VacuousGeographyToGeometrySink.cs | 62 + src/Sinks/Geometry/BuidLRSMultiLineSink.cs | 83 + src/Sinks/Geometry/BuildLRSMultiLineSink.cs | 73 + .../Geometry/BuildMultiLineFromLinesSink.cs | 84 ++ .../Geometry/ClipMGeometrySegmentSink.cs | 272 ++++ .../Geometry/ConvertXYZ2XYMGeometrySink.cs | 40 + .../ExtractPolygonFromLineGeometrySink.cs | 118 ++ .../Geometry/GISTypeCheckGeometrySink.cs | 82 + src/Sinks/Geometry/GeometryFilters.cs | 340 +++++ .../Geometry}/GeometryToPointGeographySink.cs | 132 +- .../Sinks/Geometry}/GeometryTransformer.cs | 104 +- .../Geometry/LineStringMergeGeometrySink.cs | 95 ++ .../Geometry}/LocateAlongGeometrySink.cs | 214 +-- .../Geometry/LocateMAlongGeometrySink.cs | 133 ++ .../Geometry/MultiplyMeasureGeometrySink.cs | 78 + .../Geometry/PopulateGeometryMeasuresSink.cs | 131 ++ src/Sinks/Geometry/ResetMGeometrySink.cs | 73 + .../ReverseAndTranslateGeometrySink.cs | 101 ++ .../Geometry/ReverseLinearGeometrySink.cs | 99 ++ src/Sinks/Geometry/ScaleGeometrySink.cs | 92 ++ .../Sinks/Geometry}/ShiftGeometrySink.cs | 131 +- .../Geometry/SplitGeometrySegmentSink.cs | 209 +++ .../Geometry/TranslateMeasureGeometrySink.cs | 75 + src/Sinks/Geometry/Unprojector.cs | 59 + .../VacuousGeometryToGeographySink.cs | 62 + src/Types/LRSEnumerator.cs | 57 + src/Types/LRSLine.cs | 559 +++++++ src/Types/LRSMultiLine.cs | 419 ++++++ src/Types/LRSPoint.cs | 606 ++++++++ src/Types/SQL/AffineTransform.cs | 115 ++ src/Types/SQL/SqlProjection.cs | 278 ++++ {Utility => src/Types}/Vector3.cs | 197 +-- src/Types/Vertex.cs | 30 + src/Utility/EnumsAndConstants.cs | 155 ++ src/Utility/ErrorMessage.cs | 27 + src/Utility/SQLTypeConversions.cs | 31 + src/Utility/SpatialExtensions.cs | 1332 +++++++++++++++++ Utility/Util.cs => src/Utility/SpatialUtil.cs | 167 ++- src/packages.config | 4 + unittest-extension/BaseUnitTest.cs | 25 + .../Properties}/AssemblyInfo.cs | 71 +- ...SQLSpatialTools.UnitTests.Extension.csproj | 76 + unittest-extension/TestExtension.cs | 194 +++ unittest-extension/packages.config | 5 + unittest/Functions/GeneralFunctionTests.cs | 407 +++++ unittest/Functions/LRSFunctionTests.cs | 1135 ++++++++++++++ unittest/Functions/UtilFunctionTests.cs | 557 +++++++ unittest/Properties/AssemblyInfo.cs | 19 + unittest/SQLSpatialTools.UnitTests.csproj | 104 ++ unittest/Utility/UtilityFunctionTests.cs | 452 ++++++ unittest/app.config | 3 + unittest/packages.config | 6 + 155 files changed, 18357 insertions(+), 9898 deletions(-) delete mode 100644 BuildProcessTemplates/DefaultTemplate.11.1.xaml delete mode 100644 BuildProcessTemplates/DefaultTemplate.xaml delete mode 100644 BuildProcessTemplates/LabDefaultTemplate.11.xaml delete mode 100644 BuildProcessTemplates/UpgradeTemplate.xaml delete mode 100644 Functions.cs delete mode 100644 KMLProcessor/Constants.cs delete mode 100644 KMLProcessor/FilterSetSridGeographySink.cs delete mode 100644 KMLProcessor/GeographyContext.cs delete mode 100644 KMLProcessor/Import/AltitudeMode.cs delete mode 100644 KMLProcessor/Import/Geography.cs delete mode 100644 KMLProcessor/Import/GroundOverlay.cs delete mode 100644 KMLProcessor/Import/KMLParser.cs delete mode 100644 KMLProcessor/Import/KMLProcessor.cs delete mode 100644 KMLProcessor/Import/LatLonAltBox.cs delete mode 100644 KMLProcessor/Import/LatLonBox.cs delete mode 100644 KMLProcessor/Import/LatLonBoxBase.cs delete mode 100644 KMLProcessor/Import/LatLonQuad.cs delete mode 100644 KMLProcessor/Import/LineString.cs delete mode 100644 KMLProcessor/Import/LinearRing.cs delete mode 100644 KMLProcessor/Import/Model.cs delete mode 100644 KMLProcessor/Import/MultiGeometry.cs delete mode 100644 KMLProcessor/Import/Placemark.cs delete mode 100644 KMLProcessor/Import/Point.cs delete mode 100644 KMLProcessor/Import/Polygon.cs delete mode 100644 KMLProcessor/Import/Region.cs delete mode 100644 KMLProcessor/KmlFunctions.cs delete mode 100644 KMLProcessor/Utilities.cs delete mode 100644 Projections/AlbersEqualAreaProjection.cs delete mode 100644 Projections/LambertConformalConicProjection.cs delete mode 100644 Projections/ObliqueMercatorProjection.cs delete mode 100644 SQL Scripts/Register.sql delete mode 100644 SQL Scripts/Unregister.sql create mode 100644 SQLSpatialTools.sln.DotSettings delete mode 100644 SQLSpatialTools.suo delete mode 100644 Sinks/GeographyFilters.cs delete mode 100644 Sinks/GeometryFilters.cs delete mode 100644 Sinks/Projector.cs delete mode 100644 Sinks/Unprojector.cs delete mode 100644 Sinks/VacuousGeographyToGeometrySink.cs delete mode 100644 Sinks/VacuousGeometryToGeographySink.cs create mode 100644 SpatialTools.pfx delete mode 100644 Types/AffineTransform.cs delete mode 100644 Types/SqlProjection.cs rename {Aggregates => src/Aggregates}/GeographyUnionAggregate.cs (77%) rename {Aggregates => src/Aggregates}/GeometryEnvelopeAggregate.cs (64%) create mode 100644 src/Functions/General/Geography.cs create mode 100644 src/Functions/General/Geometry.cs create mode 100644 src/Functions/LRS/Geometry.cs create mode 100644 src/Functions/Util/Geometry.cs create mode 100644 src/KMLProcessor/Constants.cs rename {KMLProcessor => src/KMLProcessor}/Export/ExportContext.cs (77%) rename {KMLProcessor => src/KMLProcessor}/Export/KeyholeMarkupLanguageBase.cs (67%) rename {KMLProcessor => src/KMLProcessor}/Export/KeyholeMarkupLanguageGeography.cs (55%) rename {KMLProcessor => src/KMLProcessor}/Export/KeyholeMarkupLanguageGeometry.cs (62%) create mode 100644 src/KMLProcessor/FilterSetSridGeographySink.cs create mode 100644 src/KMLProcessor/GeographyContext.cs create mode 100644 src/KMLProcessor/Import/AltitudeMode.cs create mode 100644 src/KMLProcessor/Import/Geography.cs create mode 100644 src/KMLProcessor/Import/GroundOverlay.cs create mode 100644 src/KMLProcessor/Import/KMLParser.cs create mode 100644 src/KMLProcessor/Import/KMLProcessor.cs create mode 100644 src/KMLProcessor/Import/LatLonAltBox.cs create mode 100644 src/KMLProcessor/Import/LatLonBox.cs create mode 100644 src/KMLProcessor/Import/LatLonBoxBase.cs create mode 100644 src/KMLProcessor/Import/LatLonQuad.cs create mode 100644 src/KMLProcessor/Import/LineString.cs create mode 100644 src/KMLProcessor/Import/LinearRing.cs create mode 100644 src/KMLProcessor/Import/Model.cs create mode 100644 src/KMLProcessor/Import/MultiGeometry.cs create mode 100644 src/KMLProcessor/Import/Placemark.cs create mode 100644 src/KMLProcessor/Import/Point.cs create mode 100644 src/KMLProcessor/Import/Polygon.cs create mode 100644 src/KMLProcessor/Import/Region.cs rename {KMLProcessor => src/KMLProcessor}/KMLException.cs (69%) create mode 100644 src/KMLProcessor/KmlFunctions.cs create mode 100644 src/KMLProcessor/Utilities.cs create mode 100644 src/Projections/AlbersEqualAreaProjection.cs rename {Projections => src/Projections}/EquirectangularProjection.cs (80%) rename {Projections => src/Projections}/GnomonicProjection.cs (62%) create mode 100644 src/Projections/LambertConformalConicProjection.cs rename {Projections => src/Projections}/MathX.cs (70%) rename {Projections => src/Projections}/MercatorProjection.cs (65%) create mode 100644 src/Projections/ObliqueMercatorProjection.cs rename {Projections => src/Projections}/Projection.cs (61%) rename Projections/TranverseMercatorProjection.cs => src/Projections/TransverseMercatorProjection.cs (85%) create mode 100644 src/Properties/AssemblyInfo.cs rename Resource.Designer.cs => src/Resource/Resource.Designer.cs (97%) rename Resource.resx => src/Resource/Resource.resx (97%) create mode 100644 src/SQL Scripts/Register.sql create mode 100644 src/SQL Scripts/RegisterAndValidate.sql create mode 100644 src/SQL Scripts/Unregister.sql rename {SQL Scripts => src/SQL Scripts}/aggregate_example.sql (98%) create mode 100644 src/SQL Scripts/lrs_geometry_example.sql rename {SQL Scripts => src/SQL Scripts}/makevalid_example.sql (62%) rename {SQL Scripts => src/SQL Scripts}/projection_example.sql (98%) rename {SQL Scripts => src/SQL Scripts}/transform_example.sql (98%) create mode 100644 src/SQL Scripts/util_geometry_example.sql rename SQLSpatialTools.csproj => src/SQLSpatialTools.csproj (57%) rename {Sinks => src/Sinks/Geography}/DensifyGeographySink.cs (59%) create mode 100644 src/Sinks/Geography/GeographyFilters.cs rename {Sinks => src/Sinks/Geography}/LocateAlongGeographySink.cs (65%) create mode 100644 src/Sinks/Geography/Projector.cs create mode 100644 src/Sinks/Geography/VacuousGeographyToGeometrySink.cs create mode 100644 src/Sinks/Geometry/BuidLRSMultiLineSink.cs create mode 100644 src/Sinks/Geometry/BuildLRSMultiLineSink.cs create mode 100644 src/Sinks/Geometry/BuildMultiLineFromLinesSink.cs create mode 100644 src/Sinks/Geometry/ClipMGeometrySegmentSink.cs create mode 100644 src/Sinks/Geometry/ConvertXYZ2XYMGeometrySink.cs create mode 100644 src/Sinks/Geometry/ExtractPolygonFromLineGeometrySink.cs create mode 100644 src/Sinks/Geometry/GISTypeCheckGeometrySink.cs create mode 100644 src/Sinks/Geometry/GeometryFilters.cs rename {Sinks => src/Sinks/Geometry}/GeometryToPointGeographySink.cs (58%) rename {Sinks => src/Sinks/Geometry}/GeometryTransformer.cs (55%) create mode 100644 src/Sinks/Geometry/LineStringMergeGeometrySink.cs rename {Sinks => src/Sinks/Geometry}/LocateAlongGeometrySink.cs (59%) create mode 100644 src/Sinks/Geometry/LocateMAlongGeometrySink.cs create mode 100644 src/Sinks/Geometry/MultiplyMeasureGeometrySink.cs create mode 100644 src/Sinks/Geometry/PopulateGeometryMeasuresSink.cs create mode 100644 src/Sinks/Geometry/ResetMGeometrySink.cs create mode 100644 src/Sinks/Geometry/ReverseAndTranslateGeometrySink.cs create mode 100644 src/Sinks/Geometry/ReverseLinearGeometrySink.cs create mode 100644 src/Sinks/Geometry/ScaleGeometrySink.cs rename {Sinks => src/Sinks/Geometry}/ShiftGeometrySink.cs (63%) create mode 100644 src/Sinks/Geometry/SplitGeometrySegmentSink.cs create mode 100644 src/Sinks/Geometry/TranslateMeasureGeometrySink.cs create mode 100644 src/Sinks/Geometry/Unprojector.cs create mode 100644 src/Sinks/Geometry/VacuousGeometryToGeographySink.cs create mode 100644 src/Types/LRSEnumerator.cs create mode 100644 src/Types/LRSLine.cs create mode 100644 src/Types/LRSMultiLine.cs create mode 100644 src/Types/LRSPoint.cs create mode 100644 src/Types/SQL/AffineTransform.cs create mode 100644 src/Types/SQL/SqlProjection.cs rename {Utility => src/Types}/Vector3.cs (68%) create mode 100644 src/Types/Vertex.cs create mode 100644 src/Utility/EnumsAndConstants.cs create mode 100644 src/Utility/ErrorMessage.cs create mode 100644 src/Utility/SQLTypeConversions.cs create mode 100644 src/Utility/SpatialExtensions.cs rename Utility/Util.cs => src/Utility/SpatialUtil.cs (67%) create mode 100644 src/packages.config create mode 100644 unittest-extension/BaseUnitTest.cs rename {Properties => unittest-extension/Properties}/AssemblyInfo.cs (59%) create mode 100644 unittest-extension/SQLSpatialTools.UnitTests.Extension.csproj create mode 100644 unittest-extension/TestExtension.cs create mode 100644 unittest-extension/packages.config create mode 100644 unittest/Functions/GeneralFunctionTests.cs create mode 100644 unittest/Functions/LRSFunctionTests.cs create mode 100644 unittest/Functions/UtilFunctionTests.cs create mode 100644 unittest/Properties/AssemblyInfo.cs create mode 100644 unittest/SQLSpatialTools.UnitTests.csproj create mode 100644 unittest/Utility/UtilityFunctionTests.cs create mode 100644 unittest/app.config create mode 100644 unittest/packages.config diff --git a/BuildProcessTemplates/DefaultTemplate.11.1.xaml b/BuildProcessTemplates/DefaultTemplate.11.1.xaml deleted file mode 100644 index 60eac4b..0000000 --- a/BuildProcessTemplates/DefaultTemplate.11.1.xaml +++ /dev/null @@ -1,543 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - [New Microsoft.TeamFoundation.Build.Workflow.Activities.BuildSettings()] - [False] - [New Microsoft.TeamFoundation.Build.Workflow.Activities.TestSpecList(New Microsoft.TeamFoundation.Build.Workflow.Activities.AgileTestPlatformSpec("**\*test*.dll"))] - ["$(BuildDefinitionName)_$(Date:yyyyMMdd)$(Rev:.r)"] - [False] - [True] - [True] - [Microsoft.TeamFoundation.Build.Workflow.Activities.CleanWorkspaceOption.All] - - - - [Microsoft.TeamFoundation.Build.Workflow.Activities.CodeAnalysisOption.AsConfigured] - [True] - [Microsoft.TeamFoundation.Build.Workflow.Activities.ToolPlatform.Auto] - [True] - [New Microsoft.TeamFoundation.Build.Workflow.Activities.SourceAndSymbolServerSettings(True, Nothing)] - [True] - - - - [New Microsoft.TeamFoundation.Build.Workflow.Activities.AgentSettings() With {.MaxWaitTime = New System.TimeSpan(4, 0, 0), .MaxExecutionTime = New System.TimeSpan(0, 0, 0), .TagComparison = Microsoft.TeamFoundation.Build.Workflow.Activities.TagComparison.MatchExactly }] - [Microsoft.TeamFoundation.Build.Workflow.BuildVerbosity.Normal] - - - - - - - All - 11.0 - Assembly references and imported namespaces serialized as XML namespaces - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/BuildProcessTemplates/DefaultTemplate.xaml b/BuildProcessTemplates/DefaultTemplate.xaml deleted file mode 100644 index eae035b..0000000 --- a/BuildProcessTemplates/DefaultTemplate.xaml +++ /dev/null @@ -1,602 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Assembly references and imported namespaces serialized as XML namespaces - - - True - - - - - - - - - True - - - - - - - True - - - - - - - - - - - True - - - - - - - - - - - - - - - - - - - - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - - - - - - - - - - - - - - - - - - - True - - - - - - - - - - - - True - - - - - - - - - - - - - - True - - - - - - - - - - - - - - - - - - - True - - - - - - - - - - - - - - - - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - - - - - - - - - - - - - - - - - - - - - - True - - - - - - - - - True - - - - - - - - - - - - - - - True - - - - - - True - - - - - - - - - - - - - - - - - - - - - - - - - True - - - - - - - - - - - - - - - - - - False - - - - - - - - - - True - - - - - - - - - - - - - - - - - - - - - - - - - - True - - - - - - - - - - - - - - - - - - - - - True - - - - - - - - - - - - - - - - - - True - - - - - - - - - - - - - - - True - - - - - - - - - - - - - - - - - - - - - False - - - - - - - - - - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - False - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - - - - - - - - - - - - - - - - - - - - - - - - - False - - - - - - - - - - - - - - - - - - - - - - - - - - False - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/BuildProcessTemplates/LabDefaultTemplate.11.xaml b/BuildProcessTemplates/LabDefaultTemplate.11.xaml deleted file mode 100644 index 542717f..0000000 --- a/BuildProcessTemplates/LabDefaultTemplate.11.xaml +++ /dev/null @@ -1,208 +0,0 @@ - - - - - - - - - - - - - - - 11.0 - - - - - - 920,3702 - Assembly references and imported namespaces serialized as XML namespaces - - - - - - - - - - - - - - - - - - - - - True - - - - - - - [LabWorkflowParameters.BuildDetails.BuildUri] - - - [ChildBuildDetail.Uri] - - - - - - - - - - - - [BuildLocation] - - - [If(LabWorkflowParameters.BuildDetails.Configuration Is Nothing, BuildLocation, If(LabWorkflowParameters.BuildDetails.Configuration.IsEmpty Or (SelectedBuildDetail.Information.GetNodesByType(Microsoft.TeamFoundation.Build.Common.InformationTypes.ConfigurationSummary, True)).Count = 1, BuildLocation, If(LabWorkflowParameters.BuildDetails.Configuration.IsPlatformEmptyOrAnyCpu, BuildLocation + "\" + LabWorkflowParameters.BuildDetails.Configuration.Configuration, BuildLocation + "\" + LabWorkflowParameters.BuildDetails.Configuration.Platform + "\" + LabWorkflowParameters.BuildDetails.Configuration.Configuration)))] - - - - - - - - - - - - [LabEnvironmentUri] - - - [LabWorkflowParameters.EnvironmentDetails.LabEnvironmentUri.ToString()] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [PostDeploymentSnapshotName] - - - [If(LabWorkflowParameters.BuildDetails.IsTeamSystemBuild = True,String.Format("{0}_{1}_{2}", LabWorkflowParameters.DeploymentDetails.PostDeploymentSnapshotName, BuildNumber,BuildDetail.BuildNumber),String.Format("{0}_{1}", LabWorkflowParameters.DeploymentDetails.PostDeploymentSnapshotName, BuildDetail.BuildNumber))] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [BuildStatus] - - - [Microsoft.TeamFoundation.Build.Client.BuildStatus.PartiallySucceeded] - - - - - - - [BuildStatus] - - - [Microsoft.TeamFoundation.Build.Client.BuildStatus.Failed] - - - - - - - - - - - - \ No newline at end of file diff --git a/BuildProcessTemplates/UpgradeTemplate.xaml b/BuildProcessTemplates/UpgradeTemplate.xaml deleted file mode 100644 index 4877aa3..0000000 --- a/BuildProcessTemplates/UpgradeTemplate.xaml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - [New Microsoft.TeamFoundation.Build.Workflow.Activities.AgentSettings() With {.MaxWaitTime = New System.TimeSpan(4, 0, 0), .MaxExecutionTime = New System.TimeSpan(0, 0, 0), .TagComparison = Microsoft.TeamFoundation.Build.Workflow.Activities.TagComparison.MatchExactly }] - - - - [Microsoft.TeamFoundation.Build.Workflow.Activities.ToolPlatform.Auto] - [False] - [False] - - - - - - - - - - [Microsoft.TeamFoundation.VersionControl.Client.RecursionType.OneLevel] - [Microsoft.TeamFoundation.Build.Workflow.BuildVerbosity.Normal] - - - - All - Assembly references and imported namespaces serialized as XML namespaces - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Functions.cs b/Functions.cs deleted file mode 100644 index 4613e59..0000000 --- a/Functions.cs +++ /dev/null @@ -1,421 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -using System; -using Microsoft.SqlServer.Types; -using System.Data.SqlTypes; - -namespace SQLSpatialTools -{ - // This class contains functions that can be registered in SQL Server. - public class Functions - { - const double THRESHOLD = .01; // 1 cm tolerance in most SRIDs - - // Make our ShiftGeometrySink into a function call by hooking it into a simple pipeline. - public static SqlGeometry ShiftGeometry(SqlGeometry g, double xShift, double yShift) - { - // create a sink that will create a geometry instance - SqlGeometryBuilder b = new SqlGeometryBuilder(); - - // create a sink to do the shift and plug it in to the builder - ShiftGeometrySink s = new ShiftGeometrySink(xShift, yShift, b); - - // plug our sink into the geometry instance and run the pipeline - g.Populate(s); - - // the end of our pipeline is now populated with the shifted geometry instance - return b.ConstructedGeometry; - } - - // Make our LocateAlongGeographySink into a function call. This function just hooks up - // and runs a pipeline using the sink. - public static SqlGeography LocateAlongGeog(SqlGeography g, double distance) - { - SqlGeographyBuilder b = new SqlGeographyBuilder(); - LocateAlongGeographySink p = new LocateAlongGeographySink(distance, b); - g.Populate(p); - return b.ConstructedGeography; - } - - // Make our LocateAlongGeometrySink into a function call. This function just hooks up - // and runs a pipeline using the sink. - public static SqlGeometry LocateAlongGeom(SqlGeometry g, double distance) - { - SqlGeometryBuilder b = new SqlGeometryBuilder(); - LocateAlongGeometrySink p = new LocateAlongGeometrySink(distance, b); - g.Populate(p); - return b.ConstructedGeometry; - } - - // Find the point that is the given distance from the start point in the direction of the end point. - // The distance must be less than the distance between these two points. - public static SqlGeography InterpolateBetweenGeog(SqlGeography start, SqlGeography end, double distance) - { - // We need to check a few prequisites. - - // We only operate on points. - - if (start.STGeometryType().Value != "Point") - { - throw new ArgumentException("Start value must be a point."); - } - - if (end.STGeometryType().Value != "Point") - { - throw new ArgumentException("Start value must be a point."); - } - - // The SRIDs also have to match - int srid = start.STSrid.Value; - if (srid != end.STSrid.Value) - { - throw new ArgumentException("The start and end SRIDs must match."); - } - - // Finally, the distance has to fall between these points. - if (distance > start.STDistance(end)) - { - throw new ArgumentException("The distance value provided exceeds the distance between the two points."); - } - else if (distance < 0) - { - throw new ArgumentException("The distance must be positive."); - } - - // We'll just do this by binary search---surely this could be more efficient, but this is - // relatively easy. - // - // Note that we can't just take the take the linear combination of end vectors because we - // aren't working on a sphere. - - // We are going to do our binary search using 3D Cartesian values, however - Vector3 startCart = Util.GeographicToCartesian(start); - Vector3 endCart = Util.GeographicToCartesian(end); - Vector3 currentCart; - - SqlGeography current; - double currentDistance; - - // Keep refining until we slip below the THRESHOLD value. - do - { - currentCart = (startCart + endCart) / 2; - current = Util.CartesianToGeographic(currentCart, srid); - currentDistance = start.STDistance(current).Value; - if (distance <= currentDistance) endCart = currentCart; - else startCart = currentCart; - } while (Math.Abs(currentDistance - distance) > THRESHOLD); - - return current; - } - - // Find the point that is the given distance from the start point in the direction of the end point. - // The distance must be less than the distance between these two points. - public static SqlGeometry InterpolateBetweenGeom(SqlGeometry start, SqlGeometry end, double distance) - { - // We need to check a few prequisites. - - // We only operate on points. - - if (start.STGeometryType().Value != "Point") - { - throw new ArgumentException("Start value must be a point."); - } - - if (end.STGeometryType().Value != "Point") - { - throw new ArgumentException("Start value must be a point."); - } - - // The SRIDs also have to match - int srid = start.STSrid.Value; - if (srid != end.STSrid.Value) - { - throw new ArgumentException("The start and end SRIDs must match."); - } - - // Finally, the distance has to fall between these points. - double length = start.STDistance(end).Value; - if (distance > start.STDistance(end)) - { - throw new ArgumentException("The distance value provided exceeds the distance between the two points."); - } - else if (distance < 0) - { - throw new ArgumentException("The distance must be positive."); - } - - // Since we're working on a Cartesian plane, this is now pretty simple. - double f = distance/length; // The fraction of the way from start to end. - double newX = (start.STX.Value * (1-f)) + (end.STX.Value * f); - double newY = (start.STY.Value * (1-f)) + (end.STY.Value * f); - return SqlGeometry.Point(newX, newY, srid); - } - - // This function is used for generating a new geography object where additional points are inserted - // along every line in such a way that the angle between two consecutive points does not - // exceed a prescribed angle. The points are generated between the unit vectors that correspond - // to the line's start and end along the great-circle arc on the unit sphere. This follows the - // definition of geodetic lines in SQL Server. - public static SqlGeography DensifyGeography(SqlGeography g, double maxAngle) - { - SqlGeographyBuilder b = new SqlGeographyBuilder(); - g.Populate(new DensifyGeographySink(b, maxAngle)); - return b.ConstructedGeography; - } - - // This implements a completely trivial conversion from geometry to geography, simply taking each - // point (x,y) --> (long, lat). The result is assigned the given SRID. - public static SqlGeography VacuousGeometryToGeography(SqlGeometry toConvert, int targetSrid) - { - SqlGeographyBuilder b = new SqlGeographyBuilder(); - toConvert.Populate(new VacuousGeometryToGeographySink(targetSrid, b)); - return b.ConstructedGeography; - } - - // This implements a completely trivial conversion from geography to geometry, simply taking each - // point (lat,long) --> (y, x). The result is assigned the given SRID. - public static SqlGeometry VacuousGeographyToGeometry(SqlGeography toConvert, int targetSrid) - { - SqlGeometryBuilder b = new SqlGeometryBuilder(); - toConvert.Populate(new VacuousGeographyToGeometrySink(targetSrid, b)); - return b.ConstructedGeometry; - } - - // Computes ConvexHull of input geography and returns a polygon (unless all input points are colinear). - // - public static SqlGeography ConvexHullGeography(SqlGeography geography) - { - if (geography.IsNull || geography.STIsEmpty().Value) return geography; - - SqlGeography center = geography.EnvelopeCenter(); - SqlProjection gnomonicProjection = SqlProjection.Gnomonic(center.Long.Value, center.Lat.Value); - SqlGeometry geometry = gnomonicProjection.Project(geography); - return gnomonicProjection.Unproject(geometry.MakeValid().STConvexHull()); - } - - // Computes ConvexHull of input WKT and returns a polygon (unless all input points are colinear). - // This function does not require its input to be a valid geography. This function does require - // that the WKT coordinate values are longitude/latitude values, in that order and that a valid - // geography SRID value is supplied. - // - public static SqlGeography ConvexHullGeographyFromText(string inputWKT, int srid) - { - SqlGeometry geometry = SqlGeometry.STGeomFromText(new SqlChars(inputWKT), srid); - SqlGeographyBuilder geographyBuilder = new SqlGeographyBuilder(); - geometry.Populate(new GeometryToPointGeographySink(geographyBuilder)); - return ConvexHullGeography(geographyBuilder.ConstructedGeography); - } - - // Check if an input geometry can represent a valid geography without throwing an exception. - // This function requires that the geometry be in longitude/latitude coordinates and that - // those coordinates are in correct order in the geometry instance (i.e. latitude/longitude - // not longitude/latitude). This function will return false (0) if the input geometry is not - // in the correct latitude/longitude format, including a valid geography SRID. - // - public static bool IsValidGeographyFromGeometry(SqlGeometry geometry) - { - if (geometry.IsNull) return false; - - try - { - SqlGeographyBuilder builder = new SqlGeographyBuilder(); - geometry.Populate(new VacuousGeometryToGeographySink(geometry.STSrid.Value, builder)); - SqlGeography geography = builder.ConstructedGeography; - return true; - } - catch (FormatException) - { - // Syntax error - return false; - } - catch (ArgumentException) - { - // Semantical (Geometrical) error - return false; - } - } - - // Check if an input WKT can represent a valid geography. This function requires that - // the WTK coordinate values are longitude/latitude values, in that order and that a valid - // geography SRID value is supplied. This function will not throw an exception even in - // edge conditions (i.e. longitude/latitude coordinates are reversed to latitude/longitude). - // - public static bool IsValidGeographyFromText(string inputWKT, int srid) - { - try - { - // If parse succeeds then our input is valid - SqlGeography.STGeomFromText(new SqlChars(inputWKT), srid); - return true; - } - catch (FormatException) - { - // Syntax error - return false; - } - catch (ArgumentException) - { - // Semantical (Geometrical) error - return false; - } - } - - // Convert an input geometry instance to a valid geography instance. - // This function requires that the WKT coordinate values are longitude/latitude values, - // in that order and that a valid geography SRID value is supplied. - // - public static SqlGeography MakeValidGeographyFromGeometry(SqlGeometry geometry) - { - if (geometry.IsNull) return SqlGeography.Null; - if (geometry.STIsEmpty().Value) return CreateEmptyGeography(geometry.STSrid.Value); - - // Extract vertices from our input to be able to compute geography EnvelopeCenter - SqlGeographyBuilder pointSetBuilder = new SqlGeographyBuilder(); - geometry.Populate(new GeometryToPointGeographySink(pointSetBuilder)); - SqlGeography center; - try - { - center = pointSetBuilder.ConstructedGeography.EnvelopeCenter(); - } - catch (ArgumentException) - { - // Input is larger than a hemisphere. - return SqlGeography.Null; - } - - // Construct Gnomonic projection centered on input geography - SqlProjection gnomonicProjection = SqlProjection.Gnomonic(center.Long.Value, center.Lat.Value); - - // Project, run geometry MakeValid and unproject - SqlGeometryBuilder geometryBuilder = new SqlGeometryBuilder(); - geometry.Populate(new VacuousGeometryToGeographySink(geometry.STSrid.Value, new Projector(gnomonicProjection, geometryBuilder))); - SqlGeometry outGeometry = MakeValidForGeography(geometryBuilder.ConstructedGeometry); - - try - { - return gnomonicProjection.Unproject(outGeometry); - } - catch (ArgumentException) - { - // Try iteratively to reduce the object to remove very close vertices. - for (double tollerance = 1e-4; tollerance <= 1e6; tollerance *= 2) - { - try - { - return gnomonicProjection.Unproject(outGeometry.Reduce(tollerance)); - } - catch (ArgumentException) - { - // keep trying - } - } - return SqlGeography.Null; - } - } - - private static SqlGeography CreateEmptyGeography(int srid) - { - SqlGeographyBuilder b = new SqlGeographyBuilder(); - b.SetSrid(srid); - b.BeginGeography(OpenGisGeographyType.GeometryCollection); - b.EndGeography(); - return b.ConstructedGeography; - } - - private static SqlGeometry MakeValidForGeography(SqlGeometry geometry) - { - // Note: This function relies on an undocumented feature of the planar Union and MakeValid - // that polygon rings in their result will always be oriented using the same rule that - // is used in geography. But, it is not good practice to rely on such fact in production code. - - if (geometry.STIsValid().Value && !geometry.STIsEmpty().Value) - return geometry.STUnion(geometry.STPointN(1)); - - return geometry.MakeValid(); - } - - // Convert an input WKT to a valid geography instance. - // This function requires that the WKT coordinate values are longitude/latitude values, - // in that order and that a valid geography SRID value is supplied. - // - public static SqlGeography MakeValidGeographyFromText(string inputWKT, int srid) - { - return MakeValidGeographyFromGeometry(SqlGeometry.STGeomFromText(new SqlChars(inputWKT), srid)); - } - - // Selectively filter unwanted artifacts in input object: - // - empty shapes (if [filterEmptyShapes] is true) - // - points (if [filterPoints] is true) - // - linestrings shorter than provided tolerance (if lineString.STLength < [lineStringTolerance]) - // - polygon rings thinner than provied tolerance (if ring.STArea < ring.STLength * [ringTolerance]) - // - general behaviour: Returned spatial objects will always to the simplest OGC construction - // - public static SqlGeometry FilterArtifactsGeometry(SqlGeometry g, bool filterEmptyShapes, bool filterPoints, double lineStringTolerance, double ringTolerance) - { - if (g == null || g.IsNull) - return g; - - SqlGeometryBuilder b = new SqlGeometryBuilder(); - IGeometrySink filter = b; - - if (filterEmptyShapes) - filter = new GeometryEmptyShapeFilter(filter); - if (ringTolerance > 0) - filter = new GeometryThinRingFilter(filter, ringTolerance); - if (lineStringTolerance > 0) - filter = new GeometryShortLineStringFilter(filter, lineStringTolerance); - if (filterPoints) - filter = new GeometryPointFilter(filter); - - g.Populate(filter); - g = b.ConstructedGeometry; - - if (g == null || g.IsNull || !g.STIsValid().Value) - return g; - - // Strip collections with single element - while (g.STNumGeometries().Value == 1 && g.InstanceOf("GEOMETRYCOLLECTION").Value) - g = g.STGeometryN(1); - - return g; - } - - // Selectively filter unwanted artifacts in input object: - // - empty shapes (if [filterEmptyShapes] is true) - // - points (if [filterPoints] is true) - // - linestrings shorter than provided tolerance (if lineString.STLength < [lineStringTolerance]) - // - polygon rings thinner than provied tolerance (if ring.STArea < ring.STLength * [ringTolerance]) - // - general behaviour: Returned spatial objects will always to the simplest OGC construction - // - public static SqlGeography FilterArtifactsGeography(SqlGeography g, bool filterEmptyShapes, bool filterPoints, double lineStringTolerance, double ringTolerance) - { - if (g == null || g.IsNull) - return g; - - SqlGeographyBuilder b = new SqlGeographyBuilder(); - IGeographySink filter = b; - - if (filterEmptyShapes) - filter = new GeographyEmptyShapeFilter(filter); - if (ringTolerance > 0) - filter = new GeographyThinRingFilter(filter, ringTolerance); - if (lineStringTolerance > 0) - filter = new GeographyShortLineStringFilter(filter, lineStringTolerance); - if (filterPoints) - filter = new GeographyPointFilter(filter); - - g.Populate(filter); - g = b.ConstructedGeography; - - if (g == null || g.IsNull) - return g; - - // Strip collections with single element - while (g.STNumGeometries().Value == 1 && g.InstanceOf("GEOMETRYCOLLECTION").Value) - g = g.STGeometryN(1); - - return g; - } - } -} \ No newline at end of file diff --git a/KMLProcessor/Constants.cs b/KMLProcessor/Constants.cs deleted file mode 100644 index da7130d..0000000 --- a/KMLProcessor/Constants.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - public class Constants - { - /// - /// SQL Server default SRID. SQL Server uses the default SRID of 4326, - /// which maps to the WGS 84 spatial reference system, when using methods on geography instances. - /// - /// Source: http://msdn.microsoft.com/en-us/library/bb964707.aspx - /// - public static int DefaultSRID - { - get - { - return 4326; - } - } - - /// - /// Google's KML extensions namespace - /// - public static string GxNamespace - { - get - { - return "http://www.google.com/kml/ext/2.2"; - } - } - - /// - /// KML namespace - /// - public static string KmlNamespace - { - get - { - return "http://www.opengis.net/kml/2.2"; - } - } - - /// - /// Atom namespace - /// - public static string AtomNamespace - { - get - { - return "http://www.w3.org/2005/Atom"; - } - } - } -} diff --git a/KMLProcessor/FilterSetSridGeographySink.cs b/KMLProcessor/FilterSetSridGeographySink.cs deleted file mode 100644 index 3d163e6..0000000 --- a/KMLProcessor/FilterSetSridGeographySink.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class will propagate all method calls to the given target sink, - /// except the method call to the SetSrid method. - /// - public class FilterSetSridGeographySink : IGeographySink - { - #region Constructors - - /// - /// Default Constructor - /// - /// Target sink - public FilterSetSridGeographySink(IGeographySink targetSink) - { - m_TargetSink = targetSink; - } - - #endregion - - #region Private Data - - /// - /// Target sink to propagate all method calls to it, except the method call to the SetSrid method - /// - private IGeographySink m_TargetSink; - - #endregion - - #region IGeographySink Members - - public void AddLine(double latitude, double longitude, double? z, double? m) - { - if (m_TargetSink != null) - m_TargetSink.AddLine(latitude, longitude, z, m); - } - - public void BeginFigure(double latitude, double longitude, double? z, double? m) - { - if (m_TargetSink != null) - m_TargetSink.BeginFigure(latitude, longitude, z, m); - } - - public void BeginGeography(OpenGisGeographyType type) - { - if (m_TargetSink != null) - m_TargetSink.BeginGeography(type); - } - - public void EndFigure() - { - if (m_TargetSink != null) - m_TargetSink.EndFigure(); - } - - public void EndGeography() - { - if (m_TargetSink != null) - m_TargetSink.EndGeography(); - } - - /// - /// This method call will not be propagated to the target sink - /// - /// - public void SetSrid(int srid) - { - } - - #endregion - } -} diff --git a/KMLProcessor/GeographyContext.cs b/KMLProcessor/GeographyContext.cs deleted file mode 100644 index 0b534a6..0000000 --- a/KMLProcessor/GeographyContext.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class contains the information about the context where a geography instance is found. - /// - public class GeographyContext - { - /// - /// Geography instance Id - /// - public string Id - { - get { return m_Id; } - set { m_Id = value; } - } - /// - /// Data member for the Id property - /// - protected string m_Id = null; - - /// - /// Geography instance context. It contains the information about the parent and - /// the siblings of this geography instance. - /// It also contains the information about this geography instance. - /// - public string Context - { - get { return m_Context; } - set { m_Context = value; } - } - /// - /// Data member for the Context property - /// - protected string m_Context = null; - - /// - /// The extracted geography instance - /// - public SqlGeography Shape - { - get { return m_Shape; } - set { m_Shape = value; } - } - /// - /// Data member for the Shape property - /// - protected SqlGeography m_Shape = null; - } -} diff --git a/KMLProcessor/Import/AltitudeMode.cs b/KMLProcessor/Import/AltitudeMode.cs deleted file mode 100644 index 1c778ab..0000000 --- a/KMLProcessor/Import/AltitudeMode.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// Altitude modes allowed in KML - /// - public enum AltitudeMode - { - clampToGround = 0, - relativeToGround = 1, - absolute = 2, - clampToSeaFloor = 3, - relativeToSeaFloor = 4 - } -} diff --git a/KMLProcessor/Import/Geography.cs b/KMLProcessor/Import/Geography.cs deleted file mode 100644 index c1d7ccb..0000000 --- a/KMLProcessor/Import/Geography.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// Base class for all geography instances extracted from the KML file - /// - public abstract class Geography - { - #region Public Properties - - /// - /// Id of the placemark which contains this geography instance - /// - public string Id - { - get { return m_Id; } - set { m_Id = value; } - } - /// - /// Data member for the Id property - /// - protected string m_Id = null; - - /// - /// True if point(s) which are part of this geography instance - /// should be connected to the ground when they are visualy represented in some visualisation tool - /// - public bool Extrude - { - get { return m_Extrude; } - set - { - m_Extrude = value; - } - } - /// - /// Data member for the Extrude property - /// - protected bool m_Extrude = false; - - /// - /// True if all lines in this geography instance should follow the terrain. - /// This flag is not applicable just for Point instance. - /// - public bool Tesselate - { - get { return m_Tesselate; } - set - { - m_Tesselate = value; - } - } - /// - /// Data member for the Tesselate property - /// - protected bool m_Tesselate = false; - - /// - /// True if this geography instance is valid - /// - public bool IsValid - { - get - { - // This implementation is base on the property that the Sql Server 2008 (10.0 Katmai) - // will throw an exception if the geography instance is not valid. - - try - { - SqlGeographyBuilder constructed = new SqlGeographyBuilder(); - constructed.SetSrid(Constants.DefaultSRID); - - Populate(constructed); - - SqlGeography g = constructed.ConstructedGeography; - - return true; - } - catch (Exception) - { - return false; - } - } - } - - /// - /// The context where this geography instance is found. It will - /// contain the information about the parent and the siblings of this - /// geography instance, and the information about this geography instance - /// it self. - /// - public string Context - { - get { return m_Context; } - set { m_Context = value; } - } - /// - /// Data member for the Context property - /// - protected string m_Context; - - #endregion - - #region Abstract Members - - /// - /// This method populates the given sink with the data from this geography instance - /// - /// Sink to be populated - public abstract void Populate(IGeographySink sink); - - /// - /// SqlGeography instance well-known text. - /// - public abstract string WKT - { - get; - } - - #endregion - - #region Public Methods - - /// - /// This method returns the geography instance that corresponds to this KML geography - /// - /// If true and this geography instance is invalid then the MakeValid - /// function will be executed on this geography instance - /// The geography instance that corresponds to this KML geography - public SqlGeography ToSqlGeography(bool makeValid) - { - if (IsValid) - { - SqlGeographyBuilder constructed = new SqlGeographyBuilder(); - constructed.SetSrid(Constants.DefaultSRID); - Populate(constructed); - return constructed.ConstructedGeography; - } - else if (makeValid) - { - SqlGeographyBuilder constructed = new SqlGeographyBuilder(); - constructed.SetSrid(Constants.DefaultSRID); - MakeValid(constructed); - return constructed.ConstructedGeography; - } - - throw new KMLException("Invalid geography instance."); - } - - /// - /// This method returns a string representation of this object - /// - /// WKT for this geography instance - public override string ToString() - { - return WKT; - } - - /// - /// This method populates the given sink with the data from this geography instance. - /// If this geography instance is invalid and the makeValid flag is set then a valid geography instance - /// will be constructed and the given sink will be populated with that instance. - /// - /// Sink to be populated - /// If true and this geography instance is invalid then the MakeValid - /// function will be executed on this geography instance - public void Populate( - IGeographySink sink, - bool makeValid) - { - if (makeValid) - { - if (IsValid) - Populate(sink); - else - MakeValid(sink); - } - else - { - Populate(sink); - } - } - - /// - /// This method populates the given sink with the valid geography instance constructed from this geography instance. - /// - /// Sink to be populated - public void MakeValid(IGeographySink sink) - { - // 1. Creates the valid geography for this WKT - SqlGeography vg = SQLSpatialTools.Functions.MakeValidGeographyFromText(this.WKT, Constants.DefaultSRID); - - // 2. Populates the given sink with the valid geography - vg.Populate(new FilterSetSridGeographySink(sink)); - } - - #endregion - - #region Internal Properties - - /// - /// Id which was assigned to this geography instance when it was inserted in the database. - /// If the value is null then it is not stored in the database yet - /// - internal int? DbId - { - get { return m_DbId; } - set - { - m_DbId = value; - } - } - /// - /// Data member for the DbId property - /// - private int? m_DbId; - - #endregion - } -} - diff --git a/KMLProcessor/Import/GroundOverlay.cs b/KMLProcessor/Import/GroundOverlay.cs deleted file mode 100644 index b161deb..0000000 --- a/KMLProcessor/Import/GroundOverlay.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class holds the information about the ground overlay extracted from the KML file - /// - public class GroundOverlay : Geography - { - #region Public Properties - - /// - /// Ground Overlay Name - /// - public string Name - { - get { return m_Name; } - internal set - { - if (string.IsNullOrEmpty(value)) - m_Name = ""; - else - m_Name = value; - } - } - /// - /// Data member for the Name property - /// - protected string m_Name = ""; - - /// - /// Box defined inside this ground overlay. - /// - public Geography Box - { - get { return m_Box; } - internal set - { - m_Box = value; - } - } - /// - /// Data member for the Box property - /// - protected Geography m_Box = null; - - /// - /// Region defined inside this ground overlay. - /// - public Region Region - { - get { return m_Region; } - internal set - { - m_Region = value; - } - } - /// - /// Data member for the Region property - /// - protected Region m_Region; - - /// - /// SqlGeography instance well-known text. - /// - public override string WKT - { - get - { - if (Box != null) - { - return Box.WKT; - } - else if (Region != null) - { - return Region.WKT; - } - else - throw new KMLException("WKT is not defined."); - } - } - - #endregion - - #region Geography Methods - - /// - /// This method populates the given sink with the data from this geography instance - /// - /// Sink to be populated - public override void Populate(Microsoft.SqlServer.Types.IGeographySink sink) - { - if (Box != null) - { - Box.Populate(sink); - } - else if (Region != null) - { - Region.Populate(sink); - } - } - - #endregion - } -} diff --git a/KMLProcessor/Import/KMLParser.cs b/KMLProcessor/Import/KMLParser.cs deleted file mode 100644 index 7926317..0000000 --- a/KMLProcessor/Import/KMLParser.cs +++ /dev/null @@ -1,1211 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Xml; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class parses the given KML string - /// - public class KMLParser - { - #region Constructors - - /// - /// Constructor. Accepts the string in the KML format. - /// - /// KML string to be parsed - public KMLParser(string kml) - { - m_Document = new XmlDocument(); - m_Document.LoadXml(kml); - } - - #endregion - - #region Public Data - - /// - /// Geography instances extracted from the KML string - /// - public IList Geographies - { - get - { - return m_Geographies; - } - } - - /// - /// Placemarks extracted from the KML string - /// - public IList Placemarks - { - get - { - return m_Placemarks; - } - } - - /// - /// Models extracted from the KML string - /// - public IList Models - { - get - { - return m_Models; - } - } - - /// - /// Regions extracted from the KML string - /// - public IList Regions - { - get - { - return m_Regions; - } - } - - /// - /// Ground overlays extracted from the KML string - /// - public IList GroundOverlays - { - get - { - return m_GroundOverlays; - } - } - - #endregion - - #region Protected Data - - /// - /// Xml document whose data will be traversed - /// - protected XmlDocument m_Document; - - /// - /// Geography instances extracted from the KML string - /// - protected List m_Geographies = new List(); - - /// - /// Placemarks extracted from the KML string - /// - protected List m_Placemarks = new List(); - - /// - /// Models extracted from the KML string - /// - protected List m_Models = new List(); - - /// - /// Regions extracted from the KML string - /// - protected List m_Regions = new List(); - - /// - /// Ground overlays extracted from the KML string - /// - protected List m_GroundOverlays = new List(); - - #endregion - - #region Protected Methods - - /// - /// This method visits the given KML node - /// - /// KML node to be visited - protected void Visit(XmlNode node) - { - if (node == null) return; - - string nodeName = node.Name.ToLowerInvariant(); - XmlElement elem = node as XmlElement; - - if (elem != null) - { - if (nodeName.Equals("point")) - { - VisitPoint(elem); - } - else if (nodeName.Equals("linestring")) - { - VisitLineString(elem); - } - else if (nodeName.Equals("linearring")) - { - VisitLinearRing(elem); - } - else if (nodeName.Equals("polygon")) - { - VisitPolygon(elem); - } - else if (nodeName.Equals("multigeometry")) - { - VisitMultiGeometry(elem); - } - else if (nodeName.Equals("placemark")) - { - VisitPlacemark(elem); - } - else if (nodeName.Equals("model")) - { - VisitModel(elem); - } - else if (nodeName.Equals("region")) - { - VisitRegion(elem); - } - else if (nodeName.Equals("groundoverlay")) - { - VisitGroundOverlay(elem); - } - else - { - // Visits the child nodes - foreach (XmlNode child in node.ChildNodes) - { - Visit(child); - } - } - } - } - - /// - /// This method visits a KML Ground Overlay element - /// - /// KML Ground Overlay element - protected void VisitGroundOverlay(XmlElement element) - { - if (element == null) return; - - GroundOverlay groundOverlay = new GroundOverlay(); - - VisitGeography(element, groundOverlay); - groundOverlay.Name = GetChildElementText(element, "name"); - - XmlElement latLonAltBoxElem = GetFirstChild(element, "LatLonAltBox"); - XmlElement latLonBoxElem = GetFirstChild(element, "LatLonBox"); - XmlElement latLonQuadElem = GetFirstChild(element, "gx:LatLonQuad"); - - int numOfGegoraphiesBefore = m_Geographies.Count; - - if (latLonAltBoxElem != null) - { - VisitLatLonAltBox(latLonAltBoxElem); - } - else if (latLonBoxElem != null) - { - VisitLatLonBox(latLonBoxElem); - } - else if (latLonQuadElem != null) - { - VisitLatLonQuad(latLonQuadElem); - } - - int numOfGeographiesAfter = m_Geographies.Count; - - if (numOfGeographiesAfter == numOfGegoraphiesBefore + 1) - { - groundOverlay.Box = m_Geographies[m_Geographies.Count - 1]; - m_Geographies.RemoveAt(m_Geographies.Count - 1); - } - else if (numOfGeographiesAfter > numOfGegoraphiesBefore + 1) - { - // Multiple geography instances found in a single ground overlay. - // The last geography instance is the border. - - groundOverlay.Box = m_Geographies[m_Geographies.Count - 1]; - m_Geographies.RemoveAt(m_Geographies.Count - 1); - } - - #region Check if a region is also defined inside this ground overlay - - XmlElement regionElem = GetFirstChild(element, "Region"); - if (regionElem != null) - { - int numOfRegionsBefore = m_Regions.Count; - - VisitRegion(regionElem); - - int numOfRegionsAfter = m_Regions.Count; - - if (numOfRegionsAfter == numOfRegionsBefore + 1) - { - groundOverlay.Region = m_Regions[numOfRegionsAfter - 1]; - } - else if (numOfRegionsAfter > numOfRegionsBefore + 1) - { - // Multiple regions found in a single ground overlay. The last region is the border. - - groundOverlay.Region = m_Regions[numOfRegionsAfter - 1]; - } - } - - #endregion - - m_GroundOverlays.Add(groundOverlay); - } - - /// - /// This method visits a KML placemark element - /// - /// KML placemark element - protected void VisitPlacemark(XmlElement elem) - { - if (elem == null) return; - - Placemark placemark = new Placemark(); - - placemark.Id = GetAttribute(elem, "id"); - - // Visits the child nodes - foreach (XmlNode child in elem.ChildNodes) - { - string childName = child.Name.ToLowerInvariant(); - if (childName.Equals("name")) - { - if (child.HasChildNodes) - { - placemark.Name = child.ChildNodes[0].InnerText; - } - } - else if (childName.Equals("description")) - { - if (child.HasChildNodes) - { - placemark.Description = child.ChildNodes[0].InnerText; - } - } - else if (childName.Equals("address")) - { - if (child.HasChildNodes) - { - placemark.Address = child.ChildNodes[0].InnerText; - } - } - else if (childName.Equals("lookat") && (child as XmlElement) != null) - { - int numOfGeographiesBefore = m_Geographies.Count; - - VisitLookAt(child as XmlElement); - - int numOfGeographiesAfter = m_Geographies.Count; - - if (numOfGeographiesAfter > numOfGeographiesBefore) - { - // Point found where this placemark looks at - - placemark.LookAt = m_Geographies[m_Geographies.Count - 1]; - m_Geographies.RemoveAt(m_Geographies.Count - 1); - } - } - else - { - int numOfGeographiesBefore = m_Geographies.Count; - - Visit(child); - - int numOfGeographiesAfter = m_Geographies.Count; - - if (numOfGeographiesAfter > numOfGeographiesBefore) - { - // Found the geography instance which describes this placemark - - placemark.Geography = m_Geographies[m_Geographies.Count - 1]; - } - } - } - - m_Placemarks.Add(placemark); - } - - /// - /// This method visits a LookAt element - /// - /// KML lookat element to be visited - protected void VisitLookAt(XmlElement elem) - { - if (elem == null) return; - - VisitLonLatAltElement(elem); - } - - /// - /// This method parses the given coordinates string. - /// - /// String with coordinates to be parsed - /// List of points contained in the given string - protected IList ParseCoordinates(string coordinates) - { - List points = new List(); - - if (string.IsNullOrEmpty(coordinates) || coordinates.Trim().Length == 0) - return points; - - // Removes the spaces inside the touples - while (coordinates.Contains(", ")) - { - coordinates = coordinates.Replace(", ", ","); - } - - // Splits the tuples - string[] coordinate = coordinates.Trim().Split(' ', '\t', '\n'); - if (coordinate == null) - return points; - - // Process each tuple - foreach (string p in coordinate) - { - string tp = p.Trim(); - if (tp.Length == 0) - continue; - - string[] cords = tp.Split(','); - - try - { - double longitude = double.Parse(cords[0], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture); - double latitude = double.Parse(cords[1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture); - double? altitude = null; - - if (cords.Length == 3) // the altitude is optional - { - altitude = double.Parse(cords[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture); - } - - Point point = new Point(); - point.Longitude = longitude; - point.Latitude = latitude; - point.Altitude = altitude; - - // If point is the same as the previous point then skip it - if (points.Count > 0 && points[points.Count - 1].Equals(point)) - continue; - - points.Add(point); - } - catch (Exception exc) - { - throw new KMLException("Coordinate is in wrong format: " + tp, exc); - } - } - - return points; - } - - /// - /// This method performs the visit which is common to all geography instances. - /// It stores extracted information into the given geography object. - /// - /// Xml element to be visited - /// Geography object where data should be stored - protected void VisitGeography(XmlElement elem, Geography geography) - { - #region The id in the KML string - - geography.Id = GetAttribute(elem, "id"); - - #endregion - - #region The extrude flag - - XmlElement extrudeElem = GetFirstChild(elem, "extrude"); - if (extrudeElem != null && extrudeElem.HasChildNodes) - { - string val = extrudeElem.ChildNodes[0].InnerText; - if (val != null && val.Trim().Equals("1")) - { - geography.Extrude = true; - } - } - - #endregion - - #region The tessellate flag - - XmlElement tessellateElem = GetFirstChild(elem, "tessellate"); - if (tessellateElem != null && tessellateElem.HasChildNodes) - { - string val = tessellateElem.ChildNodes[0].InnerText; - if (val != null && val.Trim().Equals("1")) - { - geography.Tesselate = true; - } - } - - #endregion - } - - /// - /// This method visits a KML Point element - /// - /// KML Point element to be visited - protected void VisitPoint(XmlElement elem) - { - if (elem == null) return; - - // Extracts the point coordinates - XmlElement coordinates = GetFirstChild(elem, "coordinates"); - if (coordinates == null || !coordinates.HasChildNodes) - return; - - XmlNode coordinatesTextNode = coordinates.ChildNodes[0]; - - string coordinatesText = coordinatesTextNode.InnerText; - - IList pts = ParseCoordinates(coordinatesText); - - if (pts.Count == 0) - return; - - Point point = pts[0]; - - // Extracts the altitude mode - point.Measure = GetAltitudeModeCode(elem); - - // Extracts the other non-geographic data - VisitGeography(elem, point); - - // Sets the geography instance context - point.Context = (elem.ParentNode != null) ? elem.ParentNode.OuterXml : elem.OuterXml; - - m_Geographies.Add(point); - } - - /// - /// This method visits a KML LineString element - /// - /// KML LineString element to be visited - protected void VisitLineString(XmlElement elem) - { - VisitLineString(elem, false); - } - - /// - /// This method visits a KML LineString element - /// - /// KML line string element to be visited - /// True if the line is the ring - protected void VisitLineString( - XmlElement elem, - bool isLineRing) - { - if (elem == null) return; - - XmlNode coordinates = GetFirstChild(elem, "coordinates"); - if (coordinates == null || !coordinates.HasChildNodes) - return; - - XmlNode txt = coordinates.ChildNodes[0]; - - string allCoordinates = txt.InnerText; - - IList points = ParseCoordinates(allCoordinates); - if (points == null || points.Count == 0) - return; - - LineString ls = null; - if (isLineRing) - { - ls = new LinearRing(); - } - else - { - ls = new LineString(); - } - - ls.Points.AddRange(points); - - int? altitudeModeCode = GetAltitudeModeCode(elem); - - foreach (Point p in ls.Points) - { - p.Measure = altitudeModeCode; - } - - // Extract other non-geographic data - VisitGeography(elem, ls); - - #region Handle tesselate flag - - ls.StoreTesselateFlag(); - - #endregion - - // Sets the geography instance context - ls.Context = (elem.ParentNode != null) ? elem.ParentNode.OuterXml : elem.OuterXml; - - m_Geographies.Add(ls); - } - - /// - /// This method visits a KML LinearRing element - /// - /// KML LinearRing element to be visited - protected void VisitLinearRing(XmlElement elem) - { - VisitLineString(elem, true); - } - - /// - /// This method visits a KML Polygon element - /// - /// KML Polygon element to be visited - protected void VisitPolygon(XmlElement elem) - { - if (elem == null) return; - - #region Extract Outer Border - - XmlElement outerBoundaryIs = GetFirstChild(elem, "outerBoundaryIs"); - if (outerBoundaryIs == null) - return; - - XmlElement outerLinearRing = GetFirstChild(outerBoundaryIs, "LinearRing"); - if (outerLinearRing == null) - return; - - int numOfGeographiesBefore = this.Geographies.Count; - - VisitLinearRing(outerLinearRing); - - int numOfGeographiesAfter = this.Geographies.Count; - - // Extracts the outer ring - if (numOfGeographiesAfter == numOfGeographiesBefore) - throw new KMLException("Outer ring not found!"); - - int index = this.Geographies.Count - 1; - Geography g = this.Geographies[index]; - this.m_Geographies.RemoveAt(index); - - LinearRing outerRing = g as LinearRing; - if (outerRing == null) - throw new KMLException("Outer ring not created!"); - - Polygon polygon = new Polygon(); - - bool isValidPolygon = true; - - #region Check outer ring orientation. Outer boundary should be oriented counter-clockwise - - if (!outerRing.IsValid) - { - // Checks if just the ring's orientation is wrong - outerRing.SwitchOrientation(); - - if (!outerRing.IsValid) - { - isValidPolygon = false; - outerRing.SwitchOrientation(); - } - } - - #endregion - - polygon.OuterRing = outerRing; - - #endregion - - #region Extract Inner Borders - - XmlNodeList innerBoundaryIsLst = elem.GetElementsByTagName("innerBoundaryIs"); - if (innerBoundaryIsLst != null) - { - foreach (XmlNode innerBorderNode in innerBoundaryIsLst) - { - XmlElement innerBoundaryIs = innerBorderNode as XmlElement; - if (innerBoundaryIs == null) - continue; - - XmlElement innerLinearRing = GetFirstChild(innerBoundaryIs, "LinearRing"); - if (innerLinearRing == null) - continue; - - numOfGeographiesBefore = this.Geographies.Count; - - VisitLinearRing(innerLinearRing); - - numOfGeographiesAfter = this.Geographies.Count; - - // Extracts the inner ring - if (numOfGeographiesBefore == numOfGeographiesAfter) - throw new KMLException("Inner ring not found!"); - - index = this.Geographies.Count - 1; - g = this.Geographies[index]; - this.m_Geographies.RemoveAt(index); - - LinearRing innerRing = g as LinearRing; - if (innerRing == null) - throw new KMLException("Inner ring not created!"); - - #region Check the ring's orientation. Inner rings should be clockwise oriented - - if (isValidPolygon) - { - if (innerRing.IsValid) - { - // The inner rings should be in the opposite direction from the outer ring - - innerRing.SwitchOrientation(); - } - } - - #endregion - - polygon.InnerRing.Add(innerRing); - } - } - - #endregion - - // Extracts the other non-geographic data - VisitGeography(elem, polygon); - - #region Extract altitude mode - - int? altitudeModeCode = GetAltitudeModeCode(elem); - if (altitudeModeCode.HasValue) - { - // Updates all the points on the outer bound - if (polygon.OuterRing != null) - { - foreach (Point p in polygon.OuterRing.Points) - { - p.Measure = altitudeModeCode; - } - } - - // Update all the points on all the inner bounds - if (polygon.InnerRing != null) - { - foreach (LinearRing r in polygon.InnerRing) - { - if (r.Points == null) - return; - - foreach (Point p1 in r.Points) - { - p1.Measure = altitudeModeCode; - } - } - } - } - - #endregion - - #region Handles tesselate flag - - if (polygon.Tesselate) - { - if (polygon.OuterRing != null) - polygon.OuterRing.StoreTesselateFlag(); - - if (polygon.InnerRing != null) - { - foreach (LinearRing r in polygon.InnerRing) - r.StoreTesselateFlag(); - } - } - - #endregion - - // Sets the geography instance context - polygon.Context = (elem.ParentNode != null) ? elem.ParentNode.OuterXml : elem.OuterXml; - - m_Geographies.Add(polygon); - } - - /// - /// This method visits a KML MultyGeometry element - /// - /// KML MultyGeometry element to be visited - protected void VisitMultiGeometry(XmlElement element) - { - if (element == null) - return; - - MultiGeometry mg = new MultiGeometry(); - - if (element.HasChildNodes) - { - foreach (XmlNode child in element.ChildNodes) - { - int lastIndexBefore = this.Geographies.Count - 1; - - Visit(child); - - int lastIndex = this.Geographies.Count - 1; - - if (lastIndex > lastIndexBefore) - { - Geography childGeography = this.Geographies[lastIndex]; - this.Geographies.RemoveAt(lastIndex); - mg.Geographies.Add(childGeography); - } - } - } - - // Sets the geography instance context - mg.Context = (element.ParentNode != null) ? element.ParentNode.OuterXml : element.OuterXml; - - this.Geographies.Add(mg); - } - - /// - /// This method visits a KML Region element - /// - /// KML Region element to be visited - protected void VisitRegion(XmlElement element) - { - if (element == null) return; - - Region r = new Region(); - - VisitGeography(element, r); - - XmlElement latLonAltBoxElem = GetFirstChild(element, "LatLonAltBox"); - XmlElement latLonBoxElem = GetFirstChild(element, "LatLonBox"); - XmlElement latLonQuadElem = GetFirstChild(element, "gx:LatLonQuad"); - - int numOfGegoraphiesBefore = m_Geographies.Count; - - if (latLonAltBoxElem != null) - { - VisitLatLonAltBox(latLonAltBoxElem); - } - else if (latLonBoxElem != null) - { - VisitLatLonBox(latLonBoxElem); - } - else if (latLonQuadElem != null) - { - VisitLatLonQuad(latLonQuadElem); - } - - int numOfGeographiesAfter = m_Geographies.Count; - - if (numOfGeographiesAfter > numOfGegoraphiesBefore) - { - // Found region's box - - r.Box = m_Geographies[m_Geographies.Count - 1]; - m_Geographies.RemoveAt(m_Geographies.Count - 1); - } - - m_Regions.Add(r); - } - - /// - /// This method visits a KML LatLonAltBox element - /// - /// KML LatLonAltBox element to be visited - protected void VisitLatLonAltBox(XmlElement element) - { - if (element == null) return; - - #region Required properties - - double north = GetChildElementText(element, "north"); - double south = GetChildElementText(element, "south"); - double west = GetChildElementText(element, "west"); - double east = GetChildElementText(element, "east"); - - #endregion - - #region Optional properties - - double minAltitude = 0; - double maxAltitude = 0; - AltitudeMode? altitudeMode; - string minAltitudeS = GetChildElementText(element, "minAltitude"); - if (!string.IsNullOrEmpty(minAltitudeS)) - { - minAltitude = double.Parse(minAltitudeS); - } - - string maxAltitudeS = GetChildElementText(element, "maxAltitude"); - if (!string.IsNullOrEmpty(maxAltitudeS)) - { - maxAltitude = double.Parse(maxAltitudeS); - } - - altitudeMode = GetAltitudeMode(element); - - #endregion - - LatLonAltBox b = new LatLonAltBox(); - b.North = north; - b.South = south; - b.West = west; - b.East = east; - b.MinAltitude = minAltitude; - b.MaxAltitude = maxAltitude; - b.AltitudeMode = altitudeMode; - - m_Geographies.Add(b); - } - - /// - /// This method visits a KML LatLonBox element - /// - /// KML LatLonBox element to be visited - protected void VisitLatLonBox(XmlElement element) - { - if (element == null) return; - - #region Required properties - - double north = GetChildElementText(element, "north"); - double south = GetChildElementText(element, "south"); - double west = GetChildElementText(element, "west"); - double east = GetChildElementText(element, "east"); - - #endregion - - #region Optional properties - - double rotation = 0; - - string rotationS = GetChildElementText(element, "rotation"); - if (!string.IsNullOrEmpty(rotationS)) - { - rotation = double.Parse(rotationS); - } - - #endregion - - LatLonBox b = new LatLonBox(); - b.North = north; - b.South = south; - b.West = west; - b.East = east; - b.Rotation = rotation; - - m_Geographies.Add(b); - } - - /// - /// This method visits a KML LatLonQuad element - /// - /// LatLonQuad KML element to be visited - protected void VisitLatLonQuad(XmlElement element) - { - if (element == null) return; - - string coordinates = GetChildElementText(element, "coordinates"); - - IList points = ParseCoordinates(coordinates); - if (points == null || points.Count != 4) - throw new KMLException("In KML element LatLonQuad has to have exactly the 4 coordinates."); - - // Closes the polygon. Stores the last point the same as the first one. - points.Add(points[0].Clone()); - - LatLonQuad llq = new LatLonQuad(); - - llq.Points.Clear(); - llq.Points.AddRange(points); - - m_Geographies.Add(llq); - } - - /// - /// This method visits a KML element. Method extracts the child elements: - /// Longitude, Latitude, Altitude and AltitudeMode and creates the point instance from those values. - /// - /// KML element to be visited - protected void VisitLonLatAltElement(XmlElement element) - { - double longitude = 0, latitude = 0, altitude = 0; - int? altitudeMode = null; - try - { - XmlElement longitudeElem = GetFirstChild(element, "longitude"); - if (longitudeElem != null && longitudeElem.HasChildNodes) - { - string s = longitudeElem.ChildNodes[0].InnerText; - longitude = double.Parse(s); - } - XmlElement latitudeElem = GetFirstChild(element, "latitude"); - if (latitudeElem != null && latitudeElem.HasChildNodes) - { - string s = latitudeElem.ChildNodes[0].InnerText; - latitude = double.Parse(s); - } - XmlElement altitudeElem = GetFirstChild(element, "altitude"); - if (altitudeElem != null && altitudeElem.HasChildNodes) - { - string s = altitudeElem.ChildNodes[0].InnerText; - altitude = double.Parse(s); - } - - altitudeMode = GetAltitudeModeCode(element); - } - catch (Exception exc) - { - throw new KMLException("Location coordinates in wrong format!", exc); - } - - Point pt = new Point(longitude, latitude, altitude, altitudeMode); - m_Geographies.Add(pt); - } - - /// - /// This method visits a KML Model element - /// - /// KML model element to be visited - protected void VisitModel(XmlElement element) - { - if (element == null) return; - - Model m = new Model(); - - #region Extract Location - - XmlElement locationElem = GetFirstChild(element, "location"); - if (locationElem != null) - { - VisitLonLatAltElement(locationElem); - - m.Location = m_Geographies[m_Geographies.Count - 1] as Point; - m_Geographies.RemoveAt(m_Geographies.Count - 1); - } - - #endregion - - #region Extract other non-geographic data - - VisitGeography(element, m); - - XmlElement orientationElem = GetFirstChild(element, "orientation"); - if (orientationElem != null) - { - m.OrientationHeading = GetChildElementText(orientationElem, "heading", 0); - m.OrientationTilt = GetChildElementText(orientationElem, "tilt", 0); - m.OrientationRoll = GetChildElementText(orientationElem, "roll", 0); - } - - XmlElement scaleElem = GetFirstChild(element, "scale"); - if (scaleElem != null) - { - m.ScaleX = GetChildElementText(scaleElem, "x", 1); - m.ScaleY = GetChildElementText(scaleElem, "y", 1); - m.ScaleZ = GetChildElementText(scaleElem, "z", 1); - } - - #endregion - - m_Models.Add(m); - } - - #endregion - - #region Public methods - - /// - /// This method parses the given KML string - /// - public void Parse() - { - // Visits the all child nodes - foreach (XmlNode n in m_Document.ChildNodes) - { - Visit(n); - } - } - - #endregion - - #region Public Static Methods - - /// - /// This method parses the given XML file context - /// - /// File full path - /// KMLParser with loaded file context - public static KMLParser ParseFile(string fileFullPathName) - { - string text = System.IO.File.ReadAllText(fileFullPathName); - - KMLParser p = new KMLParser(text); - - return p; - } - - /// - /// This method returns the first child element with the given name in the given parent element - /// - /// Parent element - /// Child element name - /// Child element if it exists, null othervise - public static XmlElement GetFirstChild(XmlElement element, string childElementName) - { - if (element == null || element.HasChildNodes == false || string.IsNullOrEmpty(childElementName)) - return null; - - childElementName = childElementName.ToLowerInvariant(); - - foreach (XmlNode node in element.ChildNodes) - { - XmlElement c = node as XmlElement; - if (c == null) - continue; - - if (c.Name.ToLowerInvariant().Equals(childElementName)) - return c; - } - - return null; - } - - /// - /// This method determines the altitude mode for the given KML geograpy element. - /// - /// KML geography element whose altitude mode should be found - /// Altitude mode, or null if the altitude mode is not found - private static AltitudeMode? GetAltitudeMode(XmlElement elem) - { - string altitudeModeVal = "clamptoground"; - XmlElement altitudeMode = GetFirstChild(elem, "altitudeMode"); - if (altitudeMode == null) - altitudeMode = GetFirstChild(elem, "gx:altitudeMode"); - - if (altitudeMode == null) - return null; - - if (altitudeMode != null && altitudeMode.HasChildNodes && altitudeMode.ChildNodes[0].Value != null) - { - altitudeModeVal = altitudeMode.ChildNodes[0].Value.Trim().ToLowerInvariant(); - } - - if (altitudeModeVal.Equals("clamptoground")) - return AltitudeMode.clampToGround; - else if (altitudeModeVal.Equals("relativetoground")) - return AltitudeMode.relativeToGround; - else if (altitudeModeVal.Equals("absolute")) - return AltitudeMode.absolute; - else if (altitudeModeVal.Equals("relativetoseafloor")) - return AltitudeMode.relativeToSeaFloor; - else if (altitudeModeVal.Equals("clamptoseafloor")) - return AltitudeMode.clampToSeaFloor; - - throw new KMLException("Altitude mode not supported: " + altitudeModeVal); - } - - /// - /// This method determines the altitude mode code for the given KML element. - /// - /// KML element - /// Altitude mode code, or null if the altitude mode is not found - public static int? GetAltitudeModeCode(XmlElement elem) - { - AltitudeMode? altitideMode = GetAltitudeMode(elem); - - if (altitideMode == null) - return null; - - return (int)altitideMode; - } - - /// - /// This method returns the tesselate flag value for the given KML element. - /// - /// KML element - /// Tesselate flag value - public static bool GetTesselateFlag(XmlElement elem) - { - bool tessalateFlag = false; - XmlNode tessellate = GetFirstChild(elem, "tessellate"); - if (tessellate != null && tessellate.HasChildNodes) - { - XmlNode tessTxt = tessellate.ChildNodes[0]; - if (tessTxt.InnerText != null && tessTxt.InnerText.Trim().Equals("1")) - tessalateFlag = true; - } - - return tessalateFlag; - } - - /// - /// This method returns the attribute value of the given xml element. - /// - /// Xml element whose attribute is requested - /// Attribute name - /// Attribute value, or null value if the attribute is not set - public static string GetAttribute(XmlElement elem, string attributeName) - { - XmlAttribute attribute = elem.Attributes[attributeName]; - if (attribute == null) - return null; - - return attribute.Value; - } - - /// - /// This method returns the inner text in the child element of the given parent element - /// - /// Parent element - /// Child element name - /// Child element inner text - public static string GetChildElementText(XmlElement element, string childElementName) - { - if (element == null || !element.HasChildNodes) return null; - - XmlElement child = GetFirstChild(element, childElementName); - if (child == null) return null; - - return child.InnerText; - } - - /// - /// This method returns the inner text in the child element of the given parent element. - /// The inner text value is converted to the given type T - /// - /// Type to convert the inner text value into - /// Parent element - /// Child element name - /// Child text value converted into the type T - public static T GetChildElementText(XmlElement element, string childElementName) - { - string childInnerText = GetChildElementText(element, childElementName); - - if (string.IsNullOrEmpty(childInnerText)) - return default(T); - - return (T)Convert.ChangeType(childInnerText, typeof(T)); - } - - /// - /// This method returns the inner text in the child element of the given parent element. - /// The inner text value is converted to the given type T. If the child with the given name doesn't exist then - /// the given default value will be returned. - /// - /// Type to convert the inner text value into - /// Parent element - /// Child element name - /// In case that the child doesn't exist, this value will be returned - /// Child text value converted into the type T, or the default value if the child is not found - public static T GetChildElementText(XmlElement element, string childElementName, T defaultValue) - { - string childInnerText = GetChildElementText(element, childElementName); - - if (string.IsNullOrEmpty(childInnerText)) - return defaultValue; - - return (T)Convert.ChangeType(childInnerText, typeof(T)); - } - - #endregion - } -} diff --git a/KMLProcessor/Import/KMLProcessor.cs b/KMLProcessor/Import/KMLProcessor.cs deleted file mode 100644 index dd5183a..0000000 --- a/KMLProcessor/Import/KMLProcessor.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class processes the given KML string and produces the equivalent SqlGeography instance - /// using sinks. - /// - public class KMLProcessor - { - #region Constructors - - /// - /// Constructor. Accepts the KML string. - /// - /// KML data to be parsed - public KMLProcessor(string kml) - { - m_Kml = kml; - - m_Parser = new KMLParser(kml); - m_Parser.Parse(); - } - - #endregion - - #region Public Properties - - /// - /// Spatial reference identifier (SRID). - /// - public int Srid - { - get - { - return m_Srid; - } - set - { - m_Srid = value; - } - } - /// - /// Data member for the Srid property - /// - protected int m_Srid = Constants.DefaultSRID; - - /// - /// Placemarks extracted from the given KML string - /// - public IList Placemarks - { - get - { - return m_Parser.Placemarks; - } - } - - /// - /// Models extracted from the given KML string - /// - public IList Models - { - get - { - return m_Parser.Models; - } - } - - /// - /// Regions extracted from the given KML string - /// - public IList Regions - { - get - { - return m_Parser.Regions; - } - } - - /// - /// Geography instances extracted from the given KML string - /// - public IList Geographies - { - get - { - return m_Parser.Geographies; - } - } - - /// - /// Ground overlays extracted from the given KML string - /// - public IList GroundOverlays - { - get - { - return m_Parser.GroundOverlays; - } - } - - #endregion - - #region Public Methods - - /// - /// This method populates the given sink with the information about the geography instance - /// extracted from the KML string - /// - /// Sink to be filled - /// If true and the extracted geography instance is invalid then the MakeValid - /// function will be executed on the extracted geography instance - public void Populate( - IGeographySink sink, - bool makeValid) - { - sink.SetSrid(m_Srid); - - int numOfGeographies = m_Parser.Geographies.Count; - if (numOfGeographies == 1) - { - m_Parser.Geographies[0].Populate(sink, makeValid); - } - else if (numOfGeographies > 1) - { - sink.BeginGeography(OpenGisGeographyType.GeometryCollection); - - foreach (Geography g in m_Parser.Geographies) - { - g.Populate(sink, makeValid); - } - - sink.EndGeography(); - } - else - { - // Geography instance is not found. The empty geography collection will be generated. - sink.BeginGeography(OpenGisGeographyType.GeometryCollection); - sink.EndGeography(); - } - } - - #endregion - - #region Protected Data - - /// - /// KML/XML parser - /// - protected KMLParser m_Parser; - - /// - /// KML string to be parsed - /// - protected string m_Kml; - - #endregion - } -} diff --git a/KMLProcessor/Import/LatLonAltBox.cs b/KMLProcessor/Import/LatLonAltBox.cs deleted file mode 100644 index 9102d84..0000000 --- a/KMLProcessor/Import/LatLonAltBox.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class contains the information about the LatLonAltBox element extracted from KML - /// - public class LatLonAltBox : LatLonBoxBase - { - #region Public Data - - /// - /// The minimal altitude - /// - public double MinAltitude - { - get { return m_MinAltitude; } - set - { - if (value < 0) - throw new KMLException("Altitude has to be greater than 0"); - - m_MinAltitude = value; - - if (m_MaxAltitude < m_MinAltitude) - m_MaxAltitude = m_MinAltitude; - } - } - /// - /// Data member for the MinAltitude property - /// - protected double m_MinAltitude = 0; - - /// - /// The maximal altitude - /// - public double MaxAltitude - { - get { return m_MaxAltitude; } - set - { - if (value < 0) - throw new KMLException("Altitude has to be greater than 0"); - - m_MaxAltitude = value; - if (m_MinAltitude > m_MaxAltitude) - m_MinAltitude = m_MaxAltitude; - } - } - /// - /// Data member for the property MaxAltitude - /// - protected double m_MaxAltitude = 0; - - /// - /// Altitude mode - /// - public AltitudeMode? AltitudeMode - { - get { return m_AltitudeMode; } - set { m_AltitudeMode = value; } - } - /// - /// Data member for the AltitudeMode property - /// - protected AltitudeMode? m_AltitudeMode = null; - - #endregion - - #region Geography Methods - - /// - /// This method populates the given sink with the data from this geography instance - /// - /// Sink to be populated - public override void Populate(Microsoft.SqlServer.Types.IGeographySink sink) - { - // Initializes the altitude to the maximal value - m_Altitude = MaxAltitude; - - // Converts and stores the altitude mode to the spatial m coordinate - if (AltitudeMode != null) - m_Measure = (int)AltitudeMode.Value; - - base.Populate(sink); - } - - #endregion - } -} diff --git a/KMLProcessor/Import/LatLonBox.cs b/KMLProcessor/Import/LatLonBox.cs deleted file mode 100644 index 52c600e..0000000 --- a/KMLProcessor/Import/LatLonBox.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class holds the information about the LatLonBox instance extracted from the KML file - /// - public class LatLonBox : LatLonBoxBase - { - #region Public Data - - /// - /// The rotation angle. - /// - public double Rotation - { - get { return m_Rotation; } - set - { - m_Rotation = value; - } - } - /// - /// Data member for the property Rotation - /// - protected double m_Rotation = 0; - - #endregion - } -} diff --git a/KMLProcessor/Import/LatLonBoxBase.cs b/KMLProcessor/Import/LatLonBoxBase.cs deleted file mode 100644 index aee6c78..0000000 --- a/KMLProcessor/Import/LatLonBoxBase.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// Base class for the all KML boxes. All KML boxes will be converted to polygon instances. - /// - public class LatLonBoxBase : Geography - { - #region Public data - - /// - /// The north side latitude - /// - public double North - { - get { return m_North; } - set - { - m_North = value; - } - } - /// - /// Data member for the North property - /// - protected double m_North = 0; - - /// - /// The south side latitude - /// - public double South - { - get { return m_South; } - set - { - m_South = value; - } - } - /// - /// Data member for the South property - /// - protected double m_South = 0; - - /// - /// The east side longitude - /// - public double East - { - get { return m_East; } - set - { - m_East = value; - } - } - /// - /// Data member for the East property - /// - protected double m_East = 0; - - /// - /// The west side longitude - /// - public double West - { - get { return m_West; } - set - { - m_West = value; - } - } - /// - /// Data member for the West property - /// - protected double m_West = 0; - - /// - /// Altitude - /// - public double? Altitude - { - get { return m_Altitude; } - set - { - m_Altitude = value; - } - } - /// - /// Data member for the Altitude property - /// - protected double? m_Altitude = null; - - /// - /// Measure - /// - public double? Measure - { - get { return m_Measure; } - set - { - m_Measure = value; - } - } - /// - /// Data member for the Measure property - /// - protected double? m_Measure = null; - - /// - /// SqlGeography instance well-known text. - /// - public override string WKT - { - get - { - CheckCoordinates(); - - string wkt = "polygon(("; - - // (west, south) - wkt += new Point(West, South, m_Altitude, m_Measure).Coordinate; - - // (east, south) - wkt += "," + new Point(East, South, m_Altitude, m_Measure).Coordinate; - - // (east, notrh) - wkt += "," + new Point(East, North, m_Altitude, m_Measure).Coordinate; - - // (west, north) - wkt += "," + new Point(West, North, m_Altitude, m_Measure).Coordinate; - - // (west, south) - wkt += "," + new Point(West, South, m_Altitude, m_Measure).Coordinate; - - wkt += "))"; - return wkt; - } - } - - #endregion - - #region Geography Methods - - /// - /// This method populates the given sink with the data from this geography instance - /// - /// Sink to be populated - public override void Populate(Microsoft.SqlServer.Types.IGeographySink sink) - { - CheckCoordinates(); - - // The coordinates for this geography instance will be: - // (west, south), (east, south), (east, notrh), (west, north), (west, south) - sink.BeginGeography(OpenGisGeographyType.Polygon); - - // (west, south) - sink.BeginFigure(South, West, m_Altitude, m_Measure); - - // (east, south) - sink.AddLine(South, East, m_Altitude, m_Measure); - - // (east, notrh) - sink.AddLine(North, East, m_Altitude, m_Measure); - - // (west, north) - sink.AddLine(North, West, m_Altitude, m_Measure); - - // (west, south) - sink.AddLine(South, West, m_Altitude, m_Measure); - - sink.EndFigure(); - - sink.EndGeography(); - } - - #endregion - - #region Protected Methods - - /// - /// This method checks if the south latitude is less then the north latitude. - /// If not then the south and the north will be swapped. - /// This method also checks if the west longitude if less then the east longitude. - /// If not then the east and the west will be swapped. - /// - protected void CheckCoordinates() - { - // Checks if the South and the North latitude are in the range (-90, 90) - South = Utilities.ShiftInRange(South, 90); - North = Utilities.ShiftInRange(North, 90); - - // Checks if the West and the East longitude are in the range (-180, 180) - West = Utilities.ShiftInRange(West, 180); - East = Utilities.ShiftInRange(East, 180); - - // Ensures that the south is less then the north - if (South > North) - { - double tmp = South; - South = North; - North = tmp; - } - - // Ensures that the west is less then the east - if (West > East) - { - double tmp = West; - West = East; - East = tmp; - } - } - - #endregion - } -} diff --git a/KMLProcessor/Import/LatLonQuad.cs b/KMLProcessor/Import/LatLonQuad.cs deleted file mode 100644 index 6a4beeb..0000000 --- a/KMLProcessor/Import/LatLonQuad.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class holds the information about a LatLonQuad instance extracted from the KML file - /// - public class LatLonQuad : Geography - { - #region Public Data - - /// - /// The quad vertices - /// - public List Points - { - get { return m_Points; } - } - /// - /// Data member for the Points property - /// - protected List m_Points = new List(); - - /// - /// SqlGeography instance well-known text. - /// - public override string WKT - { - get - { - if (Points == null || Points.Count == 0) - throw new KMLException("WKT is not defined."); - - string wkt = "polygon(("; - - bool first = true; - foreach (Point p in Points) - { - if (first) - first = false; - else - wkt += ", "; - - wkt += p.WKT; - } - - wkt += "))"; - return wkt; - } - } - - #endregion - - #region Geography methods - - /// - /// This method populates the given sink with the data from this geography instance - /// - /// Sink to be populated - public override void Populate(Microsoft.SqlServer.Types.IGeographySink sink) - { - if (Points == null || Points.Count == 0) - return; - - sink.BeginGeography(OpenGisGeographyType.Polygon); - - sink.BeginFigure(Points[0].Latitude, Points[0].Longitude, Points[0].Altitude, Points[0].Measure); - - for (int i = 1; i < Points.Count; i++) - { - sink.AddLine(Points[i].Latitude, Points[i].Longitude, Points[i].Altitude, Points[i].Measure); - } - - sink.EndFigure(); - - sink.EndGeography(); - } - - #endregion - } -} diff --git a/KMLProcessor/Import/LineString.cs b/KMLProcessor/Import/LineString.cs deleted file mode 100644 index cb0ba56..0000000 --- a/KMLProcessor/Import/LineString.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class contains the information about a line string extracted from the KML file - /// - public class LineString : Geography - { - #region Private Fields - - /// - /// OpenGisGeographyType for this geography instance - /// - protected OpenGisGeographyType m_OpenGisGeographyType = OpenGisGeographyType.LineString; - - #endregion - - #region Public Properties - - /// - /// The list of points - /// - public List Points - { - get { return m_Points; } - } - /// - /// Data member for the Points property - /// - public List m_Points = new List(); - - /// - /// SqlGeography instance well-known text. - /// - public override string WKT - { - get - { - return "LINESTRING" + Vertices; - } - } - - /// - /// Returns the vertices of this linear ring. The returned string will be in the following format: - /// (V0, V1, V2,..., Vn, V0). Vi will be in the format: longitude latitude [altitude [measure]] - /// - public string Vertices - { - get - { - string result = "("; - bool isFirst = true; - foreach (Point p in Points) - { - if (!isFirst) - result += ", "; - else - isFirst = false; - - result += p.Longitude + " " + p.Latitude; - if (p.Altitude.HasValue) - { - result += " " + p.Altitude.Value; - - if (p.Measure.HasValue) - result += " " + p.Measure.Value; - } - } - result += ")"; - - return result; - } - } - - #endregion - - #region Geography Methods - - /// - /// This method populates the given sink with the data from this geography instance - /// - /// Sink to be populated - public override void Populate(IGeographySink sink) - { - if (Points == null || Points.Count == 0) - return; - - sink.BeginGeography(m_OpenGisGeographyType); - sink.BeginFigure(Points[0].Latitude, Points[0].Longitude, Points[0].Altitude, Points[0].Measure); - - for (int i = 1; i < Points.Count; i++) - { - sink.AddLine(Points[i].Latitude, Points[i].Longitude, Points[i].Altitude, Points[i].Measure); - } - - sink.EndFigure(); - sink.EndGeography(); - } - - #endregion - - #region Public Methods - - /// - /// This method stores the tesellate flag. M coordinates of all the point will be set - /// to the "clamp to ground" value. - /// - public void StoreTesselateFlag() - { - if (Tesselate) - { - foreach (Point p in Points) - { - if (!p.Measure.HasValue) - { - p.Measure = (int)AltitudeMode.clampToGround; - } - } - } - } - - #endregion - } -} diff --git a/KMLProcessor/Import/LinearRing.cs b/KMLProcessor/Import/LinearRing.cs deleted file mode 100644 index 79522f9..0000000 --- a/KMLProcessor/Import/LinearRing.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.SqlServer.Types; -using System.Data.SqlTypes; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class contains the data about a linear ring extracted from the KML file - /// - public class LinearRing : LineString - { - #region Constructors - - /// - /// Default constructor - /// - public LinearRing() - { - m_OpenGisGeographyType = OpenGisGeographyType.Polygon; - } - - #endregion - - #region Public Mehods - - /// - /// This method switchs the ring's orientation - /// - public void SwitchOrientation() - { - int i = 1; - int j = Points.Count - 2; // index of the element before last - while (i < j) - { - Point tmp = m_Points[i]; - m_Points[i] = m_Points[j]; - m_Points[j] = tmp; - - i++; - j--; - } - } - - #endregion - - #region Public Properties - - /// - /// SqlGeography instance well-known text. - /// - public override string WKT - { - get - { - return "POLYGON(" + Vertices + ")"; - } - } - - #endregion - } -} diff --git a/KMLProcessor/Import/Model.cs b/KMLProcessor/Import/Model.cs deleted file mode 100644 index 463e71c..0000000 --- a/KMLProcessor/Import/Model.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class holds the information about a KML Model element - /// - public class Model : Geography - { - #region Public Data - - /// - /// The location of the model - /// - public Point Location - { - get { return m_Location; } - set - { - m_Location = value; - } - } - /// - /// Data member for the Location property - /// - protected Point m_Location = null; - - /// - /// The orientation heading - /// - public double OrientationHeading - { - get { return m_OrientationHeading; } - set { m_OrientationHeading = value; } - } - /// - /// Data member for the OrientationHeading property - /// - protected double m_OrientationHeading; - - /// - /// The orientation tilt - /// - public double OrientationTilt - { - get { return m_OrientationTilt; } - set { m_OrientationTilt = value; } - } - /// - /// Data member for the OrientationTilt property - /// - protected double m_OrientationTilt; - - /// - /// The orientation roll - /// - public double OrientationRoll - { - get { return m_OrientationRoll; } - set { m_OrientationRoll = value; } - } - /// - /// Data member for the OrientationRoll property - /// - protected double m_OrientationRoll; - - /// - /// The x-axis scale factor - /// - public double ScaleX - { - get { return m_ScaleX; } - set { m_ScaleX = value; } - } - /// - /// Data member for the ScaleX property - /// - protected double m_ScaleX; - - /// - /// The y-axis scale factor - /// - public double ScaleY - { - get { return m_ScaleY; } - set { m_ScaleY = value; } - } - /// - /// Data member for the ScaleY property - /// - protected double m_ScaleY; - - /// - /// The z-axis scale factor - /// - public double ScaleZ - { - get { return m_ScaleZ; } - set { m_ScaleZ = value; } - } - /// - /// Data member for the ScaleZ property - /// - protected double m_ScaleZ; - - /// - /// SqlGeography instance well-known text. - /// - public override string WKT - { - get - { - if (Location != null) - return Location.WKT; - - throw new KMLException("WKT is not defined."); - } - } - - #endregion - - #region Geography Methods - - /// - /// This method populates the given sink with the data from this geography instance - /// - /// Sink to be populated - public override void Populate(Microsoft.SqlServer.Types.IGeographySink sink) - { - if (Location != null) - { - Location.Populate(sink); - } - } - - #endregion - } -} diff --git a/KMLProcessor/Import/MultiGeometry.cs b/KMLProcessor/Import/MultiGeometry.cs deleted file mode 100644 index eff470a..0000000 --- a/KMLProcessor/Import/MultiGeometry.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class holds the information about a collection of the KML geographies - /// - public class MultiGeometry : Geography - { - #region Public Properties - - /// - /// Geographies contained in this geography instance - /// - public IList Geographies - { - get - { - return m_Geographies; - } - } - /// - /// Data member for the Geographies property - /// - protected List m_Geographies = new List(); - - /// - /// SqlGeography instance well-known text. - /// - public override string WKT - { - get - { - string wkt = "GEOMETRYCOLLECTION("; - - bool firstGeography = true; - foreach (Geography g in m_Geographies) - { - if (firstGeography) - firstGeography = false; - else - wkt += ", "; - - wkt += g.WKT; - } - - wkt += ")"; - return wkt; - } - } - - #endregion - - #region Geography Methods - - /// - /// This method populates the given sink with the information about this multy geometry instance - /// - /// Sink to be populated - public override void Populate(Microsoft.SqlServer.Types.IGeographySink sink) - { - sink.BeginGeography(OpenGisGeographyType.GeometryCollection); - - if (this.Geographies != null && this.Geographies.Count > 0) - { - foreach (Geography g in this.Geographies) - { - g.Populate(sink); - } - } - - sink.EndGeography(); - } - - #endregion - } -} diff --git a/KMLProcessor/Import/Placemark.cs b/KMLProcessor/Import/Placemark.cs deleted file mode 100644 index d8c5b41..0000000 --- a/KMLProcessor/Import/Placemark.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class contains the information about a placemark extracted from the KML file - /// - public class Placemark - { - #region Public Properties - - /// - /// Identifier - /// - public string Id - { - get { return m_Id; } - set { m_Id = value; } - } - /// - /// Data member for the Id property - /// - protected string m_Id = null; - - /// - /// Name - /// - public string Name - { - get { return m_Name; } - set - { - if (string.IsNullOrEmpty(value)) - { - m_Name = ""; - } - else - { - m_Name = value; - } - } - } - /// - /// Data member for the property Name - /// - protected string m_Name = ""; - - /// - /// Description - /// - public string Description - { - get { return m_Description; } - set - { - if (string.IsNullOrEmpty(value)) - m_Description = ""; - else - m_Description = value; - } - } - /// - /// Data member for the Description property - /// - protected string m_Description = ""; - - /// - /// Address - /// - public string Address - { - get { return m_Address; } - set - { - if (string.IsNullOrEmpty(value)) - m_Address = ""; - else - m_Address = value; - } - } - /// - /// Data member for the Address property - /// - protected string m_Address = ""; - - /// - /// The geography instance which describes this placemark - /// - public Geography Geography - { - get { return m_Geography; } - set { m_Geography = value; } - } - /// - /// Data member for the Geography property - /// - protected Geography m_Geography; - - /// - /// The geography instance where this placemark looks at - /// - public Geography LookAt - { - get { return m_LookAt; } - set { m_LookAt = value; } - } - /// - /// Data member for the LookAt property - /// - protected Geography m_LookAt; - - #endregion - } -} diff --git a/KMLProcessor/Import/Point.cs b/KMLProcessor/Import/Point.cs deleted file mode 100644 index 88ec397..0000000 --- a/KMLProcessor/Import/Point.cs +++ /dev/null @@ -1,219 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class holds the information about a Point extracted from the KML file - /// - public class Point : Geography - { - #region Constructors - - /// - /// Default constructor - /// - public Point() - : this(0, 0, null, null) - { - } - - /// - /// Constructor. Constructs the point using the given longitude and latitude. - /// - /// Longitude - /// Latitude - public Point(double longitude, double latitude) - : this(longitude, latitude, null, null) - { - } - - /// - /// Constructor. Constructs the point using the given longitude, latitude and altitude - /// - /// Longitude - /// Latitude - /// Altitude - public Point(double longitude, double latitude, double? altitude) - : this(longitude, latitude, altitude, null) - { - } - - /// - /// Constructor. Constructs the point using the given longitude, latitude, altitude and measure. - /// - /// Longitude - /// Latitude - /// Altitude - /// Measure - public Point(double longitude, double latitude, double? altitude, double? measure) - { - Longitude = longitude; - Latitude = latitude; - Altitude = altitude; - Measure = measure; - } - - #endregion - - #region Public Properties - - /// - /// Longitude. - /// - public double Longitude - { - get { return m_Longitude; } - set - { - m_Longitude = value; - } - } - /// - /// Data member for the Longitude property - /// - protected double m_Longitude = 0; - - /// - /// Latitude. - /// - public double Latitude - { - get { return m_Latitude; } - set - { - m_Latitude = value; - } - } - /// - /// Data member for the Latitude property - /// - protected double m_Latitude = 0; - - /// - /// Altitude. - /// - public double? Altitude - { - get { return m_Altitude; } - set - { - m_Altitude = value; - } - } - /// - /// Data member for the Altitude property - /// - protected double? m_Altitude = null; - - /// - /// Measure - /// - public double? Measure - { - get { return m_Measure; } - set { m_Measure = value; } - } - /// - /// Data member for the Measure property - /// - protected double? m_Measure = null; - - /// - /// SqlGeography instance well-known text. - /// - public override string WKT - { - get - { - string wkt = String.Format("POINT({0})", Coordinate); - - return wkt; - } - } - - /// - /// Point's coordinate in the format: longitude, latitude [, altitude] - /// - public string Coordinate - { - get - { - string wkt = String.Format("{0} {1}", m_Longitude, m_Latitude); - - if (m_Altitude.HasValue) - wkt += " " + m_Altitude.Value; - - if (m_Measure.HasValue) - wkt += " " + m_Measure.Value; - - return wkt; - } - } - - #endregion - - #region Geography methods - - /// - /// This method populates the given sink with the data about this geography instance - /// - /// Sink to be populated - public override void Populate(IGeographySink sink) - { - sink.BeginGeography(OpenGisGeographyType.Point); - sink.BeginFigure(Latitude, Longitude, Altitude, Measure); - - sink.EndFigure(); - sink.EndGeography(); - } - - #endregion - - #region Public Methods - - /// - /// This method clones this point - /// - /// Clone of this point - public Point Clone() - { - Point c = new Point(); - - c.Longitude = Longitude; - c.Latitude = Latitude; - c.Altitude = Altitude; - c.Measure = Measure; - - return c; - } - - #endregion - - #region Overriden Base Methods - - /// - /// This method compares the given object with this point - /// - /// Object to be compared with this point - /// - public override bool Equals(object obj) - { - if (this == obj) - return true; - - Point rhs = obj as Point; - if (rhs == null) - return false; - - return (m_Longitude == rhs.m_Longitude && - m_Latitude == rhs.m_Latitude && - ((m_Altitude == null && rhs.m_Altitude == null) || (m_Altitude.Value == rhs.m_Altitude.Value)) && - ((m_Measure == null && rhs.m_Measure == null) || (m_Measure.Value == rhs.m_Measure.Value))); - } - - #endregion - } -} diff --git a/KMLProcessor/Import/Polygon.cs b/KMLProcessor/Import/Polygon.cs deleted file mode 100644 index b561468..0000000 --- a/KMLProcessor/Import/Polygon.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class contains the information about a polygon extracted from the KML file - /// - public class Polygon : Geography - { - #region Public Properties - - /// - /// Outer border - /// - public LinearRing OuterRing - { - get { return m_OuterRing; } - set - { - m_OuterRing = value; - } - } - /// - /// Data member for the OuterRing property - /// - protected LinearRing m_OuterRing = null; - - /// - /// Inner borders/rings - /// - public IList InnerRing - { - get { return m_InnerRing; } - } - /// - /// Data member for the InnerRing property - /// - public List m_InnerRing = new List(); - - /// - /// SqlGeography instance well-known text. - /// - public override string WKT - { - get - { - string wkt = "POLYGON("; - - if (m_OuterRing != null) - wkt += m_OuterRing.Vertices; - else - throw new Exception("Outer ring is not set."); - - foreach (LinearRing ir in m_InnerRing) - { - wkt += ", " + ir.Vertices; - } - - wkt += ")"; - - return wkt; - } - } - - #endregion - - #region Geography Methods - - /// - /// This method populates the given sink with the data from this geography instance - /// - /// Sink to be populated - public override void Populate(IGeographySink sink) - { - if (this.OuterRing == null || this.OuterRing.Points == null || this.OuterRing.Points.Count == 0) - return; - - sink.BeginGeography(OpenGisGeographyType.Polygon); - - // Populates the outer boundary - sink.BeginFigure( - this.OuterRing.Points[0].Latitude, - this.OuterRing.Points[0].Longitude, - this.OuterRing.Points[0].Altitude, - this.OuterRing.Points[0].Measure); - - for (int i = 1; i < this.OuterRing.Points.Count; i++) - { - sink.AddLine( - this.OuterRing.Points[i].Latitude, - this.OuterRing.Points[i].Longitude, - this.OuterRing.Points[i].Altitude, - this.OuterRing.Points[i].Measure); - } - - sink.EndFigure(); - - if (this.InnerRing != null && this.InnerRing.Count > 0) - { - // Populates the inner boundaries - - for (int j = 0; j < this.InnerRing.Count; j++) - { - if (this.InnerRing[j].Points == null || this.InnerRing[j].Points.Count == 0) - continue; - - sink.BeginFigure( - this.InnerRing[j].Points[0].Latitude, - this.InnerRing[j].Points[0].Longitude, - this.InnerRing[j].Points[0].Altitude, - this.InnerRing[j].Points[0].Measure); - - for (int i = 1; i < this.InnerRing[j].Points.Count; i++) - { - sink.AddLine( - this.InnerRing[j].Points[i].Latitude, - this.InnerRing[j].Points[i].Longitude, - this.InnerRing[j].Points[i].Altitude, - this.InnerRing[j].Points[i].Measure); - } - - sink.EndFigure(); - } - } - - sink.EndGeography(); - } - - #endregion - } -} - diff --git a/KMLProcessor/Import/Region.cs b/KMLProcessor/Import/Region.cs deleted file mode 100644 index dd5cb70..0000000 --- a/KMLProcessor/Import/Region.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class holds the information about a Region extracted from the KML file - /// - public class Region : Geography - { - #region Public Data - - /// - /// Box which defines the border of this region - /// - public Geography Box - { - get { return m_Box; } - set { m_Box = value; } - } - /// - /// Data member for the Box property - /// - protected Geography m_Box; - - /// - /// SqlGeography instance well-known text. - /// - public override string WKT - { - get - { - if (Box != null) - { - return Box.WKT; - } - - throw new KMLException("WKT is not defined."); - } - } - - #endregion - - #region Geography Methods - - /// - /// This method populates the given sink with the data from this geography instance - /// - /// Sink to be populated - public override void Populate(Microsoft.SqlServer.Types.IGeographySink sink) - { - if (Box != null) - { - Box.Populate(sink); - } - } - - #endregion - } -} diff --git a/KMLProcessor/KmlFunctions.cs b/KMLProcessor/KmlFunctions.cs deleted file mode 100644 index 522d835..0000000 --- a/KMLProcessor/KmlFunctions.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System; -using System.Data; -using System.Data.SqlClient; -using System.Data.SqlTypes; -using Microsoft.SqlServer.Server; -using Microsoft.SqlServer.Types; -using Microsoft.SqlServer.SpatialToolbox.KMLProcessor; -using System.IO; -using System.Xml; -using System.Collections; -using System.Collections.ObjectModel; - -/// -/// This class contains the KML import/export functions -/// -public partial class KMLFunctions -{ - #region Public Methods - - #region Import Methods - - /// - /// This function parses the given KML string and returns the extracted geography instance. - /// - /// KML string to be parsed - /// If true and the extracted geography instance is invalid then the MakeValid - /// function will be executed on the extracted geography instance - /// Extracted geography instance - [Microsoft.SqlServer.Server.SqlFunction] - public static SqlGeography KmlToGeography( - SqlString kml, - SqlBoolean makeValid) - { - if (kml == null || string.IsNullOrEmpty(kml.Value)) - { - return SqlGeography.Null; - } - - SqlGeographyBuilder constructed = new SqlGeographyBuilder(); - Microsoft.SqlServer.SpatialToolbox.KMLProcessor.KMLProcessor parser = new Microsoft.SqlServer.SpatialToolbox.KMLProcessor.KMLProcessor(kml.Value.Trim()); - parser.Populate(constructed, makeValid.Value); - return constructed.ConstructedGeography; - } - - #endregion - - #region TVF Import Functions - - /// - /// This TVF function parses the given KML string. All geography instances will be extracted and - /// returned along with the context where they were found. - /// One record will be returned for each extracted geography instance. - /// - /// The return table format is: (Id NVARCHAR(200), Context NVARCHAR(MAX), Shape Geography) - /// - /// KML string which should be processed - /// If true and extracted geography instance is invalid then the MakeValid - /// function will be executed on that geography instance - [Microsoft.SqlServer.Server.SqlFunction( - FillRowMethodName = "DecodeGeographyContext", - TableDefinition = "Id NVARCHAR(200), Context NVARCHAR(MAX), Shape Geography" - )] - public static IEnumerable ImportKml( - SqlString kml, - SqlBoolean makeValid) - { - if (kml == null || string.IsNullOrEmpty(kml.Value)) - return new Collection(); - - Collection geographies = new Collection(); - - Microsoft.SqlServer.SpatialToolbox.KMLProcessor.KMLProcessor parser = - new Microsoft.SqlServer.SpatialToolbox.KMLProcessor.KMLProcessor(kml.Value.Trim()); - - foreach (Geography p in parser.Geographies) - { - GeographyContext ge = new GeographyContext(); - ge.Id = p.Id; - ge.Context = p.Context; - ge.Shape = p.ToSqlGeography(makeValid.Value); - - geographies.Add(ge); - } - - return geographies; - } - - /// - /// This method is a helper method for the TVF function ImportKml. A Sql Server uses it to decode a row - /// returned by the ImportKml function. - /// - /// A row returned by the ImportKml function - /// Id extracted from a rowData - /// Context extracted from a rowData - /// Shape extracted from a rowData - public static void DecodeGeographyContext( - object rowData, - out SqlString Id, - out SqlString Context, - out SqlGeography Shape) - { - GeographyContext data = rowData as GeographyContext; - - Id = data.Id; - Context = data.Context; - Shape = data.Shape; - } - - #endregion - - #region Export Methods - - /// - /// This method converts the given geography instance into the KML format - /// - /// Geography instance to be converted - /// KML representation of the given geograpky instance - [Microsoft.SqlServer.Server.SqlFunction] - public static SqlString ExportToKml(SqlGeography g) - { - if (g == null || g.IsNull) - return new SqlString(""); - - MemoryStream stream = new MemoryStream(); - XmlWriter writer = XmlWriter.Create(stream); - - KeyholeMarkupLanguageGeography sink = new KeyholeMarkupLanguageGeography(writer); - - g.Populate(sink); - - sink.FinalizeKMLDocument(); - - writer.Flush(); - stream.Seek(0, SeekOrigin.Begin); - byte[] ba = stream.ToArray(); - char[] sData = System.Text.ASCIIEncoding.UTF8.GetChars(ba); - string s = new string(sData); - writer.Close(); - - // Removes everything before - if (s.IndexOf("") > 0) - s = s.Remove(0, s.IndexOf("")); - - return new SqlString(s); - } - - /// - /// This method converts the given geography instance represented as a WKT string, into the string in the KML format. - /// - /// Geography instance represented as the WKT string - /// The KML string - [Microsoft.SqlServer.Server.SqlFunction] - public static SqlString ExportWKTToKml(SqlString wkt) - { - if (wkt == null || string.IsNullOrEmpty(wkt.Value)) - return new SqlString(""); - - SqlGeography g = SqlGeography.Parse(wkt); - return ExportToKml(g); - } - - #endregion - - #endregion - - #region Protected Methods - - /// - /// This method stores the given box into the database - /// - /// Box to be saved - /// The KmlId to connect this object to - /// Sql connection - /// Sql transaction - /// The database id which is assigned to the given box - protected static int? SaveLatLonBox(Geography box, int insertedKmlId, SqlConnection con, SqlTransaction tran) - { - #region Insert Geography - - int? insertedGeographyId = null; - - if (box != null) - { - SqlCommand saveGeographyCmd = - new SqlCommand( - " insert into Geographies(Geography, KmlId) " + - " values(geography::Parse('" + box.ToString() + "'), @KmlId);select scope_identity()", con, tran); - - AddParameter(saveGeographyCmd, "KmlId", insertedKmlId, DbType.Int32); - - insertedGeographyId = (int)((decimal)saveGeographyCmd.ExecuteScalar()); - box.DbId = insertedGeographyId; - } - - #endregion - - #region Insert Lat Lon Box - - SqlCommand saveLatLonBox = - new SqlCommand( - " insert into LatLonBoxes(BoxType, GeographyId, rotation, MinAlt, MaxAlt, AltitudeMode) " + - " values(@BoxType, @GeographyId, @rotation, @MinAlt, @MaxAlt, @AltitudeMode);select scope_identity()", - con, tran); - - AddParameter(saveLatLonBox, "GeographyId", insertedGeographyId, DbType.Int32); - - LatLonBox latLonBox = box as LatLonBox; - LatLonAltBox latLonAltBox = box as LatLonAltBox; - LatLonQuad latLonQuad = box as LatLonQuad; - - string BoxType = ""; - - if (latLonBox != null) - { - BoxType = "LatLonBox"; - AddParameter(saveLatLonBox, "rotation", latLonBox.Rotation, DbType.Double); - } - else - { - AddParameter(saveLatLonBox, "rotation", null, DbType.Double); - } - - if (latLonAltBox != null) - { - BoxType = "LatLonAltBox"; - - AddParameter(saveLatLonBox, "MinAlt", latLonAltBox.MinAltitude, DbType.Double); - AddParameter(saveLatLonBox, "MaxAlt", latLonAltBox.MaxAltitude, DbType.Double); - AddParameter(saveLatLonBox, "AltitudeMode", latLonAltBox.AltitudeMode.ToString(), DbType.String); - } - else - { - AddParameter(saveLatLonBox, "MinAlt", null, DbType.Double); - AddParameter(saveLatLonBox, "MaxAlt", null, DbType.Double); - AddParameter(saveLatLonBox, "AltitudeMode", null, DbType.Double); - } - - if (latLonQuad != null) - { - BoxType = "LatLonQuad"; - } - - AddParameter(saveLatLonBox, "BoxType", BoxType, DbType.String); - - int? insertedLatLonBox = (int)((decimal)saveLatLonBox.ExecuteScalar()); - - #endregion - - return insertedLatLonBox; - } - - /// - /// This method adds a new parameter to the given sql command - /// - /// Command to add parameter to - /// Parameter name - /// Parameter value - /// Parameter database type - protected static void AddParameter(SqlCommand cmd, string parameterName, object value, DbType type) - { - if (value == null) - value = DBNull.Value; - - cmd.Parameters.Add(new SqlParameter() - { - ParameterName = parameterName, - Value = value, - DbType = type - }); - } - - #endregion -} - - diff --git a/KMLProcessor/Utilities.cs b/KMLProcessor/Utilities.cs deleted file mode 100644 index 8cdc15d..0000000 --- a/KMLProcessor/Utilities.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Xml; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class contains the utility methods - /// - public class Utilities - { - /// - /// This method shifts the given value into the range [-upperBorder, upperBorder] - /// - /// The value to be shifted - /// The range's upper bound - /// The shifted value - public static double ShiftInRange(double value, double upperBorder) - { - upperBorder = Math.Abs(upperBorder); - - if (upperBorder == 0) - throw new KMLException("The range has to be greater then 0."); - - double rangeSize = 2 * upperBorder; - - if (value > upperBorder) - { - int k = (int)((value + upperBorder) / rangeSize); - value = value - k * rangeSize; - } - else if (value < -1 * upperBorder) - { - int k = (int)((value - upperBorder) / rangeSize); - value = value - k * rangeSize; - } - - return value; - } - } -} diff --git a/Projections/AlbersEqualAreaProjection.cs b/Projections/AlbersEqualAreaProjection.cs deleted file mode 100644 index aa7b303..0000000 --- a/Projections/AlbersEqualAreaProjection.cs +++ /dev/null @@ -1,85 +0,0 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2008 Microsoft Corporation. -// -// References: http://mathworld.wolfram.com/AlbersEqual-AreaConicProjection.html -//------------------------------------------------------------------------------ -using System; -using System.Collections.Generic; - -namespace SQLSpatialTools -{ - // EPSG Code: 9822 - // OGC WKT Name: Albers_Conic_Equal_Area - internal sealed class AlbersEqualAreaProjection : Projection - { - // longitude0, latitude0, parallel1, parallel2 - public AlbersEqualAreaProjection(Dictionary parameters) - : base(parameters) - { - double latitude0_rad = InputLatitude("latitude0"); - double parallel1_rad = InputLatitude("parallel1"); - double parallel2_rad = InputLatitude("parallel2"); - - double cos_parallel1 = Math.Cos(parallel1_rad); - double sin_parallel1 = Math.Sin(parallel1_rad); - - _n2 = sin_parallel1 + Math.Sin(parallel2_rad); - if (Math.Abs(_n2) <= MathX.Tolerance) - { - throw new ArgumentOutOfRangeException(); - } - - _n = _n2 / 2; - _n_half = _n2 / 4; - _inv_n2 = 1 / _n2; - _inv_n = 2 / _n2; - - _c = MathX.Square(cos_parallel1) + sin_parallel1 * _n2; - _c_over_n2 = _c * _inv_n2; - - double a = _c - Math.Sin(latitude0_rad) * _n2; - if (a < 0) - { - throw new ArgumentOutOfRangeException(); - } - _ro_0 = Math.Sqrt(a) * _inv_n; - } - - protected internal override void Project(double latitude, double longitude, out double x, out double y) - { - double a = _c - Math.Sin(latitude) * _n2; - if (a < 0) - { - throw new ArgumentOutOfRangeException("latitude"); - } - - double ro = Math.Sqrt(a) * _inv_n; - - double tetha = longitude * _n; - x = ro * Math.Sin(tetha); - y = _ro_0 - ro * Math.Cos(tetha); - } - - protected internal override void Unproject(double x, double y, out double latitude, out double longitude) - { - double ros = x * x + (_ro_0 - y) * (_ro_0 - y); - double a = _c_over_n2 - ros * _n_half; - if (Double.IsNaN(a) || Math.Abs(a) > 1 + MathX.Tolerance) - { - throw new ArgumentOutOfRangeException("x, y"); - } - - latitude = Math.Asin(MathX.Clamp(1, a)); - longitude = MathX.Clamp(Math.PI, MathX.Atan2(x, _ro_0 - y, "x, y") * _inv_n); - } - - private readonly double _c; - private readonly double _c_over_n2; - private readonly double _ro_0; - private readonly double _n2; - private readonly double _n; - private readonly double _n_half; - private readonly double _inv_n2; - private readonly double _inv_n; - } -} \ No newline at end of file diff --git a/Projections/LambertConformalConicProjection.cs b/Projections/LambertConformalConicProjection.cs deleted file mode 100644 index 04ffaa9..0000000 --- a/Projections/LambertConformalConicProjection.cs +++ /dev/null @@ -1,77 +0,0 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2008 Microsoft Corporation. -// -// References: http://mathworld.wolfram.com/LambertConformalConicProjection.html -//------------------------------------------------------------------------------ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; - -namespace SQLSpatialTools -{ - internal sealed class LambertConformalConicProjection : Projection - { - // Angles are in degrees. - // longitude0: - // latitude: latitude for the origin of cartesian coordinates - // fi1: standard parallel - // fi2: standard parallel - // - public LambertConformalConicProjection(Dictionary parameters) - : base(parameters) - { - double latitude_rad = InputLatitude("latitude0"); - double fi1_rad = InputLatitude("fi1"); - double fi2_rad = InputLatitude("fi2"); - - if (Math.Abs(fi1_rad - fi2_rad) <= MathX.Tolerance) - { - throw new ArgumentException(Resource.Fi1AndFi2MustBeDifferent); - } - - // a != 0 and b != 0 because fi1 != fi2 - double a = Math.Log(Math.Cos(fi1_rad) / Math.Cos(fi2_rad)); - double b = Math.Log(Math.Tan(Math.PI / 4 + fi2_rad / 2) / Math.Tan(Math.PI / 4 + fi1_rad / 2)); - Debug.Assert(!Double.IsNaN(a) && !Double.IsInfinity(a) && Math.Abs(a) > 0, a.ToString(CultureInfo.InvariantCulture)); - Debug.Assert(!Double.IsNaN(b) && !Double.IsInfinity(b) && Math.Abs(b) > 0, b.ToString(CultureInfo.InvariantCulture)); - - _n = a / b; - _nInv = b / a; - - // tan_fi1 > 0 because fi1 is in range (-Pi/2, Pi/2) - double tan_fi1 = Math.Tan(Math.PI / 4 + fi1_rad / 2); - Debug.Assert(!Double.IsInfinity(tan_fi1) && tan_fi1 > 0, tan_fi1.ToString(CultureInfo.InvariantCulture)); - _f = Math.Cos(fi1_rad) * Math.Pow(tan_fi1, _n) * _nInv; - Debug.Assert(Math.Abs(_f) > MathX.Tolerance); - - // tan_latitude > 0 because latitude is in range (-Pi/2, Pi/2) - double tan_latitude = Math.Tan(Math.PI / 4 + latitude_rad / 2); - Debug.Assert(!Double.IsInfinity(tan_latitude) && tan_latitude > 0, tan_latitude.ToString(CultureInfo.InvariantCulture)); - _ro_0 = _f * Math.Pow(tan_latitude, -_n); - } - - protected internal override void Project(double latitude, double longitude, out double x, out double y) - { - double a = Math.Tan(Math.PI / 4 + latitude / 2); - double ro = _f * (a <= MathX.Tolerance ? 0 : Math.Pow(a, -_n)); - double theta = _n * longitude; - - x = ro * Math.Sin(theta); - y = _ro_0 - ro * Math.Cos(theta); - } - - protected internal override void Unproject(double x, double y, out double latitude, out double longitude) - { - double ro = Math.Sign(_n) * Math.Sqrt(x * x + MathX.Square(_ro_0 - y)); - // If ro is zero or very small then then latitude will be +-Pi/2 depending on the sign of f. - latitude = 2 * Math.Atan(Math.Pow(_f / ro, _nInv)) - Math.PI / 2; - longitude = MathX.Clamp(Math.PI, Math.Atan2(x, _ro_0 - y) * _nInv); - } - - private readonly double _n; - private readonly double _nInv; // = 1 / n - private readonly double _f; - private readonly double _ro_0; - } -} \ No newline at end of file diff --git a/Projections/ObliqueMercatorProjection.cs b/Projections/ObliqueMercatorProjection.cs deleted file mode 100644 index 0f13efc..0000000 --- a/Projections/ObliqueMercatorProjection.cs +++ /dev/null @@ -1,88 +0,0 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2008 Microsoft Corporation. -// -// References: http://mathworld.wolfram.com/MercatorProjection.html -//------------------------------------------------------------------------------ -using System; -using System.Collections.Generic; - -namespace SQLSpatialTools -{ - internal sealed class ObliqueMercatorProjection : Projection - { - private const double MaxY = 5; - - // Angles are in degrees. - // longitude0: - // fi1: - // lambda1: - // fi2: - // lambda2: - // - public ObliqueMercatorProjection(Dictionary parameters) - : base(parameters) - { - double fi1_rad = InputLatitude("fi1"); - double fi2_rad = InputLatitude("fi2"); - - double lambda1_rad = InputLongitude("lambda1"); - double lambda2_rad = InputLongitude("lambda2"); - - double cos_fi1 = Math.Cos(fi1_rad); - double sin_fi1 = Math.Sin(fi1_rad); - - double cos_fi2 = Math.Cos(fi2_rad); - double sin_fi2 = Math.Sin(fi2_rad); - - double a = cos_fi1 * sin_fi2 * Math.Cos(lambda1_rad); - double b = sin_fi1 * cos_fi2 * Math.Cos(lambda2_rad); - double c = sin_fi1 * cos_fi2 * Math.Sin(lambda2_rad); - double d = cos_fi1 * sin_fi2 * Math.Sin(lambda1_rad); - double lambda_p = Math.Atan2(a - b, c - d); - - double fi_p = Math.Atan2(-Math.Cos(lambda_p - lambda1_rad), Math.Tan(fi1_rad)); - _cos_fi_p = Math.Cos(fi_p); - _sin_fi_p = Math.Sin(fi_p); - } - - protected internal override void Project(double latitude, double longitude, out double x, out double y) - { - double sin_lat = Math.Sin(latitude); - double cos_lat = Math.Cos(latitude); - - double sin_long = Math.Sin(longitude); - double cos_long = Math.Cos(longitude); - - double a = _sin_fi_p * sin_lat - _cos_fi_p * cos_lat * sin_long; // sin_fi_p - if (Double.IsNaN(a) || Math.Abs(a) > 1) - { - throw new ArgumentOutOfRangeException("latitude, longitude"); - } - - x = MathX.Atan2(sin_lat / cos_lat * _cos_fi_p + sin_long * _sin_fi_p, cos_long, "latitude, longitude"); - y = MathX.Clamp(MaxY, Math.Log((1 + a) / (1 - a)) / 2); // protect againt +-Infinity - } - - protected internal override void Unproject(double x, double y, out double latitude, out double longitude) - { - double sin_x = Math.Sin(x); - double cos_x = Math.Cos(x); - - double sinh_y = Math.Sinh(y); - double cosh_y = Math.Cosh(y); - - // cosh_y >= 1 by definition - double a = (_sin_fi_p * sinh_y + _cos_fi_p * sin_x) / cosh_y; - if (Math.Abs(a) > 1) - { - throw new ArgumentOutOfRangeException("x, y"); - } - - latitude = Math.Asin(a); - longitude = MathX.Atan2(_sin_fi_p * sin_x - _cos_fi_p * sinh_y, cos_x, "x, y"); - } - - private readonly double _cos_fi_p; // = cos(fi_p) - private readonly double _sin_fi_p; // = sin(fi_p) - } -} \ No newline at end of file diff --git a/SQL Scripts/Register.sql b/SQL Scripts/Register.sql deleted file mode 100644 index bbdd72b..0000000 --- a/SQL Scripts/Register.sql +++ /dev/null @@ -1,96 +0,0 @@ --- Copyright (c) Microsoft Corporation. All rights reserved. - --- Install the SQLSpatialTools assembly and all its functions into the current database - --- Enable CLR -sp_configure 'clr enabled', 1 -reconfigure -go - --- !!! Insert the path to the SQLSpatialTools assembly here !!! -create assembly SQLSpatialTools from '' -go - - --- Create types -create type Projection external name SQLSpatialTools.[SQLSpatialTools.SqlProjection] -go - -create type AffineTransform external name SQLSpatialTools.[SQLSpatialTools.AffineTransform] -go - - --- Register the functions... -create function ShiftGeometry(@g geometry, @xShift float, @yShift float) returns geometry -as external name SQLSpatialTools.[SQLSpatialTools.Functions].ShiftGeometry -go - -create function LocateAlongGeog(@g geography, @distance float) returns geography -as external name SQLSpatialTools.[SQLSpatialTools.Functions].LocateAlongGeog -go - -create function LocateAlongGeom(@g geometry, @distance float) returns geometry -as external name SQLSpatialTools.[SQLSpatialTools.Functions].LocateAlongGeom -go - -create function InterpolateBetweenGeog(@start geography, @end geography, @distance float) returns geography -as external name SQLSpatialTools.[SQLSpatialTools.Functions].InterpolateBetweenGeog -go - -create function InterpolateBetweenGeom(@start geometry, @end geometry, @distance float) returns geometry -as external name SQLSpatialTools.[SQLSpatialTools.Functions].InterpolateBetweenGeom -go - -create function VacuousGeometryToGeography(@toConvert geometry, @targetSrid int) returns geography -as external name SQLSpatialTools.[SQLSpatialTools.Functions].VacuousGeometryToGeography -go - -create function VacuousGeographyToGeometry(@toConvert geography, @targetSrid int) returns geometry -as external name SQLSpatialTools.[SQLSpatialTools.Functions].VacuousGeographyToGeometry -go - -create function ConvexHullGeography(@geog geography) returns geography -as external name SQLSpatialTools.[SQLSpatialTools.Functions].ConvexHullGeography -go - -create function ConvexHullGeographyFromText(@inputWKT nvarchar(max), @srid int) returns geography -as external name SQLSpatialTools.[SQLSpatialTools.Functions].ConvexHullGeographyFromText -go - -create function IsValidGeographyFromGeometry(@inputGeometry geometry) returns bit -as external name SQLSpatialTools.[SQLSpatialTools.Functions].IsValidGeographyFromGeometry -go - -create function IsValidGeographyFromText(@inputWKT nvarchar(max), @srid int) returns bit -as external name SQLSpatialTools.[SQLSpatialTools.Functions].IsValidGeographyFromText -go - -create function MakeValidGeographyFromGeometry(@inputGeometry geometry) returns geography -as external name SQLSpatialTools.[SQLSpatialTools.Functions].MakeValidGeographyFromGeometry -go - -create function MakeValidGeographyFromText(@inputWKT nvarchar(max), @srid int) returns geography -as external name SQLSpatialTools.[SQLSpatialTools.Functions].MakeValidGeographyFromText -go - -create function FilterArtifactsGeometry(@g geometry, @filterEmptyShapes bit, @filterPoints bit, @lineStringTolerance float(53), @ringTolerance float(53)) returns geometry -as external name SQLSpatialTools.[SQLSpatialTools.Functions].FilterArtifactsGeometry -go - -create function FilterArtifactsGeography(@g geography, @filterEmptyShapes bit, @filterPoints bit, @lineStringTolerance float(53), @ringTolerance float(53)) returns geography -as external name SQLSpatialTools.[SQLSpatialTools.Functions].FilterArtifactsGeography -go - --- Create aggregates. - -create aggregate GeometryEnvelopeAggregate(@geom geometry) returns geometry -external name SQLSpatialTools.[SQLSpatialTools.GeometryEnvelopeAggregate] -go - -create aggregate GeographyCollectionAggregate(@geog geography) returns geography -external name SQLSpatialTools.[SQLSpatialTools.GeographyCollectionAggregate] -go - -create aggregate GeographyUnionAggregate(@geog geography) returns geography -external name SQLSpatialTools.[SQLSpatialTools.GeographyUnionAggregate] -go \ No newline at end of file diff --git a/SQL Scripts/Unregister.sql b/SQL Scripts/Unregister.sql deleted file mode 100644 index c231e52..0000000 --- a/SQL Scripts/Unregister.sql +++ /dev/null @@ -1,36 +0,0 @@ --- Copyright (c) Microsoft Corporation. All rights reserved. - --- Drop the SQLSpatialTools assembly and all its functions from the current database - --- Drop the aggregates... -drop aggregate GeometryEnvelopeAggregate -drop aggregate GeographyCollectionAggregate -drop aggregate GeographyUnionAggregate - --- Drop the functions... -drop function ShiftGeometry -drop function LocateAlongGeog -drop function LocateAlongGeom -drop function InterpolateBetweenGeog -drop function InterpolateBetweenGeom -drop function VacuousGeometryToGeography -drop function VacuousGeographyToGeometry - -drop function ConvexHullGeography -drop function ConvexHullGeographyFromText -drop function IsValidGeographyFromGeometry -drop function IsValidGeographyFromText -drop function MakeValidGeographyFromGeometry -drop function MakeValidGeographyFromText - -drop function FilterArtifactsGeometry -drop function FilterArtifactsGeography - --- Drop the types... -drop type Projection -drop type AffineTransform - --- Drop the assembly... -drop assembly SQLSpatialTools - - diff --git a/SQLSpatialTools.sln b/SQLSpatialTools.sln index 8364fef..818f745 100644 --- a/SQLSpatialTools.sln +++ b/SQLSpatialTools.sln @@ -1,7 +1,21 @@  -Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLSpatialTools", "SQLSpatialTools.csproj", "{09428C16-7DAE-4C28-A853-E4F04DD0BEDD}" +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2027 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLSpatialTools", "src\SQLSpatialTools.csproj", "{09428C16-7DAE-4C28-A853-E4F04DD0BEDD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLSpatialTools.UnitTests", "unittest\SQLSpatialTools.UnitTests.csproj", "{5FCDB549-1B10-4115-9A3F-AA55584FEA23}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{FDEEA275-E34A-465E-836C-F297B4FECDB9}" + ProjectSection(SolutionItems) = preProject + License.txt = License.txt + Readme.txt = Readme.txt + SpatialTools.pfx = SpatialTools.pfx + SQLSpatialTools.sln.DotSettings = SQLSpatialTools.sln.DotSettings + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLSpatialTools.UnitTests.Extension", "unittest-extension\SQLSpatialTools.UnitTests.Extension.csproj", "{98EFE72E-E303-4BDE-B86F-5F704DB3F5BE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -13,8 +27,31 @@ Global {09428C16-7DAE-4C28-A853-E4F04DD0BEDD}.Debug|Any CPU.Build.0 = Debug|Any CPU {09428C16-7DAE-4C28-A853-E4F04DD0BEDD}.Release|Any CPU.ActiveCfg = Release|Any CPU {09428C16-7DAE-4C28-A853-E4F04DD0BEDD}.Release|Any CPU.Build.0 = Release|Any CPU + {5FCDB549-1B10-4115-9A3F-AA55584FEA23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5FCDB549-1B10-4115-9A3F-AA55584FEA23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5FCDB549-1B10-4115-9A3F-AA55584FEA23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5FCDB549-1B10-4115-9A3F-AA55584FEA23}.Release|Any CPU.Build.0 = Release|Any CPU + {98EFE72E-E303-4BDE-B86F-5F704DB3F5BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98EFE72E-E303-4BDE-B86F-5F704DB3F5BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98EFE72E-E303-4BDE-B86F-5F704DB3F5BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98EFE72E-E303-4BDE-B86F-5F704DB3F5BE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {28138FE6-3CEC-4C1C-BFCA-35186C9246EF} + EndGlobalSection + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection EndGlobal diff --git a/SQLSpatialTools.sln.DotSettings b/SQLSpatialTools.sln.DotSettings new file mode 100644 index 0000000..c2ca56a --- /dev/null +++ b/SQLSpatialTools.sln.DotSettings @@ -0,0 +1,46 @@ + + ABBC + AC + BC + BCAC + DB + GIS + KML + LRS + LSLS + LSMLS + MLSLS + MLSMLS + MSSQL + OGC + OSS + SQL + SRID + ST + WKT + XY + XYM + XYZ + <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Types.LRSLine" FunctionMask="System.Collections.IEnumerable.GetEnumerator" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Types.LRSMultiLine" FunctionMask="System.Collections.IEnumerable.GetEnumerator" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.LocateMAlongGeometrySink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.PopulateGeometryMeasuresSink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.SplitGeometrySegmentSink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.ReverseAndTranslateGeometrySink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.GeometryPointFilter" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.ReverseLinearGeometrySink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.GeometryToPointGeographySink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.ScaleMeasureGeometrySink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.BuildLRSMultiLineSink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.TranslateMeasureGeometrySink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.ClipMGeometrySegmentSink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.LineStringMergeGeometrySink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.ShiftGeometrySink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.VacuousGeometryToGeographySink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.UnProjector" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.ResetMGeometrySink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.BuildMultiLineFromLinesSink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.ValidateLinearMeasureGeometrySink" FunctionMask="AddCircularArc" IsEnabled="True" /><Filter ModuleMask="SQLSpatialTools" ModuleVersionMask="*" ClassMask="SQLSpatialTools.Sinks.Geometry.LocateAlongGeometrySink" FunctionMask="AddCircularArc" IsEnabled="True" /></ExcludeFilters></data> + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + \ No newline at end of file diff --git a/SQLSpatialTools.suo b/SQLSpatialTools.suo deleted file mode 100644 index f71ef82eb6cdce26d730d0c7e136d1d004e8c860..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43008 zcmeI5dypK(eaA-#V_pd$u)sVIEZ*qQo!$Zq-AfYYWIa|7z(?S?TOFs{d+2s0OGY@D zIPoJ8+t`UIjGYkUDw0qxoK&S8hl*{=_ z%K|mMcb?z^!4koRf{O(25G)m3EVxo|iQt`rO9ht;;FBwKyh?DjphM6lSSEOv;99|U z!F7V`1?0a`$D0JJ1-A&63pxe23RVcZ1>{?)<0`>5f;ED*f^~xRf(?S(1h)&`E!Zfy zL$FEEBj^?M3Hk*Cg3W?00^XaIb|}wI!7hiqS?_7*y$ljClq99u^dRo~!r14r#zf@h{R#XMz`J;fIIoCE@q$kL*&8dilAJ zyj%S6xYgmjgWu=?j_3g78a=>oV5`p20h}YtRtNB#@_t-kbpY3_{Bv$~0CI*7=n!xn znMM9NNB)xXj||@|u<}nnrr@* z?%x^Yze~qK!8XBr1?Z`10r{T}j=Oc-BiJk0C%9X1kKkUx`vl~@FF11kfX;^n4+=&E zqk^JfzhF%8kbry#f@4X?XM%IC(}o8Hvx2gqA~+=Y9l`qr;>wif{bPQLA zyxr0-GpZ%$1wXIV07cP&C#EHZojRvR$5&o9*-qr^3Zx)57 zBACi^xIS?DIkOkWaJdlTfd=sx)ME^a^9w4&tZF->Izx+HqRE(km-V}-JDoa%lAia| ztxwSCfpjP9|82bo`uB@6BdXh^o<@(Cg=1J}qdu;pu0UJ#8`utuf|J1&T8HOplL=j$ zc526h|F#Lezp?gDtN$LAZ8nt#7kvNY=Y=$gw*T=yEEM?SfSwLHh*}Paf`?R2-pd{6 zF{Udeoy`V4&zF`g@}EC>f&;uZsJpbncArnD#IZ%6x2N?C&yDN(qq@6aec8|ddZZ{ zfAu90{<~E##TIid$0f@9Qc#dA{MdN)lC^)aXJ|zD(U|b^?LOz)N~8+_WK_DLcAYd zq4&r2MpQ?Hnj_RER&Ur^z1SB08`=+RMUffNIcY!L?^A2ow|t;nXpi3UfB(+lDd4tB zgZeMc{%fWTwQMJ}>Nb^oR`f;Bn%<(io{ie?NxpriE$zqH$@D+eGhTmcYPoCU?x)|t zdl$;gwB)Ly`)2G_sH6Yf?T-)xypf%*)$JF9ELr-m+tL04H$DlD*_h4Un{;?3$e4{E zZ$fsG{-2|+D}MURZw0T-#$Vq8pAY_3!fox(ueFVTjs5q>ZNneR4`W|?oN>v<(F|M9 zO4a1!i^|wwjALkX5?&bIv)`A?qWYX_8?Hx^hfO)IQ6pYQwCW2@*1tXdM_m6@Tg=Hm z+;}vMPiNzjzd07C+>|sle&>R(uZ_=WTwW{aW1jiejiYB{;V@RmTZsqR&>de#GyaR` znxy}H@kjfS`S`(XpFCgumOHccPkjp<{evtprZ#e*lRAm{n{Nbpvhm~j%}&_g_<+gj ziXShZ9sg$GU+HFl`{QpkwJZKj%KKjeQWpODy)Qm=Gyb;{W!{?oAL$tivS2?HG@?cK zRjS6m*2=JQ`22aR%5i%l>Bk(d8o8!hl8c{y0M~dxx8#_(dd!WF(D;Wf_ZGco`>*&o zJgLc;6LTAvmoNU4C*$LCyoR0X6R@hvlp46lZm6u40Ava+~yzrQqwD z`859H?K;~dn|VNE<1TSUwU2mE+}fvpZBQIz@>t*Kkm|HbKCo4)UqPDOc(GmTzgAB& z2HB>T#UnngXOlK6y$7v;U2FOX`)jjr*H8L1wmf>@?|%0N`U+BYf048hdk_7jBi?T2 zN!S0jzfT{xzH!w=ht-ZoLqz@m;6VoGJN}`D_!A2w8;1I)M-ELErz+)+;i=J%La|aA zpBgI<9bP`Elgji^f3ZAKnVuOcA1sw;hAZR4rOI@c{=rVHm#qGwS9aBZjG>T9F>;XFF7T*sax>UnuqC9d8n{+u%j?C zJ3dn>5A7Y$uc^!qPnGvi&rUY=mf^9n+2Yu6rI`7abq>{Xd(-kdwc0bK;>74n z|2-0T5Dtse@RiL5vype{ocZId1pnieBNo7w|CpDy@7ybS!%qfJKdApl<}^J00kh%R z3H$Q~Ww}n^ihr~6TCTlYvR6|5hlOoI|Hp)rnLj%d?IQo+wJE!_<}R^CSK+W$V$8^B z5Pv_E-{(u!h%7SZW9ujM-_6Q~PnR;(e}b4B6W2jr`j2K3&l3+}t%#&I%Gj68WEVtL zG=>@9**3TB&`cS4miNpGOgW54%=ls_y$)$tW^GN>LA9mPp_x#${~?vHq}=GPLy`pa zZihw|SZmCd_US)1N=3)Ajw3pH-lHwR3f)TL)crnAi`)KgrVL5`>s7tb+7oK6X;A>J zpT&nai2s<+SSE1gzn#kK8~!>b@e}`r+`CHPb&~(3de7wz;NPXZw*O7yhg0ao3ab4f zzm2nXknsECzpEO+Ur^qq7)(m(_>Zc+(THJRZ=Q(%r~X#=xY9qae_c7hHV>Ms?U4go zYDDxwR*SwY#%bgFD`<*X_GYfdUR37?Bb7x`q8&OCn@0MpKU%fq$0z>tB-crF74&h@ z>sRYHer)>QZK42CR^z_r!k32rnwgsy;LVKriGszZe^~l#z0SH7A)Dks=1eBVb@=1< z1+p-$XtFdNa7)1dmIr+_b^gIEyMsK)JD$*4wI$j&)YS1=4Qu`4sf}tR>|by%*Zbzf z|H-(iXq|V64p`q=w!n%2Kkea)|8vSq3Sl4xD&^|eu~%0(dqjQXsJMe3GvqsC zKSjBY5nnt{ymiTYrf*JuagfURZBe1?hgv{9Wri+=*Uh1K|&yBvn5)q~2|p#8pidZv;ia>s%F zP)_LF2HZ_!tL1-=#*ozVdE3Y_>+!s2$x|=DMd*K0b+3{h4gA%B&y)R7`V(+twQm@0 zF`xKh!`s9zBLPId5d%k@ZqgT@oQrtrw6FiOo%J`hp3h8Rv-1a7BhAb|)~kP%7m^fs z^`=c5;}l3n+t-h_`H=YLF#%(O59|2rg5L;!e^keh1?Sv&zdsQiKNY$E8J+)j z@cXkmJ|%coKSSMB=8?GNL*Pp81iH9uVzQh+GIFm~&mx>0|2u-W*LyxX7t z%FN`N`4m$lV}p6#=Z^gOqhWm+$JsvYyl1*c;xo%`z04=u)c)yFqNlAdpT9(vwDQVa zdwTDBrSZ|s`O2Fkvy8ocg&j*D z?>+0wkK8c$eBs}YZ+rbO{^>`Wkjh#E8r7X;U*1MV+Z}qwMCkj>Ac=HYNs9lJL|!XP z(RF~ff5CrBgl1CL*0MUP$LWmjOM!2JQ8@YnNyU?CQ|<9N&W{I!KB`uYSYd+Oku{~FC>{@d$W$Eg0>s-9-l z$2}#TS5j$+gurix7IeZtr~6#>Ule~<{7Jvc#++hSlKyQNKl8O;AYeo(={<7Q`{rrp zy&A_qsfUpVQo3bQOEQMkI)~Io(RH>~!iaG?;K+M%E;Cj#BjGK2H*4?UYUHU_K8x>N z!uLQR(XOk{8`tgl>6=|+IDZ?P87@4EX02{=tq~R;7 zrhv3r9=`#9W@$i{K$m6~h`R35b=IhLNxoNl_s{8p-6|PtbSvgz{b&BKQ%Bne&E3d2 ziR%Bx@KG*CDzE=6Gt`aU!mLT4z`Pxr*jYC*BV`H+vL$sH4nPVfqav`o& zBsP3=hIULFzi;U+^bwCV-?+|N<9fsMwc|kT7<`3Fm1wINU{iN;EO zg+AuH7_*p|*<{RvpH2UWnA$H(ffYAcz5TK=dKI6sMG;WD6~ob^dJXuQ={Ikaow3%p zI_z1ufA8c;tbbD?cDGWW$xuC#uwszX2UTjbhB0!}-%V)R_iq{xuqiX^t(ZGVr-NB3 zWZzT^2Yq&}w550O&bxFY&sMuj-r2#x0*m`T?v#8IC2AsK!zi?*#c4)tlx=&?WvgY+ zBjYYRKgIWp3d^`9=Q5_#uX@B5`ksl33(K^%Wo3%DDSKg6)cTCacS@e{5{0G8n^IBh z44jR|1i)eMX%nT=S~;Xvt9YLo6?77NpA{v$_|ek$z-YJs(Zu8z=Iy`52ZDWo)K~j$ zYk6Fwo>UX{!n2HjYsFX9^WR3_7Uf_1s7Ce_U<|uMCBQ#juheN;Z<{Zz4{6^?!*ynt zT>2i~>gpqFY-Ktwq|lNKO3p z*}H^Yq_VebXQwpkr_Y~Z)6F!&bakpUkH} zG^=vZ{w`W2iY;lu;YTDI-i*bn?H2p2)jKOxOZ2fD-rA8g*^wkf)wXBvsYSwkcawEX zmxY`3_V#(>YPUoqQgxxqe=5w$U#)U1yuERVo2BP}$mmBs7z#zvzLq9ZG}Jx|XPmQ&3dXe7d$IM`-qC>p`HQutaI(byqu zt=az`A8@$J@CkvojHqO2iCfm$CAeJr8J6^OX-qtwqiHnU&fhkGG50s>eNHnfh8rN5h3s z6K{98%Su?5*3BQr+cf$sZ655sInybspZFR|GL{bO-aWz+O_p~{MoVZoC|aiET067a ziam2FOY2&)o>o0|q>xnxv@^S$7_AX<{%tvPSGtakk0WWTy!CsOZ?b&H(Ks0UOh}Qu zxjDTLyi4uD?k}~Y%b`%KWj{(8{NG9GtELtaC3#v;>C}qL8=iT-Nj0Wp)S4U2xjeSU zjEs}ifI{vw1hgkJ&v{bIddsXo)c$SFEl;^h z=d9-1G^$_2R?S<>XL%7rOUiX-9s1@ixoK8Ht?MD3^5!r`ovdlJdmgfLIagQ*&Ucxt z=MxI#?Inn6sI-|Z@V0B@W+ec7(d-T#wnOVIqH%*b}2=)PTu`}mh42OxBmPc zt=FpfH+B$u(C6545C=*meUI9XeG#&yD({HrsPD91j2krCnX~S$Ra>k#)=}XnA)3hW zbYp&>ynSQQ{;jddxi^DqR^I97;}OqhcS^HgrI|4=h<>cnFm_5*Dr}uXUD2-CR3w5} z0kf+y{xYLG!jk1Z`b?)_T#FHPN$-MY-Q}N;MfBMnIZH!S8eQh!;H|uS`*g|}pBHNz zk<4&!97@qH&WZZR4HqT7!BkQo*V0mrZ|GQQPAQv0o6S|2J2%_Y|PKdAFu1FVVV`Yg|X#wMYN4 zBq=eTn>&Nj8V>FskgXe|oq3DtUE=%McW5VvBibivjd!Eoixt1$$3;XdeOE^Umn}i{ zF8J*2fK|Y#=>4J}Gb-%SLoC^9pBJOw^?{yPRP6uDDyv0mXMl2tR#bF1W$ImxwNtyd z4#*x!!`>|n_FGBzTS4(J44E%D)GEh)bG+wDzv}W48E2y+X+vW%4Qa&~JG>&H{qklc zTQr@@;g>hVw{@!iM>VE5zL{!z-qUbBTE_>L`lcs8Sd$;dPT}&~y|u$Hbs4)3-v)M` zS;K)g!EaXGHhh`J^fayD^{}+nB?(96ttd$xztva^S>o77pj!Ux$*Ugn(?ZkNtcs26 zQLE9>;v{PH^Cr2hzKGX4lqh?aF8@)=v{&w{ThEq0TE=#0gtsRs94${f?SgX>>gVB5l!O{*?bkrMY|1@To=fjSGO!tN9r&9}|Daw^bpO z!vq#qKVb&ChW0(uEPc`!tMtcbMbIUDaHdCRdy2>fqp7;=4_>THQZ zD^=P+|7K`^`2yg7+b2ht@Y45pV(C)(fQcCG(!>Xpw0ug${0(oy~!#s6Eveb@;>;`r~1|OyFAo=>FSINa|#2l>h3yuHoAY8UOzSmksdB diff --git a/Sinks/GeographyFilters.cs b/Sinks/GeographyFilters.cs deleted file mode 100644 index 448c1a1..0000000 --- a/Sinks/GeographyFilters.cs +++ /dev/null @@ -1,345 +0,0 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2010 Microsoft Corporation. -//------------------------------------------------------------------------------ -using System; -using System.Collections.Generic; -using Microsoft.SqlServer.Types; - -namespace SQLSpatialTools -{ - public class GeographyEmptyShapeFilter : IGeographySink - { - private IGeographySink m_sink; - private Queue m_types = new Queue(); - private bool m_root = true; - - public GeographyEmptyShapeFilter(IGeographySink sink) - { - m_sink = sink; - } - - public void SetSrid(int srid) - { - m_sink.SetSrid(srid); - } - - public void BeginGeography(OpenGisGeographyType type) - { - if (m_root) - { - m_root = false; - m_sink.BeginGeography(type); - } - else - { - m_types.Enqueue(type); - } - } - - public void EndGeography() - { - if (m_types.Count > 0) - m_types.Dequeue(); - else - m_sink.EndGeography(); - } - - public void BeginFigure(double latitude, double longitude, double? z, double? m) - { - while (m_types.Count > 0) - m_sink.BeginGeography(m_types.Dequeue()); - m_sink.BeginFigure(latitude, longitude, z, m); - } - - public void AddLine(double latitude, double longitude, double? z, double? m) - { - m_sink.AddLine(latitude, longitude, z, m); - } - - public void EndFigure() - { - m_sink.EndFigure(); - } - } - - public class GeographyPointFilter : IGeographySink - { - private IGeographySink m_sink; - private int m_depth; - private bool m_root = true; - - public GeographyPointFilter(IGeographySink sink) - { - m_sink = sink; - } - - public void SetSrid(int srid) - { - m_sink.SetSrid(srid); - } - - public void BeginGeography(OpenGisGeographyType type) - { - if (type == OpenGisGeographyType.Point || type == OpenGisGeographyType.MultiPoint) - { - if (m_root) - { - m_root = false; - m_sink.BeginGeography(OpenGisGeographyType.GeometryCollection); - m_sink.EndGeography(); - } - m_depth++; - } - else - { - m_root = false; - m_sink.BeginGeography(type); - } - } - - public void EndGeography() - { - if (m_depth > 0) - m_depth--; - else - m_sink.EndGeography(); - } - - public void BeginFigure(double latitude, double longitude, double? z, double? m) - { - if (m_depth == 0) - m_sink.BeginFigure(latitude, longitude, z, m); - } - - public void AddLine(double latitude, double longitude, double? z, double? m) - { - if (m_depth == 0) - m_sink.AddLine(latitude, longitude, z, m); - } - - public void EndFigure() - { - if (m_depth == 0) - m_sink.EndFigure(); - } - } - - public class GeographyShortLineStringFilter : IGeographySink - { - private IGeographySink m_sink; - private double m_tolerance; - private int m_srid; - private bool m_insideLineString; - private List m_figure = new List(); - - public GeographyShortLineStringFilter(IGeographySink sink, double tolerance) - { - m_sink = sink; - m_tolerance = tolerance; - } - - public void SetSrid(int srid) - { - m_srid = srid; - m_sink.SetSrid(srid); - } - - public void BeginGeography(OpenGisGeographyType type) - { - m_sink.BeginGeography(type); - m_insideLineString = type == OpenGisGeographyType.LineString; - } - - public void EndGeography() - { - m_sink.EndGeography(); - } - - public void BeginFigure(double latitude, double longitude, double? z, double? m) - { - if (m_insideLineString) - { - m_figure.Clear(); - m_figure.Add(new Vertex(latitude, longitude, z, m)); - } - else - { - m_sink.BeginFigure(latitude, longitude, z, m); - } - } - - public void AddLine(double latitude, double longitude, double? z, double? m) - { - if (m_insideLineString) - { - m_figure.Add(new Vertex(latitude, longitude, z, m)); - } - else - { - m_sink.AddLine(latitude, longitude, z, m); - } - } - - public void EndFigure() - { - if (m_insideLineString) - { - if (!IsShortLineString()) - { - PopulateFigure(m_sink); - } - } - else - { - m_sink.EndFigure(); - } - } - - private bool IsShortLineString() - { - try - { - SqlGeographyBuilder b = new SqlGeographyBuilder(); - b.SetSrid(m_srid); - b.BeginGeography(OpenGisGeographyType.LineString); - PopulateFigure(b); - b.EndGeography(); - SqlGeography g = b.ConstructedGeography; - return g.STLength().Value < m_tolerance; - } - catch (FormatException) { } - catch (ArgumentException) { } - return false; - } - - private void PopulateFigure(IGeographySink sink) - { - m_figure[0].BeginFigure(sink); - for (int i = 1; i < m_figure.Count; i++) - m_figure[i].AddLine(sink); - sink.EndFigure(); - } - } - - public class GeographyThinRingFilter : IGeographySink - { - private IGeographySink m_sink; - private double m_tolerance; - private bool m_insidePolygon; - private int m_srid; - private List m_figure = new List(); - - public GeographyThinRingFilter(IGeographySink sink, double tolerance) - { - m_sink = sink; - m_tolerance = tolerance; - } - - public void SetSrid(int srid) - { - m_srid = srid; - m_sink.SetSrid(srid); - } - - public void BeginGeography(OpenGisGeographyType type) - { - m_sink.BeginGeography(type); - m_insidePolygon = type == OpenGisGeographyType.Polygon; - } - - public void EndGeography() - { - m_sink.EndGeography(); - } - - public void BeginFigure(double latitude, double longitude, double? z, double? m) - { - if (m_insidePolygon) - { - m_figure.Clear(); - m_figure.Add(new Vertex(latitude, longitude, z, m)); - } - else - { - m_sink.BeginFigure(latitude, longitude, z, m); - } - } - - public void AddLine(double latitude, double longitude, double? z, double? m) - { - if (m_insidePolygon) - { - m_figure.Add(new Vertex(latitude, longitude, z, m)); - } - else - { - m_sink.AddLine(latitude, longitude, z, m); - } - } - - public void EndFigure() - { - if (m_insidePolygon) - { - if (!IsThinRing()) - { - PopulateFigure(m_sink, false); - } - } - else - { - m_sink.EndFigure(); - } - } - - private bool IsThinRing() - { - SqlGeography poly = RingToPolygon(true); - if (poly == null) - { - // ring was not valid, try with different orientation - poly = RingToPolygon(false); - if (poly == null) - { - // if both orientations are invalid, we are dealing with very thin ring - // so just return true - return true; - } - } - return poly.STArea().Value < m_tolerance * poly.STLength().Value; - } - - private SqlGeography RingToPolygon(bool reverse) - { - try - { - SqlGeographyBuilder b = new SqlGeographyBuilder(); - b.SetSrid(m_srid); - b.BeginGeography(OpenGisGeographyType.Polygon); - PopulateFigure(b, reverse); - b.EndGeography(); - return b.ConstructedGeography; - } - catch (FormatException) { } - catch (ArgumentException) { } - return null; - } - - private void PopulateFigure(IGeographySink sink, bool reverse) - { - if (reverse) - { - m_figure[m_figure.Count - 1].BeginFigure(sink); - for (int i = m_figure.Count - 2; i >= 0; i--) - m_figure[i].AddLine(sink); - } - else - { - m_figure[0].BeginFigure(sink); - for (int i = 1; i < m_figure.Count; i++) - m_figure[i].AddLine(sink); - } - sink.EndFigure(); - } - } -} \ No newline at end of file diff --git a/Sinks/GeometryFilters.cs b/Sinks/GeometryFilters.cs deleted file mode 100644 index 8f14c16..0000000 --- a/Sinks/GeometryFilters.cs +++ /dev/null @@ -1,339 +0,0 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2010 Microsoft Corporation. -//------------------------------------------------------------------------------ -using System; -using System.Collections.Generic; -using Microsoft.SqlServer.Types; - -namespace SQLSpatialTools -{ - struct Vertex - { - double x; - double y; - double? z; - double? m; - - public Vertex(double x_, double y_, double? z_, double? m_) - { - x = x_; - y = y_; - z = z_; - m = m_; - } - - public void BeginFigure(IGeometrySink sink) { sink.BeginFigure(x, y, z, m); } - public void AddLine(IGeometrySink sink) { sink.AddLine(x, y, z, m); } - - public void BeginFigure(IGeographySink sink) { sink.BeginFigure(x, y, z, m); } - public void AddLine(IGeographySink sink) { sink.AddLine(x, y, z, m); } - } - - public class GeometryEmptyShapeFilter : IGeometrySink - { - private IGeometrySink m_sink; - private Queue m_types = new Queue(); - private bool m_root = true; - - public GeometryEmptyShapeFilter(IGeometrySink sink) - { - m_sink = sink; - } - - public void SetSrid(int srid) - { - m_sink.SetSrid(srid); - } - - public void BeginGeometry(OpenGisGeometryType type) - { - if (m_root) - { - m_root = false; - m_sink.BeginGeometry(type); - } - else - { - m_types.Enqueue(type); - } - } - - public void EndGeometry() - { - if (m_types.Count > 0) - m_types.Dequeue(); - else - m_sink.EndGeometry(); - } - - public void BeginFigure(double latitude, double longitude, double? z, double? m) - { - while (m_types.Count > 0) - m_sink.BeginGeometry(m_types.Dequeue()); - m_sink.BeginFigure(latitude, longitude, z, m); - } - - public void AddLine(double latitude, double longitude, double? z, double? m) - { - m_sink.AddLine(latitude, longitude, z, m); - } - - public void EndFigure() - { - m_sink.EndFigure(); - } - } - - public class GeometryPointFilter : IGeometrySink - { - private IGeometrySink m_sink; - private int m_depth; - private bool m_root = true; - - public GeometryPointFilter(IGeometrySink sink) - { - m_sink = sink; - } - - public void SetSrid(int srid) - { - m_sink.SetSrid(srid); - } - - public void BeginGeometry(OpenGisGeometryType type) - { - if (type == OpenGisGeometryType.Point || type == OpenGisGeometryType.MultiPoint) - { - if (m_root) - { - m_root = false; - m_sink.BeginGeometry(OpenGisGeometryType.GeometryCollection); - m_sink.EndGeometry(); - } - m_depth++; - } - else - { - m_sink.BeginGeometry(type); - } - } - - public void EndGeometry() - { - if (m_depth > 0) - m_depth--; - else - m_sink.EndGeometry(); - } - - public void BeginFigure(double x, double y, double? z, double? m) - { - if (m_depth == 0) - m_sink.BeginFigure(x, y, z, m); - } - - public void AddLine(double x, double y, double? z, double? m) - { - m_sink.AddLine(x, y, z, m); - } - - public void EndFigure() - { - if (m_depth == 0) - m_sink.EndFigure(); - } - } - - public class GeometryShortLineStringFilter : IGeometrySink - { - private IGeometrySink m_sink; - private double m_tolerance; - private int m_srid; - private bool m_insideLineString; - private List m_figure = new List(); - - public GeometryShortLineStringFilter(IGeometrySink sink, double tolerance) - { - m_sink = sink; - m_tolerance = tolerance; - } - - public void SetSrid(int srid) - { - m_srid = srid; - m_sink.SetSrid(srid); - } - - public void BeginGeometry(OpenGisGeometryType type) - { - m_sink.BeginGeometry(type); - m_insideLineString = type == OpenGisGeometryType.LineString; - } - - public void EndGeometry() - { - m_sink.EndGeometry(); - } - - public void BeginFigure(double x, double y, double? z, double? m) - { - if (m_insideLineString) - { - m_figure.Clear(); - m_figure.Add(new Vertex(x, y, z, m)); - } - else - { - m_sink.BeginFigure(x, y, z, m); - } - } - - public void AddLine(double x, double y, double? z, double? m) - { - if (m_insideLineString) - { - m_figure.Add(new Vertex(x, y, z, m)); - } - else - { - m_sink.AddLine(x, y, z, m); - } - } - - public void EndFigure() - { - if (m_insideLineString) - { - if (!IsShortLineString()) - { - PopulateFigure(m_sink); - } - } - else - { - m_sink.EndFigure(); - } - } - - private bool IsShortLineString() - { - try - { - SqlGeometryBuilder b = new SqlGeometryBuilder(); - b.SetSrid(m_srid); - b.BeginGeometry(OpenGisGeometryType.LineString); - PopulateFigure(b); - b.EndGeometry(); - return b.ConstructedGeometry.STLength().Value < m_tolerance; - } - catch (ArgumentException) { } - catch (FormatException) { } - return true; - } - - private void PopulateFigure(IGeometrySink sink) - { - m_figure[0].BeginFigure(sink); - for (int i = 1; i < m_figure.Count; i++) - m_figure[i].AddLine(sink); - sink.EndFigure(); - } - } - - public class GeometryThinRingFilter : IGeometrySink - { - private IGeometrySink m_sink; - private double m_tolerance; - private bool m_insidePolygon; - private int m_srid; - private List m_figure = new List(); - - public GeometryThinRingFilter(IGeometrySink sink, double tolerance) - { - m_sink = sink; - m_tolerance = tolerance; - } - - public void SetSrid(int srid) - { - m_srid = srid; - m_sink.SetSrid(srid); - } - - public void BeginGeometry(OpenGisGeometryType type) - { - m_sink.BeginGeometry(type); - m_insidePolygon = type == OpenGisGeometryType.Polygon; - } - - public void EndGeometry() - { - m_sink.EndGeometry(); - } - - public void BeginFigure(double x, double y, double? z, double? m) - { - if (m_insidePolygon) - { - m_figure.Clear(); - m_figure.Add(new Vertex(x, y, z, m)); - } - else - { - m_sink.BeginFigure(x, y, z, m); - } - } - - public void AddLine(double x, double y, double? z, double? m) - { - if (m_insidePolygon) - { - m_figure.Add(new Vertex(x, y, z, m)); - } - else - { - m_sink.AddLine(x, y, z, m); - } - } - - public void EndFigure() - { - if (m_insidePolygon) - { - if (!IsThinRing()) - { - PopulateFigure(m_sink); - } - } - else - { - m_sink.EndFigure(); - } - } - - private bool IsThinRing() - { - try - { - SqlGeometryBuilder b = new SqlGeometryBuilder(); - b.SetSrid(m_srid); - b.BeginGeometry(OpenGisGeometryType.Polygon); - PopulateFigure(b); - b.EndGeometry(); - SqlGeometry poly = b.ConstructedGeometry.MakeValid(); - return poly.STArea().Value < m_tolerance * poly.STLength().Value; - } - catch (ArgumentException) { } - catch (FormatException) { } - return true; - } - - private void PopulateFigure(IGeometrySink sink) - { - m_figure[0].BeginFigure(sink); - for (int i = 1; i < m_figure.Count; i++) - m_figure[i].AddLine(sink); - sink.EndFigure(); - } - } -} \ No newline at end of file diff --git a/Sinks/Projector.cs b/Sinks/Projector.cs deleted file mode 100644 index 57b0454..0000000 --- a/Sinks/Projector.cs +++ /dev/null @@ -1,54 +0,0 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2008 Microsoft Corporation. -//------------------------------------------------------------------------------ -using System; -using Microsoft.SqlServer.Types; - -namespace SQLSpatialTools -{ - public sealed class Projector : IGeographySink - { - private readonly SqlProjection _projection; - private readonly IGeometrySink _sink; - - public Projector(SqlProjection projection, IGeometrySink sink) - { - _projection = projection; - _sink = sink; - } - - public void BeginGeography(OpenGisGeographyType type) - { - _sink.BeginGeometry((OpenGisGeometryType)type); - } - - public void EndGeography() - { - _sink.EndGeometry(); - } - - public void BeginFigure(double latitude, double longitude, Nullable z, Nullable m) - { - double x, y; - _projection.ProjectPoint(latitude, longitude, out x, out y); - _sink.BeginFigure(x, y, z, m); - } - - public void AddLine(double latitude, double longitude, Nullable z, Nullable m) - { - double x, y; - _projection.ProjectPoint(latitude, longitude, out x, out y); - _sink.AddLine(x, y, z, m); - } - - public void EndFigure() - { - _sink.EndFigure(); - } - - public void SetSrid(int srid) - { - _sink.SetSrid(srid); - } - } -} \ No newline at end of file diff --git a/Sinks/Unprojector.cs b/Sinks/Unprojector.cs deleted file mode 100644 index 2a78cad..0000000 --- a/Sinks/Unprojector.cs +++ /dev/null @@ -1,54 +0,0 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2008 Microsoft Corporation. -//------------------------------------------------------------------------------ -using Microsoft.SqlServer.Types; - -namespace SQLSpatialTools -{ - public sealed class Unprojector : IGeometrySink - { - private readonly SqlProjection _projection; - private readonly IGeographySink _sink; - - public Unprojector(SqlProjection projection, IGeographySink sink, int newSrid) - { - _projection = projection; - _sink = sink; - _sink.SetSrid(newSrid); - } - - public void BeginGeometry(OpenGisGeometryType type) - { - _sink.BeginGeography((OpenGisGeographyType)type); - } - - public void EndGeometry() - { - _sink.EndGeography(); - } - - public void BeginFigure(double x, double y, double? z, double? m) - { - double latitude, longitude; - _projection.UnprojectPoint(x, y, out latitude, out longitude); - _sink.BeginFigure(latitude, longitude, z, m); - } - - public void AddLine(double x, double y, double? z, double? m) - { - double latitude, longitude; - _projection.UnprojectPoint(x, y, out latitude, out longitude); - _sink.AddLine(latitude, longitude, z, m); - } - - public void EndFigure() - { - _sink.EndFigure(); - } - - public void SetSrid(int srid) - { - // Input argument not used since a new srid is defined in the constructor. - } - } -} \ No newline at end of file diff --git a/Sinks/VacuousGeographyToGeometrySink.cs b/Sinks/VacuousGeographyToGeometrySink.cs deleted file mode 100644 index a992220..0000000 --- a/Sinks/VacuousGeographyToGeometrySink.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace SQLSpatialTools -{ - /** - * This class implements a completely trivial conversion from geography to geometry, simply taking each - * point (lat,long) --> (y, x). The class takes a target geometry sink, as well as the target SRID to - * assign to the results. - */ - public class VacuousGeographyToGeometrySink : IGeographySink - { - private readonly IGeometrySink _target; - private readonly int _targetSrid; - - public VacuousGeographyToGeometrySink(int targetSrid, IGeometrySink target) - { - _target = target; - _targetSrid = targetSrid; - } - - public void AddLine(double latitude, double longitude, double? z, double? m) - { - _target.AddLine(longitude, latitude, z, m); - } - - public void BeginFigure(double latitude, double longitude, double? z, double? m) - { - _target.BeginFigure(longitude, latitude, z, m); - } - - public void BeginGeography(OpenGisGeographyType type) - { - // Convert geography to geometry types... - _target.BeginGeometry((OpenGisGeometryType) type); - } - - public void EndFigure() - { - _target.EndFigure(); - } - - public void EndGeography() - { - _target.EndGeometry(); - } - - public void SetSrid(int srid) - { - _target.SetSrid(_targetSrid); - } - } -} diff --git a/Sinks/VacuousGeometryToGeographySink.cs b/Sinks/VacuousGeometryToGeographySink.cs deleted file mode 100644 index 597c915..0000000 --- a/Sinks/VacuousGeometryToGeographySink.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace SQLSpatialTools -{ - /** - * This class implements a completely trivial conversion from geometry to geography, simply taking each - * point (x, y) --> (long, lat). The class takes a target geography sink, as well as the target SRID to - * assign to the results. - */ - public class VacuousGeometryToGeographySink : IGeometrySink - { - private readonly IGeographySink _target; - private readonly int _targetSrid; - - public VacuousGeometryToGeographySink(int targetSrid, IGeographySink target) - { - _target = target; - _targetSrid = targetSrid; - } - - public void AddLine(double x, double y, double? z, double? m) - { - _target.AddLine(y, x, z, m); - } - - public void BeginFigure(double x, double y, double? z, double? m) - { - _target.BeginFigure(y, x, z, m); - } - - public void BeginGeometry(OpenGisGeometryType type) - { - // Convert geography to geometry types... - _target.BeginGeography((OpenGisGeographyType) type); - } - - public void EndFigure() - { - _target.EndFigure(); - } - - public void EndGeometry() - { - _target.EndGeography(); - } - - public void SetSrid(int srid) - { - _target.SetSrid(_targetSrid); - } - } -} diff --git a/SpatialTools.pfx b/SpatialTools.pfx new file mode 100644 index 0000000000000000000000000000000000000000..4a6c71d1c5e7d54b5fcfd59f417a4680c01bc9b4 GIT binary patch literal 1764 zcmZXTc|4SP9LJw$jL}Te$k8~)QK2R48DlW(T1R%>Bo@h;48oXy{2+`V- zJDGJ9Au38*2CG8D4#kwLdsnhgz4mzRYhT~j_xF50@6Ye|yncWDz7PY&Lx4gM166`T ztEYXNwy_5k0m~VvEF=S!31cS25NiHUBvgrH2vxwi4DMpo?!QinVn9SW1JMjI5Vs+D z6y|RX|6B~YE0sKd%G+zU9f1(}8OcE8*6N!1e+<#EGjQ3$9_%PQ<1c0>p4Um|Dn@DUX1l9reeJrcOH{vL0Q zIjzt!b3i*AINr7qBed7M^w8b#w>tt?>zKZc>!sL-)cTWhapEg`_95f*d7FXXShY>} zMmWfBojTq?`a=AD)UZ=ZBroslk^6lL=c_B7T%%dgckF;$!!8eOjMd03J3|!$@)eiR zE;c(cKkWN{YJBBnoigkF)LMwt$Yi6gi$tgyLLdOO`J{0*A-q9C&t$>{38_8|cW=#f zR$j<=c+typ`abXbO^;)=Eq=(oZDhjigVfUI4n03-d%dZq!AG#Qa2aVFa0Ho<6W z%r6a(u>x zSwfPNqOR(@*&s$^th;Lv&{M>hl}!3r$6^*__ar_2kRQ7C^QM(_gdt+3_!(HH-gxCp z!xO%-+T9*vQsA`@j#4>T&nn*&^Igj+wLLtdTTM92AI)~lD)CmSl$qiw0?kXakJa;9 z%8fc#4Y7ojAxPq%XO5O4ppZxq008L3zfh83uV4$r0cOBaKo770%;6@%rTN)IfGMnW zV6O)!n(!`jfCxaaf?%x+r=Jo_xO8AbfGYtyi9&0jKqSb}69);73if~<5Cr%GG$0%} z3-@Ut5>Nu1;q?;<0|H@Z4(nKe3bSz7(*R#sQ$C+k0vtc@i-O%LAON7jllbVL+l!#! z;cSHw2LS@`r|#bb{QtOabylG^YTF-u8rNh7A~|3;U7J3pJXcv}BdOxt_@%e?_mPgl zbc!Ok_zGrG<;AP865_O|;y|*XV%R}9zqdy#qJT`FCz|x6OK~w7q5H#1Vyv!Ub>VT+nq=P&q}_e)pv={p(P#X7tpBNdLp69 z3Rg9IG@rcEzjEa0Y_c*}54YWpTb=D}Kk9yq;H6ji2YI0=ddj$FRBK~zWJKG&2%qh0 zoRO5LNEB^+vdNgQ(Rycmskxn)7sKXVvukCj`wS-RBC*+KZN%TFW#9G#;?mh_E!?WZ z97*?JQYy-tPk6^;dDqZF{7p6{C=A>D-_&FtQFE(zFRTQ2Z`mdGMXoOCY6yrr3)`uk z4kc>?7ncRnK}_=1!RMKUoPhNY1qLR|GxGHu^~~Oj8#l;)*M5+|R_A&Q6d%v#&Pi6` zr>wP}PZ&CEPwZLAy!cGw$@)-A-t~^dmY&;J>+qrROG;}wsl>=9U)VJwV_wq+YTVYx zda(YT`bqxKp*^UKp*TZ_~k?zL7-F8*Q$~oPI!n3?M~F1SPB? zCWMehN}kN6GuBMantrKxu{2w*>8H8>156LRu24OzGjCdA(Gr}UEE@N^;27`I1O5|) C2>Lbv literal 0 HcmV?d00001 diff --git a/Types/AffineTransform.cs b/Types/AffineTransform.cs deleted file mode 100644 index 737f7f7..0000000 --- a/Types/AffineTransform.cs +++ /dev/null @@ -1,127 +0,0 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2008 Microsoft Corporation. -//------------------------------------------------------------------------------ - -using System; -using System.IO; -using System.Data.SqlTypes; -using Microsoft.SqlServer.Server; -using Microsoft.SqlServer.Types; - -namespace SQLSpatialTools -{ - [Serializable] - [SqlUserDefinedType(Format.UserDefined, IsByteOrdered = false, MaxByteSize = -1, IsFixedLength = false)] - public sealed class AffineTransform : INullable, IBinarySerialize - { - public double ax, bx, cx; - public double ay, by, cy; - - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public static AffineTransform Translate(double dx, double dy) - { - AffineTransform t = new AffineTransform(); - t.ax = 1; - t.by = 1; - t.cx = dx; - t.cy = dy; - return t; - } - - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public static AffineTransform Rotate(double angleDeg) - { - double angle = Util.ToRadians(angleDeg); - AffineTransform t = new AffineTransform(); - t.ax = Math.Cos(angle); - t.ay = Math.Sin(angle); - t.bx = -t.ay; - t.by = t.ax; - return t; - } - - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public static AffineTransform Scale(double sx, double sy) - { - AffineTransform t = new AffineTransform(); - t.ax = sx; - t.by = sy; - return t; - } - - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public SqlGeometry Apply(SqlGeometry geometry) - { - SqlGeometryBuilder builder = new SqlGeometryBuilder(); - geometry.Populate(new GeometryTransformer(builder, this)); - return builder.ConstructedGeometry; - } - - public double GetX(double x, double y) - { - return ax * x + bx * y + cx; - } - - public double GetY(double x, double y) - { - return ay * x + by * y + cy; - } - - public void Read(BinaryReader r) - { - ax = r.ReadDouble(); - bx = r.ReadDouble(); - cx = r.ReadDouble(); - ay = r.ReadDouble(); - by = r.ReadDouble(); - cy = r.ReadDouble(); - } - - public void Write(BinaryWriter w) - { - w.Write(ax); - w.Write(bx); - w.Write(cx); - w.Write(ay); - w.Write(by); - w.Write(cy); - } - - public bool IsNull - { - get { return false; } - } - - public static AffineTransform Null - { - [SqlMethod(IsDeterministic = true, IsPrecise = true)] - get - { - return null; - } - } - - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public static AffineTransform Parse(SqlString str) - { - AffineTransform tranform = new AffineTransform(); - - string[] args = str.ToString().Split(' '); - tranform.ax = Double.Parse(args[0]); - tranform.bx = Double.Parse(args[1]); - tranform.cx = Double.Parse(args[2]); - tranform.ay = Double.Parse(args[3]); - tranform.by = Double.Parse(args[4]); - tranform.cy = Double.Parse(args[5]); - - return tranform; - } - - [return: SqlFacet(MaxSize = -1)] - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public override string ToString() - { - return ax + " " + bx + " " + cx + " " + ay + " " + by + " " + cy; - } - } -} \ No newline at end of file diff --git a/Types/SqlProjection.cs b/Types/SqlProjection.cs deleted file mode 100644 index 7b79c1c..0000000 --- a/Types/SqlProjection.cs +++ /dev/null @@ -1,284 +0,0 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2008 Microsoft Corporation. -//------------------------------------------------------------------------------ -using System; -using System.Collections.Generic; -using System.Data.SqlTypes; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Reflection; -using Microsoft.SqlServer.Server; -using Microsoft.SqlServer.Types; - -namespace SQLSpatialTools -{ - [Serializable] - [SqlUserDefinedType(Format.UserDefined, IsByteOrdered = false, MaxByteSize = -1, IsFixedLength = false)] - public sealed class SqlProjection : INullable, IBinarySerialize - { - private Projection _projection; - - public SqlProjection() - { - } - - internal SqlProjection(Projection projection) - { - Debug.Assert(projection != null); - _projection = projection; - } - - public static SqlProjection Null - { - [SqlMethod(IsDeterministic = true, IsPrecise = true)] - get - { - return new SqlProjection(); - } - } - - // TODO use WKT projection description format - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public static SqlProjection Parse(SqlString s) - { - if (s.Value.Equals("NULL")) - { - return SqlProjection.Null; - } - else - { - string[] a = s.Value.Split(' '); - ConstructorInfo cons = Type.GetType(a[0]).GetConstructor(new Type[] { typeof(Dictionary) }); - return new SqlProjection((Projection)cons.Invoke(new object[] { Projection.ParseParameters(a[1]) })); - } - } - - // TODO use WKT projection description format - [return: SqlFacet(MaxSize = -1)] - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public override string ToString() - { - if (_projection == null) - { - return "NULL"; - } - else - { - return _projection.GetType().FullName + " " + _projection.Parameters; - } - } - - public bool IsNull - { - [SqlMethod(IsDeterministic = true, IsPrecise = true)] - get { return _projection == null; } - } - - public void Read(BinaryReader r) - { - if (r != null) - { - string name = r.ReadString(); - if (name.Equals("")) - { - _projection = null; - } - else - { - ConstructorInfo cons = Type.GetType(name).GetConstructor(new Type[] { typeof(Dictionary) }); - _projection = (Projection)cons.Invoke(new object[] { Projection.ParseParameters(r.ReadString()) }); - } - } - } - - public void Write(BinaryWriter w) - { - if (w != null) - { - if (_projection == null) - { - w.Write(""); - } - else - { - w.Write(_projection.GetType().FullName); - w.Write(_projection.Parameters); - } - } - } - - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public static SqlProjection AlbersEqualArea(double longitude0, double latitude0, double parallel1, double parallel2) - { - Dictionary parameters = new Dictionary(); - parameters["longitude0"] = longitude0; - parameters["latitude0"] = latitude0; - parameters["parallel1"] = parallel1; - parameters["parallel2"] = parallel2; - return new SqlProjection(new AlbersEqualAreaProjection(parameters)); - } - - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public static SqlProjection Equirectangular(double longitude0, double parallel) - { - Dictionary parameters = new Dictionary(); - parameters["longitude0"] = longitude0; - parameters["parallel"] = parallel; - return new SqlProjection(new EquirectangularProjection(parameters)); - } - - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public static SqlProjection LambertConformalConic(double longitude0, double latitude, double fi1, double fi2) - { - Dictionary parameters = new Dictionary(); - parameters["longitude0"] = longitude0; - parameters["latitude0"] = latitude; - parameters["fi1"] = fi1; - parameters["fi2"] = fi2; - return new SqlProjection(new LambertConformalConicProjection(parameters)); - } - - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public static SqlProjection Mercator(double longitude0) - { - Dictionary parameters = new Dictionary(); - parameters["longitude0"] = longitude0; - return new SqlProjection(new MercatorProjection(parameters)); - } - - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public static SqlProjection ObliqueMercator(double longitude0, double fi1, double lambda1, double fi2, double lambda2) - { - Dictionary parameters = new Dictionary(); - parameters["longitude0"] = longitude0; - parameters["fi1"] = fi1; - parameters["lambda1"] = lambda1; - parameters["fi2"] = fi2; - parameters["lambda2"] = lambda2; - return new SqlProjection(new ObliqueMercatorProjection(parameters)); - } - - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public static SqlProjection TranverseMercator(double longitude0) - { - Dictionary parameters = new Dictionary(); - parameters["longitude0"] = longitude0; - return new SqlProjection(new TranverseMercatorProjection(parameters)); - } - - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public static SqlProjection Gnomonic(double longitude, double latitude) - { - Dictionary parameters = new Dictionary(); - parameters["longitude0"] = 0; - parameters["longitude1"] = longitude; - parameters["latitude1"] = latitude; - return new SqlProjection(new GnommonicProjection(parameters)); - } - - private static void ThrowIfArgumentNull(INullable argument, string name) - { - if (argument == null || argument.IsNull) - { - throw new ArgumentNullException(name); - } - } - - // INSTANCE METHODS AND FIELDS - - // Projects geography onto geometry. - // Returns valid projected SqlGeometry object. - // Constructed geometry will have the same SRID as geography. - // - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public SqlGeometry Project(SqlGeography geography) - { - ThrowIfArgumentNull(geography, "geography"); - SqlGeometryBuilder builder = new SqlGeometryBuilder(); - geography.Populate(new Projector(this, builder)); - return builder.ConstructedGeometry; - } - - // Uprojects geometry producing geography. - // Returns a valid uprojected SqlGeography object. - // - // SRID taken from the geometry object may not be valid for geography, - // in which case a new SRID must be given. - // - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public SqlGeography UnprojectWithSRID(SqlGeometry geometry, SqlInt32 newSrid) - { - ThrowIfArgumentNull(geometry, "geometry"); - ThrowIfArgumentNull(newSrid, "newSrid"); - SqlGeographyBuilder builder = new SqlGeographyBuilder(); - geometry.Populate(new Unprojector(this, builder, newSrid.Value)); - return builder.ConstructedGeography; - } - - // Uprojects geometry producing geography. - // This method assumes that the SRID of geometry object is valid for geography. - // Returns a valid uprojected SqlGeography object. - // - [SqlMethod(IsDeterministic = true, IsPrecise = false)] - public SqlGeography Unproject(SqlGeometry geometry) - { - ThrowIfArgumentNull(geometry, "geometry"); - SqlGeographyBuilder builder = new SqlGeographyBuilder(); - geometry.Populate(new Unprojector(this, builder, geometry.STSrid.Value)); // NOTE srid will be reused - return builder.ConstructedGeography; - } - - // Longitude and latitude are in degrees. - // Input Latitude must be in range [-90, 90]. - // Input Longitude must be in range [-15069, 15069]. - // - public void ProjectPoint(double latitudeDeg, double longitudeDeg, out double x, out double y) - { - double latitude = MathX.InputLat(latitudeDeg, 90, "latitude"); - double longitude = MathX.NormalizeLongitudeRad(MathX.InputLong(longitudeDeg, 15069, "longitude") - _projection.CentralLongitudeRad); - - _projection.Project(latitude, longitude, out x, out y); - - if (Double.IsNaN(x)) - { - throw new ArgumentOutOfRangeException("x"); - } - if (Double.IsNaN(y)) - { - throw new ArgumentOutOfRangeException("y"); - } - } - - // Longitude and latitude are in degrees. - // Output Latitude will be in range [-90, 90]. - // Output Longitude will be in range [-180, 180]. - // - public void UnprojectPoint(double x, double y, out double latitudeDeg, out double longitudeDeg) - { - if (Double.IsNaN(x)) - { - throw new ArgumentException(Resource.InputCoordinateIsNaN, "x"); - } - if (Double.IsNaN(y)) - { - throw new ArgumentException(Resource.InputCoordinateIsNaN, "y"); - } - - double latitude, longitude; - _projection.Unproject(x, y, out latitude, out longitude); - - if (Double.IsNaN(latitude) || latitude < -Math.PI / 2 || latitude > Math.PI / 2) - { - throw new ArgumentOutOfRangeException("latitude", String.Format(CultureInfo.InvariantCulture, Resource.OutputLatitudeIsOutOfRange, latitude)); - } - if (Double.IsNaN(longitude) || longitude < -Math.PI || longitude > Math.PI) - { - throw new ArgumentOutOfRangeException("longitude", String.Format(CultureInfo.InvariantCulture, Resource.OutputLongitudeIsOutOfRange, longitude)); - } - - latitudeDeg = MathX.Clamp(90, Util.ToDegrees(latitude)); - longitudeDeg = MathX.NormalizeLongitudeDeg(Util.ToDegrees(longitude + _projection.CentralLongitudeRad)); - } - } -} \ No newline at end of file diff --git a/Aggregates/GeographyUnionAggregate.cs b/src/Aggregates/GeographyUnionAggregate.cs similarity index 77% rename from Aggregates/GeographyUnionAggregate.cs rename to src/Aggregates/GeographyUnionAggregate.cs index 9693265..3129240 100644 --- a/Aggregates/GeographyUnionAggregate.cs +++ b/src/Aggregates/GeographyUnionAggregate.cs @@ -1,303 +1,313 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2008 Microsoft Corporation. -//------------------------------------------------------------------------------ - -using System; -using System.IO; -using System.Diagnostics; -using System.Runtime.InteropServices; -using Microsoft.SqlServer.Server; -using Microsoft.SqlServer.Types; - -namespace SQLSpatialTools -{ - [Serializable] - [StructLayout(LayoutKind.Sequential)] - [SqlUserDefinedAggregate( - Format.UserDefined, - IsInvariantToDuplicates = true, - IsInvariantToNulls = true, - IsInvariantToOrder = true, - IsNullIfEmpty = false, - MaxByteSize = -1)] - public class GeographyUnionAggregate : IBinarySerialize - { - private GeographyCollectionAggregate aggregate; - - public void Init() - { - if (aggregate == null) aggregate = new GeographyCollectionAggregate(); - aggregate.Init(); - } - - public void Accumulate(SqlGeography g) - { - aggregate.Accumulate(g); - } - - public void Merge(GeographyUnionAggregate group) - { - aggregate.Merge(group.aggregate); - } - - public SqlGeography Terminate() - { - SqlGeography g = aggregate.Terminate(); - - if (g.IsNull || g.STIsEmpty().Value) - return g; - - try - { - // Force self union of the collection. - return g.STUnion(g.STPointN(1)); - } - catch (ArgumentException) - { - // Union of an object with its first point could fail is the object - // is slightly smaller than a hemisphere. So, do the more expensive, - // but correct union here. - return g.STUnion(g); - } - } - - public void Read(BinaryReader r) - { - if (aggregate == null) aggregate = new GeographyCollectionAggregate(); - aggregate.Read(r); - } - - public void Write(BinaryWriter w) - { - aggregate.Write(w); - } - } - - [Serializable] - [StructLayout(LayoutKind.Sequential)] - [SqlUserDefinedAggregate( - Format.UserDefined, - IsInvariantToDuplicates = true, - IsInvariantToNulls = true, - IsInvariantToOrder = true, - IsNullIfEmpty = false, - MaxByteSize = -1)] - public class GeographyCollectionAggregate : IBinarySerialize - { - private SqlGeographyBuilder m_builder; - private IGeographySink m_sink; - private int m_srid; - private bool m_error; - - public void Init() - { - m_srid = -1; - m_builder = null; - m_sink = null; - m_error = false; - } - - private bool IsInitialState() - { - return m_builder == null && !m_error; - } - - public void Accumulate(SqlGeography g) - { - if (g == null || g.IsNull || m_error) return; - Reset(g.STSrid.Value); - if (m_sink != null) g.Populate(m_sink); - } - - private void Reset(int srid) - { - if (m_builder == null) - { - m_srid = srid; - m_builder = new SqlGeographyBuilder(); - m_builder.SetSrid(m_srid); - m_builder.BeginGeography(OpenGisGeographyType.GeometryCollection); - m_sink = new StripSRID(m_builder); - } - else if (srid != m_srid) - { - m_srid = -1; - m_builder = null; - m_sink = null; - m_error = true; - } - } - - private SqlGeography ConstructedGeography() - { - SqlGeography g = SqlGeography.Null; - if (!m_error) - { - try - { - m_builder.EndGeography(); - g = m_builder.ConstructedGeography; - } - catch (ArgumentException) - { - // Result is larger than a hemisphere! - } - } - Init(); - return g; - } - - public void Merge(GeographyCollectionAggregate group) - { - if (group.IsInitialState()) return; - - SqlGeography g = group.ConstructedGeography(); - if (g.IsNull) - { - m_srid = -1; - m_builder = null; - m_sink = null; - m_error = true; - } - else - { - Reset(g.STSrid.Value); - g.Populate(new StripCollection(m_builder)); - } - } - - public SqlGeography Terminate() - { - if (IsInitialState()) return SqlGeography.Null; - - SqlGeography g = ConstructedGeography(); - Init(); - return g; - } - - public void Read(BinaryReader r) - { - if (r.ReadBoolean()) - { - Init(); - } - else - { - SqlGeography g = new SqlGeography(); - g.Read(r); - if (g.IsNull) - { - m_srid = -1; - m_error = true; - m_builder = null; - m_sink = null; - } - else - { - m_srid = g.STSrid.Value; - m_builder = new SqlGeographyBuilder(); - m_sink = new StripSRID(m_builder); - m_builder.SetSrid(m_srid); - m_builder.BeginGeography(OpenGisGeographyType.GeometryCollection); - g.Populate(new StripCollection(m_builder)); - } - } - } - - public void Write(BinaryWriter w) - { - w.Write(IsInitialState()); - if (!IsInitialState()) - { - ConstructedGeography().Write(w); - Init(); - } - } - } - - internal class StripSRID : IGeographySink - { - private readonly IGeographySink m_sink; - - public StripSRID(IGeographySink sink) - { - m_sink = sink; - } - - public void BeginGeography(OpenGisGeographyType type) - { - m_sink.BeginGeography(type); - } - - public void BeginFigure(double latitude, double longitude, double? z, double? m) - { - m_sink.BeginFigure(latitude, longitude, z, m); - } - - public void AddLine(double latitude, double longitude, double? z, double? m) - { - m_sink.AddLine(latitude, longitude, z, m); - } - - public void EndFigure() - { - m_sink.EndFigure(); - } - - public void EndGeography() - { - m_sink.EndGeography(); - } - - public void SetSrid(int srid) { } - } - - internal class StripCollection : IGeographySink - { - private readonly IGeographySink m_sink; - private int m_depth; - - public StripCollection(IGeographySink sink) - { - m_sink = sink; - } - - public void BeginGeography(OpenGisGeographyType type) - { - if (m_depth > 0) - { - m_sink.BeginGeography(type); - } - else - { - Debug.Assert(OpenGisGeographyType.GeometryCollection == type); - } - m_depth += 1; - } - - public void BeginFigure(double latitude, double longitude, double? z, double? m) - { - m_sink.BeginFigure(latitude, longitude, z, m); - } - - public void AddLine(double latitude, double longitude, double? z, double? m) - { - m_sink.AddLine(latitude, longitude, z, m); - } - - public void EndFigure() - { - m_sink.EndFigure(); - } - - public void EndGeography() - { - m_depth -= 1; - if (m_depth > 0) m_sink.EndGeography(); - } - - public void SetSrid(int srid) - { - } - } +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.IO; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.SqlServer.Server; +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.Aggregates +{ + [Serializable] + [StructLayout(LayoutKind.Sequential)] + [SqlUserDefinedAggregate( + Format.UserDefined, + IsInvariantToDuplicates = true, + IsInvariantToNulls = true, + IsInvariantToOrder = true, + IsNullIfEmpty = false, + MaxByteSize = -1)] + public class GeographyUnionAggregate : IBinarySerialize + { + private GeographyCollectionAggregate aggregate; + + public void Init() + { + if (aggregate == null) aggregate = new GeographyCollectionAggregate(); + aggregate.Init(); + } + + public void Accumulate(SqlGeography g) + { + aggregate.Accumulate(g); + } + + public void Merge(GeographyUnionAggregate group) + { + aggregate.Merge(group.aggregate); + } + + public SqlGeography Terminate() + { + var g = aggregate.Terminate(); + + if (g.IsNull || g.STIsEmpty().Value) + return g; + + try + { + // Force self union of the collection. + return g.STUnion(g.STPointN(1)); + } + catch (ArgumentException) + { + // Union of an object with its first point could fail is the object + // is slightly smaller than a hemisphere. So, do the more expensive, + // but correct union here. + return g.STUnion(g); + } + } + + public void Read(BinaryReader r) + { + if (aggregate == null) aggregate = new GeographyCollectionAggregate(); + aggregate.Read(r); + } + + public void Write(BinaryWriter w) + { + aggregate.Write(w); + } + } + + [Serializable] + [StructLayout(LayoutKind.Sequential)] + [SqlUserDefinedAggregate( + Format.UserDefined, + IsInvariantToDuplicates = true, + IsInvariantToNulls = true, + IsInvariantToOrder = true, + IsNullIfEmpty = false, + MaxByteSize = -1)] + public class GeographyCollectionAggregate : IBinarySerialize + { + private SqlGeographyBuilder m_builder; + private IGeographySink110 m_sink; + private int m_srid; + private bool m_error; + + public void Init() + { + m_srid = -1; + m_builder = null; + m_sink = null; + m_error = false; + } + + private bool IsInitialState() + { + return m_builder == null && !m_error; + } + + public void Accumulate(SqlGeography g) + { + if (g == null || g.IsNull || m_error) return; + Reset(g.STSrid.Value); + if (m_sink != null) g.Populate(m_sink); + } + + private void Reset(int srid) + { + if (m_builder == null) + { + m_srid = srid; + m_builder = new SqlGeographyBuilder(); + m_builder.SetSrid(m_srid); + m_builder.BeginGeography(OpenGisGeographyType.GeometryCollection); + m_sink = new StripSRID(m_builder); + } + else if (srid != m_srid) + { + m_srid = -1; + m_builder = null; + m_sink = null; + m_error = true; + } + } + + private SqlGeography ConstructedGeography() + { + SqlGeography g = SqlGeography.Null; + if (!m_error) + { + try + { + m_builder.EndGeography(); + g = m_builder.ConstructedGeography; + } + catch (ArgumentException) + { + // Result is larger than a hemisphere! + } + } + Init(); + return g; + } + + public void Merge(GeographyCollectionAggregate group) + { + if (group.IsInitialState()) return; + + SqlGeography g = group.ConstructedGeography(); + if (g.IsNull) + { + m_srid = -1; + m_builder = null; + m_sink = null; + m_error = true; + } + else + { + Reset(g.STSrid.Value); + g.Populate(new StripCollection(m_builder)); + } + } + + public SqlGeography Terminate() + { + if (IsInitialState()) return SqlGeography.Null; + + SqlGeography g = ConstructedGeography(); + Init(); + return g; + } + + public void Read(BinaryReader r) + { + if (r.ReadBoolean()) + { + Init(); + } + else + { + SqlGeography g = new SqlGeography(); + g.Read(r); + if (g.IsNull) + { + m_srid = -1; + m_error = true; + m_builder = null; + m_sink = null; + } + else + { + m_srid = g.STSrid.Value; + m_builder = new SqlGeographyBuilder(); + m_sink = new StripSRID(m_builder); + m_builder.SetSrid(m_srid); + m_builder.BeginGeography(OpenGisGeographyType.GeometryCollection); + g.Populate(new StripCollection(m_builder)); + } + } + } + + public void Write(BinaryWriter w) + { + w.Write(IsInitialState()); + if (!IsInitialState()) + { + ConstructedGeography().Write(w); + Init(); + } + } + } + + internal class StripSRID : IGeographySink110 + { + private readonly IGeographySink110 _sink; + + public StripSRID(IGeographySink110 sink) + { + _sink = sink; + } + + public void BeginGeography(OpenGisGeographyType type) + { + _sink.BeginGeography(type); + } + + public void BeginFigure(double latitude, double longitude, double? z, double? m) + { + _sink.BeginFigure(latitude, longitude, z, m); + } + + public void AddLine(double latitude, double longitude, double? z, double? m) + { + _sink.AddLine(latitude, longitude, z, m); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + _sink.EndFigure(); + } + + public void EndGeography() + { + _sink.EndGeography(); + } + + public void SetSrid(int srid) { } + } + + internal class StripCollection : IGeographySink110 + { + private readonly IGeographySink110 _sink; + private int _depth; + + public StripCollection(IGeographySink110 sink) + { + _sink = sink; + } + + public void BeginGeography(OpenGisGeographyType type) + { + if (_depth > 0) + { + _sink.BeginGeography(type); + } + else + { + Debug.Assert(OpenGisGeographyType.GeometryCollection == type); + } + _depth += 1; + } + + public void BeginFigure(double latitude, double longitude, double? z, double? m) + { + _sink.BeginFigure(latitude, longitude, z, m); + } + + public void AddLine(double latitude, double longitude, double? z, double? m) + { + _sink.AddLine(latitude, longitude, z, m); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + _sink.EndFigure(); + } + + public void EndGeography() + { + _depth -= 1; + if (_depth > 0) _sink.EndGeography(); + } + + public void SetSrid(int srid) + { + } + } } \ No newline at end of file diff --git a/Aggregates/GeometryEnvelopeAggregate.cs b/src/Aggregates/GeometryEnvelopeAggregate.cs similarity index 64% rename from Aggregates/GeometryEnvelopeAggregate.cs rename to src/Aggregates/GeometryEnvelopeAggregate.cs index 0c2bb91..5b60903 100644 --- a/Aggregates/GeometryEnvelopeAggregate.cs +++ b/src/Aggregates/GeometryEnvelopeAggregate.cs @@ -1,101 +1,106 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2008 Microsoft Corporation. -//------------------------------------------------------------------------------ - -using System; -using System.Runtime.InteropServices; -using Microsoft.SqlServer.Server; -using Microsoft.SqlServer.Types; - -namespace SQLSpatialTools -{ - [Serializable] - [StructLayout(LayoutKind.Sequential)] - [SqlUserDefinedAggregate( - Format.Native, - IsInvariantToDuplicates = true, - IsInvariantToNulls = true, - IsInvariantToOrder = true, - IsNullIfEmpty = true)] - public class GeometryEnvelopeAggregate : IGeometrySink - { - private double minX, maxX, minY, maxY; - private int lastSrid; - private bool failed; - - public void SetSrid(int srid) - { - if (lastSrid != -1 && lastSrid != srid) failed = true; - lastSrid = srid; - } - - public void BeginGeometry(OpenGisGeometryType type) { } - - public void BeginFigure(double x, double y, double? z, double? m) - { - IncludePoint(x, y); - } - - public void AddLine(double x, double y, double? z, double? m) - { - IncludePoint(x, y); - } - - public void EndFigure() { } - - public void EndGeometry() { } - - public void Init() - { - minX = minY = Double.PositiveInfinity; - maxX = maxY = Double.NegativeInfinity; - lastSrid = -1; - failed = false; - } - - public void Accumulate(SqlGeometry geometry) - { - if (geometry != null) geometry.Populate(this); - } - - public void Merge(GeometryEnvelopeAggregate group) - { - minX = Math.Min(minX, group.minX); - maxX = Math.Max(maxX, group.maxX); - minY = Math.Min(minY, group.minY); - maxY = Math.Max(maxY, group.maxY); - - if (group.lastSrid != -1) - { - if (lastSrid != -1 && lastSrid != group.lastSrid) failed = true; - lastSrid = group.lastSrid; - } - if (group.failed) failed = true; - } - - public SqlGeometry Terminate() - { - if (failed) return SqlGeometry.Null; - - SqlGeometryBuilder b = new SqlGeometryBuilder(); - b.SetSrid(lastSrid); - b.BeginGeometry(OpenGisGeometryType.Polygon); - b.BeginFigure(minX, minY); - b.AddLine(maxX, minY); - b.AddLine(maxX, maxY); - b.AddLine(minX, maxY); - b.AddLine(minX, minY); - b.EndFigure(); - b.EndGeometry(); - return b.ConstructedGeometry; - } - - private void IncludePoint(double x, double y) - { - minX = Math.Min(minX, x); - maxX = Math.Max(maxX, x); - minY = Math.Min(minY, y); - maxY = Math.Max(maxY, y); - } - } +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Runtime.InteropServices; +using Microsoft.SqlServer.Server; +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.Aggregates +{ + [Serializable] + [StructLayout(LayoutKind.Sequential)] + [SqlUserDefinedAggregate( + Format.Native, + IsInvariantToDuplicates = true, + IsInvariantToNulls = true, + IsInvariantToOrder = true, + IsNullIfEmpty = true)] + public class GeometryEnvelopeAggregate : IGeometrySink110 + { + private double minX, maxX, minY, maxY; + private int lastSrid; + private bool failed; + + public void SetSrid(int srid) + { + if (lastSrid != -1 && lastSrid != srid) failed = true; + lastSrid = srid; + } + + public void BeginGeometry(OpenGisGeometryType type) { } + + public void BeginFigure(double x, double y, double? z, double? m) + { + IncludePoint(x, y); + } + + public void AddLine(double x, double y, double? z, double? m) + { + IncludePoint(x, y); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() { } + + public void EndGeometry() { } + + public void Init() + { + minX = minY = double.PositiveInfinity; + maxX = maxY = double.NegativeInfinity; + lastSrid = -1; + failed = false; + } + + public void Accumulate(SqlGeometry geometry) + { + geometry?.Populate(this); + } + + public void Merge(GeometryEnvelopeAggregate group) + { + minX = Math.Min(minX, group.minX); + maxX = Math.Max(maxX, group.maxX); + minY = Math.Min(minY, group.minY); + maxY = Math.Max(maxY, group.maxY); + + if (group.lastSrid != -1) + { + if (lastSrid != -1 && lastSrid != group.lastSrid) failed = true; + lastSrid = group.lastSrid; + } + if (group.failed) failed = true; + } + + public SqlGeometry Terminate() + { + if (failed) return SqlGeometry.Null; + + var builder = new SqlGeometryBuilder(); + builder.SetSrid(lastSrid); + builder.BeginGeometry(OpenGisGeometryType.Polygon); + builder.BeginFigure(minX, minY); + builder.AddLine(maxX, minY); + builder.AddLine(maxX, maxY); + builder.AddLine(minX, maxY); + builder.AddLine(minX, minY); + builder.EndFigure(); + builder.EndGeometry(); + return builder.ConstructedGeometry; + } + + private void IncludePoint(double x, double y) + { + minX = Math.Min(minX, x); + maxX = Math.Max(maxX, x); + minY = Math.Min(minY, y); + maxY = Math.Max(maxY, y); + } + } } \ No newline at end of file diff --git a/src/Functions/General/Geography.cs b/src/Functions/General/Geography.cs new file mode 100644 index 0000000..0950236 --- /dev/null +++ b/src/Functions/General/Geography.cs @@ -0,0 +1,346 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Data.SqlTypes; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Sinks.Geography; +using SQLSpatialTools.Sinks.Geometry; +using SQLSpatialTools.Types.SQL; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.Functions.General +{ + /// + /// This class contains General Geography type functions that can be registered in SQL Server. + /// + public static class Geography + { + /// + /// Make our LocateAlongGeographySink into a function call. + /// This function just hooks up and runs a pipeline using the sink. + /// + /// Sql Geography + /// Distance at which the point to be located + /// + public static SqlGeography LocatePointAlongGeog(SqlGeography geography, double distance) + { + var geogBuilder = new SqlGeographyBuilder(); + var geogSink = new LocateAlongGeographySink(distance, geogBuilder); + geography.Populate(geogSink); + return geogBuilder.ConstructedGeography; + } + + /// + /// Find the point that is the given distance from the start point in the direction of the end point. + /// The distance must be less than the distance between these two points. + /// + /// Start Geography Point + /// End Geography Point + /// Distance at which the point to be located + /// + public static SqlGeography InterpolateBetweenGeog(SqlGeography start, SqlGeography end, double distance) + { + // We need to check a few prerequisite. + // We only operate on points. + if (!start.IsPoint() || !end.IsPoint()) + { + throw new ArgumentException(ErrorMessage.PointCompatible); + } + + // The SRIDs also have to match + var srid = start.STSrid.Value; + if (srid != end.STSrid.Value) + { + throw new ArgumentException(ErrorMessage.SRIDCompatible); + } + + // Finally, the distance has to fall between these points. + var length = start.STDistance(end).Value; + if (distance > length) + { + throw new ArgumentException(ErrorMessage.DistanceMustBeBetweenTwoPoints); + } + + if (distance < 0) + { + throw new ArgumentException(ErrorMessage.DistanceMustBePositive); + } + + // We'll just do this by binary search---surely this could be more efficient, + // but this is relatively easy. + // + // Note that we can't just take the take the linear combination of end vectors because we + // aren't working on a sphere. + + // We are going to do our binary search using 3D Cartesian values, however + var startCart = SpatialUtil.GeographicToCartesian(start); + var endCart = SpatialUtil.GeographicToCartesian(end); + + SqlGeography current; + double currentDistance; + + // Keep refining until we slip below the THRESHOLD value. + do + { + var currentCart = (startCart + endCart) / 2; + current = SpatialUtil.CartesianToGeographic(currentCart, srid); + currentDistance = start.STDistance(current).Value; + + if (distance <= currentDistance) + endCart = currentCart; + else + startCart = currentCart; + } while (Math.Abs(currentDistance - distance) > Constants.Tolerance); + + return current; + } + + /// + /// This function is used for generating a new geography object where additional points are inserted + /// along every line in such a way that the angle between two consecutive points does not + /// exceed a prescribed angle. The points are generated between the unit vectors that correspond + /// to the line's start and end along the great-circle arc on the unit sphere. This follows the + /// definition of geodetic lines in SQL Server. + /// + /// Input Sql geography + /// Max Angle + /// + public static SqlGeography DensifyGeography(SqlGeography geography, double maxAngle) + { + var geogBuilder = new SqlGeographyBuilder(); + geography.Populate(new DensifyGeographySink(geogBuilder, maxAngle)); + return geogBuilder.ConstructedGeography; + } + + /// + /// Performs a complete trivial conversion from geography to geometry, simply taking each + ///point (lat,long) -> (y, x). The result is assigned the given SRID. + /// + /// + /// + /// + public static SqlGeometry VacuousGeographyToGeometry(SqlGeography toConvert, int targetSrid) + { + var geomBuilder = new SqlGeometryBuilder(); + toConvert.Populate(new VacuousGeographyToGeometrySink(targetSrid, geomBuilder)); + return geomBuilder.ConstructedGeometry; + } + + /// + /// Computes ConvexHull of input geography and returns a polygon (unless all input points are collinear). + /// + /// Input Sql Geography + /// + public static SqlGeography ConvexHullGeography(SqlGeography geography) + { + if (geography.IsNull || geography.STIsEmpty().Value) return geography; + + var center = geography.EnvelopeCenter(); + var gnomonicProjection = SqlProjection.Gnomonic(center.Long.Value, center.Lat.Value); + var geometry = gnomonicProjection.Project(geography); + return gnomonicProjection.Unproject(geometry.MakeValid().STConvexHull()); + } + + /// + /// Computes ConvexHull of input WKT and returns a polygon (unless all input points are collinear). + /// This function does not require its input to be a valid geography. This function does require + /// that the WKT coordinate values are longitude/latitude values, in that order and that a valid + /// geography SRID value is supplied. + /// + /// Input Well Known Text + /// Spatial Reference Identifier + /// + public static SqlGeography ConvexHullGeographyFromText(string inputWKT, int srid) + { + var geometry = SqlGeometry.STGeomFromText(new SqlChars(inputWKT), srid); + var geographyBuilder = new SqlGeographyBuilder(); + geometry.Populate(new GeometryToPointGeographySink(geographyBuilder)); + return ConvexHullGeography(geographyBuilder.ConstructedGeography); + } + + /// + /// Check if an input geometry can represent a valid geography without throwing an exception. + /// This function requires that the geometry be in longitude/latitude coordinates and that + /// those coordinates are in correct order in the geometry instance (i.e. latitude/longitude + /// not longitude/latitude). This function will return false (0) if the input geometry is not + /// in the correct latitude/longitude format, including a valid geography SRID. + /// + /// Input Sql Geometry + /// + public static bool IsValidGeographyFromGeometry(SqlGeometry geometry) + { + if (geometry.IsNull) return false; + + try + { + var geogBuilder = new SqlGeographyBuilder(); + geometry.Populate(new VacuousGeometryToGeographySink(geometry.STSrid.Value, geogBuilder)); + // ReSharper disable once UnusedVariable + var geography = geogBuilder.ConstructedGeography; + return true; + } + catch (FormatException) + { + // Syntax error + return false; + } + catch (ArgumentException) + { + // Semantic (Geometrical) error + return false; + } + } + + /// + /// Check if an input WKT can represent a valid geography. This function requires that + /// the WTK coordinate values are longitude/latitude values, in that order and that a valid + /// geography SRID value is supplied. This function will not throw an exception even in + /// edge conditions (i.e. longitude/latitude coordinates are reversed to latitude/longitude). + /// + /// Input Well Known Text + /// Spatial Reference Identifier + /// + public static bool IsValidGeographyFromText(string inputWKT, int srid) + { + try + { + // If parse succeeds then our input is valid + inputWKT.GetGeog(srid); + return true; + } + catch (FormatException) + { + // Syntax error + return false; + } + catch (ArgumentException) + { + // Semantic (Geometrical) error + return false; + } + } + + /// + /// Convert an input geometry instance to a valid geography instance. + /// This function requires that the WKT coordinate values are longitude/latitude values, + /// in that order and that a valid geography SRID value is supplied. + /// + /// Input Sql Geometry + /// + public static SqlGeography MakeValidGeographyFromGeometry(SqlGeometry geometry) + { + if (geometry.IsNull) return SqlGeography.Null; + if (geometry.STIsEmpty().Value) return CreateEmptyGeography(geometry.STSrid.Value); + + // Extract vertices from our input to be able to compute geography EnvelopeCenter + var pointSetBuilder = new SqlGeographyBuilder(); + geometry.Populate(new GeometryToPointGeographySink(pointSetBuilder)); + SqlGeography center; + try + { + center = pointSetBuilder.ConstructedGeography.EnvelopeCenter(); + } + catch (ArgumentException) + { + // Input is larger than a hemisphere. + return SqlGeography.Null; + } + + // Construct Gnomonic projection centered on input geography + var gnomonicProjection = SqlProjection.Gnomonic(center.Long.Value, center.Lat.Value); + + // Project, run geometry MakeValid and unproject + var geometryBuilder = new SqlGeometryBuilder(); + geometry.Populate(new VacuousGeometryToGeographySink(geometry.STSrid.Value, new Projector(gnomonicProjection, geometryBuilder))); + var outGeometry = Geometry.MakeValidForGeography(geometryBuilder.ConstructedGeometry); + + try + { + return gnomonicProjection.Unproject(outGeometry); + } + catch (ArgumentException) + { + // Try iteratively to reduce the object to remove very close vertices. + for (var tolerance = 1e-4; tolerance <= 1e6; tolerance *= 2) + { + try + { + return gnomonicProjection.Unproject(outGeometry.Reduce(tolerance)); + } + catch (ArgumentException) + { + // keep trying + } + } + return SqlGeography.Null; + } + } + + /// + /// Constructs an empty Sql Geography + /// + /// Spatial Reference Identifier + /// + private static SqlGeography CreateEmptyGeography(int srid) + { + var geogBuilder = new SqlGeographyBuilder(); + geogBuilder.SetSrid(srid); + geogBuilder.BeginGeography(OpenGisGeographyType.GeometryCollection); + geogBuilder.EndGeography(); + return geogBuilder.ConstructedGeography; + } + + /// + /// Convert an input WKT to a valid geography instance. + /// This function requires that the WKT coordinate values are longitude/latitude values, + /// in that order and that a valid geography SRID value is supplied. + /// + /// Input Well Know Text + /// Spatial Reference Identifier + /// + public static SqlGeography MakeValidGeographyFromText(string inputWKT, int srid) + { + return MakeValidGeographyFromGeometry(inputWKT.GetGeom(srid)); + } + + // Selectively filter unwanted artifacts in input object: + // - empty shapes (if [filterEmptyShapes] is true) + // - points (if [filterPoints] is true) + // - line strings shorter than provided tolerance (if lineString.STLength < [lineStringTolerance]) + // - polygon rings thinner than provided tolerance (if ring.STArea < ring.STLength * [ringTolerance]) + // - general behavior: Returned spatial objects will always to the simplest OGC construction + // + public static SqlGeography FilterArtifactsGeography(SqlGeography geography, bool filterEmptyShapes, bool filterPoints, double lineStringTolerance, double ringTolerance) + { + if (geography == null || geography.IsNull) + return geography; + + var geogBuilder = new SqlGeographyBuilder(); + IGeographySink110 filter = geogBuilder; + + if (filterEmptyShapes) + filter = new GeographyEmptyShapeFilter(filter); + if (ringTolerance > 0) + filter = new GeographyThinRingFilter(filter, ringTolerance); + if (lineStringTolerance > 0) + filter = new GeographyShortLineStringFilter(filter, lineStringTolerance); + if (filterPoints) + filter = new GeographyPointFilter(filter); + + geography.Populate(filter); + geography = geogBuilder.ConstructedGeography; + + if (geography == null || geography.IsNull) + return geography; + + // Strip collections with single element + while (geography.STNumGeometries().Value == 1 && geography.InstanceOf("GEOMETRYCOLLECTION").Value) + geography = geography.STGeometryN(1); + + return geography; + } + + } +} diff --git a/src/Functions/General/Geometry.cs b/src/Functions/General/Geometry.cs new file mode 100644 index 0000000..dd10c88 --- /dev/null +++ b/src/Functions/General/Geometry.cs @@ -0,0 +1,209 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Sinks.Geometry; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.Functions.General +{ + /// + /// This class contains General Geometry type functions that can be registered in SQL Server. + /// + public static class Geometry + { + /// + /// Selectively filter unwanted artifacts in input object: + /// - empty shapes (if [filterEmptyShapes] is true) + /// - points (if [filterPoints] is true) + /// - line strings shorter than provided tolerance (if lineString.STLength less than lineStringTolerance) + /// - polygon rings thinner than provided tolerance (if ring.STArea less than ring.STLength * ringTolerance) + /// - general behavior: Returned spatial objects will always to the simplest OGC construction + /// + /// + /// + /// + /// + /// + /// + public static SqlGeometry FilterArtifactsGeometry(SqlGeometry geometry, bool filterEmptyShapes, bool filterPoints, double lineStringTolerance, double ringTolerance) + { + if (geometry == null || geometry.IsNull) + return geometry; + + var geomBuilder = new SqlGeometryBuilder(); + IGeometrySink110 filter = geomBuilder; + + if (filterEmptyShapes) + filter = new GeometryEmptyShapeFilter(filter); + if (ringTolerance > 0) + filter = new GeometryThinRingFilter(filter, ringTolerance); + if (lineStringTolerance > 0) + filter = new GeometryShortLineStringFilter(filter, lineStringTolerance); + if (filterPoints) + filter = new GeometryPointFilter(filter); + + geometry.Populate(filter); + geometry = geomBuilder.ConstructedGeometry; + + if (geometry == null || geometry.IsNull || !geometry.STIsValid().Value) + return geometry; + + // Strip collections with single element + while (geometry.STNumGeometries().Value == 1 && geometry.InstanceOf("GEOMETRYCOLLECTION").Value) + geometry = geometry.STGeometryN(1); + + return geometry; + } + + /// + /// Convert Z co-ordinate of geom from XYZ to XYM + /// + /// Well Know Text with x,y,z representation + /// Spatial Reference Identifier + /// + public static SqlGeometry GeomFromXYMText(string wktXYM, int srid) + { + var res = new ConvertXYZ2XYMGeometrySink(); + var geom = wktXYM.GetGeom(srid); + geom.Populate(res); + return res.ConstructedGeometry; + } + + /// + /// Find the point that is the given distance from the start point in the direction of the end point. + /// The distance must be less than the distance between these two points. + /// + /// Starting Geometry Point + /// End Geometry Point + /// Distance measure of the point to locate + /// + public static SqlGeometry InterpolateBetweenGeom(SqlGeometry start, SqlGeometry end, double distance) + { + // We need to check a few prerequisites. + // We only operate on points. + if (!start.IsPoint() || !end.IsPoint()) + { + throw new ArgumentException(ErrorMessage.PointCompatible); + } + + // The SRIDs also have to match + var srid = start.STSrid.Value; + if (srid != end.STSrid.Value) + { + throw new ArgumentException(ErrorMessage.SRIDCompatible); + } + + // Finally, the distance has to fall between these points. + var length = start.STDistance(end).Value; + if (distance > start.STDistance(end)) + { + throw new ArgumentException(ErrorMessage.DistanceMustBeBetweenTwoPoints); + } + + if (distance < 0) + { + throw new ArgumentException(ErrorMessage.DistanceMustBePositive); + } + + // Since we're working on a Cartesian plane, this is now pretty simple. + // The fraction of the way from start to end. + var fraction = distance / length; + var newX = (start.STX.Value * (1 - fraction)) + (end.STX.Value * fraction); + var newY = (start.STY.Value * (1 - fraction)) + (end.STY.Value * fraction); + return SqlGeometry.Point(newX, newY, srid); + } + + // Make our LocateAlongGeometrySink into a function call. This function just hooks up + // and runs a pipeline using the sink. + public static SqlGeometry LocatePointAlongGeom(SqlGeometry geometry, double distance) + { + var geometryBuilder = new SqlGeometryBuilder(); + var geometrySink = new LocateAlongGeometrySink(distance, geometryBuilder); + geometry.Populate(geometrySink); + return geometryBuilder.ConstructedGeometry; + } + + /// + /// Make the input geometry valid to use in geography manipulation + /// + /// Input geometry + /// + public static SqlGeometry MakeValidForGeography(SqlGeometry geometry) + { + // Note: This function relies on an undocumented feature of the planar Union and MakeValid + // that polygon rings in their result will always be oriented using the same rule that + // is used in geography. But, it is not good practice to rely on such fact in production code. + + if (geometry.STIsValid().Value && !geometry.STIsEmpty().Value) + return geometry.STUnion(geometry.STPointN(1)); + + return geometry.MakeValid(); + } + + /// + /// Reverse the Line Segment. + /// + /// Input SqlGeometry + /// + public static SqlGeometry ReverseLinestring(SqlGeometry geometry) + { + if (!geometry.IsLineString()) + throw new ArgumentException(ErrorMessage.LineStringCompatible); + + var geomBuilder = new SqlGeometryBuilder(); + + geomBuilder.SetSrid((int)geometry.STSrid); + geomBuilder.BeginGeometry(OpenGisGeometryType.LineString); + geomBuilder.BeginFigure(geometry.STEndPoint().STX.Value, geometry.STEndPoint().STY.Value); + for (var i = (int)geometry.STNumPoints() - 1; i >= 1; i--) + { + geomBuilder.AddLine( + geometry.STPointN(i).STX.Value, + geometry.STPointN(i).STY.Value); + } + geomBuilder.EndFigure(); + geomBuilder.EndGeometry(); + return geomBuilder.ConstructedGeometry; + } + + /// + /// Shift the input Geometry x and y co-ordinate by specified amount + /// Make our ShiftGeometrySink into a function call by hooking it into a simple pipeline. + /// + /// Input Geometry + /// X value to shift + /// Y value to shift + /// Shifted Geometry + public static SqlGeometry ShiftGeometry(SqlGeometry geometry, double xShift, double yShift) + { + // create a sink that will create a geometry instance + var geometryBuilder = new SqlGeometryBuilder(); + + // create a sink to do the shift and plug it in to the builder + var geomSink = new ShiftGeometrySink(xShift, yShift, geometryBuilder); + + // plug our sink into the geometry instance and run the pipeline + geometry.Populate(geomSink); + + // the end of our pipeline is now populated with the shifted geometry instance + return geometryBuilder.ConstructedGeometry; + } + + /// + /// This implements a completely trivial conversion from geometry to geography, simply taking each + /// point (x,y) --> (long, lat). The result is assigned the given SRID. + /// + /// Input Geometry to convert + /// Target SRID + /// Converted Geography + public static SqlGeography VacuousGeometryToGeography(SqlGeometry toConvert, int targetSrid) + { + var geographyBuilder = new SqlGeographyBuilder(); + toConvert.Populate(new VacuousGeometryToGeographySink(targetSrid, geographyBuilder)); + return geographyBuilder.ConstructedGeography; + } + } +} \ No newline at end of file diff --git a/src/Functions/LRS/Geometry.cs b/src/Functions/LRS/Geometry.cs new file mode 100644 index 0000000..ed5e7a2 --- /dev/null +++ b/src/Functions/LRS/Geometry.cs @@ -0,0 +1,1120 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Data.SqlTypes; +using System.Linq; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Sinks.Geometry; +using SQLSpatialTools.Types; +using SQLSpatialTools.Utility; +using Ext = SQLSpatialTools.Utility.SpatialExtensions; + +namespace SQLSpatialTools.Functions.LRS +{ + /// + /// This provides LRS data manipulation on planar Geometry data type. + /// + public static class Geometry + { + /// + /// Clip a geometry segment based on specified measure. + ///
If the clipped start and end point is within tolerance of shape point then shape point is returned as start and end of clipped Geom segment. + ///
This function just hooks up and runs a pipeline using the sink. + ///
+ /// Input Geometry + /// Start Measure + /// End Measure + /// Tolerance Value + /// Clipped Segment + public static SqlGeometry ClipGeometrySegment(SqlGeometry geometry, double clipStartMeasure, double clipEndMeasure, double tolerance = Constants.Tolerance) + { + return ClipAndRetainMeasure(geometry, clipStartMeasure, clipEndMeasure, tolerance, false); + } + + /// + /// Clip a geometry segment and retains its measure. + /// + /// The geometry. + /// The clip start measure. + /// The clip end measure. + /// The tolerance. + /// if set to true [retain measure]. + /// + private static SqlGeometry ClipAndRetainMeasure(SqlGeometry geometry, double clipStartMeasure, double clipEndMeasure, double tolerance, bool retainMeasure) + { + Ext.ThrowIfNotLRSType(geometry); + Ext.ValidateLRSDimensions(ref geometry); + + // if not multiline just return the line segment or point post clipping + if (!geometry.IsMultiLineString()) + return ClipLineSegment(geometry, clipStartMeasure, clipEndMeasure, tolerance, retainMeasure); + + // for multi line + var multiLine = geometry.GetLRSMultiLine(); + + var clippedSegments = new List(); + foreach (var line in multiLine) + { + var segment = ClipLineSegment(line.ToSqlGeometry(), clipStartMeasure, clipEndMeasure, tolerance, retainMeasure); + // add only line segments + if (segment.IsNotNullOrEmpty()) + clippedSegments.Add(segment); + } + + if (!clippedSegments.Any()) return SqlGeometry.Null; + { + // if one segment then it is a POINT or LINESTRING, so return straight away. + if (clippedSegments.Count == 1) + return clippedSegments.First(); + + var geomBuilder = new SqlGeometryBuilder(); + // count only LINESTRING + var multiLineGeomSink = new BuildMultiLineFromLinesSink(geomBuilder, clippedSegments.Count(segment => segment.IsLineString())); + + foreach (var geom in clippedSegments) + { + // ignore points + if (geom.IsLineString()) + geom.Populate(multiLineGeomSink); + } + + return geomBuilder.ConstructedGeometry; + } + + } + + /// + /// Clip a geometry segment based on specified measure. + ///
If the clipped start and end point is within tolerance of shape point then shape point is returned as start and end of clipped Geom segment. + ///
This function just hooks up and runs a pipeline using the sink. + ///
+ /// Input Geometry + /// Start Measure + /// End Measure + /// Tolerance Value + /// Flag to retain clip measures + /// Clipped Segment + private static SqlGeometry ClipLineSegment(SqlGeometry geometry, double clipStartMeasure, double clipEndMeasure, double tolerance, bool retainClipMeasure) + { + var startMeasureInvalid = false; + var endMeasureInvalid = false; + + // reassign clip start and end measure based upon there difference + if (clipStartMeasure > clipEndMeasure) + { + var shiftObj = clipStartMeasure; + clipStartMeasure = clipEndMeasure; + clipEndMeasure = shiftObj; + } + + // if point then compute here and return + if (geometry.IsPoint()) + { + var pointMeasure = geometry.HasM ? geometry.M.Value : 0; + var isClipMeasureEqual = clipStartMeasure.EqualsTo(clipEndMeasure); + // no tolerance check, if both start and end measure is point measure then return point + if (isClipMeasureEqual && pointMeasure.EqualsTo(clipStartMeasure)) + return geometry; + + if (isClipMeasureEqual && (clipStartMeasure > pointMeasure || clipStartMeasure < pointMeasure)) + Ext.ThrowLRSError(LRSErrorCodes.InvalidLRSMeasure); + // if clip measure fall behind or beyond point measure then return null + else if ((clipStartMeasure < pointMeasure && clipEndMeasure < pointMeasure) || (clipStartMeasure > pointMeasure && clipEndMeasure > pointMeasure)) + return null; + // else throw invalid LRS error. + else + Ext.ThrowLRSError(LRSErrorCodes.InvalidLRS); + } + + // Get the measure progress of linear geometry and reassign the start and end measures based upon the progression + var measureProgress = geometry.STLinearMeasureProgress(); + var geomStartMeasure = measureProgress == LinearMeasureProgress.Increasing ? geometry.GetStartPointMeasure() : geometry.GetEndPointMeasure(); + var geomEndMeasure = measureProgress == LinearMeasureProgress.Increasing ? geometry.GetEndPointMeasure() : geometry.GetStartPointMeasure(); + + // if clip start measure matches geom start measure and + // clip end measure matches geom end measure then return the input geom + if (clipStartMeasure.EqualsTo(geomStartMeasure) && clipEndMeasure.EqualsTo(geomEndMeasure)) + return geometry; + + // Check if clip start and end measures are beyond geom start and end point measures + var isStartBeyond = clipStartMeasure < geomStartMeasure; + var isEndBeyond = clipEndMeasure > geomEndMeasure; + + // When clip measure range is not beyond range; then don't consider tolerance on extreme math; as per Oracle + var isExtremeMeasuresMatch = Ext.IsExtremeMeasuresMatch(geomStartMeasure, geomEndMeasure, clipStartMeasure, clipEndMeasure); + + // don't throw error when measure is not in the range + // rather reassign segment start and end measure + // if they are beyond the range or matching with the start and end point measure of input geometry + if (!clipStartMeasure.IsWithinRange(geometry)) + { + if (isStartBeyond || isExtremeMeasuresMatch) + { + if (clipStartMeasure <= geomStartMeasure) + clipStartMeasure = geomStartMeasure; + } + else + startMeasureInvalid = true; + } + + // end point check + if (!clipEndMeasure.IsWithinRange(geometry)) + { + if (isEndBeyond || isExtremeMeasuresMatch) + { + if (clipEndMeasure >= geomEndMeasure) + clipEndMeasure = geomEndMeasure; + } + else + endMeasureInvalid = true; + } + + // if both clip start and end measure are reassigned to invalid then return null + if (startMeasureInvalid || endMeasureInvalid) + return null; + + // Post adjusting if clip start measure matches geom start measure and + // clip end measure matches geom end measure then return the input geom + if (clipStartMeasure.EqualsTo(geomStartMeasure) && clipEndMeasure.EqualsTo(geomEndMeasure)) + return geometry; + + // if clip start and end measure are equal post adjusting then we will return a shape point + if (clipStartMeasure.EqualsTo(clipEndMeasure) && (isStartBeyond || isEndBeyond)) + { + if (isStartBeyond) + return measureProgress == LinearMeasureProgress.Increasing ? geometry.STStartPoint() : geometry.STEndPoint(); + return measureProgress == LinearMeasureProgress.Increasing ? geometry.STEndPoint() : geometry.STStartPoint(); + } + + // if both clip start and end measure is same then don't check for distance tolerance + if (clipStartMeasure.NotEqualsTo(clipEndMeasure)) + { + var clipStartPoint = LocatePointWithTolerance(geometry, clipStartMeasure, out bool isClipStartShapePoint, tolerance); + var clipEndPoint = LocatePointWithTolerance(geometry, clipEndMeasure, out bool isClipEndShapePoint, tolerance); + if (clipStartPoint.IsWithinTolerance(clipEndPoint, tolerance)) + { + // if any one of them is a shape point return null + if (isClipStartShapePoint || isClipEndShapePoint) + return null; + else + { + var lrsLine = new LRSLine((int)clipStartPoint.STSrid); + // based on measure progress re-arrange the points. + if (measureProgress == LinearMeasureProgress.Increasing) + { + lrsLine.AddPoint(clipStartPoint); + lrsLine.AddPoint(clipEndPoint); + } + else + { + lrsLine.AddPoint(clipEndPoint); + lrsLine.AddPoint(clipStartPoint); + } + return lrsLine.ToSqlGeometry(); + } + } + } + + var geometryBuilder = new SqlGeometryBuilder(); + var geomSink = new ClipMGeometrySegmentSink(clipStartMeasure, clipEndMeasure, geometryBuilder, tolerance, retainClipMeasure); + geometry.Populate(geomSink); + return geometryBuilder.ConstructedGeometry; + } + + /// + /// calculate measure value across shape points. + /// + /// Input Geometry + /// + /// + /// + public static SqlGeometry ConvertToLrsGeom(SqlGeometry geometry, double? startMeasure, double? endMeasure) + { //Line string geometry should not contain measure information. + Ext.ThrowIfNotLRSType(geometry); + if (geometry.HasZ || geometry.HasM) + throw new ArgumentException(ErrorMessage.LineOrMultiLineStringCompatible); + + var NullConditonFirst = (startMeasure == null || endMeasure == null); + var NullConditionSecond = (startMeasure == null && endMeasure == null); + + //if start measure or end measure is null then it returns null geometry + if (NullConditonFirst &&!NullConditionSecond) + { + return null; + } + //point always takes start measure value if start and end measure values are not null + if (geometry.IsPoint()) + { + var pointMeasure = NullConditionSecond ? 0 : (double)startMeasure; + return Ext.GetPointWithUpdatedM(geometry, pointMeasure); + } + + // segment length + var segmentLength = geometry.STLength().Value; + + // As per requirement; + // the default value of start point is 0 when null is specified + // the default value of end point is cartographic length of the segment when null is specified + var localStartMeasure = startMeasure ?? 0; + var localEndMeasure = endMeasure ?? segmentLength; + //internally ConvertToLrsGeom uses PopulateGeometry functionalities + var geomSink = new PopulateGeometryMeasuresSink(localStartMeasure, localEndMeasure, segmentLength); + geometry.Populate(geomSink); + return geomSink.GetConstructedGeom(); + } + + /// + /// Get end point measure of a LRS Geom Segment. + /// + /// Input Geometry + /// End measure + public static SqlDouble GetEndMeasure(SqlGeometry geometry) + { + Ext.ThrowIfNotLRSType(geometry); + Ext.ValidateLRSDimensions(ref geometry); + return geometry.GetEndPointMeasure(); + } + + /// + /// Get start point measure of a LRS Geom Segment. + /// + /// Input Geometry + /// Start measure + public static SqlDouble GetStartMeasure(SqlGeometry geometry) + { + Ext.ThrowIfNotLRSType(geometry); + Ext.ValidateLRSDimensions(ref geometry); + return geometry.GetStartPointMeasure(); + } + + /// + /// Find the point with specified measure, going from the start point in the direction of the end point. + /// The measure must be between measures of these two points. + /// + /// Start Geometry Point + /// End Geometry Point + /// Measure at which the point is to be found + /// + public static SqlGeometry InterpolateBetweenGeom(SqlGeometry startPoint, SqlGeometry endPoint, double measure) + { + // We need to check a few prerequisite. + // We only operate on points. + Ext.ThrowIfNotPoint(startPoint, endPoint); + Ext.ThrowIfSRIDDoesNotMatch(startPoint, endPoint); + Ext.ValidateLRSDimensions(ref startPoint); + Ext.ValidateLRSDimensions(ref endPoint); + Ext.ThrowIfMeasureIsNotInRange(measure, startPoint, endPoint); + + // The SRIDs also have to match + var srid = startPoint.STSrid.Value; + + // Since we're working on a Cartesian plane, this is now pretty simple. + // The fraction of the way from start to end. + var fraction = (measure - startPoint.M.Value) / (endPoint.M.Value - startPoint.M.Value); + var newX = (startPoint.STX.Value * (1 - fraction)) + (endPoint.STX.Value * fraction); + var newY = (startPoint.STY.Value * (1 - fraction)) + (endPoint.STY.Value * fraction); + + //There's no way to know Z, so just put NULL there + return Ext.GetPoint(newX, newY, null, measure, srid); + } + + /// + /// Method returns the Merge position of two LRS segments bound by the tolerance, if the segments are connected + /// Otherwise return False in string format + /// + /// Geometry Segment 1 + /// Geometry Segment 2 + /// tolerance + /// Merge position if connected else false + public static string GetMergePosition(SqlGeometry geometry1, SqlGeometry geometry2, double tolerance = Constants.Tolerance) + { + // if the segments are connected, return the merge position + var isConnected = CheckIfConnected(geometry1, geometry2, tolerance, out var mergePosition); + // for not connected segments, return false in string type + return isConnected ? mergePosition.ToString() : "false"; + } + + /// + /// Checks if two geometric segments are spatially connected. + /// + /// + /// + /// + /// SqlBoolean + public static SqlBoolean IsConnected(SqlGeometry geometry1, SqlGeometry geometry2, double tolerance = Constants.Tolerance) + { + return CheckIfConnected(geometry1, geometry2, tolerance, out _); + } + + /// + /// Checks if two geometric segments are spatially connected with merge position information + /// + /// First Geometry + /// Second Geometry + /// Distance Threshold range; default 0.01F + /// + /// SqlBoolean + private static SqlBoolean CheckIfConnected(SqlGeometry geometry1, SqlGeometry geometry2, double tolerance, out MergePosition mergePosition) + { + Ext.ThrowIfNotLRSType(geometry1, geometry2); + Ext.ThrowIfSRIDDoesNotMatch(geometry1, geometry2); + Ext.ValidateLRSDimensions(ref geometry1); + Ext.ValidateLRSDimensions(ref geometry2); + + // check for point type + if (geometry1.IsPoint() && geometry2.IsPoint()) + { + var result = geometry1.IsXYWithinRange(geometry2, tolerance); + mergePosition = result ? MergePosition.BothEnds : MergePosition.None; + return result; + } + + // Geometry 1 points + var geometry1StartPoint = geometry1.STStartPoint(); + var geometry1EndPoint = geometry1.STEndPoint(); + + // Geometry 2 points + var geometry2StartPoint = geometry2.STStartPoint(); + var geometry2EndPoint = geometry2.STEndPoint(); + + // If the points doesn't coincide, check for the point co-ordinate difference and whether it falls within the tolerance + // distance not considered as per Oracle. + // Comparing geom1 start point x and y co-ordinate difference against geom2 start and end point x and y co-ordinates + var isStartStartConnected = geometry1StartPoint.STEquals(geometry2StartPoint) || geometry1StartPoint.IsXYWithinRange(geometry2StartPoint, tolerance); + var isStartEndConnected = geometry1StartPoint.STEquals(geometry2EndPoint) || geometry1StartPoint.IsXYWithinRange(geometry2EndPoint, tolerance); + var isEndStartConnected = geometry1EndPoint.STEquals(geometry2StartPoint) || geometry1EndPoint.IsXYWithinRange(geometry2StartPoint, tolerance); + var isEndEndConnected = geometry1EndPoint.STEquals(geometry2EndPoint) || geometry1EndPoint.IsXYWithinRange(geometry2EndPoint, tolerance); + var isBothEndsConnected = isStartStartConnected && isEndEndConnected; + var isCrossEndsConnected = isStartEndConnected && isEndStartConnected; + + mergePosition = MergePosition.None; + + if (isStartStartConnected) + mergePosition = MergePosition.StartStart; + if (isStartEndConnected) + mergePosition = MergePosition.StartEnd; + + if (isEndStartConnected) + mergePosition = MergePosition.EndStart; + if (isEndEndConnected) + mergePosition = MergePosition.EndEnd; + + if (isBothEndsConnected) + mergePosition = MergePosition.BothEnds; + if (isCrossEndsConnected) + mergePosition = MergePosition.CrossEnds; + + if (isStartStartConnected || isStartEndConnected || isEndStartConnected || isEndEndConnected) + return true; + return false; + } + + /// + /// Checks if an LRS point is valid. + /// + /// Sql Geometry. + /// + public static SqlBoolean IsValidPoint(SqlGeometry geometry) + { + if (geometry.IsNullOrEmpty() || !geometry.STIsValid() || !geometry.IsPoint()) + return false; + + // check if the point has measure value + if (!geometry.M.IsNull) + return true; + + // if m is null; the check if frame from x,y,z where z is m + if (geometry.STGetDimension() != DimensionalInfo.Dim3D) return false; + geometry = geometry.ConvertTo2DimensionWithMeasure(); + return !geometry.M.IsNull; + } + + /// + /// Locate the Geometry Point along the specified measure on the Geometry. + /// This function just hooks up and runs a pipeline using the sink. + /// + /// Input Geometry + /// Measure of the Geometry point to locate + /// Geometry Point + public static SqlGeometry LocatePointAlongGeom(SqlGeometry geometry, double measure) + { + // Invoking locate point without tolerance + return LocatePointWithTolerance(geometry, measure, out _, 0); + } + + /// + /// Locates the point with tolerance. + /// + /// The geometry. + /// The measure. + /// + /// The tolerance. + /// + private static SqlGeometry LocatePointWithTolerance(SqlGeometry geometry, double measure, out bool isShapePoint, double tolerance = Constants.Tolerance) + { + Ext.ThrowIfNotLRSType(geometry); + Ext.ValidateLRSDimensions(ref geometry); + Ext.ThrowIfMeasureIsNotInRange(measure, geometry); + + // If input geom is point; its a no-op just return the same. + if (geometry.IsPoint()) + { + isShapePoint = true; + return geometry; + } + + var geomBuilder = new SqlGeometryBuilder(); + var geomSink = new LocateMAlongGeometrySink(measure, geomBuilder, tolerance); + geometry.Populate(geomSink); + + // if point is not derived then the measure is not in range. + if (!geomSink.IsPointDerived) + Ext.ThrowLRSError(LRSErrorCodes.InvalidLRSMeasure); + + isShapePoint = geomSink.IsShapePoint; + return geomBuilder.ConstructedGeometry; + } + + /// + /// Merge two geometry segments to one geometry. + /// This function just hooks up and runs a pipeline using the sink. + /// + /// First Geometry + /// Second Geometry + /// Tolerance + /// Returns Merged Geometry Segments + public static SqlGeometry MergeGeometrySegments(SqlGeometry geometry1, SqlGeometry geometry2, double tolerance = Constants.Tolerance) + { + Ext.ThrowIfNotLRSType(geometry1, geometry2); + Ext.ThrowIfSRIDDoesNotMatch(geometry1, geometry2); + Ext.ValidateLRSDimensions(ref geometry1); + Ext.ValidateLRSDimensions(ref geometry2); + + // return object + SqlGeometry returnGeom = null; + + // returning geometry2 if both the geometries are points + if (geometry1.CheckGeomPoint() && geometry2.CheckGeomPoint()) + return geometry2; + + // If either of the input geom is point; then return the other geometry. + if (geometry1.CheckGeomPoint()) + return geometry2; + + if (geometry2.CheckGeomPoint()) + return geometry1; + + var isConnected = CheckIfConnected(geometry1, geometry2, tolerance, out var mergePosition); + var mergeType = geometry1.GetMergeType(geometry2); + + if (isConnected) + { + switch (mergeType) + { + case MergeInputType.LSLS: + returnGeom = MergeConnectedLineStrings(geometry1, geometry2, mergePosition, out _); + break; + case MergeInputType.LSMLS: + case MergeInputType.MLSLS: + case MergeInputType.MLSMLS: + returnGeom = MergeConnectedMultiLineStrings(geometry1, geometry2, mergePosition); + break; + } + } + else + { + // construct multi line + returnGeom = MergeDisconnectedLineSegments(geometry1, geometry2); + } + return returnGeom; + } + + /// + /// Merge the segments bound with tolerance and resets the measure from zero + /// + /// + /// + /// + /// SqlGeometry + public static SqlGeometry MergeAndResetGeometrySegments(SqlGeometry geometry1, SqlGeometry geometry2, double tolerance = Constants.Tolerance) + { + var resultantGeometry = MergeGeometrySegments(geometry1, geometry2, tolerance); + return PopulateGeometryMeasures(resultantGeometry, null, null); + } + + /// + /// Method will merge simple line strings with tolerance and returns the merged line segment by considering measure and direction of the first geometry. + /// + /// + /// + /// + /// + /// SqlGeometry + private static SqlGeometry MergeConnectedLineStrings(SqlGeometry geometry1, SqlGeometry geometry2, MergePosition mergePosition, out double measureDifference) + { + // geometry 1 and geometry 2 to be 2D line strings with measure 'm' + Ext.ThrowIfNotLine(geometry1, geometry2); + // offset measure difference. + double offsetM; + + // references governs the order of geometries to get merge + SqlGeometry targetSegment, sourceSegment; + + // check direction of measure. + var isSameDirection = geometry1.STSameDirection(geometry2); + + // segments must be connected in any of the following position. + switch (mergePosition) + { + case MergePosition.EndStart: + case MergePosition.CrossEnds: + { + // Single negation of measure is needed for geometry 2, + // if both segments are differ in measure variation + if (!isSameDirection) + geometry2 = MultiplyGeometryMeasures(geometry2, -1); + + offsetM = geometry1.STEndPoint().GetPointOffset(geometry2.STStartPoint()); + geometry2 = TranslateMeasure(geometry2, offsetM); + sourceSegment = geometry1; + targetSegment = geometry2; + break; + } + case MergePosition.EndEnd: + case MergePosition.BothEnds: + { + // Double negation is needed for geometry 2, i.e., both segments differ from measure variation, + // also, geometry 2 has been traversed from ending point to the starting point + if (isSameDirection) + geometry2 = MultiplyGeometryMeasures(geometry2, -1); + + offsetM = geometry1.STEndPoint().GetPointOffset(geometry2.STEndPoint()); + // Reverse the geometry 2, since it has been traversed from ending point to the starting point + // scale the measures of geometry 2 based on the offset measure difference between them + geometry2 = ReverseAndTranslateGeometry(geometry2, offsetM); + // start traversing from the geometry 1, hence g1 would be the source geometry + sourceSegment = geometry1; + targetSegment = geometry2; + break; + } + case MergePosition.StartStart: + { + // Double negation is needed for geometry 2, i.e., both segments differ from measure variation, + // also, geometry 2 has been traversed from ending point to the starting point + if (isSameDirection) + geometry2 = MultiplyGeometryMeasures(geometry2, -1); + + offsetM = geometry1.STStartPoint().GetPointOffset(geometry2.STStartPoint()); + // Reverse the geometry 2, since it has been traversed from ending point to the starting point + // scale the measures of geometry 2 based on the offset measure difference between them + geometry2 = ReverseAndTranslateGeometry(geometry2, offsetM); + // the starting point of g1 will become the intermediate point of resultant, so source geometry would be geometry 2 + sourceSegment = geometry2; + targetSegment = geometry1; + break; + } + case MergePosition.StartEnd: + { + // Single negation of measure is needed for geometry 2 + // if both segments are differ in measure variation + if (!isSameDirection) + geometry2 = MultiplyGeometryMeasures(geometry2, -1); + + offsetM = (geometry1.STStartPoint().M.Value - geometry2.STEndPoint().M.Value); + // scale the measures of geometry 2 based on the offset measure difference between them + geometry2 = TranslateMeasure(geometry2, offsetM); + // the starting point of g1 will become the intermediate point of resultant, so source geometry would be geometry 2 + sourceSegment = geometry2; + targetSegment = geometry1; + break; + } + default: + throw new Exception("Invalid Merge Coordinate position"); + } + measureDifference = offsetM; // gives the offset measure difference for the caller method consumption + // Builder for resultant merged geometry to store + var geomBuilder = new SqlGeometryBuilder(); + + // Building a line segment from the range of points by excluding the last point ( Merging point ) + var segment1 = new LineStringMergeGeometrySink(geomBuilder, true, sourceSegment.STNumPoints()); + sourceSegment.Populate(segment1); + + // Continuing to build segment from the points of second geometry + var segment2 = new LineStringMergeGeometrySink(geomBuilder, false, targetSegment.STNumPoints()); + targetSegment.Populate(segment2); + + return geomBuilder.ConstructedGeometry; + } + + /// + /// Merges the Multi line and line string combinations with connected structure + /// + /// + /// + /// + /// SqlGeometry of type MultiLineString + private static SqlGeometry MergeConnectedMultiLineStrings(SqlGeometry geometry1, SqlGeometry geometry2, MergePosition mergePosition) + { + // check direction of measure. + var isSameDirection = geometry1.STSameDirection(geometry2); + + SqlGeometry sourceSegment, targetSegment, mergedSegment; + + var segment1 = geometry1.GetLRSMultiLine(); + var segment2 = geometry2.GetLRSMultiLine(); + + switch (mergePosition) + { + case MergePosition.EndEnd: + case MergePosition.BothEnds: + { + // Double Negation of measure is needed, since geometry2 has been traversed from end point to the starting point also differ from measure variation + if (isSameDirection) + segment2.ScaleMeasure(-1); + + sourceSegment = segment1.GetLastLine().ToSqlGeometry(); + targetSegment = segment2.GetLastLine().ToSqlGeometry(); + // Generating merged segment of geometry1 and geometry2 + mergedSegment = MergeConnectedLineStrings(sourceSegment, targetSegment, mergePosition, out var measureDifference); + + var mergedGeom = mergedSegment.GetLRSMultiLine(); + + segment1.RemoveLast(); // Removing merging line segment from the geometry1 + segment2.RemoveLast(); // Removing merging line segment from the geometry2 + + segment2.ReverseLinesAndPoints(); // Traversing from end to the start of geometry2. So reversing the + segment2.TranslateMeasure(measureDifference); // Translating the offset measure difference in segment2 + + segment1.Add(mergedGeom); // appending merged segment line to the segment1 , since geometry1 would be the beginning geometry of the resultant geometry + segment1.Add(segment2); // appending remaining segment of updated geometry2 with the segment1 + return segment1.ToSqlGeometry(); // converting to SqlGeometry type + } + case MergePosition.EndStart: + case MergePosition.CrossEnds: + { + // Negation of measure is needed, since measure variation of geometry2 differs from that of geometry1 + if (!isSameDirection) + segment2.ScaleMeasure(-1); + + sourceSegment = segment1.GetLastLine().ToSqlGeometry(); + targetSegment = segment2.GetFirstLine().ToSqlGeometry(); + // Generating merged segment of geometry1 and geometry2 + mergedSegment = MergeConnectedLineStrings(sourceSegment, targetSegment, mergePosition, out var measureDifference); + + var mergedGeom = mergedSegment.GetLRSMultiLine(); + + segment1.RemoveLast(); // Removing merging line segment from the geometry1 + segment2.RemoveFirst(); // Removing merging line segment from the geometry2 + + segment2.TranslateMeasure(measureDifference); // Translating the offset measure difference in segment2 + + segment1.Add(mergedGeom); // Appending merged segment line to the segment1 , since geometry1 would be the beginning geometry of the resultant geometry + segment1.Add(segment2); // Appending remaining segment of updated geometry2 with the segment1 + return segment1.ToSqlGeometry(); // converting to SqlGeometry type + } + case MergePosition.StartEnd: + { + // Negation of measure is needed, since measure variation of geometry2 differs from that of geometry1 + if (!isSameDirection) + segment2.ScaleMeasure(-1); + + sourceSegment = segment1.GetFirstLine().ToSqlGeometry(); + targetSegment = segment2.GetLastLine().ToSqlGeometry(); + // Generating merged segment of geometry1 and geometry2 + mergedSegment = MergeConnectedLineStrings(sourceSegment, targetSegment, mergePosition, out var measureDifference); + + var mergedGeom = mergedSegment.GetLRSMultiLine(); + + segment1.RemoveFirst(); // Removing merging line segment from the geometry1 + segment2.RemoveLast(); // Removing merging line segment from the geometry2 + + segment2.TranslateMeasure(measureDifference); // Translating the offset measure difference in segment2 + + segment2.Add(mergedGeom); // Appending merged segment line to the segment2 ,since geometry1 would be the beginning geometry of the resultant geometry + segment2.Add(segment1); // Appending remaining segments of geometry1 with the geometry2 + return segment2.ToSqlGeometry(); // converting to SqlGeometry type + } + case MergePosition.StartStart: + { + // Double Negation of measure is needed, since geometry2 has been traversed from end point to the starting point also differ from measure variation + if (isSameDirection) + segment2.ScaleMeasure(-1); + + sourceSegment = segment1.GetFirstLine().ToSqlGeometry(); + targetSegment = segment2.GetFirstLine().ToSqlGeometry(); + // Generating merged segment of geometry1 and geometry2 + mergedSegment = MergeConnectedLineStrings(sourceSegment, targetSegment, mergePosition, out var measureDifference); + + var mergedGeom = mergedSegment.GetLRSMultiLine(); + + segment1.RemoveFirst(); // Removing merging line segment from the geometry1 + segment2.RemoveFirst(); // Removing merging line segment from the geometry2 + + segment2.ReverseLinesAndPoints(); // Reversing the lines and its corresponding points of segment2, Since it has been traversed from end to start + segment2.TranslateMeasure(measureDifference); // Translating the offset measure difference in segment2 + + segment2.Add(mergedGeom); // Appending merged segment line to the segment2 ,since geometry1 would be the beginning geometry of the resultant geometry + segment2.Add(segment1); // Appending remaining segments of geometry1 with the geometry2 + return segment2.ToSqlGeometry(); // converting to SqlGeometry type + } + default: + return null; + } + } + + /// + /// Build MULTILINESTRING from two input geometry segments [LINESTRING, MULTILINESTRING] + /// This should be called for merging geom segments when they are not connected. + /// Here the offset measure is updated with the measure of second segment. + /// + /// The geometry1. + /// The geometry2. + /// + private static SqlGeometry MergeDisconnectedLineSegments(SqlGeometry geometry1, SqlGeometry geometry2) + { + var isSameDirection = geometry1.STSameDirection(geometry2); + var firstSegmentDirection = geometry1.STLinearMeasureProgress(); + if (!isSameDirection) + geometry2 = MultiplyGeometryMeasures(geometry2, -1); + + var offsetM = geometry1.GetOffset(geometry2); + var doUpdateM = false; + + if (isSameDirection) + { + if (firstSegmentDirection == LinearMeasureProgress.Increasing && offsetM > 0) + doUpdateM = true; + + if (firstSegmentDirection == LinearMeasureProgress.Decreasing && offsetM < 0) + doUpdateM = true; + } + else + doUpdateM = true; + + var mergedLRSMultiLine = geometry1.GetLRSMultiLine(); + mergedLRSMultiLine.Add(geometry2.GetLRSMultiLine(doUpdateM, offsetM)); + + return mergedLRSMultiLine.ToSqlGeometry(); + } + + /// + /// Multiply the measure values of Linear Geometry + /// Works only for POINT, LINESTRING, MULTILINESTRING Geometry. + /// + /// Input Geometry + /// Measure to be Multiplied + /// + public static SqlGeometry MultiplyGeometryMeasures(SqlGeometry geometry, double multiplyMeasure) + { + Ext.ThrowIfNotLRSType(geometry); + Ext.ValidateLRSDimensions(ref geometry); + + // if scale measure is zero; return the input + if (multiplyMeasure.EqualsTo(0)) + return geometry; + + var geometryBuilder = new SqlGeometryBuilder(); + var geomSink = new MultiplyMeasureGeometrySink(geometryBuilder, multiplyMeasure); + geometry.Populate(geomSink); + return geometryBuilder.ConstructedGeometry; + } + + /// + /// Returns the geometric segment at a specified offset from a geometric segment. + /// Works only for LineString and MultiLineString Geometry; Point is not supported. + /// + /// Input Geometry + /// Start Measure + /// End Measure + /// Offset value + /// Tolerance + /// Offset Geometry Segment + public static SqlGeometry OffsetGeometrySegment(SqlGeometry geometry, double startMeasure, double endMeasure, double offset, double tolerance = Constants.Tolerance) + { + // If point throw invalid LRS Segment error. + if (geometry.IsPoint()) + Ext.ThrowLRSError(LRSErrorCodes.InvalidLRS); + + Ext.ThrowIfNotLineOrMultiLine(geometry); + Ext.ValidateLRSDimensions(ref geometry); + + // to retain clip measures on offset + var clippedGeometry = ClipAndRetainMeasure(geometry, startMeasure, endMeasure, tolerance, true); + + // if clipped segment is null; then return null. + if (clippedGeometry == null) + return null; + + // Explicit handle if clipped segment is Point + // As point has a single co-ordinate we need to consider the angle from input segment, not from the clipped segment + var lrsSegment = clippedGeometry.IsPoint() ? geometry.GetLRSMultiLine() : clippedGeometry.GetLRSMultiLine(); + + if (clippedGeometry.IsPoint()) + { + // Computing offset + var parallelSegment = lrsSegment.ComputeOffset(offset, tolerance); + + // get the offset point at clipped measure + var offsetPoint = parallelSegment.GetPointAtM(clippedGeometry.M.Value); + return offsetPoint?.ToSqlGeometry(); + } + else + { + // removing collinear points + lrsSegment.RemoveCollinearPoints(); + + // Computing offset + var parallelSegment = lrsSegment.ComputeOffset(offset, tolerance); + + // if it is a two point line string; then check for distance + if (parallelSegment.Is2PointLine) + { + var firstPoint = parallelSegment.GetFirstLine().GetStartPoint(); + var secondPoint = parallelSegment.GetFirstLine().GetEndPoint(); + + if (firstPoint.IsXYWithinTolerance(secondPoint, tolerance)) + { + // always the resultant is first point from the parallel segment + // and measure being updated with minimum of start and end measure; + firstPoint.M = Math.Min(startMeasure, endMeasure); + return firstPoint.ToSqlGeometry(); + } + } + parallelSegment.PopulateMeasures(parallelSegment.GetStartPointM(), parallelSegment.GetEndPointM()); + + return parallelSegment.ToSqlGeometry(); + } + } + + /// + /// (Re)populate measures across shape points. + /// + /// Input Geometry + /// + /// + /// + public static SqlGeometry PopulateGeometryMeasures(SqlGeometry geometry, double? startMeasure, double? endMeasure) + { + Ext.ThrowIfNotLRSType(geometry); + Ext.ValidateLRSDimensions(ref geometry); + + // check for point type + if (geometry.CheckGeomPoint()) + { + // if anyone measure is null; then point measure is 0 else point measure is end measure + var pointMeasure = startMeasure == null || endMeasure == null ? 0 : (double)endMeasure; + return Ext.GetPointWithUpdatedM(geometry, pointMeasure); + } + + // if anyone is null then assign null to other + if (startMeasure == null || endMeasure == null) + startMeasure = endMeasure = null; + + // segment length + var segmentLength = geometry.STLength().Value; + + // As per requirement; + // the default value of start point is 0 when null is specified + // the default value of end point is cartographic length of the segment when null is specified + var localStartMeasure = startMeasure ?? 0; + var localEndMeasure = endMeasure ?? segmentLength; + + var geomSink = new PopulateGeometryMeasuresSink(localStartMeasure, localEndMeasure, segmentLength); + geometry.Populate(geomSink); + return geomSink.GetConstructedGeom(); + } + /// + /// Resets Geometry Measure values. + /// + /// Input Geometry + /// + public static SqlGeometry ResetMeasure(SqlGeometry geometry) + { + Ext.ThrowIfNotLRSType(geometry); + Ext.ValidateLRSDimensions(ref geometry); + + var geomBuilder = new SqlGeometryBuilder(); + var geomSink = new ResetMGeometrySink(geomBuilder); + geometry.Populate(geomSink); + return geomBuilder.ConstructedGeometry; + } + + /// + /// Reverse Linear Geometry + /// Works only for POINT, LINESTRING, MULTILINESTRING Geometry. + /// + /// Input Geometry + /// + public static SqlGeometry ReverseLinearGeometry(SqlGeometry geometry) + { + Ext.ThrowIfNotLRSType(geometry); + Ext.ValidateLRSDimensions(ref geometry); + + // if point its no-op + if (geometry.IsPoint()) + return geometry; + + var geometryBuilder = new SqlGeometryBuilder(); + var geomSink = new ReverseLinearGeometrySink(geometryBuilder); + geometry.Populate(geomSink); + return geometryBuilder.ConstructedGeometry; + } + + /// + /// Reverse and translate Linear Geometry + /// Works only for POINT, LINESTRING, MULTILINESTRING Geometry. + /// + /// Input Geometry + /// + /// + public static SqlGeometry ReverseAndTranslateGeometry(SqlGeometry geometry, double translateMeasure) + { + Ext.ThrowIfNotLRSType(geometry); + Ext.ValidateLRSDimensions(ref geometry); + + // if point then return new point with updated measure + if (geometry.IsPoint()) + return Ext.GetPointWithUpdatedM(geometry, geometry.M.Value + translateMeasure); + + var geometryBuilder = new SqlGeometryBuilder(); + var geomSink = new ReverseAndTranslateGeometrySink(geometryBuilder, translateMeasure); + geometry.Populate(geomSink); + return geometryBuilder.ConstructedGeometry; + } + + /// + /// Scale the measure values of the Linear Geometry + /// Works only for POINT, LINESTRING, MULTILINESTRING Geometry. + /// + /// Input Geometry + /// Start Measure + /// End Measure + /// Measure to be Multiplied + /// + public static SqlGeometry ScaleGeometrySegment(SqlGeometry geometry, double startMeasure, double endMeasure, double shiftMeasure) + { + Ext.ThrowIfNotLRSType(geometry); + Ext.ValidateLRSDimensions(ref geometry); + Ext.ThrowIfMeasureNotLinear(geometry); + + var geometryBuilder = new SqlGeometryBuilder(); + var geomSink = new ScaleGeometrySink(geometryBuilder, geometry.GetStartPointMeasure(), geometry.GetEndPointMeasure(), startMeasure, endMeasure, shiftMeasure); + geometry.Populate(geomSink); + return geometryBuilder.ConstructedGeometry; + } + + /// + /// Split a geometry into geometry segments based on split measure. + /// This function just hooks up and runs a pipeline using the sink. + /// + /// Input Geometry + /// + /// First Geometry Segment + /// Second Geometry Segment + public static void SplitGeometrySegment(SqlGeometry geometry, double splitMeasure, out SqlGeometry geometry1, out SqlGeometry geometry2) + { + Ext.ThrowIfNotLRSType(geometry); + Ext.ValidateLRSDimensions(ref geometry); + + // assign default null values to out param. + geometry1 = null; + geometry2 = null; + + // for point validation. + if (geometry.CheckGeomPoint()) + { + var pointMeasure = geometry.HasM ? geometry.M.Value : 0; + if (pointMeasure.NotEqualsTo(splitMeasure)) + Ext.ThrowLRSError(LRSErrorCodes.InvalidLRSMeasure); + return; + } + + var startPointM = geometry.GetStartPointMeasure(); + var endPointM = geometry.GetEndPointMeasure(); + var ifNotLinear = !geometry.STHasLinearMeasure(); + + // if start point and end point is equal and it is equal to split measure then return null + if (geometry.STHasEqualStartAndEndMeasure() && startPointM.EqualsTo(splitMeasure)) + return; + + // if start measure is split measure then segment 1 is null and segment 2 is input geom + if (startPointM.EqualsTo(splitMeasure)) + { + geometry2 = geometry; + return; + } + + // if end measure is split measure then segment 2 is null and segment 1 is input geom + if (endPointM.EqualsTo(splitMeasure)) + { + geometry1 = geometry; + return; + } + + // if geom is multiline and the segment doesn't has linear measure; + // reassign the split measure to start or end measure as per progress + if (geometry.IsMultiLineString() && ifNotLinear) + { + var maxMeasure = startPointM > endPointM ? startPointM : endPointM; + if (splitMeasure > maxMeasure) + splitMeasure = maxMeasure; + } + + // measure range validation is handled inside LocatePoint + var splitPoint = LocatePointAlongGeom(geometry, splitMeasure); + + var geomSink = new SplitGeometrySegmentSink(splitPoint); + geometry.Populate(geomSink); + geometry1 = geomSink.Segment1; + geometry2 = geomSink.Segment2; + } + + /// + /// Translates the measure values of Input Geometry + /// + /// The geometry. + /// The translate measure. + /// SqlGeometry with translated measure. + public static SqlGeometry TranslateMeasure(SqlGeometry geometry, double translateMeasure) + { + Ext.ThrowIfNotLRSType(geometry); + Ext.ValidateLRSDimensions(ref geometry); + + var geomBuilder = new SqlGeometryBuilder(); + var geomSink = new TranslateMeasureGeometrySink(geomBuilder, translateMeasure); + geometry.Populate(geomSink); + return geomBuilder.ConstructedGeometry; + } + + /// + /// Validates the LRS geometry. + /// + /// The input SqlGeometry. + /// TRUE if Valid; 13331 - if Invalid; 13333 - if Invalid Measure + public static string ValidateLRSGeometry(SqlGeometry geometry) + { + // throw if type apart from POINT, LINESTRING, MULTILINESTRING is given as input. + Ext.ThrowIfNotLRSType(geometry); + + // check for dimension + if (geometry.STGetDimension() == DimensionalInfo.Dim2D) + { + // If there is no measure value; return invalid. + return LRSErrorCodes.InvalidLRS.Value(); + } + + // convert to valid 3 point LRS co-ordinate. + Ext.ValidateLRSDimensions(ref geometry); + + // return invalid if empty or is of geometry collection + if (geometry.IsNullOrEmpty() || !geometry.STIsValid() || geometry.IsGeometryCollection()) + return LRSErrorCodes.InvalidLRS.Value(); + + // return invalid if geometry doesn't or have null values or checks if the measures are in linear range. + return geometry.STHasLinearMeasure() ? LRSErrorCodes.ValidLRS.Value() : LRSErrorCodes.InvalidLRSMeasure.Value(); + } + } +} diff --git a/src/Functions/Util/Geometry.cs b/src/Functions/Util/Geometry.cs new file mode 100644 index 0000000..253d62c --- /dev/null +++ b/src/Functions/Util/Geometry.cs @@ -0,0 +1,202 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Sinks.Geometry; +using SQLSpatialTools.Utility; +using Ext = SQLSpatialTools.Utility.SpatialExtensions; + +namespace SQLSpatialTools.Functions.Util +{ + /// + /// Utility class to manipulate planar Geometry data type. + /// + public static class Geometry + { + public static SqlGeometry ExtractGeometry(SqlGeometry sqlGeometry, int elementIndex, int ringIndex = 0) + { + if (sqlGeometry.IsNullOrEmpty()) + return sqlGeometry; + + // GEOMETRYCOLLECTION + if(sqlGeometry.IsGeometryCollection()) + { + if (elementIndex == 0 || elementIndex > sqlGeometry.STNumGeometries()) + Ext.ThrowInvalidElementIndex(); + + // reset geometry and element index and pass through + sqlGeometry = sqlGeometry.STGeometryN(elementIndex); + elementIndex = 1; + + // if the resultant is MULTILINE; reset the ring index and assign it to element index + if(sqlGeometry.IsMultiLineString()) + { + elementIndex = ringIndex; + ringIndex = 0; + } + } + + // Handle for Curve Polygon with Compound Curve + if(sqlGeometry.IsCurvePolygon()) + { + if (elementIndex != 1) + Ext.ThrowInvalidElementIndex(); + if (ringIndex == 0) + return sqlGeometry; + // re-assign sub component; if it is a COMPOUND CURVE + var subComponent = ringIndex == 1 ? sqlGeometry.STExteriorRing() : sqlGeometry.STInteriorRingN(ringIndex - 1); // subtracting exterior ring count + if (subComponent.IsCompoundCurve()) + { + sqlGeometry = subComponent; + elementIndex = 1; + ringIndex = 1; + } + } + + var isSimpleType = sqlGeometry.IsPoint() || sqlGeometry.IsLineString() || sqlGeometry.IsCircularString(); + + // if simple type then return input geometry when index is 1 or 0 + if (isSimpleType) + { + if (elementIndex != 1) + Ext.ThrowInvalidElementIndex(); + + if (ringIndex > 1) + Ext.ThrowInvalidSubElementIndex(); + + if (isSimpleType && ringIndex <= 1) + return sqlGeometry; + } + + // MULTIPOINT + if (sqlGeometry.IsMultiPoint()) + { + if (elementIndex != 1) + Ext.ThrowInvalidElementIndex(); + + if (ringIndex == 0) + return sqlGeometry; + + var obtainedGeom = sqlGeometry.STGeometryN(ringIndex); + if (obtainedGeom == null || obtainedGeom.IsNull) + Ext.ThrowInvalidSubElementIndex(); + + return obtainedGeom; + } + + // MULTILINESTRING + if (sqlGeometry.IsMultiLineString()) + { + if (elementIndex > sqlGeometry.STNumGeometries()) + Ext.ThrowInvalidElementIndex(); + + if (ringIndex > 1) + Ext.ThrowInvalidSubElementIndex(); + + return sqlGeometry.STGeometryN(elementIndex); + } + + // COMPOUND CURVE + if (sqlGeometry.IsCompoundCurve()) + { + if (elementIndex != 1) + Ext.ThrowInvalidElementIndex(); + + if (ringIndex > 1) + Ext.ThrowInvalidSubElementIndex(); + + return sqlGeometry; + } + + // POLYGON and CURVEPOLYGON + if (sqlGeometry.IsPolygon() || sqlGeometry.IsCurvePolygon()) + { + if (elementIndex != 1) + Ext.ThrowInvalidElementIndex(); + + // if sub element index is zero then return the input geometry + if (ringIndex == 0) + return sqlGeometry; + + if (ringIndex > sqlGeometry.STNumInteriorRing() + 1) + Ext.ThrowInvalidSubElementIndex(); + + return GetPolygon(sqlGeometry, ringIndex); + } + + // MULTIPOLYGON + if (sqlGeometry.IsMultiPolygon()) + { + if (elementIndex == 0 || elementIndex > sqlGeometry.STNumGeometries()) + Ext.ThrowInvalidElementIndex(); + + sqlGeometry = sqlGeometry.STGeometryN(elementIndex); + + // if sub element index is zero then return the input geometry + if (ringIndex == 0) + return sqlGeometry; + + if (ringIndex > sqlGeometry.STNumInteriorRing() + 1) + Ext.ThrowInvalidSubElementIndex(); + + return GetPolygon(sqlGeometry, ringIndex); + } + + return sqlGeometry; + } + + /// + /// Build Polygon from Linestring + /// + /// Input Sql Geometry + /// Polygon Ring Index + /// Is Polygon type is CurvePolygon + /// + private static SqlGeometry GetPolygon(SqlGeometry sqlGeometry, int ringIndex) + { + if (ringIndex == 1) + sqlGeometry = sqlGeometry.STExteriorRing(); + else + sqlGeometry = sqlGeometry.STInteriorRingN(ringIndex - 1); + + var polygonBuilder = new SqlGeometryBuilder(); + var polygonSink = new ExtractPolygonFromLineGeometrySink(polygonBuilder, ringIndex != 1); + sqlGeometry.Populate(polygonSink); + return polygonBuilder.ConstructedGeometry; + } + + /// + /// Utility method for converting Polygon types to LineString types. + /// + /// The Input SqlGeometry + /// SqlGeometry + public static SqlGeometry PolygonToLine(SqlGeometry geometry) + { + // Do manipulation only if it a polygon type + // else return the input geometry as is + if (geometry.IsPolygon(false)) + return geometry.GetLineWKTFromPolygon().GetGeom(geometry.STSrid); + else if (geometry.IsMultiPolygon(false)) + return geometry.GetLineWKTFromMultiPolygon().GetGeom(geometry.STSrid); + else if (geometry.IsCurvePolygon(false)) + return geometry.GetLineWKTFromCurvePolygon().GetGeom(geometry.STSrid); + else + return geometry; + } + + /// + /// Utility Method for removing the consecutive duplicate vertices + /// + /// + /// + /// + public static SqlGeometry RemoveDuplicateVertices(SqlGeometry geometry, double tolerance = Constants.Tolerance) + { + if(!geometry.STIsValid()) + Ext.ThrowIfInvalidGeometry(); + + return geometry.Reduce(tolerance); + } + } +} diff --git a/src/KMLProcessor/Constants.cs b/src/KMLProcessor/Constants.cs new file mode 100644 index 0000000..ae643fc --- /dev/null +++ b/src/KMLProcessor/Constants.cs @@ -0,0 +1,32 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +namespace SQLSpatialTools.KMLProcessor +{ + public static class Constants + { + /// + /// SQL Server default SRID. SQL Server uses the default SRID of 4326, + /// which maps to the WGS 84 spatial reference system, when using methods on geography instances. + /// + /// Source: http://msdn.microsoft.com/en-us/library/bb964707.aspx + /// + public static int DefaultSRID => 4326; + + /// + /// Google's KML extensions namespace + /// + public static string GxNamespace => "http://www.google.com/kml/ext/2.2"; + + /// + /// KML namespace + /// + public static string KmlNamespace => "http://www.opengis.net/kml/2.2"; + + /// + /// Atom namespace + /// + public static string AtomNamespace => "http://www.w3.org/2005/Atom"; + } +} diff --git a/KMLProcessor/Export/ExportContext.cs b/src/KMLProcessor/Export/ExportContext.cs similarity index 77% rename from KMLProcessor/Export/ExportContext.cs rename to src/KMLProcessor/Export/ExportContext.cs index 7933c28..a30a31c 100644 --- a/KMLProcessor/Export/ExportContext.cs +++ b/src/KMLProcessor/Export/ExportContext.cs @@ -1,137 +1,132 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class will keep track of the context where some action will be executed - /// during the KML export process. This class will keep track of the number of figures previously - /// started in a spatial object, it will also keep track of the spatial object nesting level and the - /// type of each opened/started spatial object - /// Type of the spatial object: OpenGisGeographyType or OpenGisGeometryType - /// - internal class ExportContext - { - #region Private Data - - /// - /// This list will simulate the stack of spatial object types. When a spatial object instance - /// is visited, its type will be stored on the end of this list, and the visit will - /// proceed on its child objects. So this list will store the types of all - /// parents of the currently visited spatial object. When a visit to a spatial object is finished, - /// the type of that object will be removed from the end of this list. - /// - private List m_Type = new List(); - - /// - /// This structure is equivalent to the m_Type structure, except it will store the information - /// about the number of figures which are visited in each spatial object. - /// - private List m_Figures = new List(); - - /// - /// Represents the depth in the spatial object tree. It will show the distance from the currently - /// spatial object to the root spatial object. - /// - private int m_Depth = 0; - - #endregion - - #region Public Methods - - /// - /// This method should be called when a visit to the spatial object is started. - /// The context will be updated with the information about the given spatial object type. - /// - /// Type of spatial object - public void BeginSpatialObject(T type) - { - m_Depth += 1; // Increment the tree depth - - // Add information about this spatial object in the stack of visited spatial objects - if (m_Depth > m_Type.Count) - { - m_Type.Add(type); - m_Figures.Add(0); - } - else - { - m_Type[m_Depth - 1] = type; - m_Figures[m_Depth - 1] = 0; - } - } - - /// - /// This method should be called when the visit to spatial object is finished - /// - public void EndSpatialObject() - { - m_Depth -= 1; - } - - /// - /// This method should be called when new figure visit is started - /// - public void BeginFigure() - { - m_Figures[m_Depth - 1] += 1; - } - - /// - /// This method should be called when a figure visit is finished - /// - public void EndFigure() - { - // Nothing to be done. We calculates the total number of figures inside the spatial object, - // so we don't need to decrease the counter - } - - #endregion - - #region Public Properties - - /// - /// The type of spatial object which is currently being processed - /// - public T Type - { - get - { - if (m_Depth == 0 || m_Depth > m_Type.Count) - throw new KMLException("Type tree is invalid!"); - - return m_Type[m_Depth - 1]; - } - } - - /// - /// True if the figure which is currently being processed is the first figure in - /// the current spatial object - /// - public bool IsFirstFigure - { - get - { - return Figures == 1; - } - } - - /// - /// The number of visited figures in the current spatial object - /// - public int Figures - { - get - { - if (m_Depth == 0 || m_Depth > m_Figures.Count) - throw new KMLException("Figure tree is invalid!"); - - return m_Figures[m_Depth - 1]; - } - } - - #endregion - } -} +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System.Collections.Generic; + +namespace SQLSpatialTools.KMLProcessor.Export +{ + /// + /// This class will keep track of the context where some action will be executed + /// during the KML export process. This class will keep track of the number of figures previously + /// started in a spatial object, it will also keep track of the spatial object nesting level and the + /// type of each opened/started spatial object + /// Type of the spatial object: OpenGisGeographyType or OpenGisGeometryType + /// + internal class ExportContext + { + #region Private Data + + /// + /// This list will simulate the stack of spatial object types. When a spatial object instance + /// is visited, its type will be stored on the end of this list, and the visit will + /// proceed on its child objects. So this list will store the types of all + /// parents of the currently visited spatial object. When a visit to a spatial object is finished, + /// the type of that object will be removed from the end of this list. + /// + private readonly List _type = new List(); + + /// + /// This structure is equivalent to the m_Type structure, except it will store the information + /// about the number of figures which are visited in each spatial object. + /// + private readonly List _figures = new List(); + + /// + /// Represents the depth in the spatial object tree. It will show the distance from the currently + /// spatial object to the root spatial object. + /// + private int _depth; + + #endregion + + #region Public Methods + + /// + /// This method should be called when a visit to the spatial object is started. + /// The context will be updated with the information about the given spatial object type. + /// + /// Type of spatial object + public void BeginSpatialObject(T type) + { + _depth += 1; // Increment the tree depth + + // Add information about this spatial object in the stack of visited spatial objects + if (_depth > _type.Count) + { + _type.Add(type); + _figures.Add(0); + } + else + { + _type[_depth - 1] = type; + _figures[_depth - 1] = 0; + } + } + + /// + /// This method should be called when the visit to spatial object is finished + /// + public void EndSpatialObject() + { + _depth -= 1; + } + + /// + /// This method should be called when new figure visit is started + /// + public void BeginFigure() + { + _figures[_depth - 1] += 1; + } + + /// + /// This method should be called when a figure visit is finished + /// + public void EndFigure() + { + // Nothing to be done. We calculates the total number of figures inside the spatial object, + // so we don't need to decrease the counter + } + + #endregion + + #region Public Properties + + /// + /// The type of spatial object which is currently being processed + /// + public T Type + { + get + { + if (_depth == 0 || _depth > _type.Count) + throw new KMLException("Type tree is invalid!"); + + return _type[_depth - 1]; + } + } + + /// + /// True if the figure which is currently being processed is the first figure in + /// the current spatial object + /// + public bool IsFirstFigure => Figures == 1; + + /// + /// The number of visited figures in the current spatial object + /// + public int Figures + { + get + { + if (_depth == 0 || _depth > _figures.Count) + throw new KMLException("Figure tree is invalid!"); + + return _figures[_depth - 1]; + } + } + + #endregion + } +} diff --git a/KMLProcessor/Export/KeyholeMarkupLanguageBase.cs b/src/KMLProcessor/Export/KeyholeMarkupLanguageBase.cs similarity index 67% rename from KMLProcessor/Export/KeyholeMarkupLanguageBase.cs rename to src/KMLProcessor/Export/KeyholeMarkupLanguageBase.cs index 6048071..97b6cfd 100644 --- a/KMLProcessor/Export/KeyholeMarkupLanguageBase.cs +++ b/src/KMLProcessor/Export/KeyholeMarkupLanguageBase.cs @@ -1,120 +1,119 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// The base class for all KML exporters - /// - public class KeyholeMarkupLanguageBase - { - #region Constructors - - /// - /// Constructor. Creates a KeyholeMarkupLanguageBase object which will fill - /// the given xml writer with a spatial data in the KML format - /// - /// The Xml writter to be filled with a spatial data in the KML format - public KeyholeMarkupLanguageBase(System.Xml.XmlWriter writer) - { - _writer = writer; - - BeginKMLDocument(); - } - - #endregion - - #region Protected helper methods - - /// - /// This method will create the necessary start elements in the KML file - /// - protected void BeginKMLDocument() - { - // Creates the beginning of the KML file as: - - _writer.WriteStartElement("kml", Constants.KmlNamespace); - _writer.WriteAttributeString("xmlns", "gx", null, Constants.GxNamespace); - _writer.WriteAttributeString("xmlns", "kml", null, Constants.KmlNamespace); - _writer.WriteAttributeString("xmlns", "atom", null, Constants.AtomNamespace); - - StartElement("Document"); // - StartElement("name"); // - WriteString("Generated document"); // Generated document - EndElement(); // - - StartElement("Placemark"); // - StartElement("name"); // - WriteString("Placemark exported from sql spatial"); // Placemark exported from sql spatial - EndElement(); // - } - - /// - /// This method will start a tag for xml element with the given name - /// - /// Xml element name to be created - protected void StartElement(string name) - { - _writer.WriteStartElement(name); - } - - /// - /// This method will close the last opened (and not closed) element. (A end tag will be added) - /// - protected void EndElement() - { - _writer.WriteEndElement(); - } - - /// - /// This method will write the given text into the xml writter - /// - /// The text to be written - protected void WriteString(string text) - { - _writer.WriteString(text); - } - - #endregion - - #region Public Methods - - /// - /// This method will close this sink. It will close all xml elements which - /// were created during sink population. - /// - public void FinalizeKMLDocument() - { - EndElement(); // - EndElement(); // - EndElement(); // - } - - /// - /// This method will set the spatial reference system identifier (SrId) - /// - /// Spatial reference system identifier (SrId) - public void SetSrid(int srid) - { - m_SrId = srid; - } - - #endregion - - #region Protected Data - - /// - /// Xml writter to be filled with data - /// - protected System.Xml.XmlWriter _writer; - - /// - /// Spatial reference system identifier (SrId) for this file - /// - protected int m_SrId; - - #endregion - } -} +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +namespace SQLSpatialTools.KMLProcessor.Export +{ + /// + /// The base class for all KML exporters + /// + public class KeyholeMarkupLanguageBase + { + #region Constructors + + /// + /// Constructor. Creates a KeyholeMarkupLanguageBase object which will fill + /// the given xml writer with a spatial data in the KML format + /// + /// The Xml writer to be filled with a spatial data in the KML format + protected KeyholeMarkupLanguageBase(System.Xml.XmlWriter writer) + { + Writer = writer; + + BeginKMLDocument(); + } + + #endregion + + #region Protected helper methods + + /// + /// This method will create the necessary start elements in the KML file + /// + private void BeginKMLDocument() + { + // Creates the beginning of the KML file as: + + Writer.WriteStartElement("kml", Constants.KmlNamespace); + Writer.WriteAttributeString("xmlns", "gx", null, Constants.GxNamespace); + Writer.WriteAttributeString("xmlns", "kml", null, Constants.KmlNamespace); + Writer.WriteAttributeString("xmlns", "atom", null, Constants.AtomNamespace); + + StartElement("Document"); // + StartElement("name"); // + WriteString("Generated document"); // Generated document + EndElement(); // + + StartElement("Placemark"); // + StartElement("name"); // + WriteString("Placemark exported from sql spatial"); // Placemark exported from sql spatial + EndElement(); // + } + + /// + /// This method will start a tag for xml element with the given name + /// + /// Xml element name to be created + protected void StartElement(string name) + { + Writer.WriteStartElement(name); + } + + /// + /// This method will close the last opened (and not closed) element. (A end tag will be added) + /// + protected void EndElement() + { + Writer.WriteEndElement(); + } + + /// + /// This method will write the given text into the xml writer + /// + /// The text to be written + protected void WriteString(string text) + { + Writer.WriteString(text); + } + + #endregion + + #region Public Methods + + /// + /// This method will close this sink. It will close all xml elements which + /// were created during sink population. + /// + public void FinalizeKMLDocument() + { + EndElement(); // + EndElement(); // + EndElement(); // + } + + /// + /// This method will set the spatial reference system identifier (SrId) + /// + /// Spatial reference system identifier (SrId) + public void SetSrid(int srid) + { + SRID = srid; + } + + #endregion + + #region Protected Data + + /// + /// Xml writer to be filled with data + /// + protected readonly System.Xml.XmlWriter Writer; + + /// + /// Spatial reference system identifier (SrId) for this file + /// + protected int SRID; + + #endregion + } +} diff --git a/KMLProcessor/Export/KeyholeMarkupLanguageGeography.cs b/src/KMLProcessor/Export/KeyholeMarkupLanguageGeography.cs similarity index 55% rename from KMLProcessor/Export/KeyholeMarkupLanguageGeography.cs rename to src/KMLProcessor/Export/KeyholeMarkupLanguageGeography.cs index 005b800..95e1acc 100644 --- a/KMLProcessor/Export/KeyholeMarkupLanguageGeography.cs +++ b/src/KMLProcessor/Export/KeyholeMarkupLanguageGeography.cs @@ -1,239 +1,247 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class is the geography sink. It will export the given geography instances - /// into the KML format. - /// - internal class KeyholeMarkupLanguageGeography : KeyholeMarkupLanguageBase, IGeographySink - { - #region Constructors - - /// - /// Constructor. Creates a KeyholeMarkupLanguageGeography sink which will fill the given - /// xml writer with data in the KML format - /// - /// Xml writter to be filled with data - public KeyholeMarkupLanguageGeography(System.Xml.XmlWriter writer) - : base(writer) - { - } - - #endregion - - #region IGeographySink interface - - /// - /// This method will be called when a new geography instance should be passed to the sink - /// - /// Geography type - public void BeginGeography(OpenGisGeographyType type) - { - m_Context.BeginSpatialObject(type); - - switch (type) - { - case OpenGisGeographyType.Point: - { - StartElement("Point"); - break; - } - case OpenGisGeographyType.LineString: - { - StartElement("LineString"); - break; - } - case OpenGisGeographyType.Polygon: - { - StartElement("Polygon"); - break; - } - case OpenGisGeographyType.MultiPoint: - case OpenGisGeographyType.MultiLineString: - case OpenGisGeographyType.MultiPolygon: - case OpenGisGeographyType.GeometryCollection: - { - StartElement("MultiGeometry"); - break; - } - default: - { - throw new KMLException("Type not supported: " + type.ToString()); - } - } - } - - /// - /// This method will be called when the geography instance should be finalized - /// - public void EndGeography() - { - m_Context.EndSpatialObject(); - - EndElement(); - } - - /// - /// This method will be called when a new figure is passed to sink - /// - /// X coordinate - /// Y coordinate - /// Z coordinate - /// M coordinate - public void BeginFigure(double x, double y, Nullable z, Nullable m) - { - m_Context.BeginFigure(); - - #region Export of Altitude Mode flag - - if ((m_Context.Type == OpenGisGeographyType.Polygon && m_Context.IsFirstFigure) || - m_Context.Type == OpenGisGeographyType.Point || - m_Context.Type == OpenGisGeographyType.LineString) - { - // The following code exports the geography instance's altitude mode. - // Altitude mode is stored as the m coordinate of every point, - // but altitude mode is a geography instance property, so it must be the same for all vertices, - // and we take it from the m coordinate of the first point. - - if (m.HasValue) - { - int altitudeModeCode = (int)m.Value; - if (Enum.IsDefined(typeof(AltitudeMode), altitudeModeCode)) - { - AltitudeMode altitudeMode = (AltitudeMode)altitudeModeCode; - - switch (altitudeMode) - { - case AltitudeMode.absolute: - case AltitudeMode.relativeToGround: - case AltitudeMode.relativeToSeaFloor: - { - StartElement("altitudeMode"); - WriteString(altitudeMode.ToString()); - EndElement(); - break; - } - case AltitudeMode.clampToGround: - case AltitudeMode.clampToSeaFloor: - { - _writer.WriteStartElement("altitudeMode", Constants.GxNamespace); - WriteString(altitudeMode.ToString()); - _writer.WriteEndElement(); - break; - } - default: - { - throw new KMLException("Altitude mode not supported: " + altitudeMode.ToString()); - } - } - } - } - } - - #endregion - - #region Export of Tesselate Flag - - // If the altitude mode is "clamp to ground" or "clamp to sea floor" then a tesselate flag will be exported - - if (m_Context.Type == OpenGisGeographyType.LineString || - (m_Context.Type == OpenGisGeographyType.Polygon && m_Context.IsFirstFigure)) - { - if (m.HasValue) - { - int altitudeModeCode = (int)m.Value; - if (Enum.IsDefined(typeof(AltitudeMode), altitudeModeCode)) - { - AltitudeMode altitudeMode = (AltitudeMode)altitudeModeCode; - if (altitudeMode == AltitudeMode.clampToGround || - altitudeMode == AltitudeMode.clampToSeaFloor) - { - StartElement("tessellate"); - WriteString("1"); - EndElement(); - } - } - } - } - - #endregion - - if (m_Context.Type == OpenGisGeographyType.Point || - m_Context.Type == OpenGisGeographyType.LineString) - { - StartElement("coordinates"); - } - else if (m_Context.Type == OpenGisGeographyType.Polygon) - { - StartElement(m_Context.IsFirstFigure ? "outerBoundaryIs" : "innerBoundaryIs"); - StartElement("LinearRing"); - StartElement("coordinates"); - } - - _writer.WriteValue(Utilities.ShiftInRange(y, 180)); - _writer.WriteValue(","); - _writer.WriteValue(Utilities.ShiftInRange(x, 90)); - if (z != null && z.HasValue) - { - _writer.WriteValue(","); - _writer.WriteValue(z.Value); - } - } - - /// - /// This method should be called when a figure is finalized - /// - public void EndFigure() - { - m_Context.EndFigure(); - - if (m_Context.Type == OpenGisGeographyType.Point || - m_Context.Type == OpenGisGeographyType.LineString) - { - EndElement(); - } - else if (m_Context.Type == OpenGisGeographyType.Polygon) - { - EndElement(); // Closing coordinates tag - EndElement(); // Closing LinearRing tag - EndElement(); // Closing outerBoundaryIs / innerBoundaryIs tag - } - } - - /// - /// This method should be called when a new line is passed to the sink - /// - /// X coordinate - /// Y coordinate - /// Z coordinate - /// M coordinate - public void AddLine(double x, double y, Nullable z, Nullable m) - { - _writer.WriteValue("\r\n"); - - _writer.WriteValue(Utilities.ShiftInRange(y, 180)); - _writer.WriteValue(","); - _writer.WriteValue(Utilities.ShiftInRange(x, 90)); - if (z != null && z.HasValue) - { - _writer.WriteValue(","); - _writer.WriteValue(z.Value); - } - } - - #endregion - - #region Private Data - - /// - /// Export execution context - /// - private ExportContext m_Context = new ExportContext(); - - #endregion - } -} +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.KMLProcessor.Import; + +namespace SQLSpatialTools.KMLProcessor.Export +{ + /// + /// This class is the geography sink. It will export the given geography instances + /// into the KML format. + /// + internal class KeyholeMarkupLanguageGeography : KeyholeMarkupLanguageBase, IGeographySink110 + { + #region Constructors + + /// + /// Constructor. Creates a KeyholeMarkupLanguageGeography sink which will fill the given + /// xml writer with data in the KML format + /// + /// Xml writer to be filled with data + public KeyholeMarkupLanguageGeography(System.Xml.XmlWriter writer) + : base(writer) + { + } + + #endregion + + #region IGeographySink110 interface + + /// + /// This method will be called when a new geography instance should be passed to the sink + /// + /// Geography type + public void BeginGeography(OpenGisGeographyType type) + { + _context.BeginSpatialObject(type); + + switch (type) + { + case OpenGisGeographyType.Point: + { + StartElement("Point"); + break; + } + case OpenGisGeographyType.LineString: + { + StartElement("LineString"); + break; + } + case OpenGisGeographyType.Polygon: + { + StartElement("Polygon"); + break; + } + case OpenGisGeographyType.MultiPoint: + case OpenGisGeographyType.MultiLineString: + case OpenGisGeographyType.MultiPolygon: + case OpenGisGeographyType.GeometryCollection: + { + StartElement("MultiGeometry"); + break; + } + default: + { + throw new KMLException("Type not supported: " + type.ToString()); + } + } + } + + /// + /// This method will be called when the geography instance should be finalized + /// + public void EndGeography() + { + _context.EndSpatialObject(); + + EndElement(); + } + + /// + /// This method will be called when a new figure is passed to sink + /// + /// X coordinate + /// Y coordinate + /// Z coordinate + /// M coordinate + public void BeginFigure(double x, double y, double? z, double? m) + { + _context.BeginFigure(); + + #region Export of Altitude Mode flag + + if ((_context.Type == OpenGisGeographyType.Polygon && _context.IsFirstFigure) || + _context.Type == OpenGisGeographyType.Point || + _context.Type == OpenGisGeographyType.LineString) + { + // The following code exports the geography instance's altitude mode. + // Altitude mode is stored as the m coordinate of every point, + // but altitude mode is a geography instance property, so it must be the same for all vertices, + // and we take it from the m coordinate of the first point. + + if (m.HasValue) + { + var altitudeModeCode = (int)m.Value; + if (Enum.IsDefined(typeof(AltitudeMode), altitudeModeCode)) + { + var altitudeMode = (AltitudeMode)altitudeModeCode; + + switch (altitudeMode) + { + case AltitudeMode.Absolute: + case AltitudeMode.RelativeToGround: + case AltitudeMode.RelativeToSeaFloor: + { + StartElement("altitudeMode"); + WriteString(altitudeMode.ToString()); + EndElement(); + break; + } + case AltitudeMode.ClampToGround: + case AltitudeMode.ClampToSeaFloor: + { + Writer.WriteStartElement("altitudeMode", Constants.GxNamespace); + WriteString(altitudeMode.ToString()); + Writer.WriteEndElement(); + break; + } + default: + { + throw new KMLException("Altitude mode not supported: " + altitudeMode.ToString()); + } + } + } + } + } + + #endregion + + #region Export of Tessellate Flag + + // If the altitude mode is "clamp to ground" or "clamp to sea floor" then a tessellate flag will be exported + + if (_context.Type == OpenGisGeographyType.LineString || + (_context.Type == OpenGisGeographyType.Polygon && _context.IsFirstFigure)) + { + if (m.HasValue) + { + var altitudeModeCode = (int)m.Value; + if (Enum.IsDefined(typeof(AltitudeMode), altitudeModeCode)) + { + var altitudeMode = (AltitudeMode)altitudeModeCode; + if (altitudeMode == AltitudeMode.ClampToGround || + altitudeMode == AltitudeMode.ClampToSeaFloor) + { + StartElement("tessellate"); + WriteString("1"); + EndElement(); + } + } + } + } + + #endregion + + if (_context.Type == OpenGisGeographyType.Point || + _context.Type == OpenGisGeographyType.LineString) + { + StartElement("coordinates"); + } + else if (_context.Type == OpenGisGeographyType.Polygon) + { + StartElement(_context.IsFirstFigure ? "outerBoundaryIs" : "innerBoundaryIs"); + StartElement("LinearRing"); + StartElement("coordinates"); + } + + Writer.WriteValue(Utilities.ShiftInRange(y, 180)); + Writer.WriteValue(","); + Writer.WriteValue(Utilities.ShiftInRange(x, 90)); + if (z.HasValue) + { + Writer.WriteValue(","); + Writer.WriteValue(z.Value); + } + } + + /// + /// This method should be called when a figure is finalized + /// + public void EndFigure() + { + _context.EndFigure(); + + if (_context.Type == OpenGisGeographyType.Point || + _context.Type == OpenGisGeographyType.LineString) + { + EndElement(); + } + else if (_context.Type == OpenGisGeographyType.Polygon) + { + EndElement(); // Closing coordinates tag + EndElement(); // Closing LinearRing tag + EndElement(); // Closing outerBoundaryIs / innerBoundaryIs tag + } + } + + /// + /// This method should be called when a new line is passed to the sink + /// + /// X coordinate + /// Y coordinate + /// Z coordinate + /// M coordinate + public void AddLine(double x, double y, double? z, double? m) + { + Writer.WriteValue("\r\n"); + + Writer.WriteValue(Utilities.ShiftInRange(y, 180)); + Writer.WriteValue(","); + Writer.WriteValue(Utilities.ShiftInRange(x, 90)); + if (z.HasValue) + { + Writer.WriteValue(","); + Writer.WriteValue(z.Value); + } + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + #endregion + + #region Private Data + + /// + /// Export execution context + /// + private readonly ExportContext _context = new ExportContext(); + + #endregion + } +} diff --git a/KMLProcessor/Export/KeyholeMarkupLanguageGeometry.cs b/src/KMLProcessor/Export/KeyholeMarkupLanguageGeometry.cs similarity index 62% rename from KMLProcessor/Export/KeyholeMarkupLanguageGeometry.cs rename to src/KMLProcessor/Export/KeyholeMarkupLanguageGeometry.cs index e5aa6fc..437efcd 100644 --- a/KMLProcessor/Export/KeyholeMarkupLanguageGeometry.cs +++ b/src/KMLProcessor/Export/KeyholeMarkupLanguageGeometry.cs @@ -1,166 +1,174 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.SqlServer.Types; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// This class is the geometry sink. It will export the given geometry instances - /// into the KML format. - /// - internal class KeyholeMarkupLanguageGeometry : KeyholeMarkupLanguageBase, IGeometrySink - { - #region Constructors - - /// - /// Constructor. Creates a KeyholeMarkupLanguageGeometry sink which will fill the given - /// xml writer with data in the KML format - /// - /// Xml writter to be filled with data - public KeyholeMarkupLanguageGeometry(System.Xml.XmlWriter writer) - : base(writer) - { - } - - #endregion - - #region IGeometrySink interface - - /// - /// This method will be called when a new geometry instance is passed to the sink - /// - /// Geometry type - public void BeginGeometry(OpenGisGeometryType type) - { - m_Context.BeginSpatialObject(type); - - switch (type) - { - case OpenGisGeometryType.Point: - { - StartElement("Point"); - StartElement("coordinates"); - break; - } - case OpenGisGeometryType.LineString: - { - StartElement("LineString"); - StartElement("coordinates"); - break; - } - case OpenGisGeometryType.Polygon: - { - StartElement("Polygon"); - break; - } - case OpenGisGeometryType.MultiPoint: - case OpenGisGeometryType.MultiLineString: - case OpenGisGeometryType.MultiPolygon: - case OpenGisGeometryType.GeometryCollection: - { - StartElement("MultiGeometry"); - break; - } - default: - { - throw new KMLException("Type not supported: " + type.ToString()); - } - } - } - - /// - /// This method will be called when the geometry instance is finalized - /// - public void EndGeometry() - { - // Gets the type of the geometry instance which should be finalized - OpenGisGeometryType type = m_Context.Type; - - m_Context.EndSpatialObject(); - - if (type == OpenGisGeometryType.Point || - type == OpenGisGeometryType.LineString) - { - EndElement(); - } - - EndElement(); - } - - /// - /// This method will be called when a new figure is passed to the sink - /// - /// X coordinate - /// Y coordinate - /// Z coordinate - /// M coordinate - public void BeginFigure(double x, double y, Nullable z, Nullable m) - { - m_Context.BeginFigure(); - - if (m_Context.Type == OpenGisGeometryType.Polygon) - { - StartElement(m_Context.IsFirstFigure ? "outerBoundaryIs" : "innerBoundaryIs"); - StartElement("LinearRing"); - StartElement("coordinates"); - } - - _writer.WriteValue(y); - _writer.WriteValue(","); - _writer.WriteValue(x); - if (z != null && z.HasValue) - { - _writer.WriteValue(","); - _writer.WriteValue(z.Value); - } - } - - /// - /// This method should be called when a figure is finalized - /// - public void EndFigure() - { - m_Context.EndFigure(); - - if (m_Context.Type == OpenGisGeometryType.Polygon) - { - EndElement(); // Closing coordinates tag - EndElement(); // Closing LinearRing tag - EndElement(); // Closing outerBoundaryIs / innerBoundaryIs tag - } - } - - /// - /// This method should be called when a new line is be passed to the sink - /// - /// X coordinate - /// Y coordinate - /// Z coordinate - /// M coordinate - public void AddLine(double x, double y, Nullable z, Nullable m) - { - _writer.WriteValue("\r\n"); - - _writer.WriteValue(y); - _writer.WriteValue(","); - _writer.WriteValue(x); - if (z != null && z.HasValue) - { - _writer.WriteValue(","); - _writer.WriteValue(z.Value); - } - } - - #endregion - - #region Private Data - - /// - /// Export execution context - /// - private ExportContext m_Context = new ExportContext(); - - #endregion - } -} +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; +// ReSharper disable UnusedMember.Global + +namespace SQLSpatialTools.KMLProcessor.Export +{ + /// + /// This class is the geometry sink. It will export the given geometry instances + /// into the KML format. + /// + internal class KeyholeMarkupLanguageGeometry : KeyholeMarkupLanguageBase, IGeometrySink110 + { + #region Constructors + + /// + /// Constructor. Creates a KeyholeMarkupLanguageGeometry sink which will fill the given + /// xml writer with data in the KML format + /// + /// Xml writer to be filled with data + public KeyholeMarkupLanguageGeometry(System.Xml.XmlWriter writer) + : base(writer) + { + } + + #endregion + + #region IGeometrySink interface + + /// + /// This method will be called when a new geometry instance is passed to the sink + /// + /// Geometry type + public void BeginGeometry(OpenGisGeometryType type) + { + _context.BeginSpatialObject(type); + + switch (type) + { + case OpenGisGeometryType.Point: + { + StartElement("Point"); + StartElement("coordinates"); + break; + } + case OpenGisGeometryType.LineString: + { + StartElement("LineString"); + StartElement("coordinates"); + break; + } + case OpenGisGeometryType.Polygon: + { + StartElement("Polygon"); + break; + } + case OpenGisGeometryType.MultiPoint: + case OpenGisGeometryType.MultiLineString: + case OpenGisGeometryType.MultiPolygon: + case OpenGisGeometryType.GeometryCollection: + { + StartElement("MultiGeometry"); + break; + } + default: + { + throw new KMLException("Type not supported: " + type.ToString()); + } + } + } + + /// + /// This method will be called when the geometry instance is finalized + /// + public void EndGeometry() + { + // Gets the type of the geometry instance which should be finalized + var type = _context.Type; + + _context.EndSpatialObject(); + + if (type == OpenGisGeometryType.Point || + type == OpenGisGeometryType.LineString) + { + EndElement(); + } + + EndElement(); + } + + /// + /// This method will be called when a new figure is passed to the sink + /// + /// X coordinate + /// Y coordinate + /// Z coordinate + /// M coordinate + public void BeginFigure(double x, double y, double? z, double? m) + { + _context.BeginFigure(); + + if (_context.Type == OpenGisGeometryType.Polygon) + { + StartElement(_context.IsFirstFigure ? "outerBoundaryIs" : "innerBoundaryIs"); + StartElement("LinearRing"); + StartElement("coordinates"); + } + + Writer.WriteValue(y); + Writer.WriteValue(","); + Writer.WriteValue(x); + if (z.HasValue) + { + Writer.WriteValue(","); + Writer.WriteValue(z.Value); + } + } + + /// + /// This method should be called when a figure is finalized + /// + public void EndFigure() + { + _context.EndFigure(); + + if (_context.Type == OpenGisGeometryType.Polygon) + { + EndElement(); // Closing coordinates tag + EndElement(); // Closing LinearRing tag + EndElement(); // Closing outerBoundaryIs / innerBoundaryIs tag + } + } + + /// + /// This method should be called when a new line is be passed to the sink + /// + /// X coordinate + /// Y coordinate + /// Z coordinate + /// M coordinate + public void AddLine(double x, double y, double? z, double? m) + { + Writer.WriteValue("\r\n"); + + Writer.WriteValue(y); + Writer.WriteValue(","); + Writer.WriteValue(x); + if (z.HasValue) + { + Writer.WriteValue(","); + Writer.WriteValue(z.Value); + } + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + #endregion + + #region Private Data + + /// + /// Export execution context + /// + private readonly ExportContext _context = new ExportContext(); + + #endregion + } +} diff --git a/src/KMLProcessor/FilterSetSridGeographySink.cs b/src/KMLProcessor/FilterSetSridGeographySink.cs new file mode 100644 index 0000000..96db61d --- /dev/null +++ b/src/KMLProcessor/FilterSetSridGeographySink.cs @@ -0,0 +1,80 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.KMLProcessor +{ + /// + /// This class will propagate all method calls to the given target sink, + /// except the method call to the SetSrid method. + /// + public class FilterSetSridGeographySink : IGeographySink110 + { + #region Constructors + + /// + /// Default Constructor + /// + /// Target sink + public FilterSetSridGeographySink(IGeographySink110 targetSink) + { + _targetSink = targetSink; + } + + #endregion + + #region Private Data + + /// + /// Target sink to propagate all method calls to it, except the method call to the SetSrid method + /// + private readonly IGeographySink110 _targetSink; + + #endregion + + #region IGeographySink Members + + public void AddLine(double latitude, double longitude, double? z, double? m) + { + _targetSink?.AddLine(latitude, longitude, z, m); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void BeginFigure(double latitude, double longitude, double? z, double? m) + { + _targetSink?.BeginFigure(latitude, longitude, z, m); + } + + public void BeginGeography(OpenGisGeographyType type) + { + _targetSink?.BeginGeography(type); + } + + public void EndFigure() + { + _targetSink?.EndFigure(); + } + + public void EndGeography() + { + _targetSink?.EndGeography(); + } + + /// + /// This method call will not be propagated to the target sink + /// + /// + public void SetSrid(int srid) + { + } + + #endregion + } +} diff --git a/src/KMLProcessor/GeographyContext.cs b/src/KMLProcessor/GeographyContext.cs new file mode 100644 index 0000000..569b1e2 --- /dev/null +++ b/src/KMLProcessor/GeographyContext.cs @@ -0,0 +1,31 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.KMLProcessor +{ + /// + /// This class contains the information about the context where a geography instance is found. + /// + public class GeographyContext + { + /// + /// Geography instance Id + /// + public string Id { get; set; } + + /// + /// Geography instance context. It contains the information about the parent and + /// the siblings of this geography instance. + /// It also contains the information about this geography instance. + /// + public string Context { get; set; } + + /// + /// The extracted geography instance + /// + public SqlGeography Shape { get; set; } + } +} diff --git a/src/KMLProcessor/Import/AltitudeMode.cs b/src/KMLProcessor/Import/AltitudeMode.cs new file mode 100644 index 0000000..6a0d3ef --- /dev/null +++ b/src/KMLProcessor/Import/AltitudeMode.cs @@ -0,0 +1,18 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// Altitude modes allowed in KML + /// + public enum AltitudeMode + { + ClampToGround = 0, + RelativeToGround = 1, + Absolute = 2, + ClampToSeaFloor = 3, + RelativeToSeaFloor = 4 + } +} diff --git a/src/KMLProcessor/Import/Geography.cs b/src/KMLProcessor/Import/Geography.cs new file mode 100644 index 0000000..a6b0500 --- /dev/null +++ b/src/KMLProcessor/Import/Geography.cs @@ -0,0 +1,180 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; +using Geog = SQLSpatialTools.Functions.General.Geography; +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// Base class for all geography instances extracted from the KML file + /// + public abstract class Geography + { + #region Public Properties + + /// + /// Id of the placemark which contains this geography instance + /// + public string Id { get; set; } + + /// + /// True if point(s) which are part of this geography instance + /// should be connected to the ground when they are visually represented in some visualization tool + /// + public bool Extrude { get; set; } + + /// + /// True if all lines in this geography instance should follow the terrain. + /// This flag is not applicable just for Point instance. + /// + public bool Tessellate { get; set; } + + /// + /// True if this geography instance is valid + /// + public bool IsValid + { + get + { + // This implementation is base on the property that the Sql Server 2008 (10.0 Katmai) + // will throw an exception if the geography instance is not valid. + + try + { + var constructed = new SqlGeographyBuilder(); + constructed.SetSrid(Constants.DefaultSRID); + + Populate(constructed); + + // ReSharper disable once UnusedVariable + var geography = constructed.ConstructedGeography; + + return true; + } + catch (Exception) + { + return false; + } + } + } + + /// + /// The context where this geography instance is found. It will + /// contain the information about the parent and the siblings of this + /// geography instance, and the information about this geography instance + /// it self. + /// + public string Context { get; set; } + + #endregion + + #region Abstract Members + + /// + /// This method populates the given sink with the data from this geography instance + /// + /// Sink to be populated + public abstract void Populate(IGeographySink110 sink); + + /// + /// SqlGeography instance well-known text. + /// + public abstract string WKT + { + get; + } + + #endregion + + #region Public Methods + + /// + /// This method returns the geography instance that corresponds to this KML geography + /// + /// If true and this geography instance is invalid then the MakeValid + /// function will be executed on this geography instance + /// The geography instance that corresponds to this KML geography + public SqlGeography ToSqlGeography(bool makeValid) + { + if (IsValid) + { + var constructed = new SqlGeographyBuilder(); + constructed.SetSrid(Constants.DefaultSRID); + Populate(constructed); + return constructed.ConstructedGeography; + } + if (makeValid) + { + var constructed = new SqlGeographyBuilder(); + constructed.SetSrid(Constants.DefaultSRID); + MakeValid(constructed); + return constructed.ConstructedGeography; + } + + throw new KMLException("Invalid geography instance."); + } + + /// + /// This method returns a string representation of this object + /// + /// WKT for this geography instance + public override string ToString() + { + return WKT; + } + + /// + /// This method populates the given sink with the data from this geography instance. + /// If this geography instance is invalid and the makeValid flag is set then a valid geography instance + /// will be constructed and the given sink will be populated with that instance. + /// + /// Sink to be populated + /// If true and this geography instance is invalid then the MakeValid + /// function will be executed on this geography instance + public void Populate( + IGeographySink110 sink, + bool makeValid) + { + if (makeValid) + { + if (IsValid) + Populate(sink); + else + MakeValid(sink); + } + else + { + Populate(sink); + } + } + + /// + /// This method populates the given sink with the valid geography instance constructed from this geography instance. + /// + /// Sink to be populated + private void MakeValid(IGeographySink110 sink) + { + // 1. Creates the valid geography for this WKT + var vg = Geog.MakeValidGeographyFromText(WKT, Constants.DefaultSRID); + + // 2. Populates the given sink with the valid geography + vg.Populate(new FilterSetSridGeographySink(sink)); + } + + #endregion + + #region Internal Properties + + /// + /// Id which was assigned to this geography instance when it was inserted in the database. + /// If the value is null then it is not stored in the database yet + /// + internal int? DbId { get; set; } + + #endregion + } +} diff --git a/src/KMLProcessor/Import/GroundOverlay.cs b/src/KMLProcessor/Import/GroundOverlay.cs new file mode 100644 index 0000000..c1d027a --- /dev/null +++ b/src/KMLProcessor/Import/GroundOverlay.cs @@ -0,0 +1,80 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// This class holds the information about the ground overlay extracted from the KML file + /// + public class GroundOverlay : Geography + { + #region Public Properties + + /// + /// Ground Overlay Name + /// + public string Name + { + get => _name; + internal set => _name = string.IsNullOrEmpty(value) ? "" : value; + } + /// + /// Data member for the Name property + /// + private string _name = ""; + + /// + /// Box defined inside this ground overlay. + /// + public Geography Box { private get; set; } + + /// + /// Region defined inside this ground overlay. + /// + public Region Region { private get; set; } + + /// + /// SqlGeography instance well-known text. + /// + public override string WKT + { + get + { + if (Box != null) + { + return Box.WKT; + } + + if (Region != null) + { + return Region.WKT; + } + + throw new KMLException("WKT is not defined."); + } + } + + #endregion + + #region Geography Methods + + /// + /// This method populates the given sink with the data from this geography instance + /// + /// Sink to be populated + public override void Populate(Microsoft.SqlServer.Types.IGeographySink110 sink) + { + if (Box != null) + { + Box.Populate(sink); + } + else + { + Region?.Populate(sink); + } + } + + #endregion + } +} diff --git a/src/KMLProcessor/Import/KMLParser.cs b/src/KMLProcessor/Import/KMLParser.cs new file mode 100644 index 0000000..f6abb10 --- /dev/null +++ b/src/KMLProcessor/Import/KMLParser.cs @@ -0,0 +1,1128 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Xml; +// ReSharper disable MemberCanBePrivate.Global + +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// This class parses the given KML string + /// + public class KMLParser + { + #region Constructors + + /// + /// Constructor. Accepts the string in the KML format. + /// + /// KML string to be parsed + public KMLParser(string kml) + { + Document = new XmlDocument(); + Document.LoadXml(kml); + } + + #endregion + + #region Public Data + + /// + /// Geography instances extracted from the KML string + /// + public IList Geographies { protected set; get; } = new List(); + + /// + /// Placemarks extracted from the KML string + /// + public IList Placemarks { protected set; get; } = new List(); + + /// + /// Models extracted from the KML string + /// + public IList Models { protected set; get; } = new List(); + + /// + /// Regions extracted from the KML string + /// + public IList Regions { protected set; get; } = new List(); + + /// + /// Ground overlays extracted from the KML string + /// + public IList GroundOverlays { protected set; get; } = new List(); + + #endregion + + #region Protected Data + + /// + /// Xml document whose data will be traversed + /// + protected readonly XmlDocument Document; + + #endregion + + #region Protected Methods + + /// + /// This method visits the given KML node + /// + /// KML node to be visited + protected void Visit(XmlNode node) + { + if (node == null) return; + + var nodeName = node.Name.ToLowerInvariant(); + + if (node is XmlElement elem) + { + if (nodeName.Equals("point")) + { + VisitPoint(elem); + } + else if (nodeName.Equals("linestring")) + { + VisitLineString(elem); + } + else if (nodeName.Equals("linearring")) + { + VisitLinearRing(elem); + } + else if (nodeName.Equals("polygon")) + { + VisitPolygon(elem); + } + else if (nodeName.Equals("multigeometry")) + { + VisitMultiGeometry(elem); + } + else if (nodeName.Equals("placemark")) + { + VisitPlacemark(elem); + } + else if (nodeName.Equals("model")) + { + VisitModel(elem); + } + else if (nodeName.Equals("region")) + { + VisitRegion(elem); + } + else if (nodeName.Equals("groundoverlay")) + { + VisitGroundOverlay(elem); + } + else + { + // Visits the child nodes + foreach (XmlNode child in node.ChildNodes) + { + Visit(child); + } + } + } + } + + /// + /// This method visits a KML Ground Overlay element + /// + /// KML Ground Overlay element + protected void VisitGroundOverlay(XmlElement element) + { + if (element == null) return; + + var groundOverlay = new GroundOverlay(); + + VisitGeography(element, groundOverlay); + groundOverlay.Name = GetChildElementText(element, "name"); + + var latLonAltBoxElem = GetFirstChild(element, "LatLonAltBox"); + var latLonBoxElem = GetFirstChild(element, "LatLonBox"); + var latLonQuadElem = GetFirstChild(element, "gx:LatLonQuad"); + + var numOfGeographiesBefore = Geographies.Count; + + if (latLonAltBoxElem != null) + { + VisitLatLonAltBox(latLonAltBoxElem); + } + else if (latLonBoxElem != null) + { + VisitLatLonBox(latLonBoxElem); + } + else if (latLonQuadElem != null) + { + VisitLatLonQuad(latLonQuadElem); + } + + var numOfGeographiesAfter = Geographies.Count; + + if (numOfGeographiesAfter == numOfGeographiesBefore + 1) + { + groundOverlay.Box = Geographies[Geographies.Count - 1]; + Geographies.RemoveAt(Geographies.Count - 1); + } + else if (numOfGeographiesAfter > numOfGeographiesBefore + 1) + { + // Multiple geography instances found in a single ground overlay. + // The last geography instance is the border. + + groundOverlay.Box = Geographies[Geographies.Count - 1]; + Geographies.RemoveAt(Geographies.Count - 1); + } + + #region Check if a region is also defined inside this ground overlay + + var regionElem = GetFirstChild(element, "Region"); + if (regionElem != null) + { + var numOfRegionsBefore = Regions.Count; + + VisitRegion(regionElem); + + var numOfRegionsAfter = Regions.Count; + + if (numOfRegionsAfter == numOfRegionsBefore + 1) + { + groundOverlay.Region = Regions[numOfRegionsAfter - 1]; + } + else if (numOfRegionsAfter > numOfRegionsBefore + 1) + { + // Multiple regions found in a single ground overlay. The last region is the border. + + groundOverlay.Region = Regions[numOfRegionsAfter - 1]; + } + } + + #endregion + + GroundOverlays.Add(groundOverlay); + } + + /// + /// This method visits a KML placemark element + /// + /// KML placemark element + protected void VisitPlacemark(XmlElement elem) + { + if (elem == null) return; + + var placemark = new Placemark(); + + placemark.Id = GetAttribute(elem, "id"); + + // Visits the child nodes + foreach (XmlNode child in elem.ChildNodes) + { + var childName = child.Name.ToLowerInvariant(); + if (childName.Equals("name")) + { + if (child.HasChildNodes) + { + placemark.Name = child.ChildNodes[0].InnerText; + } + } + else if (childName.Equals("description")) + { + if (child.HasChildNodes) + { + placemark.Description = child.ChildNodes[0].InnerText; + } + } + else if (childName.Equals("address")) + { + if (child.HasChildNodes) + { + placemark.Address = child.ChildNodes[0].InnerText; + } + } + else if (childName.Equals("lookat") && (child as XmlElement) != null) + { + var numOfGeographiesBefore = Geographies.Count; + + VisitLookAt(child as XmlElement); + + var numOfGeographiesAfter = Geographies.Count; + + if (numOfGeographiesAfter > numOfGeographiesBefore) + { + // Point found where this placemark looks at + + placemark.LookAt = Geographies[Geographies.Count - 1]; + Geographies.RemoveAt(Geographies.Count - 1); + } + } + else + { + var numOfGeographiesBefore = Geographies.Count; + + Visit(child); + + var numOfGeographiesAfter = Geographies.Count; + + if (numOfGeographiesAfter > numOfGeographiesBefore) + { + // Found the geography instance which describes this placemark + + placemark.Geography = Geographies[Geographies.Count - 1]; + } + } + } + + Placemarks.Add(placemark); + } + + /// + /// This method visits a LookAt element + /// + /// KML lookat element to be visited + protected void VisitLookAt(XmlElement elem) + { + if (elem == null) return; + + VisitLonLatAltElement(elem); + } + + /// + /// This method parses the given coordinates string. + /// + /// String with coordinates to be parsed + /// List of points contained in the given string + protected static IList ParseCoordinates(string coordinates) + { + var points = new List(); + + if (string.IsNullOrEmpty(coordinates) || coordinates.Trim().Length == 0) + return points; + + // Removes the spaces inside the tuples + while (coordinates.Contains(", ")) + { + coordinates = coordinates.Replace(", ", ","); + } + + // Splits the tuples + var coordinate = coordinates.Trim().Split(' ', '\t', '\n'); + + // Process each tuple + foreach (var p in coordinate) + { + var tp = p.Trim(); + if (tp.Length == 0) + continue; + + var cords = tp.Split(','); + + try + { + var longitude = double.Parse(cords[0], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture); + var latitude = double.Parse(cords[1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture); + double? altitude = null; + + if (cords.Length == 3) // the altitude is optional + { + altitude = double.Parse(cords[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture); + } + + var point = new Point {Longitude = longitude, Latitude = latitude, Altitude = altitude}; + + // If point is the same as the previous point then skip it + if (points.Count > 0 && points[points.Count - 1].Equals(point)) + continue; + + points.Add(point); + } + catch (Exception exc) + { + throw new KMLException("Coordinate is in wrong format: " + tp, exc); + } + } + + return points; + } + + /// + /// This method performs the visit which is common to all geography instances. + /// It stores extracted information into the given geography object. + /// + /// Xml element to be visited + /// Geography object where data should be stored + protected static void VisitGeography(XmlElement elem, Geography geography) + { + #region The id in the KML string + + geography.Id = GetAttribute(elem, "id"); + + #endregion + + #region The extrude flag + + var extrudeElem = GetFirstChild(elem, "extrude"); + if (extrudeElem != null && extrudeElem.HasChildNodes) + { + var val = extrudeElem.ChildNodes[0].InnerText; + if (val.Trim().Equals("1")) + { + geography.Extrude = true; + } + } + + #endregion + + #region The tessellate flag + + var tessellateElem = GetFirstChild(elem, "tessellate"); + if (tessellateElem != null && tessellateElem.HasChildNodes) + { + var val = tessellateElem.ChildNodes[0].InnerText; + if (val.Trim().Equals("1")) + { + geography.Tessellate = true; + } + } + + #endregion + } + + /// + /// This method visits a KML Point element + /// + /// KML Point element to be visited + protected void VisitPoint(XmlElement elem) + { + if (elem == null) return; + + // Extracts the point coordinates + var coordinates = GetFirstChild(elem, "coordinates"); + if (coordinates == null || !coordinates.HasChildNodes) + return; + + var coordinatesTextNode = coordinates.ChildNodes[0]; + + var coordinatesText = coordinatesTextNode.InnerText; + + var pts = ParseCoordinates(coordinatesText); + + if (pts.Count == 0) + return; + + var point = pts[0]; + + // Extracts the altitude mode + point.Measure = GetAltitudeModeCode(elem); + + // Extracts the other non-geographic data + VisitGeography(elem, point); + + // Sets the geography instance context + point.Context = (elem.ParentNode != null) ? elem.ParentNode.OuterXml : elem.OuterXml; + + Geographies.Add(point); + } + + /// + /// This method visits a KML LineString element + /// + /// KML line string element to be visited + /// True if the line is the ring + protected void VisitLineString( + XmlElement elem, + bool isLineRing = false) + { + if (elem == null) return; + + XmlNode coordinates = GetFirstChild(elem, "coordinates"); + if (coordinates == null || !coordinates.HasChildNodes) + return; + + var txt = coordinates.ChildNodes[0]; + + var allCoordinates = txt.InnerText; + + var points = ParseCoordinates(allCoordinates); + if (points == null || points.Count == 0) + return; + + var ls = isLineRing ? new LinearRing() : new LineString(); + + ls.Points.AddRange(points); + + var altitudeModeCode = GetAltitudeModeCode(elem); + + foreach (var p in ls.Points) + { + p.Measure = altitudeModeCode; + } + + // Extract other non-geographic data + VisitGeography(elem, ls); + + #region Handle tessellate flag + + ls.StoreTessellateFlag(); + + #endregion + + // Sets the geography instance context + ls.Context = (elem.ParentNode != null) ? elem.ParentNode.OuterXml : elem.OuterXml; + + Geographies.Add(ls); + } + + /// + /// This method visits a KML LinearRing element + /// + /// KML LinearRing element to be visited + protected void VisitLinearRing(XmlElement elem) + { + VisitLineString(elem, true); + } + + /// + /// This method visits a KML Polygon element + /// + /// KML Polygon element to be visited + protected void VisitPolygon(XmlElement elem) + { + if (elem == null) return; + + #region Extract Outer Border + + var outerBoundaryIs = GetFirstChild(elem, "outerBoundaryIs"); + if (outerBoundaryIs == null) + return; + + var outerLinearRing = GetFirstChild(outerBoundaryIs, "LinearRing"); + if (outerLinearRing == null) + return; + + var numOfGeographiesBefore = Geographies.Count; + + VisitLinearRing(outerLinearRing); + + var numOfGeographiesAfter = Geographies.Count; + + // Extracts the outer ring + if (numOfGeographiesAfter == numOfGeographiesBefore) + throw new KMLException("Outer ring not found!"); + + var index = Geographies.Count - 1; + var g = Geographies[index]; + Geographies.RemoveAt(index); + + if (!(g is LinearRing outerRing)) + throw new KMLException("Outer ring not created!"); + + var polygon = new Polygon(); + + var isValidPolygon = true; + + #region Check outer ring orientation. Outer boundary should be oriented counter-clockwise + + if (!outerRing.IsValid) + { + // Checks if just the ring's orientation is wrong + outerRing.SwitchOrientation(); + + if (!outerRing.IsValid) + { + isValidPolygon = false; + outerRing.SwitchOrientation(); + } + } + + #endregion + + polygon.OuterRing = outerRing; + + #endregion + + #region Extract Inner Borders + + var innerBoundaryIsLst = elem.GetElementsByTagName("innerBoundaryIs"); + foreach (XmlNode innerBorderNode in innerBoundaryIsLst) + { + if (!(innerBorderNode is XmlElement innerBoundaryIs)) + continue; + + var innerLinearRing = GetFirstChild(innerBoundaryIs, "LinearRing"); + if (innerLinearRing == null) + continue; + + numOfGeographiesBefore = Geographies.Count; + + VisitLinearRing(innerLinearRing); + + numOfGeographiesAfter = Geographies.Count; + + // Extracts the inner ring + if (numOfGeographiesBefore == numOfGeographiesAfter) + throw new KMLException("Inner ring not found!"); + + index = Geographies.Count - 1; + g = Geographies[index]; + Geographies.RemoveAt(index); + + if (!(g is LinearRing innerRing)) + throw new KMLException("Inner ring not created!"); + + #region Check the ring's orientation. Inner rings should be clockwise oriented + + if (isValidPolygon) + { + if (innerRing.IsValid) + { + // The inner rings should be in the opposite direction from the outer ring + + innerRing.SwitchOrientation(); + } + } + + #endregion + + polygon.InnerRing.Add(innerRing); + } + + #endregion + + // Extracts the other non-geographic data + VisitGeography(elem, polygon); + + #region Extract altitude mode + + var altitudeModeCode = GetAltitudeModeCode(elem); + if (altitudeModeCode.HasValue) + { + // Updates all the points on the outer bound + if (polygon.OuterRing != null) + { + foreach (var p in polygon.OuterRing.Points) + { + p.Measure = altitudeModeCode; + } + } + + // Update all the points on all the inner bounds + if (polygon.InnerRing != null) + { + foreach (var r in polygon.InnerRing) + { + if (r.Points == null) + return; + + foreach (var p1 in r.Points) + { + p1.Measure = altitudeModeCode; + } + } + } + } + + #endregion + + #region Handles tessellate flag + + if (polygon.Tessellate) + { + polygon.OuterRing?.StoreTessellateFlag(); + + if (polygon.InnerRing != null) + { + foreach (var r in polygon.InnerRing) + r.StoreTessellateFlag(); + } + } + + #endregion + + // Sets the geography instance context + polygon.Context = (elem.ParentNode != null) ? elem.ParentNode.OuterXml : elem.OuterXml; + + Geographies.Add(polygon); + } + + /// + /// This method visits a KML MultiGeometry element + /// + /// KML MultiGeometry element to be visited + protected void VisitMultiGeometry(XmlElement element) + { + if (element == null) + return; + + var mg = new MultiGeometry(); + + if (element.HasChildNodes) + { + foreach (XmlNode child in element.ChildNodes) + { + var lastIndexBefore = Geographies.Count - 1; + + Visit(child); + + var lastIndex = Geographies.Count - 1; + + if (lastIndex > lastIndexBefore) + { + var childGeography = Geographies[lastIndex]; + Geographies.RemoveAt(lastIndex); + mg.Geographies.Add(childGeography); + } + } + } + + // Sets the geography instance context + mg.Context = (element.ParentNode != null) ? element.ParentNode.OuterXml : element.OuterXml; + + Geographies.Add(mg); + } + + /// + /// This method visits a KML Region element + /// + /// KML Region element to be visited + protected void VisitRegion(XmlElement element) + { + if (element == null) return; + + var r = new Region(); + + VisitGeography(element, r); + + var latLonAltBoxElem = GetFirstChild(element, "LatLonAltBox"); + var latLonBoxElem = GetFirstChild(element, "LatLonBox"); + var latLonQuadElem = GetFirstChild(element, "gx:LatLonQuad"); + + var numOfGeographiesBefore = Geographies.Count; + + if (latLonAltBoxElem != null) + { + VisitLatLonAltBox(latLonAltBoxElem); + } + else if (latLonBoxElem != null) + { + VisitLatLonBox(latLonBoxElem); + } + else if (latLonQuadElem != null) + { + VisitLatLonQuad(latLonQuadElem); + } + + var numOfGeographiesAfter = Geographies.Count; + + if (numOfGeographiesAfter > numOfGeographiesBefore) + { + // Found region's box + + r.Box = Geographies[Geographies.Count - 1]; + Geographies.RemoveAt(Geographies.Count - 1); + } + + Regions.Add(r); + } + + /// + /// This method visits a KML LatLonAltBox element + /// + /// KML LatLonAltBox element to be visited + protected void VisitLatLonAltBox(XmlElement element) + { + if (element == null) return; + + #region Required properties + + var north = GetChildElementText(element, "north"); + var south = GetChildElementText(element, "south"); + var west = GetChildElementText(element, "west"); + var east = GetChildElementText(element, "east"); + + #endregion + + #region Optional properties + + double minAltitude = 0; + double maxAltitude = 0; + var minAltitudeS = GetChildElementText(element, "minAltitude"); + if (!string.IsNullOrEmpty(minAltitudeS)) + { + minAltitude = double.Parse(minAltitudeS); + } + + var maxAltitudeS = GetChildElementText(element, "maxAltitude"); + if (!string.IsNullOrEmpty(maxAltitudeS)) + { + maxAltitude = double.Parse(maxAltitudeS); + } + + var altitudeMode = GetAltitudeMode(element); + + #endregion + + var altBox = new LatLonAltBox + { + North = north, + South = south, + West = west, + East = east, + MinAltitude = minAltitude, + MaxAltitude = maxAltitude, + AltitudeMode = altitudeMode + }; + + Geographies.Add(altBox); + } + + /// + /// This method visits a KML LatLonBox element + /// + /// KML LatLonBox element to be visited + protected void VisitLatLonBox(XmlElement element) + { + if (element == null) return; + + #region Required properties + + var north = GetChildElementText(element, "north"); + var south = GetChildElementText(element, "south"); + var west = GetChildElementText(element, "west"); + var east = GetChildElementText(element, "east"); + + #endregion + + #region Optional properties + + double rotation = 0; + + var rotationS = GetChildElementText(element, "rotation"); + if (!string.IsNullOrEmpty(rotationS)) + { + rotation = double.Parse(rotationS); + } + + #endregion + + var altBox = new LatLonBox + { + North = north, + South = south, + West = west, + East = east, + Rotation = rotation + }; + + Geographies.Add(altBox); + } + + /// + /// This method visits a KML LatLonQuad element + /// + /// LatLonQuad KML element to be visited + protected void VisitLatLonQuad(XmlElement element) + { + if (element == null) return; + + var coordinates = GetChildElementText(element, "coordinates"); + + var points = ParseCoordinates(coordinates); + if (points == null || points.Count != 4) + throw new KMLException("In KML element LatLonQuad has to have exactly the 4 coordinates."); + + // Closes the polygon. Stores the last point the same as the first one. + points.Add(points[0].Clone()); + + var llq = new LatLonQuad(); + + llq.Points.Clear(); + llq.Points.AddRange(points); + + Geographies.Add(llq); + } + + /// + /// This method visits a KML element. Method extracts the child elements: + /// Longitude, Latitude, Altitude and AltitudeMode and creates the point instance from those values. + /// + /// KML element to be visited + protected void VisitLonLatAltElement(XmlElement element) + { + double longitude = 0, latitude = 0, altitude = 0; + int? altitudeMode; + try + { + var longitudeElem = GetFirstChild(element, "longitude"); + if (longitudeElem != null && longitudeElem.HasChildNodes) + { + var s = longitudeElem.ChildNodes[0].InnerText; + longitude = double.Parse(s); + } + var latitudeElem = GetFirstChild(element, "latitude"); + if (latitudeElem != null && latitudeElem.HasChildNodes) + { + var s = latitudeElem.ChildNodes[0].InnerText; + latitude = double.Parse(s); + } + var altitudeElem = GetFirstChild(element, "altitude"); + if (altitudeElem != null && altitudeElem.HasChildNodes) + { + var s = altitudeElem.ChildNodes[0].InnerText; + altitude = double.Parse(s); + } + + altitudeMode = GetAltitudeModeCode(element); + } + catch (Exception exc) + { + throw new KMLException("Location coordinates in wrong format!", exc); + } + + var pt = new Point(longitude, latitude, altitude, altitudeMode); + Geographies.Add(pt); + } + + /// + /// This method visits a KML Model element + /// + /// KML model element to be visited + protected void VisitModel(XmlElement element) + { + if (element == null) return; + + var m = new Model(); + + #region Extract Location + + var locationElem = GetFirstChild(element, "location"); + if (locationElem != null) + { + VisitLonLatAltElement(locationElem); + + m.Location = Geographies[Geographies.Count - 1] as Point; + Geographies.RemoveAt(Geographies.Count - 1); + } + + #endregion + + #region Extract other non-geographic data + + VisitGeography(element, m); + + var orientationElem = GetFirstChild(element, "orientation"); + if (orientationElem != null) + { + m.OrientationHeading = GetChildElementText(orientationElem, "heading", 0); + m.OrientationTilt = GetChildElementText(orientationElem, "tilt", 0); + m.OrientationRoll = GetChildElementText(orientationElem, "roll", 0); + } + + var scaleElem = GetFirstChild(element, "scale"); + if (scaleElem != null) + { + m.ScaleX = GetChildElementText(scaleElem, "x", 1); + m.ScaleY = GetChildElementText(scaleElem, "y", 1); + m.ScaleZ = GetChildElementText(scaleElem, "z", 1); + } + + #endregion + + Models.Add(m); + } + + #endregion + + #region Public methods + + /// + /// This method parses the given KML string + /// + public void Parse() + { + // Visits the all child nodes + foreach (XmlNode n in Document.ChildNodes) + { + Visit(n); + } + } + + #endregion + + #region Public Static Methods + + /// + /// This method parses the given XML file context + /// + /// File full path + /// KMLParser with loaded file context + public static KMLParser ParseFile(string fileFullPathName) + { + var text = System.IO.File.ReadAllText(fileFullPathName); + + var p = new KMLParser(text); + + return p; + } + + /// + /// This method returns the first child element with the given name in the given parent element + /// + /// Parent element + /// Child element name + /// Child element if it exists, null otherwise + public static XmlElement GetFirstChild(XmlElement element, string childElementName) + { + if (element == null || element.HasChildNodes == false || string.IsNullOrEmpty(childElementName)) + return null; + + childElementName = childElementName.ToLowerInvariant(); + + foreach (XmlNode node in element.ChildNodes) + { + var c = node as XmlElement; + if (c == null) + continue; + + if (c.Name.ToLowerInvariant().Equals(childElementName)) + return c; + } + + return null; + } + + /// + /// This method determines the altitude mode for the given KML geography element. + /// + /// KML geography element whose altitude mode should be found + /// Altitude mode, or null if the altitude mode is not found + private static AltitudeMode? GetAltitudeMode(XmlElement elem) + { + var altitudeModeVal = "clamptoground"; + var altitudeMode = GetFirstChild(elem, "altitudeMode") ?? GetFirstChild(elem, "gx:altitudeMode"); + + if (altitudeMode == null) + return null; + + if (altitudeMode.HasChildNodes && altitudeMode.ChildNodes[0].Value != null) + { + altitudeModeVal = altitudeMode.ChildNodes[0].Value.Trim().ToLowerInvariant(); + } + + if (altitudeModeVal.Equals("clamptoground")) + return AltitudeMode.ClampToGround; + if (altitudeModeVal.Equals("relativetoground")) + return AltitudeMode.RelativeToGround; + if (altitudeModeVal.Equals("absolute")) + return AltitudeMode.Absolute; + if (altitudeModeVal.Equals("relativetoseafloor")) + return AltitudeMode.RelativeToSeaFloor; + if (altitudeModeVal.Equals("clamptoseafloor")) + return AltitudeMode.ClampToSeaFloor; + + throw new KMLException("Altitude mode not supported: " + altitudeModeVal); + } + + /// + /// This method determines the altitude mode code for the given KML element. + /// + /// KML element + /// Altitude mode code, or null if the altitude mode is not found + public static int? GetAltitudeModeCode(XmlElement elem) + { + var altitudeMode = GetAltitudeMode(elem); + + if (altitudeMode == null) + return null; + + return (int)altitudeMode; + } + + /// + /// This method returns the tessellate flag value for the given KML element. + /// + /// KML element + /// Tessellate flag value + public static bool GetTessellateFlag(XmlElement elem) + { + var tessellateFlag = false; + XmlNode tessellate = GetFirstChild(elem, "tessellate"); + if (tessellate != null && tessellate.HasChildNodes) + { + var tessTxt = tessellate.ChildNodes[0]; + if (tessTxt.InnerText.Trim().Equals("1")) + tessellateFlag = true; + } + + return tessellateFlag; + } + + /// + /// This method returns the attribute value of the given xml element. + /// + /// Xml element whose attribute is requested + /// Attribute name + /// Attribute value, or null value if the attribute is not set + public static string GetAttribute(XmlElement elem, string attributeName) + { + var attribute = elem.Attributes[attributeName]; + + return attribute?.Value; + } + + /// + /// This method returns the inner text in the child element of the given parent element + /// + /// Parent element + /// Child element name + /// Child element inner text + public static string GetChildElementText(XmlElement element, string childElementName) + { + if (element == null || !element.HasChildNodes) return null; + + var child = GetFirstChild(element, childElementName); + + return child?.InnerText; + } + + /// + /// This method returns the inner text in the child element of the given parent element. + /// The inner text value is converted to the given type T + /// + /// Type to convert the inner text value into + /// Parent element + /// Child element name + /// Child text value converted into the type T + public static T GetChildElementText(XmlElement element, string childElementName) + { + var childInnerText = GetChildElementText(element, childElementName); + + if (string.IsNullOrEmpty(childInnerText)) + return default(T); + + return (T)Convert.ChangeType(childInnerText, typeof(T)); + } + + /// + /// This method returns the inner text in the child element of the given parent element. + /// The inner text value is converted to the given type T. If the child with the given name doesn't exist then + /// the given default value will be returned. + /// + /// Type to convert the inner text value into + /// Parent element + /// Child element name + /// In case that the child doesn't exist, this value will be returned + /// Child text value converted into the type T, or the default value if the child is not found + public static T GetChildElementText(XmlElement element, string childElementName, T defaultValue) + { + var childInnerText = GetChildElementText(element, childElementName); + + if (string.IsNullOrEmpty(childInnerText)) + return defaultValue; + + return (T)Convert.ChangeType(childInnerText, typeof(T)); + } + + #endregion + } +} diff --git a/src/KMLProcessor/Import/KMLProcessor.cs b/src/KMLProcessor/Import/KMLProcessor.cs new file mode 100644 index 0000000..ab11657 --- /dev/null +++ b/src/KMLProcessor/Import/KMLProcessor.cs @@ -0,0 +1,122 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using Microsoft.SqlServer.Types; +// ReSharper disable NotAccessedField.Local + +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// This class processes the given KML string and produces the equivalent SqlGeography instance + /// using sinks. + /// + public class KMLProcessor + { + #region Constructors + + /// + /// Constructor. Accepts the KML string. + /// + /// KML data to be parsed + public KMLProcessor(string kml) + { + _kml = kml; + _parser = new KMLParser(kml); + _parser.Parse(); + } + + #endregion + + #region Public Properties + + /// + /// Spatial reference identifier (SRID). + /// + // ReSharper disable MemberCanBePrivate.Global + public int Srid { get; set; } = Constants.DefaultSRID; + + /// + /// Placemarks extracted from the given KML string + /// + public IList Placemarks => _parser.Placemarks; + + /// + /// Models extracted from the given KML string + /// + public IList Models => _parser.Models; + + /// + /// Regions extracted from the given KML string + /// + public IList Regions => _parser.Regions; + + /// + /// Geography instances extracted from the given KML string + /// + public IEnumerable Geographies => _parser.Geographies; + + /// + /// Ground overlays extracted from the given KML string + /// + public IList GroundOverlays => _parser.GroundOverlays; + + #endregion + + #region Private Data + + /// + /// KML/XML parser + /// + private readonly KMLParser _parser; + + /// + /// KML string to be parsed + /// + private readonly string _kml; + + #endregion + + #region Public Methods + + /// + /// This method populates the given sink with the information about the geography instance + /// extracted from the KML string + /// + /// Sink to be filled + /// If true and the extracted geography instance is invalid then the MakeValid + /// function will be executed on the extracted geography instance + public void Populate( + IGeographySink110 sink, + bool makeValid) + { + sink.SetSrid(Srid); + + var numOfGeographies = _parser.Geographies.Count; + if (numOfGeographies == 1) + { + _parser.Geographies[0].Populate(sink, makeValid); + } + else if (numOfGeographies > 1) + { + sink.BeginGeography(OpenGisGeographyType.GeometryCollection); + + foreach (var g in _parser.Geographies) + { + g.Populate(sink, makeValid); + } + + sink.EndGeography(); + } + else + { + // Geography instance is not found. The empty geography collection will be generated. + sink.BeginGeography(OpenGisGeographyType.GeometryCollection); + sink.EndGeography(); + } + } + + #endregion + } +} diff --git a/src/KMLProcessor/Import/LatLonAltBox.cs b/src/KMLProcessor/Import/LatLonAltBox.cs new file mode 100644 index 0000000..50de90a --- /dev/null +++ b/src/KMLProcessor/Import/LatLonAltBox.cs @@ -0,0 +1,84 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// This class contains the information about the LatLonAltBox element extracted from KML + /// + public class LatLonAltBox : LatLonBoxBase + { + #region Public Data + + /// + /// The minimal altitude + /// + public double MinAltitude + { + get => _minAltitude; + set + { + if (value < 0) + throw new KMLException("Altitude has to be greater than 0"); + + _minAltitude = value; + + if (_maxAltitude < _minAltitude) + _maxAltitude = _minAltitude; + } + } + /// + /// Data member for the MinAltitude property + /// + private double _minAltitude; + + /// + /// The maximal altitude + /// + public double MaxAltitude + { + get => _maxAltitude; + set + { + if (value < 0) + throw new KMLException("Altitude has to be greater than 0"); + + _maxAltitude = value; + if (_minAltitude > _maxAltitude) + _minAltitude = _maxAltitude; + } + } + /// + /// Data member for the property MaxAltitude + /// + private double _maxAltitude; + + /// + /// Altitude mode + /// + public AltitudeMode? AltitudeMode { get; set; } + + #endregion + + #region Geography Methods + + /// + /// This method populates the given sink with the data from this geography instance + /// + /// Sink to be populated + public override void Populate(Microsoft.SqlServer.Types.IGeographySink110 sink) + { + // Initializes the altitude to the maximal value + _maxAltitude = MaxAltitude; + + // Converts and stores the altitude mode to the spatial m coordinate + if (AltitudeMode != null) + Measure = (int)AltitudeMode.Value; + + base.Populate(sink); + } + + #endregion + } +} diff --git a/src/KMLProcessor/Import/LatLonBox.cs b/src/KMLProcessor/Import/LatLonBox.cs new file mode 100644 index 0000000..e66879d --- /dev/null +++ b/src/KMLProcessor/Import/LatLonBox.cs @@ -0,0 +1,21 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// This class holds the information about the LatLonBox instance extracted from the KML file + /// + public class LatLonBox : LatLonBoxBase + { + #region Public Data + + /// + /// The rotation angle. + /// + public double Rotation { get; set; } + + #endregion + } +} diff --git a/src/KMLProcessor/Import/LatLonBoxBase.cs b/src/KMLProcessor/Import/LatLonBoxBase.cs new file mode 100644 index 0000000..c1179a1 --- /dev/null +++ b/src/KMLProcessor/Import/LatLonBoxBase.cs @@ -0,0 +1,154 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// Base class for the all KML boxes. All KML boxes will be converted to polygon instances. + /// + public class LatLonBoxBase : Geography + { + #region Public data + + /// + /// The north side latitude + /// + public double North { private get; set; } + + /// + /// The south side latitude + /// + public double South { private get; set; } + + /// + /// The east side longitude + /// + public double East { private get; set; } + + /// + /// The west side longitude + /// + public double West { private get; set; } + + // ReSharper disable UnusedAutoPropertyAccessor.Global + /// + /// Altitude + /// + public double? Altitude { private get; set; } + + /// + /// Measure + /// + public double? Measure { private get; set; } + // ReSharper restore UnusedAutoPropertyAccessor.Global + + /// + /// SqlGeography instance well-known text. + /// + public override string WKT + { + get + { + CheckCoordinates(); + + var wkt = "polygon(("; + + // (west, south) + wkt += new Point(West, South, Altitude, Measure).Coordinate; + + // (east, south) + wkt += "," + new Point(East, South, Altitude, Measure).Coordinate; + + // (east, north) + wkt += "," + new Point(East, North, Altitude, Measure).Coordinate; + + // (west, north) + wkt += "," + new Point(West, North, Altitude, Measure).Coordinate; + + // (west, south) + wkt += "," + new Point(West, South, Altitude, Measure).Coordinate; + + wkt += "))"; + return wkt; + } + } + + #endregion + + #region Geography Methods + + /// + /// This method populates the given sink with the data from this geography instance + /// + /// Sink to be populated + public override void Populate(IGeographySink110 sink) + { + CheckCoordinates(); + + // The coordinates for this geography instance will be: + // (west, south), (east, south), (east, north), (west, north), (west, south) + sink.BeginGeography(OpenGisGeographyType.Polygon); + + // (west, south) + sink.BeginFigure(South, West, Altitude, Measure); + + // (east, south) + sink.AddLine(South, East, Altitude, Measure); + + // (east, north) + sink.AddLine(North, East, Altitude, Measure); + + // (west, north) + sink.AddLine(North, West, Altitude, Measure); + + // (west, south) + sink.AddLine(South, West, Altitude, Measure); + + sink.EndFigure(); + + sink.EndGeography(); + } + + #endregion + + #region Protected Methods + + /// + /// This method checks if the south latitude is less then the north latitude. + /// If not then the south and the north will be swapped. + /// This method also checks if the west longitude if less then the east longitude. + /// If not then the east and the west will be swapped. + /// + protected void CheckCoordinates() + { + // Checks if the South and the North latitude are in the range (-90, 90) + South = Utilities.ShiftInRange(South, 90); + North = Utilities.ShiftInRange(North, 90); + + // Checks if the West and the East longitude are in the range (-180, 180) + West = Utilities.ShiftInRange(West, 180); + East = Utilities.ShiftInRange(East, 180); + + // Ensures that the south is less then the north + if (South > North) + { + var tmp = South; + South = North; + North = tmp; + } + + // Ensures that the west is less then the east + if (West > East) + { + var tmp = West; + West = East; + East = tmp; + } + } + + #endregion + } +} diff --git a/src/KMLProcessor/Import/LatLonQuad.cs b/src/KMLProcessor/Import/LatLonQuad.cs new file mode 100644 index 0000000..f4f7fca --- /dev/null +++ b/src/KMLProcessor/Import/LatLonQuad.cs @@ -0,0 +1,79 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// This class holds the information about a LatLonQuad instance extracted from the KML file + /// + public class LatLonQuad : Geography + { + #region Public Data + + /// + /// The quad vertices + /// + public List Points { get; } = new List(); + + /// + /// SqlGeography instance well-known text. + /// + public override string WKT + { + get + { + if (Points == null || Points.Count == 0) + throw new KMLException("WKT is not defined."); + + var wkt = "polygon(("; + + var first = true; + foreach (var p in Points) + { + if (first) + first = false; + else + wkt += ", "; + + wkt += p.WKT; + } + + wkt += "))"; + return wkt; + } + } + + #endregion + + #region Geography methods + + /// + /// This method populates the given sink with the data from this geography instance + /// + /// Sink to be populated + public override void Populate(IGeographySink110 sink) + { + if (Points == null || Points.Count == 0) + return; + + sink.BeginGeography(OpenGisGeographyType.Polygon); + + sink.BeginFigure(Points[0].Latitude, Points[0].Longitude, Points[0].Altitude, Points[0].Measure); + + for (var i = 1; i < Points.Count; i++) + { + sink.AddLine(Points[i].Latitude, Points[i].Longitude, Points[i].Altitude, Points[i].Measure); + } + + sink.EndFigure(); + + sink.EndGeography(); + } + + #endregion + } +} diff --git a/src/KMLProcessor/Import/LineString.cs b/src/KMLProcessor/Import/LineString.cs new file mode 100644 index 0000000..8b115bd --- /dev/null +++ b/src/KMLProcessor/Import/LineString.cs @@ -0,0 +1,116 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// This class contains the information about a line string extracted from the KML file + /// + public class LineString : Geography + { + #region Private Fields + + /// + /// OpenGisGeographyType for this geography instance + /// + protected OpenGisGeographyType GeographyType = OpenGisGeographyType.LineString; + + #endregion + + #region Public Properties + + /// + /// The list of points + /// + public List Points { get; } = new List(); + + /// + /// SqlGeography instance well-known text. + /// + public override string WKT => "LINESTRING" + Vertices; + + /// + /// Returns the vertices of this linear ring. The returned string will be in the following format: + /// (V0, V1, V2,..., Vn, V0). Vi will be in the format: longitude latitude [altitude [measure]] + /// + public string Vertices + { + get + { + var result = "("; + var isFirst = true; + foreach (var p in Points) + { + if (!isFirst) + result += ", "; + else + isFirst = false; + + result += p.Longitude + " " + p.Latitude; + if (p.Altitude.HasValue) + { + result += " " + p.Altitude.Value; + + if (p.Measure.HasValue) + result += " " + p.Measure.Value; + } + } + result += ")"; + + return result; + } + } + + #endregion + + #region Geography Methods + + /// + /// This method populates the given sink with the data from this geography instance + /// + /// Sink to be populated + public override void Populate(IGeographySink110 sink) + { + if (Points == null || Points.Count == 0) + return; + + sink.BeginGeography(GeographyType); + sink.BeginFigure(Points[0].Latitude, Points[0].Longitude, Points[0].Altitude, Points[0].Measure); + + for (var i = 1; i < Points.Count; i++) + { + sink.AddLine(Points[i].Latitude, Points[i].Longitude, Points[i].Altitude, Points[i].Measure); + } + + sink.EndFigure(); + sink.EndGeography(); + } + + #endregion + + #region Public Methods + + /// + /// This method stores the tessellate flag. M coordinates of all the point will be set + /// to the "clamp to ground" value. + /// + public void StoreTessellateFlag() + { + if (!Tessellate) return; + + foreach (var p in Points) + { + if (!p.Measure.HasValue) + { + p.Measure = (int)AltitudeMode.ClampToGround; + } + } + } + + #endregion + } +} diff --git a/src/KMLProcessor/Import/LinearRing.cs b/src/KMLProcessor/Import/LinearRing.cs new file mode 100644 index 0000000..25bd92b --- /dev/null +++ b/src/KMLProcessor/Import/LinearRing.cs @@ -0,0 +1,57 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// This class contains the data about a linear ring extracted from the KML file + /// + public class LinearRing : LineString + { + #region Constructors + + /// + /// Default constructor + /// + public LinearRing() + { + GeographyType = OpenGisGeographyType.Polygon; + } + + #endregion + + #region Public Methods + + /// + /// This method switches the ring's orientation + /// + public void SwitchOrientation() + { + var i = 1; + var j = Points.Count - 2; // index of the element before last + while (i < j) + { + var tmp = Points[i]; + Points[i] = Points[j]; + Points[j] = tmp; + + i++; + j--; + } + } + + #endregion + + #region Public Properties + + /// + /// SqlGeography instance well-known text. + /// + public override string WKT => "POLYGON(" + Vertices + ")"; + + #endregion + } +} diff --git a/src/KMLProcessor/Import/Model.cs b/src/KMLProcessor/Import/Model.cs new file mode 100644 index 0000000..7ba299d --- /dev/null +++ b/src/KMLProcessor/Import/Model.cs @@ -0,0 +1,79 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +// ReSharper disable UnusedAutoPropertyAccessor.Global +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// This class holds the information about a KML Model element + /// + public class Model : Geography + { + #region Public Data + + /// + /// The location of the model + /// + public Point Location { private get; set; } + + /// + /// The orientation heading + /// + public double OrientationHeading { get; set; } + + /// + /// The orientation tilt + /// + public double OrientationTilt { get; set; } + + /// + /// The orientation roll + /// + public double OrientationRoll { get; set; } + + /// + /// The x-axis scale factor + /// + public double ScaleX { get; set; } + + /// + /// The y-axis scale factor + /// + public double ScaleY { get; set; } + + /// + /// The z-axis scale factor + /// + public double ScaleZ { get; set; } + + /// + /// SqlGeography instance well-known text. + /// + public override string WKT + { + get + { + if (Location != null) + return Location.WKT; + + throw new KMLException("WKT is not defined."); + } + } + + #endregion + + #region Geography Methods + + /// + /// This method populates the given sink with the data from this geography instance + /// + /// Sink to be populated + public override void Populate(Microsoft.SqlServer.Types.IGeographySink110 sink) + { + Location?.Populate(sink); + } + + #endregion + } +} diff --git a/src/KMLProcessor/Import/MultiGeometry.cs b/src/KMLProcessor/Import/MultiGeometry.cs new file mode 100644 index 0000000..96d3411 --- /dev/null +++ b/src/KMLProcessor/Import/MultiGeometry.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// This class holds the information about a collection of the KML geographies + /// + public class MultiGeometry : Geography + { + #region Public Properties + + /// + /// Geographies contained in this geography instance + /// + public IList Geographies { get; set; } = new List(); + + /// + /// SqlGeography instance well-known text. + /// + public override string WKT + { + get + { + var wkt = "GEOMETRYCOLLECTION("; + + var firstGeography = true; + foreach (var g in Geographies) + { + if (firstGeography) + firstGeography = false; + else + wkt += ", "; + + wkt += g.WKT; + } + + wkt += ")"; + return wkt; + } + } + + #endregion + + #region Geography Methods + + /// + /// This method populates the given sink with the information about this multi geometry instance + /// + /// Sink to be populated + public override void Populate(IGeographySink110 sink) + { + sink.BeginGeography(OpenGisGeographyType.GeometryCollection); + + if (Geographies != null && Geographies.Count > 0) + { + foreach (var geography in Geographies) + { + geography.Populate(sink); + } + } + + sink.EndGeography(); + } + + #endregion + } +} diff --git a/src/KMLProcessor/Import/Placemark.cs b/src/KMLProcessor/Import/Placemark.cs new file mode 100644 index 0000000..52970bd --- /dev/null +++ b/src/KMLProcessor/Import/Placemark.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +// ReSharper disable UnusedAutoPropertyAccessor.Global +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// This class contains the information about a placemark extracted from the KML file + /// + public class Placemark + { + #region Public Properties + + /// + /// Identifier + /// + public string Id { get; set; } + + /// + /// Name + /// + public string Name + { + get => _mName; + set => _mName = string.IsNullOrEmpty(value) ? "" : value; + } + /// + /// Data member for the property Name + /// + private string _mName = ""; + + /// + /// Description + /// + public string Description + { + get => _mDescription; + set => _mDescription = string.IsNullOrEmpty(value) ? "" : value; + } + /// + /// Data member for the Description property + /// + private string _mDescription = ""; + + /// + /// Address + /// + public string Address + { + get => _mAddress; + set => _mAddress = string.IsNullOrEmpty(value) ? "" : value; + } + /// + /// Data member for the Address property + /// + private string _mAddress = ""; + + /// + /// The geography instance which describes this placemark + /// + public Geography Geography { get; set; } + + /// + /// The geography instance where this placemark looks at + /// + public Geography LookAt { get; set; } + + #endregion + } +} diff --git a/src/KMLProcessor/Import/Point.cs b/src/KMLProcessor/Import/Point.cs new file mode 100644 index 0000000..642130e --- /dev/null +++ b/src/KMLProcessor/Import/Point.cs @@ -0,0 +1,163 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.KMLProcessor.Import +{ +#pragma warning disable CS0659 + // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() + /// + /// This class holds the information about a Point extracted from the KML file + /// + public class Point : Geography +#pragma warning restore CS0659 + { + #region Constructors + + /// + /// Default constructor + /// + public Point() : this(0, 0) + { + } + + /// + /// Constructor. Constructs the point using the given longitude, latitude, altitude and measure. + /// + /// Longitude + /// Latitude + /// Altitude + /// Measure + public Point(double longitude, double latitude, double? altitude = null, double? measure = null) + { + Longitude = longitude; + Latitude = latitude; + Altitude = altitude; + Measure = measure; + } + + #endregion + + #region Public Properties + + /// + /// Longitude. + /// + public double Longitude { get; set; } + + /// + /// Latitude. + /// + public double Latitude { get; set; } + + /// + /// Altitude. + /// + public double? Altitude { get; set; } + + /// + /// Measure + /// + public double? Measure { get; set; } + + /// + /// SqlGeography instance well-known text. + /// + public override string WKT + { + get + { + var wkt = $"POINT({Coordinate})"; + + return wkt; + } + } + + /// + /// Point's coordinate in the format: longitude, latitude [, altitude] + /// + public string Coordinate + { + get + { + var wkt = $"{Longitude} {Latitude}"; + + if (Altitude.HasValue) + wkt += " " + Altitude.Value; + + if (Measure.HasValue) + wkt += " " + Measure.Value; + + return wkt; + } + } + + #endregion + + #region Geography methods + + /// + /// This method populates the given sink with the data about this geography instance + /// + /// Sink to be populated + public override void Populate(IGeographySink110 sink) + { + sink.BeginGeography(OpenGisGeographyType.Point); + sink.BeginFigure(Latitude, Longitude, Altitude, Measure); + + sink.EndFigure(); + sink.EndGeography(); + } + + #endregion + + #region Public Methods + + /// + /// This method clones this point + /// + /// Clone of this point + public Point Clone() + { + return new Point + { + Longitude = Longitude, + Latitude = Latitude, + Altitude = Altitude, + Measure = Measure + }; + } + + #endregion + + #region Overridden Base Methods + + /// + /// This method compares the given object with this point + /// + /// Object to be compared with this point + /// +#pragma warning disable 659 + public override bool Equals(object obj) +#pragma warning restore 659 + { + if (this == obj) + return true; + + if (!(obj is Point rhs)) + return false; + + return Longitude.EqualsTo(rhs.Longitude) && + Latitude.EqualsTo(rhs.Latitude) && + // ReSharper disable PossibleInvalidOperationException + (Altitude == null && rhs.Altitude == null || Altitude.Value.EqualsTo(rhs.Altitude.Value)) && + (Measure == null && rhs.Measure == null || (Measure.Value.EqualsTo(rhs.Measure.Value))); + // ReSharper restore PossibleInvalidOperationException + } + + #endregion + } +} diff --git a/src/KMLProcessor/Import/Polygon.cs b/src/KMLProcessor/Import/Polygon.cs new file mode 100644 index 0000000..ce44db9 --- /dev/null +++ b/src/KMLProcessor/Import/Polygon.cs @@ -0,0 +1,119 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// This class contains the information about a polygon extracted from the KML file + /// + public class Polygon : Geography + { + #region Public Properties + + /// + /// Outer border + /// + public LinearRing OuterRing { get; set; } + + /// + /// Inner borders/rings + /// + public IList InnerRing { get; } = new List(); + + /// + /// SqlGeography instance well-known text. + /// + public override string WKT + { + get + { + var wkt = "POLYGON("; + + if (OuterRing != null) + wkt += OuterRing.Vertices; + else + throw new Exception("Outer ring is not set."); + + foreach (var linearRing in InnerRing) + { + wkt += ", " + linearRing.Vertices; + } + + wkt += ")"; + + return wkt; + } + } + + #endregion + + #region Geography Methods + + /// + /// This method populates the given sink with the data from this geography instance + /// + /// Sink to be populated + public override void Populate(IGeographySink110 sink) + { + if (OuterRing?.Points == null || OuterRing.Points.Count == 0) + return; + + sink.BeginGeography(OpenGisGeographyType.Polygon); + + // Populates the outer boundary + sink.BeginFigure( + OuterRing.Points[0].Latitude, + OuterRing.Points[0].Longitude, + OuterRing.Points[0].Altitude, + OuterRing.Points[0].Measure); + + for (var i = 1; i < OuterRing.Points.Count; i++) + { + sink.AddLine( + OuterRing.Points[i].Latitude, + OuterRing.Points[i].Longitude, + OuterRing.Points[i].Altitude, + OuterRing.Points[i].Measure); + } + + sink.EndFigure(); + + if (InnerRing != null && InnerRing.Count > 0) + { + // Populates the inner boundaries + + foreach (var linearRing in InnerRing) + { + if (linearRing.Points == null || linearRing.Points.Count == 0) + continue; + + sink.BeginFigure( + linearRing.Points[0].Latitude, + linearRing.Points[0].Longitude, + linearRing.Points[0].Altitude, + linearRing.Points[0].Measure); + + for (var i = 1; i < linearRing.Points.Count; i++) + { + sink.AddLine( + linearRing.Points[i].Latitude, + linearRing.Points[i].Longitude, + linearRing.Points[i].Altitude, + linearRing.Points[i].Measure); + } + + sink.EndFigure(); + } + } + + sink.EndGeography(); + } + + #endregion + } +} diff --git a/src/KMLProcessor/Import/Region.cs b/src/KMLProcessor/Import/Region.cs new file mode 100644 index 0000000..8c9b936 --- /dev/null +++ b/src/KMLProcessor/Import/Region.cs @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +namespace SQLSpatialTools.KMLProcessor.Import +{ + /// + /// This class holds the information about a Region extracted from the KML file + /// + public class Region : Geography + { + #region Public Data + + /// + /// Box which defines the border of this region + /// + public Geography Box { private get; set; } + + /// + /// SqlGeography instance well-known text. + /// + public override string WKT + { + get + { + if (Box != null) + return Box.WKT; + + throw new KMLException("WKT is not defined."); + } + } + + #endregion + + #region Geography Methods + + /// + /// This method populates the given sink with the data from this geography instance + /// + /// Sink to be populated + public override void Populate(Microsoft.SqlServer.Types.IGeographySink110 sink) + { + Box?.Populate(sink); + } + + #endregion + } +} diff --git a/KMLProcessor/KMLException.cs b/src/KMLProcessor/KMLException.cs similarity index 69% rename from KMLProcessor/KMLException.cs rename to src/KMLProcessor/KMLException.cs index 6dbfcd2..6d308b5 100644 --- a/KMLProcessor/KMLException.cs +++ b/src/KMLProcessor/KMLException.cs @@ -1,36 +1,37 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Microsoft.SqlServer.SpatialToolbox.KMLProcessor -{ - /// - /// Instance of this class will be thrown for every exception which occures in the KML processor. - /// - public class KMLException : Exception - { - #region Constructors - - /// - /// Default constructor - /// - /// Exception message - public KMLException(string message) - : base(message) - { - } - - /// - /// Constructor. Constructs the KMLException using the given exception message and the inner exception - /// - /// Exception message - /// Inner exception - public KMLException(string message, Exception innerException) - : base(message, innerException) - { - } - - #endregion - } -} +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; + +namespace SQLSpatialTools.KMLProcessor +{ + /// + /// Instance of this class will be thrown for every exception which occurs in the KML processor. + /// + public class KMLException : Exception + { + #region Constructors + + /// + /// Default constructor + /// + /// Exception message + public KMLException(string message) + : base(message) + { + } + + /// + /// Constructor. Constructs the KMLException using the given exception message and the inner exception + /// + /// Exception message + /// Inner exception + public KMLException(string message, Exception innerException) + : base(message, innerException) + { + } + + #endregion + } +} diff --git a/src/KMLProcessor/KmlFunctions.cs b/src/KMLProcessor/KmlFunctions.cs new file mode 100644 index 0000000..e1f4d55 --- /dev/null +++ b/src/KMLProcessor/KmlFunctions.cs @@ -0,0 +1,287 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Collections; +using System.Collections.ObjectModel; +using System.Data; +using System.Data.SqlClient; +using System.Data.SqlTypes; +using System.IO; +using System.Xml; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.KMLProcessor.Export; +using SQLSpatialTools.KMLProcessor.Import; +// ReSharper disable UnusedMember.Global +// ReSharper disable MemberCanBePrivate.Global + +namespace SQLSpatialTools.KMLProcessor +{ + /// + /// This class contains the KML import/export functions + /// + public class KMLFunctions + { + #region Public Methods + + #region Import Methods + + /// + /// This function parses the given KML string and returns the extracted geography instance. + /// + /// KML string to be parsed + /// If true and the extracted geography instance is invalid then the MakeValid + /// function will be executed on the extracted geography instance + /// Extracted geography instance + [Microsoft.SqlServer.Server.SqlFunction] + public static SqlGeography KmlToGeography( + SqlString kml, + SqlBoolean makeValid) + { + if (kml == null || string.IsNullOrEmpty(kml.Value)) + { + return SqlGeography.Null; + } + + var constructed = new SqlGeographyBuilder(); + var parser = new Import.KMLProcessor(kml.Value.Trim()); + parser.Populate(constructed, makeValid.Value); + return constructed.ConstructedGeography; + } + + #endregion + + #region TVF Import Functions + + /// + /// This TVF function parses the given KML string. All geography instances will be extracted and + /// returned along with the context where they were found. + /// One record will be returned for each extracted geography instance. + /// + /// The return table format is: (Id NVARCHAR(200), Context NVARCHAR(MAX), Shape Geography) + /// + /// KML string which should be processed + /// If true and extracted geography instance is invalid then the MakeValid + /// function will be executed on that geography instance + [Microsoft.SqlServer.Server.SqlFunction( + FillRowMethodName = "DecodeGeographyContext", + TableDefinition = "Id NVARCHAR(200), Context NVARCHAR(MAX), Shape Geography" + )] + public static IEnumerable ImportKml( + SqlString kml, + SqlBoolean makeValid) + { + if (kml == null || string.IsNullOrEmpty(kml.Value)) + return new Collection(); + + var geographies = new Collection(); + + var parser = + new Import.KMLProcessor(kml.Value.Trim()); + + foreach (var p in parser.Geographies) + { + var ge = new GeographyContext + { + Id = p.Id, Context = p.Context, Shape = p.ToSqlGeography(makeValid.Value) + }; + + geographies.Add(ge); + } + + return geographies; + } + + /// + /// This method is a helper method for the TVF function ImportKml. A Sql Server uses it to decode a row + /// returned by the ImportKml function. + /// + /// A row returned by the ImportKml function + /// Id extracted from a rowData + /// Context extracted from a rowData + /// Shape extracted from a rowData + public static void DecodeGeographyContext( + object rowData, + out SqlString id, + out SqlString context, + out SqlGeography shape) + { + id = new SqlString(); + context = new SqlString(); + shape = null; + + if (!(rowData is GeographyContext data)) return; + + id = data.Id; + context = data.Context; + shape = data.Shape; + + } + + #endregion + + #region Export Methods + + /// + /// This method converts the given geography instance into the KML format + /// + /// Geography instance to be converted + /// KML representation of the given geography instance + [Microsoft.SqlServer.Server.SqlFunction] + public static SqlString ExportToKml(SqlGeography g) + { + if (g == null || g.IsNull) + return new SqlString(""); + + var stream = new MemoryStream(); + var writer = XmlWriter.Create(stream); + + var sink = new KeyholeMarkupLanguageGeography(writer); + + g.Populate(sink); + + sink.FinalizeKMLDocument(); + + writer.Flush(); + stream.Seek(0, SeekOrigin.Begin); + var ba = stream.ToArray(); + var sData = System.Text.Encoding.UTF8.GetChars(ba); + var str = new string(sData); + writer.Close(); + + // Removes everything before + var xmlIndex = str.IndexOf("", StringComparison.Ordinal); + if (xmlIndex > 0) + str = str.Remove(0, xmlIndex); + + return new SqlString(str); + } + + /// + /// This method converts the given geography instance represented as a WKT string, into the string in the KML format. + /// + /// Geography instance represented as the WKT string + /// The KML string + [Microsoft.SqlServer.Server.SqlFunction] + public static SqlString ExportWKTToKml(SqlString wkt) + { + if (wkt == null || string.IsNullOrEmpty(wkt.Value)) + return new SqlString(""); + + var g = SqlGeography.Parse(wkt); + return ExportToKml(g); + } + + #endregion + + #endregion + + #region Protected Methods + + /// + /// This method stores the given box into the database + /// + /// Box to be saved + /// The KmlId to connect this object to + /// Sql connection + /// Sql transaction + /// The database id which is assigned to the given box + protected static int? SaveLatLonBox(Geography box, int insertedKmlId, SqlConnection con, SqlTransaction tran) + { + #region Insert Geography + + int? insertedGeographyId = null; + + if (box != null) + { + var saveGeographyCmd = + new SqlCommand( + " insert into Geographies(Geography, KmlId) " + + " values(geography::Parse('" + box + "'), @KmlId);select scope_identity()", con, tran); + + AddParameter(saveGeographyCmd, "KmlId", insertedKmlId, DbType.Int32); + + insertedGeographyId = (int)((decimal)saveGeographyCmd.ExecuteScalar()); + box.DbId = insertedGeographyId; + } + + #endregion + + #region Insert Lat Lon Box + + var saveLatLonBox = + new SqlCommand( + " insert into LatLonBoxes(BoxType, GeographyId, rotation, MinAlt, MaxAlt, AltitudeMode) " + + " values(@BoxType, @GeographyId, @rotation, @MinAlt, @MaxAlt, @AltitudeMode);select scope_identity()", + con, tran); + + AddParameter(saveLatLonBox, "GeographyId", insertedGeographyId, DbType.Int32); + + var boxType = ""; + + if (box is LatLonBox latLonBox) + { + boxType = "LatLonBox"; + AddParameter(saveLatLonBox, "rotation", latLonBox.Rotation, DbType.Double); + } + else + { + AddParameter(saveLatLonBox, "rotation", null, DbType.Double); + } + + if (box is LatLonAltBox latLonAltBox) + { + boxType = "LatLonAltBox"; + + AddParameter(saveLatLonBox, "MinAlt", latLonAltBox.MinAltitude, DbType.Double); + AddParameter(saveLatLonBox, "MaxAlt", latLonAltBox.MaxAltitude, DbType.Double); + AddParameter(saveLatLonBox, "AltitudeMode", latLonAltBox.AltitudeMode.ToString(), DbType.String); + } + else + { + AddParameter(saveLatLonBox, "MinAlt", null, DbType.Double); + AddParameter(saveLatLonBox, "MaxAlt", null, DbType.Double); + AddParameter(saveLatLonBox, "AltitudeMode", null, DbType.Double); + } + + // ReSharper disable once UnusedVariable + if (box is LatLonQuad latLonQuad) + { + boxType = "LatLonQuad"; + } + + AddParameter(saveLatLonBox, "BoxType", boxType, DbType.String); + + int? insertedLatLonBox = (int)((decimal)saveLatLonBox.ExecuteScalar()); + + #endregion + + return insertedLatLonBox; + } + + /// + /// This method adds a new parameter to the given sql command + /// + /// Command to add parameter to + /// Parameter name + /// Parameter value + /// Parameter database type + protected static void AddParameter(SqlCommand cmd, string parameterName, object value, DbType type) + { + if (value == null) + value = DBNull.Value; + + cmd.Parameters.Add(new SqlParameter() + { + ParameterName = parameterName, + Value = value, + DbType = type + }); + } + + #endregion + } +} + + diff --git a/src/KMLProcessor/Utilities.cs b/src/KMLProcessor/Utilities.cs new file mode 100644 index 0000000..d6ed348 --- /dev/null +++ b/src/KMLProcessor/Utilities.cs @@ -0,0 +1,44 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.KMLProcessor +{ + /// + /// This class contains the utility methods + /// + public static class Utilities + { + /// + /// This method shifts the given value into the range [-upperBorder, upperBorder] + /// + /// The value to be shifted + /// The range's upper bound + /// The shifted value + public static double ShiftInRange(double value, double upperBorder) + { + upperBorder = Math.Abs(upperBorder); + + if (upperBorder.EqualsTo(0)) + throw new KMLException("The range has to be greater then 0."); + + var rangeSize = 2 * upperBorder; + + if (value > upperBorder) + { + var k = (int)((value + upperBorder) / rangeSize); + value -= k * rangeSize; + } + else if (value < -1 * upperBorder) + { + var k = (int)((value - upperBorder) / rangeSize); + value -= k * rangeSize; + } + + return value; + } + } +} diff --git a/src/Projections/AlbersEqualAreaProjection.cs b/src/Projections/AlbersEqualAreaProjection.cs new file mode 100644 index 0000000..f25363b --- /dev/null +++ b/src/Projections/AlbersEqualAreaProjection.cs @@ -0,0 +1,85 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// References: http://mathworld.wolfram.com/AlbersEqual-AreaConicProjection.html +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; + +namespace SQLSpatialTools.Projections +{ + // EPSG Code: 9822 + // OGC WKT Name: Albers_Conic_Equal_Area + internal sealed class AlbersEqualAreaProjection : Projection + { + // longitude0, latitude0, parallel1, parallel2 + public AlbersEqualAreaProjection(IDictionary parameters) + : base(parameters) + { + var latitude0Rad = InputLatitude("latitude0"); + var parallel1Rad = InputLatitude("parallel1"); + var parallel2Rad = InputLatitude("parallel2"); + + var cosParallel1 = Math.Cos(parallel1Rad); + var sinParallel1 = Math.Sin(parallel1Rad); + + _n2 = sinParallel1 + Math.Sin(parallel2Rad); + if (Math.Abs(_n2) <= MathX.Tolerance) + { + throw new ArgumentOutOfRangeException(); + } + + _n = _n2 / 2; + _nHalf = _n2 / 4; + var invN2 = 1 / _n2; + _invN = 2 / _n2; + + _c = MathX.Square(cosParallel1) + sinParallel1 * _n2; + _cOverN2 = _c * invN2; + + var a = _c - Math.Sin(latitude0Rad) * _n2; + if (a < 0) + { + throw new ArgumentOutOfRangeException(); + } + _ro0 = Math.Sqrt(a) * _invN; + } + + protected internal override void Project(double latitude, double longitude, out double x, out double y) + { + var a = _c - Math.Sin(latitude) * _n2; + if (a < 0) + { + throw new ArgumentOutOfRangeException(nameof(latitude)); + } + + var ro = Math.Sqrt(a) * _invN; + + var theta = longitude * _n; + x = ro * Math.Sin(theta); + y = _ro0 - ro * Math.Cos(theta); + } + + protected internal override void Unproject(double x, double y, out double latitude, out double longitude) + { + var ros = x * x + (_ro0 - y) * (_ro0 - y); + var a = _cOverN2 - ros * _nHalf; + if (double.IsNaN(a) || Math.Abs(a) > 1 + MathX.Tolerance) + { + throw new ArgumentOutOfRangeException(paramName:string.Join(",", new []{nameof(x),nameof(y)})); + } + + latitude = Math.Asin(MathX.Clamp(1, a)); + longitude = MathX.Clamp(Math.PI, MathX.Atan2(x, _ro0 - y, "x, y") * _invN); + } + + private readonly double _c; + private readonly double _cOverN2; + private readonly double _ro0; + private readonly double _n2; + private readonly double _n; + private readonly double _nHalf; + private readonly double _invN; + } +} \ No newline at end of file diff --git a/Projections/EquirectangularProjection.cs b/src/Projections/EquirectangularProjection.cs similarity index 80% rename from Projections/EquirectangularProjection.cs rename to src/Projections/EquirectangularProjection.cs index 54a3fd1..988968b 100644 --- a/Projections/EquirectangularProjection.cs +++ b/src/Projections/EquirectangularProjection.cs @@ -1,46 +1,47 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2008 Microsoft Corporation. -// -// References: http://en.wikipedia.org/wiki/Equirectangular_projection -//------------------------------------------------------------------------------ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; - -namespace SQLSpatialTools -{ - internal sealed class EquirectangularProjection : Projection - { - // Angles are in degrees. - // longitude0: - // parallel: standard parallels (north and south of the equator) where the scale of the projection is true - // - public EquirectangularProjection(Dictionary parameters) - : base(parameters) - { - double parallel_rad = InputLatitude("parallel"); - - _scale = Math.Cos(parallel_rad); - - // scale > 0 because |parallel| <= 89.9 - Debug.Assert(_scale > 0, _scale.ToString(CultureInfo.InvariantCulture)); - _scaleInv = 1 / _scale; - } - - protected internal override void Project(double latitude, double longitude, out double x, out double y) - { - x = longitude * _scale; - y = latitude; - } - - protected internal override void Unproject(double x, double y, out double latitude, out double longitude) - { - longitude = x * _scaleInv; - latitude = y; - } - - private readonly double _scale; - private readonly double _scaleInv; // = 1 / scale - } +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// References: http://en.wikipedia.org/wiki/Equirectangular_projection +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +namespace SQLSpatialTools.Projections +{ + internal sealed class EquirectangularProjection : Projection + { + // Angles are in degrees. + // longitude0: + // parallel: standard parallels (north and south of the equator) where the scale of the projection is true + // + public EquirectangularProjection(IDictionary parameters) + : base(parameters) + { + var parallelRadian = InputLatitude("parallel"); + + _scale = Math.Cos(parallelRadian); + + // scale > 0 because |parallel| <= 89.9 + Debug.Assert(_scale > 0, _scale.ToString(CultureInfo.InvariantCulture)); + _scaleInv = 1 / _scale; + } + + protected internal override void Project(double latitude, double longitude, out double x, out double y) + { + x = longitude * _scale; + y = latitude; + } + + protected internal override void Unproject(double x, double y, out double latitude, out double longitude) + { + longitude = x * _scaleInv; + latitude = y; + } + + private readonly double _scale; + private readonly double _scaleInv; // = 1 / scale + } } \ No newline at end of file diff --git a/Projections/GnomonicProjection.cs b/src/Projections/GnomonicProjection.cs similarity index 62% rename from Projections/GnomonicProjection.cs rename to src/Projections/GnomonicProjection.cs index ef08834..eeb5235 100644 --- a/Projections/GnomonicProjection.cs +++ b/src/Projections/GnomonicProjection.cs @@ -1,87 +1,87 @@ -//------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. -// -// References: http://mathworld.wolfram.com/GnomonicProjection.html -// -// Note: The gnomonic projction is the only projection that maps SqlGeography -// Polygons and LineString exactly to their SqlGeometry counterparts. -// -//------------------------------------------------------------------------------ -using System; -using System.Diagnostics; -using System.Globalization; -using System.Collections.Generic; - -namespace SQLSpatialTools -{ - // EPSG Code: None - // OGC WKT Name: Gnomonic - internal sealed class GnommonicProjection : Projection - { - // Input argument: the center of the projection - public GnommonicProjection(Dictionary parameters) - : base(parameters) - { - _center = Util.SphericalRadToCartesian(InputLatitude("latitude1", 90), InputLongitude("longitude1", 360)); - - // This projection is designed for numerical computations rather than cartography. - // The choice of coordinate basis for the tangent plane - which affects the - // orientation of the projection in the xy plane - is optimized for accuracy rather - // than good looks. The first basis vector is obtained by dropping the smallest coordinate, - // switching the other two, and flipping the sign of one of them. The second one is - // obtained by cross product. - - double[] center = { _center.x, _center.y, _center.z }; - double[] vector = new double[3]; - - int k = GetMinEntry(center); - int j = (k + 2) % 3; - int i = (j + 2) % 3; - - vector[i] = -center[j]; - vector[j] = center[i]; - vector[k] = 0; - - _xAxis = new Vector3(vector[0], vector[1], vector[2]).Unitize(); - - _yAxis = _center.CrossProduct(_xAxis); - } - - private static int GetMinEntry(double[] values) - { - int i = 0; - if (Math.Abs(values[1]) < Math.Abs(values[0])) - i = 1; - if (Math.Abs(values[2]) < Math.Abs(values[i])) - i = 2; - return i; - } - - protected internal override void Project(double latitude, double longitude, out double x, out double y) - { - Vector3 vector = Util.SphericalRadToCartesian(latitude, longitude); - double r = vector * _center; - - if (r < _tolerance) - { - throw new ArgumentOutOfRangeException("Input point is too far away from the center of projection."); - } - vector = vector / r; - - x = vector * _xAxis; - y = vector * _yAxis; - } - - protected internal override void Unproject(double x, double y, out double latitude, out double longitude) - { - Vector3 vector = _center + _xAxis * x + _yAxis * y; - latitude = Util.Latitude(vector); - longitude = Util.Longitude(vector); - } - - private readonly Vector3 _center; - private readonly Vector3 _xAxis; - private readonly Vector3 _yAxis; - private static readonly double _tolerance = 1e-8; - } +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// References: http://mathworld.wolfram.com/GnomonicProjection.html +// +// Note: The gnomonic projection is the only projection that maps SqlGeography +// Polygons and LineString exactly to their SqlGeometry counterparts. +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using SQLSpatialTools.Types; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.Projections +{ + // EPSG Code: None + // OGC WKT Name: Gnomonic + internal sealed class GnomonicProjection : Projection + { + // Input argument: the center of the projection + public GnomonicProjection(Dictionary parameters) + : base(parameters) + { + _center = SpatialUtil.SphericalRadToCartesian(InputLatitude("latitude1", 90), InputLongitude("longitude1", 360)); + + // This projection is designed for numerical computations rather than cartography. + // The choice of coordinate basis for the tangent plane - which affects the + // orientation of the projection in the xy plane - is optimized for accuracy rather + // than good looks. The first basis vector is obtained by dropping the smallest coordinate, + // switching the other two, and flipping the sign of one of them. The second one is + // obtained by cross product. + + double[] center = { _center.X, _center.Y, _center.Z }; + var vector = new double[3]; + + var k = GetMinEntry(center); + var j = (k + 2) % 3; + var i = (j + 2) % 3; + + vector[i] = -center[j]; + vector[j] = center[i]; + vector[k] = 0; + + _xAxis = new Vector3(vector[0], vector[1], vector[2]).Unitize(); + + _yAxis = _center.CrossProduct(_xAxis); + } + + private static int GetMinEntry(IList values) + { + var i = 0; + if (Math.Abs(values[1]) < Math.Abs(values[0])) + i = 1; + if (Math.Abs(values[2]) < Math.Abs(values[i])) + i = 2; + return i; + } + + protected internal override void Project(double latitude, double longitude, out double x, out double y) + { + var vector = SpatialUtil.SphericalRadToCartesian(latitude, longitude); + var r = vector * _center; + + if (r < _tolerance) + { + throw new ArgumentOutOfRangeException(nameof(latitude), "Input point is too far away from the center of projection."); + } + vector = vector / r; + + x = vector * _xAxis; + y = vector * _yAxis; + } + + protected internal override void Unproject(double x, double y, out double latitude, out double longitude) + { + var vector = _center + _xAxis * x + _yAxis * y; + latitude = SpatialUtil.Latitude(vector); + longitude = SpatialUtil.Longitude(vector); + } + + private readonly Vector3 _center; + private readonly Vector3 _xAxis; + private readonly Vector3 _yAxis; + private static readonly double _tolerance = 1e-8; + } } \ No newline at end of file diff --git a/src/Projections/LambertConformalConicProjection.cs b/src/Projections/LambertConformalConicProjection.cs new file mode 100644 index 0000000..ef1c7e6 --- /dev/null +++ b/src/Projections/LambertConformalConicProjection.cs @@ -0,0 +1,78 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// References: http://mathworld.wolfram.com/LambertConformalConicProjection.html +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +namespace SQLSpatialTools.Projections +{ + internal sealed class LambertConformalConicProjection : Projection + { + // Angles are in degrees. + // longitude0: + // latitude: latitude for the origin of Cartesian coordinates + // fi1: standard parallel + // fi2: standard parallel + // + public LambertConformalConicProjection(IDictionary parameters) + : base(parameters) + { + var latitudeRadian = InputLatitude("latitude0"); + var fi1Radian = InputLatitude("fi1"); + var fi2Radian = InputLatitude("fi2"); + + if (Math.Abs(fi1Radian - fi2Radian) <= MathX.Tolerance) + { + throw new ArgumentException(Resource.Fi1AndFi2MustBeDifferent); + } + + // a != 0 and b != 0 because fi1 != fi2 + var a = Math.Log(Math.Cos(fi1Radian) / Math.Cos(fi2Radian)); + var b = Math.Log(Math.Tan(Math.PI / 4 + fi2Radian / 2) / Math.Tan(Math.PI / 4 + fi1Radian / 2)); + Debug.Assert(!double.IsNaN(a) && !double.IsInfinity(a) && Math.Abs(a) > 0, a.ToString(CultureInfo.InvariantCulture)); + Debug.Assert(!double.IsNaN(b) && !double.IsInfinity(b) && Math.Abs(b) > 0, b.ToString(CultureInfo.InvariantCulture)); + + _n = a / b; + _nInv = b / a; + + // tan_fi1 > 0 because fi1 is in range (-Pi/2, Pi/2) + var tanFi1 = Math.Tan(Math.PI / 4 + fi1Radian / 2); + Debug.Assert(!double.IsInfinity(tanFi1) && tanFi1 > 0, tanFi1.ToString(CultureInfo.InvariantCulture)); + _f = Math.Cos(fi1Radian) * Math.Pow(tanFi1, _n) * _nInv; + Debug.Assert(Math.Abs(_f) > MathX.Tolerance); + + // tan_latitude > 0 because latitude is in range (-Pi/2, Pi/2) + var tanLatitude = Math.Tan(Math.PI / 4 + latitudeRadian / 2); + Debug.Assert(!double.IsInfinity(tanLatitude) && tanLatitude > 0, tanLatitude.ToString(CultureInfo.InvariantCulture)); + _ro0 = _f * Math.Pow(tanLatitude, -_n); + } + + protected internal override void Project(double latitude, double longitude, out double x, out double y) + { + var a = Math.Tan(Math.PI / 4 + latitude / 2); + var ro = _f * (a <= MathX.Tolerance ? 0 : Math.Pow(a, -_n)); + var theta = _n * longitude; + + x = ro * Math.Sin(theta); + y = _ro0 - ro * Math.Cos(theta); + } + + protected internal override void Unproject(double x, double y, out double latitude, out double longitude) + { + var ro = Math.Sign(_n) * Math.Sqrt(x * x + MathX.Square(_ro0 - y)); + // If ro is zero or very small then latitude will be +-Pi/2 depending on the sign of f. + latitude = 2 * Math.Atan(Math.Pow(_f / ro, _nInv)) - Math.PI / 2; + longitude = MathX.Clamp(Math.PI, Math.Atan2(x, _ro0 - y) * _nInv); + } + + private readonly double _n; + private readonly double _nInv; // = 1 / n + private readonly double _f; + private readonly double _ro0; + } +} \ No newline at end of file diff --git a/Projections/MathX.cs b/src/Projections/MathX.cs similarity index 70% rename from Projections/MathX.cs rename to src/Projections/MathX.cs index 276bf75..7e987ae 100644 --- a/Projections/MathX.cs +++ b/src/Projections/MathX.cs @@ -1,92 +1,94 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2008 Microsoft Corporation. -// -// Purpose: Misc math functions. -//------------------------------------------------------------------------------ -using System; -using System.Globalization; - -namespace SQLSpatialTools -{ - internal static class MathX - { - public static readonly double Tolerance = 1e-14; - - public static double Atan2(double y, double x, string name) - { - if (Math.Abs(y) <= Tolerance && Math.Abs(x) <= Tolerance) - { - throw new ArgumentException(name); - } - return Atan2(y, x); - } - - public static double Atan2(double y, double x) - { - double a = Math.Atan2(y, x); - return a == Math.PI ? -Math.PI : a; - } - - public static double Clamp(double limit, double a) - { - if (a > limit) - return limit; - if (a < -limit) - return -limit; - return a; - } - - public static double InputLat(double latDeg, double max, string name) - { - if (Double.IsNaN(latDeg) || latDeg < -max || latDeg > max) - { - throw new ArgumentOutOfRangeException(name, String.Format(CultureInfo.InvariantCulture, Resource.InputLatitudeIsOutOfRange, latDeg, max)); - } - return Clamp(Math.PI / 2, Util.ToRadians(latDeg)); - } - - public static double InputLong(double longDeg, double max, string name) - { - if (Double.IsNaN(longDeg) || longDeg < -max || longDeg > max) - { - throw new ArgumentOutOfRangeException("longitude", String.Format(CultureInfo.InvariantCulture, Resource.InputLongitudeIsOutOfRange, longDeg, max)); - } - return NormalizeLongitudeRad(Util.ToRadians(longDeg)); - } - - // Remainder(2, 1.5) == 0.5 - // Remainder(2, -1.5) == 0.5 - // Remainder(-2, 1.5) == 1 - // Remainder(-2, -1.5) == 1 - // def: x = y * integer + rem, 0 <= rem < x - public static double Remainder(double x, double y) - { - return x - Math.Floor(x / y) * y; - } - - // Returns longitude in range [-180, 180). - // Has the same meaning as: - // while(longitude >= 180) longitude -= 360 - // while(longitude < -180) longitude += 360 - // - public static double NormalizeLongitudeDeg(double longitude) - { - return -180 <= longitude && longitude < 180 ? longitude : Remainder(longitude + 180, 360) - 180; - } - - // Returns longitude in range [-Pi, Pi). - // Has the same meaning as: - // while(longitude >= Pi) longitude -= Pi * 2 - // while(longitude < -Pi) longitude += Pi * 2 - // - public static double NormalizeLongitudeRad(double longitude) - { - return -Math.PI <= longitude && longitude < Math.PI ? longitude : Remainder(longitude + Math.PI, Math.PI * 2) - Math.PI; - } - - public static double Square(double a) - { - return a * a; - } - } +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Purpose: Misc math functions. +//------------------------------------------------------------------------------ + +using System; +using System.Globalization; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.Projections +{ + internal static class MathX + { + public const double Tolerance = 1e-14; + + public static double Atan2(double y, double x, string name) + { + if (Math.Abs(y) <= Tolerance && Math.Abs(x) <= Tolerance) + { + throw new ArgumentException(name); + } + return Atan2(y, x); + } + + public static double Atan2(double y, double x) + { + var a = Math.Atan2(y, x); + return a.EqualsTo(Math.PI) ? - Math.PI : a; + } + + public static double Clamp(double limit, double a) + { + if (a > limit) + return limit; + if (a < -limit) + return -limit; + return a; + } + + public static double InputLat(double latDeg, double max, string name) + { + if (double.IsNaN(latDeg) || latDeg < -max || latDeg > max) + { + throw new ArgumentOutOfRangeException(name, string.Format(CultureInfo.InvariantCulture, Resource.InputLatitudeIsOutOfRange, latDeg, max)); + } + return Clamp(Math.PI / 2, SpatialUtil.ToRadians(latDeg)); + } + + public static double InputLong(double longDeg, double max, string name) + { + if (double.IsNaN(longDeg) || longDeg < -max || longDeg > max) + { + throw new ArgumentOutOfRangeException(nameof(longDeg), string.Format(CultureInfo.InvariantCulture, Resource.InputLongitudeIsOutOfRange, longDeg, max)); + } + return NormalizeLongitudeRad(SpatialUtil.ToRadians(longDeg)); + } + + // Remainder(2, 1.5) == 0.5 + // Remainder(2, -1.5) == 0.5 + // Remainder(-2, 1.5) == 1 + // Remainder(-2, -1.5) == 1 + // def: x = y * integer + rem, 0 <= rem < x + public static double Remainder(double x, double y) + { + return x - Math.Floor(x / y) * y; + } + + // Returns longitude in range [-180, 180). + // Has the same meaning as: + // while(longitude >= 180) longitude -= 360 + // while(longitude < -180) longitude += 360 + // + public static double NormalizeLongitudeDeg(double longitude) + { + return -180 <= longitude && longitude < 180 ? longitude : Remainder(longitude + 180, 360) - 180; + } + + // Returns longitude in range [-Pi, Pi). + // Has the same meaning as: + // while(longitude >= Pi) longitude -= Pi * 2 + // while(longitude < -Pi) longitude += Pi * 2 + // + public static double NormalizeLongitudeRad(double longitude) + { + return -Math.PI <= longitude && longitude < Math.PI ? longitude : Remainder(longitude + Math.PI, Math.PI * 2) - Math.PI; + } + + public static double Square(double a) + { + return a * a; + } + } } \ No newline at end of file diff --git a/Projections/MercatorProjection.cs b/src/Projections/MercatorProjection.cs similarity index 65% rename from Projections/MercatorProjection.cs rename to src/Projections/MercatorProjection.cs index f052b1f..cadddb2 100644 --- a/Projections/MercatorProjection.cs +++ b/src/Projections/MercatorProjection.cs @@ -1,47 +1,49 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2008 Microsoft Corporation. -// -// References: http://mathworld.wolfram.com/MercatorProjection.html -//------------------------------------------------------------------------------ -using System; -using System.Collections.Generic; - -namespace SQLSpatialTools -{ - internal sealed class MercatorProjection : Projection - { - private const double MaxLatitudeDeg = 89.5; - private readonly double MaxY = Math.Log(Math.Tan(Math.PI / 4 + Util.ToRadians(MaxLatitudeDeg) / 2)); - - // longitude0: Reference longitude - // - public MercatorProjection(Dictionary parameters) - : base(parameters) - { - } - - protected internal override void Project(double latitude, double longitude, out double x, out double y) - { - x = longitude; - y = MathX.Clamp(MaxY, Math.Log(Math.Tan(Math.PI / 4 + latitude / 2))); // protect againt +-Infinity - } - - protected internal override void Unproject(double x, double y, out double latitude, out double longitude) - { - longitude = x; - - if (y >= MaxY) - { - latitude = Math.PI / 2; - } - else if (y <= -MaxY) - { - latitude = -Math.PI / 2; - } - else - { - latitude = Math.Atan(Math.Exp(y)) * 2 - Math.PI / 2; - } - } - } +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// References: http://mathworld.wolfram.com/MercatorProjection.html +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.Projections +{ + internal sealed class MercatorProjection : Projection + { + private const double MaxLatitudeDeg = 89.5; + private readonly double _maxY = Math.Log(Math.Tan(Math.PI / 4 + SpatialUtil.ToRadians(MaxLatitudeDeg) / 2)); + + // longitude0: Reference longitude + // + public MercatorProjection(IDictionary parameters) + : base(parameters) + { + } + + protected internal override void Project(double latitude, double longitude, out double x, out double y) + { + x = longitude; + y = MathX.Clamp(_maxY, Math.Log(Math.Tan(Math.PI / 4 + latitude / 2))); // protect against +-Infinity + } + + protected internal override void Unproject(double x, double y, out double latitude, out double longitude) + { + longitude = x; + + if (y >= _maxY) + { + latitude = Math.PI / 2; + } + else if (y <= -_maxY) + { + latitude = -Math.PI / 2; + } + else + { + latitude = Math.Atan(Math.Exp(y)) * 2 - Math.PI / 2; + } + } + } } \ No newline at end of file diff --git a/src/Projections/ObliqueMercatorProjection.cs b/src/Projections/ObliqueMercatorProjection.cs new file mode 100644 index 0000000..38bd9ac --- /dev/null +++ b/src/Projections/ObliqueMercatorProjection.cs @@ -0,0 +1,89 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// References: http://mathworld.wolfram.com/MercatorProjection.html +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; + +namespace SQLSpatialTools.Projections +{ + internal sealed class ObliqueMercatorProjection : Projection + { + private const double MaxY = 5; + + // Angles are in degrees. + // longitude0: + // fi1: + // lambda1: + // fi2: + // lambda2: + // + public ObliqueMercatorProjection(IDictionary parameters) + : base(parameters) + { + var fi1Rad = InputLatitude("fi1"); + var fi2Rad = InputLatitude("fi2"); + + var lambda1Rad = InputLongitude("lambda1"); + var lambda2Rad = InputLongitude("lambda2"); + + var cosFi1 = Math.Cos(fi1Rad); + var sinFi1 = Math.Sin(fi1Rad); + + var cosFi2 = Math.Cos(fi2Rad); + var sinFi2 = Math.Sin(fi2Rad); + + var a = cosFi1 * sinFi2 * Math.Cos(lambda1Rad); + var b = sinFi1 * cosFi2 * Math.Cos(lambda2Rad); + var c = sinFi1 * cosFi2 * Math.Sin(lambda2Rad); + var d = cosFi1 * sinFi2 * Math.Sin(lambda1Rad); + var lambdaP = Math.Atan2(a - b, c - d); + + var fiP = Math.Atan2(-Math.Cos(lambdaP - lambda1Rad), Math.Tan(fi1Rad)); + _cosFiP = Math.Cos(fiP); + _sinFiP = Math.Sin(fiP); + } + + protected internal override void Project(double latitude, double longitude, out double x, out double y) + { + var sinLatitude = Math.Sin(latitude); + var cosLatitude = Math.Cos(latitude); + + var sinLongitude = Math.Sin(longitude); + var cosLongitude = Math.Cos(longitude); + + var a = _sinFiP * sinLatitude - _cosFiP * cosLatitude * sinLongitude; // sin_fi_p + if (double.IsNaN(a) || Math.Abs(a) > 1) + { + throw new ArgumentOutOfRangeException(string.Join(",", new[] { nameof(latitude), nameof(longitude) })); + } + + x = MathX.Atan2(sinLatitude / cosLatitude * _cosFiP + sinLongitude * _sinFiP, cosLongitude, "latitude, longitude"); + y = MathX.Clamp(MaxY, Math.Log((1 + a) / (1 - a)) / 2); // protect against +-Infinity + } + + protected internal override void Unproject(double x, double y, out double latitude, out double longitude) + { + var sinX = Math.Sin(x); + var cosX = Math.Cos(x); + + var sinhY = Math.Sinh(y); + var coshY = Math.Cosh(y); + + // cosh_y >= 1 by definition + var a = (_sinFiP * sinhY + _cosFiP * sinX) / coshY; + if (Math.Abs(a) > 1) + { + throw new ArgumentOutOfRangeException(string.Join(",",new []{nameof(x), nameof(y)})); + } + + latitude = Math.Asin(a); + longitude = MathX.Atan2(_sinFiP * sinX - _cosFiP * sinhY, cosX, "x, y"); + } + + private readonly double _cosFiP; // = cos(fi_p) + private readonly double _sinFiP; // = sin(fi_p) + } +} \ No newline at end of file diff --git a/Projections/Projection.cs b/src/Projections/Projection.cs similarity index 61% rename from Projections/Projection.cs rename to src/Projections/Projection.cs index 64203e6..c067a85 100644 --- a/Projections/Projection.cs +++ b/src/Projections/Projection.cs @@ -1,107 +1,96 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2008 Microsoft Corporation. -// -// Purpose: Abstract base class of all projections. -//------------------------------------------------------------------------------ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Text; - -namespace SQLSpatialTools -{ - internal abstract class Projection - { - private readonly Dictionary _parameters = new Dictionary(); - private readonly double _longitude0; // in radians - - public double CentralLongitudeRad - { - get { return _longitude0; } - } - - public string Parameters - { - get - { - StringBuilder a = new StringBuilder(); - foreach (string key in _parameters.Keys) - { - string value = _parameters[key].ToString("R", CultureInfo.InvariantCulture); - if (a.Length > 0) - { - a.AppendFormat(";{0}={1}", key, value); - } - else - { - a.AppendFormat("{0}={1}", key, value); - } - } - return a.ToString(); - } - } - - public static Dictionary ParseParameters(string parameters) - { - Dictionary dict = new Dictionary(); - foreach (string pair in parameters.Split(';')) - { - string[] a = pair.Split('='); - dict.Add(a[0], Double.Parse(a[1], NumberStyles.Float, CultureInfo.InvariantCulture)); - } - return dict; - } - - public Projection(Dictionary parameters) - { - Debug.Assert(parameters != null); - _parameters = new Dictionary(parameters); - _longitude0 = InputLongitude("longitude0"); - } - - // Used for validation and conversion of projection input parameters. - // - // Returns latitude converted to radians in interval (-Pi/2, Pi/2). - // Throws ArgumentOutOfRangeException if latitude is NaN or not in range [-89.9, 89.9]. - // - // Parram name: name of latitude argument in case if the exception is thrown - // - protected internal double InputLatitude(string name) - { - return InputLatitude(name, 89.9); - } - - protected internal double InputLatitude(string name, double max) - { - return MathX.InputLat(_parameters[name], max, name); - } - - // Used for validation and conversion of projection input parameters. - // - // Returns longitude converted to radians in range [-Pi, Pi). - // Throws ArgumentOutOfRangeException if longitude is NaN or Infinity. - // - // Parram name: name of longitude argument in case if the exception is thrown - // - protected internal double InputLongitude(string name) - { - return InputLongitude(name, 360); - } - - protected internal double InputLongitude(string name, double max) - { - return MathX.InputLong(_parameters[name], max, name); - } - - // Longitude and latitude are in radians. - // Latitude is assumed to be in range [-Pi/2, Pi/2]. - // Longitude is assumed to be in range [-Pi, Pi). - protected internal abstract void Project(double latitude, double longitude, out double x, out double y); - - // Longitude and latitude are in radians. - // Latitude must be in range [-Pi/2, Pi/2]. - // Longitude must be in range [-Pi, Pi). - protected internal abstract void Unproject(double x, double y, out double latitude, out double longitude); - } +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Purpose: Abstract base class of all projections. +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace SQLSpatialTools.Projections +{ + internal abstract class Projection + { + private readonly Dictionary _parameters; + + public double CentralLongitudeRad { get; } + + public string Parameters + { + get + { + var a = new StringBuilder(); + foreach (var key in _parameters.Keys) + { + var value = _parameters[key].ToString("R", CultureInfo.InvariantCulture); + a.AppendFormat(a.Length > 0 ? ";{0}={1}" : "{0}={1}", key, value); + } + return a.ToString(); + } + } + + public static Dictionary ParseParameters(string parameters) + { + return parameters.Split(';') + .Select(pair => pair.Split('=')) + .ToDictionary(a => a[0], + a => double.Parse(a[1], + NumberStyles.Float, + CultureInfo.InvariantCulture)); + } + + protected Projection(IDictionary parameters) + { + Debug.Assert(parameters != null); + _parameters = new Dictionary(parameters); + CentralLongitudeRad = InputLongitude("longitude0"); + } + + // Used for validation and conversion of projection input parameters. + // + // Returns latitude converted to radians in interval (-Pi/2, Pi/2). + // Throws ArgumentOutOfRangeException if latitude is NaN or not in range [-89.9, 89.9]. + // + // Param name: name of latitude argument in case if the exception is thrown + // + protected internal double InputLatitude(string name) + { + return InputLatitude(name, 89.9); + } + + protected internal double InputLatitude(string name, double max) + { + return MathX.InputLat(_parameters[name], max, name); + } + + // Used for validation and conversion of projection input parameters. + // + // Returns longitude converted to radians in range [-Pi, Pi). + // Throws ArgumentOutOfRangeException if longitude is NaN or Infinity. + // + // Param name: name of longitude argument in case if the exception is thrown + // + protected internal double InputLongitude(string name) + { + return InputLongitude(name, 360); + } + + protected internal double InputLongitude(string name, double max) + { + return MathX.InputLong(_parameters[name], max, name); + } + + // Longitude and latitude are in radians. + // Latitude is assumed to be in range [-Pi/2, Pi/2]. + // Longitude is assumed to be in range [-Pi, Pi). + protected internal abstract void Project(double latitude, double longitude, out double x, out double y); + + // Longitude and latitude are in radians. + // Latitude must be in range [-Pi/2, Pi/2]. + // Longitude must be in range [-Pi, Pi). + protected internal abstract void Unproject(double x, double y, out double latitude, out double longitude); + } } \ No newline at end of file diff --git a/Projections/TranverseMercatorProjection.cs b/src/Projections/TransverseMercatorProjection.cs similarity index 85% rename from Projections/TranverseMercatorProjection.cs rename to src/Projections/TransverseMercatorProjection.cs index c843007..38413ff 100644 --- a/Projections/TranverseMercatorProjection.cs +++ b/src/Projections/TransverseMercatorProjection.cs @@ -1,86 +1,87 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2008 Microsoft Corporation. -// -// References: http://mathworld.wolfram.com/MercatorProjection.html -//------------------------------------------------------------------------------ -using System; -using System.Collections.Generic; - -namespace SQLSpatialTools -{ - internal sealed class TranverseMercatorProjection : Projection - { - private const double MaxX = 10; - - // longitude0: Reference longitude. - // - public TranverseMercatorProjection(Dictionary parameters) - : base(parameters) - { - } - - // x will be in range [-MaxX, MaxX] - // y will be in range [-Pi, Pi] - protected internal override void Project(double latitude, double longitude, out double x, out double y) - { - // North pole - if (latitude >= Math.PI / 2 - MathX.Tolerance) - { - x = 0; - y = Math.PI / 2; - return; - } - - // South pole - if (latitude <= -Math.PI / 2 + MathX.Tolerance) - { - x = 0; - y = -Math.PI / 2; - return; - } - - if (Math.Abs(latitude) <= MathX.Tolerance) - { - // East of India - if (Math.Abs(longitude - Math.PI / 2) <= MathX.Tolerance) - { - x = MaxX; - y = 0; - return; - } - // West of South America - if (Math.Abs(longitude + Math.PI / 2) <= MathX.Tolerance) - { - x = -MaxX; - y = 0; - return; - } - } - - double b = Math.Cos(latitude) * Math.Sin(longitude); - x = MathX.Clamp(MaxX, Math.Log((1 + b) / (1 - b)) / 2); - y = Math.Atan2(Math.Tan(latitude), Math.Cos(longitude)); - } - - protected internal override void Unproject(double x, double y, out double latitude, out double longitude) - { - if (x >= MaxX) - { - latitude = 0; - longitude = Math.PI / 2; - } - else if (x <= -MaxX) - { - latitude = 0; - longitude = -Math.PI / 2; - } - else - { - // 1 <= cosh(x) <= cosh(MaxX) - latitude = Math.Asin(Math.Sin(y) / Math.Cosh(x)); - // In case of x=0 and y=+-Pi/2: latitude will be +-90 and longtude will be 0 - longitude = MathX.Atan2(Math.Sinh(x), Math.Cos(y)); - } - } - } +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// References: http://mathworld.wolfram.com/MercatorProjection.html +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; + +namespace SQLSpatialTools.Projections +{ + internal sealed class TransverseMercatorProjection : Projection + { + private const double MaxX = 10; + + // longitude0: Reference longitude. + // + public TransverseMercatorProjection(IDictionary parameters) + : base(parameters) + { + } + + // x will be in range [-MaxX, MaxX] + // y will be in range [-Pi, Pi] + protected internal override void Project(double latitude, double longitude, out double x, out double y) + { + // North pole + if (latitude >= Math.PI / 2 - MathX.Tolerance) + { + x = 0; + y = Math.PI / 2; + return; + } + + // South pole + if (latitude <= -Math.PI / 2 + MathX.Tolerance) + { + x = 0; + y = -Math.PI / 2; + return; + } + + if (Math.Abs(latitude) <= MathX.Tolerance) + { + // East of India + if (Math.Abs(longitude - Math.PI / 2) <= MathX.Tolerance) + { + x = MaxX; + y = 0; + return; + } + // West of South America + if (Math.Abs(longitude + Math.PI / 2) <= MathX.Tolerance) + { + x = -MaxX; + y = 0; + return; + } + } + + double b = Math.Cos(latitude) * Math.Sin(longitude); + x = MathX.Clamp(MaxX, Math.Log((1 + b) / (1 - b)) / 2); + y = Math.Atan2(Math.Tan(latitude), Math.Cos(longitude)); + } + + protected internal override void Unproject(double x, double y, out double latitude, out double longitude) + { + if (x >= MaxX) + { + latitude = 0; + longitude = Math.PI / 2; + } + else if (x <= -MaxX) + { + latitude = 0; + longitude = -Math.PI / 2; + } + else + { + // 1 <= cosh(x) <= cosh(MaxX) + latitude = Math.Asin(Math.Sin(y) / Math.Cosh(x)); + // In case of x=0 and y=+-Pi/2: latitude will be +-90 and longitude will be 0 + longitude = MathX.Atan2(Math.Sinh(x), Math.Cos(y)); + } + } + } } \ No newline at end of file diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0b7b6d1 --- /dev/null +++ b/src/Properties/AssemblyInfo.cs @@ -0,0 +1,43 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SQLSpatialTools")] +[assembly: AssemblyDescription("A collection of tools for use with the SQL Server spatial functionality.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("SQLSpatialTools")] +[assembly: AssemblyCopyright("Copyright (c) Microsoft Corporation. All rights reserved.")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: InternalsVisibleTo("SQLSpatialTools.UnitTests, PublicKey=00240000048000009400000006020000002400005253413100040000010001007d94bba1d58b134e06b24a4a03215f8fc90fe33bfe8ec3be86c0d1f26a87809ec4eb032925180a9a30c4890d480d13dff052c2ee5ae89f14f32b9bf4306e436ce5c79b53c3517e758b7db776b13a6f85e95e8267aa1df08c65bdae1453abe0eb33e625af0d18acb2aefa5460f25af0920ae310e8c1301bf5ecd7130762aa70d2")] +[assembly: InternalsVisibleTo("SQLSpatialTools.UnitTests.DDD, PublicKey=00240000048000009400000006020000002400005253413100040000010001007d94bba1d58b134e06b24a4a03215f8fc90fe33bfe8ec3be86c0d1f26a87809ec4eb032925180a9a30c4890d480d13dff052c2ee5ae89f14f32b9bf4306e436ce5c79b53c3517e758b7db776b13a6f85e95e8267aa1df08c65bdae1453abe0eb33e625af0d18acb2aefa5460f25af0920ae310e8c1301bf5ecd7130762aa70d2")] +[assembly: InternalsVisibleTo("SQLSpatialTools.UnitTests.Extension, PublicKey=00240000048000009400000006020000002400005253413100040000010001007d94bba1d58b134e06b24a4a03215f8fc90fe33bfe8ec3be86c0d1f26a87809ec4eb032925180a9a30c4890d480d13dff052c2ee5ae89f14f32b9bf4306e436ce5c79b53c3517e758b7db776b13a6f85e95e8267aa1df08c65bdae1453abe0eb33e625af0d18acb2aefa5460f25af0920ae310e8c1301bf5ecd7130762aa70d2")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ee2e7fb3-3d63-4eb8-b0a7-8b089f5e2cf3")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.3.0")] +[assembly: AssemblyFileVersion("1.0.3.0")] diff --git a/Resource.Designer.cs b/src/Resource/Resource.Designer.cs similarity index 97% rename from Resource.Designer.cs rename to src/Resource/Resource.Designer.cs index 4836a03..9c6dc1b 100644 --- a/Resource.Designer.cs +++ b/src/Resource/Resource.Designer.cs @@ -1,118 +1,118 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:2.0.50727.1433 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace SQLSpatialTools -{ - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resource { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resource() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.SqlServer.SpatialToolbox.Resource", typeof(Resource).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to fi1 and fi2 must be different. - /// - internal static string Fi1AndFi2MustBeDifferent { - get { - return ResourceManager.GetString("Fi1AndFi2MustBeDifferent", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Input coordinate is NaN.. - /// - internal static string InputCoordinateIsNaN { - get { - return ResourceManager.GetString("InputCoordinateIsNaN", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Input latitude {0} is out of range [-{1}, {1}].. - /// - internal static string InputLatitudeIsOutOfRange { - get { - return ResourceManager.GetString("InputLatitudeIsOutOfRange", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Input longitude {0} is out of range [-{1}, {1}].. - /// - internal static string InputLongitudeIsOutOfRange { - get { - return ResourceManager.GetString("InputLongitudeIsOutOfRange", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Output latitude {0} is out of range [-Pi/2, Pi/2].. - /// - internal static string OutputLatitudeIsOutOfRange { - get { - return ResourceManager.GetString("OutputLatitudeIsOutOfRange", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Output longitude {0} is out of range [-Pi, Pi).. - /// - internal static string OutputLongitudeIsOutOfRange { - get { - return ResourceManager.GetString("OutputLongitudeIsOutOfRange", resourceCulture); - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:2.0.50727.1433 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace SQLSpatialTools +{ + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.SqlServer.SpatialToolbox.Resource", typeof(Resource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to fi1 and fi2 must be different. + /// + internal static string Fi1AndFi2MustBeDifferent { + get { + return ResourceManager.GetString("Fi1AndFi2MustBeDifferent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Input coordinate is NaN.. + /// + internal static string InputCoordinateIsNaN { + get { + return ResourceManager.GetString("InputCoordinateIsNaN", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Input latitude {0} is out of range [-{1}, {1}].. + /// + internal static string InputLatitudeIsOutOfRange { + get { + return ResourceManager.GetString("InputLatitudeIsOutOfRange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Input longitude {0} is out of range [-{1}, {1}].. + /// + internal static string InputLongitudeIsOutOfRange { + get { + return ResourceManager.GetString("InputLongitudeIsOutOfRange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Output latitude {0} is out of range [-Pi/2, Pi/2].. + /// + internal static string OutputLatitudeIsOutOfRange { + get { + return ResourceManager.GetString("OutputLatitudeIsOutOfRange", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Output longitude {0} is out of range [-Pi, Pi).. + /// + internal static string OutputLongitudeIsOutOfRange { + get { + return ResourceManager.GetString("OutputLongitudeIsOutOfRange", resourceCulture); + } + } + } +} diff --git a/Resource.resx b/src/Resource/Resource.resx similarity index 97% rename from Resource.resx rename to src/Resource/Resource.resx index 1c57ccd..6f7e0de 100644 --- a/Resource.resx +++ b/src/Resource/Resource.resx @@ -1,138 +1,138 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - fi1 and fi2 must be different - - - Input coordinate is NaN. - - - Input latitude {0} is out of range [-{1}, {1}]. - - - Input longitude {0} is out of range [-{1}, {1}]. - - - Output latitude {0} is out of range [-Pi/2, Pi/2]. - - - Output longitude {0} is out of range [-Pi, Pi). - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + fi1 and fi2 must be different + + + Input coordinate is NaN. + + + Input latitude {0} is out of range [-{1}, {1}]. + + + Input longitude {0} is out of range [-{1}, {1}]. + + + Output latitude {0} is out of range [-Pi/2, Pi/2]. + + + Output longitude {0} is out of range [-Pi, Pi). + \ No newline at end of file diff --git a/src/SQL Scripts/Register.sql b/src/SQL Scripts/Register.sql new file mode 100644 index 0000000..aaf81ca --- /dev/null +++ b/src/SQL Scripts/Register.sql @@ -0,0 +1,401 @@ +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Install the SQLSpatialTools assembly and all its functions into the current database + +-- Enabling CLR prior to registering assembly and its related functions. +EXEC sp_configure 'show advanced option', '1'; +RECONFIGURE; +Go + +sp_configure 'clr enabled', 1 ; +GO +RECONFIGURE ; +GO + +EXEC sp_configure 'clr strict security', '0' +RECONFIGURE WITH OVERRIDE; +GO + +-- !!! DLL Path will be replace based upon system environment !!! +CREATE assembly SQLSpatialTools +FROM 'DLLPath' +GO + +-- Create User Defined SQL Types +CREATE TYPE Projection EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Types.SQL.SqlProjection] +GO + +CREATE TYPE AffineTransform EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Types.SQL.AffineTransform] +GO + +-- Register the functions... + +--#region General Geometry Functions +CREATE FUNCTION FilterArtifactsGeometry ( + @geometry GEOMETRY + ,@filterEmptyShapes BIT + ,@filterPoints BIT + ,@lineStringTolerance FLOAT(53) + ,@ringTolerance FLOAT(53) + ) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geometry].FilterArtifactsGeometry +GO + +CREATE FUNCTION GeomFromXYMText ( + @geometry NVARCHAR(MAX) + ,@targetSrid INT + ) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geometry].GeomFromXYMText +GO + +CREATE FUNCTION InterpolateBetweenGeom ( + @startPoint GEOMETRY + ,@endPoint GEOMETRY + ,@distance FLOAT + ) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geometry].InterpolateBetweenGeom +GO + +CREATE FUNCTION LocateAlongGeom ( + @geometry GEOMETRY + ,@distance FLOAT + ) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geometry].LocatePointAlongGeom +GO + +CREATE FUNCTION MakeValidForGeography (@geometry GEOMETRY) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geometry].MakeValidForGeography +GO + +CREATE FUNCTION ReverseLinestring (@geometry GEOMETRY) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geometry].ReverseLinestring +GO + +CREATE FUNCTION ShiftGeometry ( + @geometry GEOMETRY + ,@xShift FLOAT + ,@yShift FLOAT + ) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geometry].ShiftGeometry +GO + +CREATE FUNCTION VacuousGeometryToGeography ( + @geometryToConvert GEOMETRY + ,@targetSrid INT + ) +RETURNS GEOGRAPHY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geometry].VacuousGeometryToGeography +GO + +--#endregion + +--#region General Geography Functions + +CREATE FUNCTION ConvexHullGeographyFromText ( + @inputWKT NVARCHAR(max) + ,@srid INT + ) +RETURNS GEOGRAPHY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geography].ConvexHullGeographyFromText +GO + +CREATE FUNCTION ConvexHullGeography (@geography GEOGRAPHY) +RETURNS GEOGRAPHY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geography].ConvexHullGeography +GO + +CREATE FUNCTION DensifyGeography ( + @geography GEOGRAPHY + ,@maxAngle FLOAT + ) +RETURNS GEOGRAPHY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geography].DensifyGeography +GO + +CREATE FUNCTION FilterArtifactsGeography ( + @geography GEOGRAPHY + ,@filterEmptyShapes BIT + ,@filterPoints BIT + ,@lineStringTolerance FLOAT(53) + ,@ringTolerance FLOAT(53) + ) +RETURNS GEOGRAPHY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geography].FilterArtifactsGeography +GO + +CREATE FUNCTION InterpolateBetweenGeog ( + @startPoint GEOGRAPHY + ,@endPoint GEOGRAPHY + ,@distance FLOAT + ) +RETURNS GEOGRAPHY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geography].InterpolateBetweenGeog +GO + +CREATE FUNCTION IsValidGeographyFromGeometry (@geometry GEOMETRY) +RETURNS BIT +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geography].IsValidGeographyFromGeometry +GO + +CREATE FUNCTION IsValidGeographyFromText ( + @inputWKT NVARCHAR(MAX) + ,@srid INT + ) +RETURNS BIT +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geography].IsValidGeographyFromText +GO + +CREATE FUNCTION LocateAlongGeog ( + @geography GEOGRAPHY + ,@distance FLOAT + ) +RETURNS GEOGRAPHY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geography].LocatePointAlongGeog +GO + +CREATE FUNCTION MakeValidGeographyFromGeometry (@geometry GEOMETRY) +RETURNS GEOGRAPHY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geography].MakeValidGeographyFromGeometry +GO + +CREATE FUNCTION MakeValidGeographyFromText ( + @inputWKT NVARCHAR(MAX) + ,@srid INT + ) +RETURNS GEOGRAPHY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geography].MakeValidGeographyFromText +GO + +CREATE FUNCTION VacuousGeographyToGeometry ( + @geographyToConvert GEOGRAPHY + ,@targetSrid INT + ) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.General.Geography].VacuousGeographyToGeometry +GO + +--#endregion + +--#region LRS Geometric Functions +CREATE FUNCTION LRS_ClipGeometrySegment ( + @geometry GEOMETRY + ,@startMeasure FLOAT + ,@endMeasure FLOAT + ,@tolerance FLOAT(53) + ) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].ClipGeometrySegment +GO + +CREATE FUNCTION LRS_ConvertToLrsGeom ( + @geometry GEOMETRY + ,@startMeasure FLOAT + ,@endMeasure FLOAT + ) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].ConvertToLrsGeom +GO + +CREATE FUNCTION LRS_GetEndMeasure (@geometry GEOMETRY) +RETURNS FLOAT +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].GetEndMeasure +GO + +CREATE FUNCTION LRS_GetMergePosition ( + @geometry1 GEOMETRY + ,@geometry2 GEOMETRY + ,@tolerance FLOAT(53) + ) +RETURNS NVARCHAR(20) +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].GetMergePosition +GO + +CREATE FUNCTION LRS_GetStartMeasure (@geometry GEOMETRY) +RETURNS FLOAT +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].GetStartMeasure +GO + +CREATE FUNCTION LRS_InterpolateBetweenGeom ( + @startPoint GEOMETRY + ,@endPoint GEOMETRY + ,@measure FLOAT + ) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].InterpolateBetweenGeom +GO + +CREATE FUNCTION LRS_IsConnected ( + @geometry1 GEOMETRY + ,@geometry2 GEOMETRY + ,@tolerance FLOAT(53) + ) +RETURNS BIT +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].IsConnected +GO + +CREATE FUNCTION LRS_IsValidPoint ( + @geometry GEOMETRY + ) +RETURNS BIT +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].IsValidPoint +GO + +CREATE FUNCTION LRS_LocatePointAlongGeom ( + @geometry GEOMETRY + ,@distance FLOAT + ) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].LocatePointAlongGeom +GO + +CREATE FUNCTION LRS_MergeGeometrySegments ( + @geometry1 GEOMETRY + ,@geometry2 GEOMETRY + ,@tolerance FLOAT(53) + ) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].MergeGeometrySegments +GO + +CREATE FUNCTION LRS_MergeAndResetGeometrySegments ( + @geometry1 GEOMETRY + ,@geometry2 GEOMETRY + ,@tolerance FLOAT(53) + ) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].MergeAndResetGeometrySegments +GO + +CREATE FUNCTION LRS_OffsetGeometrySegments ( + @geometry GEOMETRY + ,@startMeasure FLOAT + ,@endMeasure FLOAT + ,@offset FLOAT + ,@tolerance FLOAT(53) + ) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].OffsetGeometrySegment +GO + +CREATE FUNCTION LRS_PopulateGeometryMeasures ( + @geometry GEOMETRY + ,@startMeasure FLOAT + ,@endMeasure FLOAT + ) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].PopulateGeometryMeasures +GO + +CREATE FUNCTION LRS_ResetMeasure(@geometry GEOMETRY) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].ResetMeasure +GO + +CREATE FUNCTION LRS_ReverseLinearGeometry (@geometry GEOMETRY) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].ReverseLinearGeometry +GO + +CREATE FUNCTION LRS_ScaleGeometrySegment (@geometry GEOMETRY + ,@startMeasure FLOAT + ,@endMeasure FLOAT + ,@shiftMeasure FLOAT) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].ScaleGeometrySegment +GO + +CREATE PROCEDURE LRS_SplitGeometrySegment @geometry GEOMETRY + ,@splitMeasure FLOAT + ,@geometry1 GEOMETRY OUTPUT + ,@geometry2 GEOMETRY OUTPUT +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].SplitGeometrySegment +GO + +CREATE FUNCTION LRS_TranslateMeasure (@geometry GEOMETRY + ,@translateMeasure FLOAT) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].TranslateMeasure +GO + +CREATE FUNCTION LRS_ValidateLRSGeometry (@geometry GEOMETRY) +RETURNS NVARCHAR(10) +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.LRS.Geometry].ValidateLRSGeometry +GO + +CREATE FUNCTION Util_PolygonToLine (@geometry GEOMETRY) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.Util.Geometry].PolygonToLine +GO + +CREATE FUNCTION Util_Extract (@geometry GEOMETRY + ,@elementIndex INTEGER + ,@ringIndex INTEGER) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.Util.Geometry].ExtractGeometry +GO + +CREATE FUNCTION Util_RemoveDuplicateVertices (@geometry GEOMETRY, @tolerance FLOAT) +RETURNS GEOMETRY +AS +EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Functions.Util.Geometry].RemoveDuplicateVertices +GO +--#endregion + +-- Create aggregates. +CREATE AGGREGATE GEOMETRYEnvelopeAggregate (@geometry GEOMETRY) +RETURNS GEOMETRY EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Aggregates.GeometryEnvelopeAggregate] +GO + +CREATE AGGREGATE GeographyCollectionAggregate (@geography GEOGRAPHY) +RETURNS GEOGRAPHY EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Aggregates.GeographyCollectionAggregate] +GO + +CREATE AGGREGATE GeographyUnionAggregate (@geography GEOGRAPHY) +RETURNS GEOGRAPHY EXTERNAL NAME SQLSpatialTools.[SQLSpatialTools.Aggregates.GeographyUnionAggregate] +GO \ No newline at end of file diff --git a/src/SQL Scripts/RegisterAndValidate.sql b/src/SQL Scripts/RegisterAndValidate.sql new file mode 100644 index 0000000..628efd6 --- /dev/null +++ b/src/SQL Scripts/RegisterAndValidate.sql @@ -0,0 +1,25 @@ +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Install the SQLSpatialTools assembly and all its functions into the current database +-- Runs all the LRS functions with sample examples. + +-- First unregister should be called +-- To run sql script inside another SQLCMD Mode should be enabled +-- To enable SQLCMD Mode: Query -> SQLCMD Mode + +-- First Unregister existing functions +:r "ScriptDirPath\Unregister.sql" + +-- Register Script Path will be replaced in run time +:r "ScriptDirPath\Register.sql" + +-- Run LRS Functions +:r "ScriptDirPath\lrs_geometry_example.sql" + +-- Run Util Functions +:r "ScriptDirPath\util_geometry_example.sql" + +-- Run Sample Projections +:r "ScriptDirPath\projection_example.sql" + +-- Run Sample Transforms +:r "ScriptDirPath\transform_example.sql" \ No newline at end of file diff --git a/src/SQL Scripts/Unregister.sql b/src/SQL Scripts/Unregister.sql new file mode 100644 index 0000000..ab7fe23 --- /dev/null +++ b/src/SQL Scripts/Unregister.sql @@ -0,0 +1,150 @@ +--WARNING! ERRORS ENCOUNTERED DURING SQL PARSING! +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Drop the SQLSpatialTools assembly and all its functions from the current database +-- Drop the aggregates... +IF OBJECT_ID('GeometryEnvelopeAggregate') IS NOT NULL + DROP AGGREGATE GeometryEnvelopeAggregate + +IF OBJECT_ID('GeographyCollectionAggregate') IS NOT NULL + DROP AGGREGATE GeographyCollectionAggregate + +IF OBJECT_ID('GeographyUnionAggregate') IS NOT NULL + DROP AGGREGATE GeographyUnionAggregate + +-- Drop the functions... +-- General Geometry +IF OBJECT_ID('FilterArtifactsGeometry') IS NOT NULL + DROP FUNCTION FilterArtifactsGeometry + +IF OBJECT_ID('GeomFromXYMText') IS NOT NULL + DROP FUNCTION GeomFromXYMText + +IF OBJECT_ID('InterpolateBetweenGeom') IS NOT NULL + DROP FUNCTION InterpolateBetweenGeom + +IF OBJECT_ID('LocateAlongGeom') IS NOT NULL + DROP FUNCTION LocateAlongGeom + +IF OBJECT_ID('MakeValidForGeography') IS NOT NULL + DROP FUNCTION MakeValidForGeography + +IF OBJECT_ID('ReverseLinestring') IS NOT NULL + DROP FUNCTION ReverseLinestring + +IF OBJECT_ID('ShiftGeometry') IS NOT NULL + DROP FUNCTION ShiftGeometry + +IF OBJECT_ID('VacuousGeometryToGeography') IS NOT NULL + DROP FUNCTION VacuousGeometryToGeography + +-- General Geography +IF OBJECT_ID('ConvexHullGeographyFromText') IS NOT NULL + DROP FUNCTION ConvexHullGeographyFromText + +IF OBJECT_ID('ConvexHullGeography') IS NOT NULL + DROP FUNCTION ConvexHullGeography + +IF OBJECT_ID('DensifyGeography') IS NOT NULL + DROP FUNCTION DensifyGeography + +IF OBJECT_ID('FilterArtifactsGeography') IS NOT NULL + DROP FUNCTION FilterArtifactsGeography + +IF OBJECT_ID('InterpolateBetweenGeog') IS NOT NULL + DROP FUNCTION InterpolateBetweenGeog + +IF OBJECT_ID('IsValidGeographyFromGeometry') IS NOT NULL + DROP FUNCTION IsValidGeographyFromGeometry + +IF OBJECT_ID('IsValidGeographyFromText') IS NOT NULL + DROP FUNCTION IsValidGeographyFromText + +IF OBJECT_ID('LocateAlongGeog') IS NOT NULL + DROP FUNCTION LocateAlongGeog + +IF OBJECT_ID('MakeValidGeographyFromGeometry') IS NOT NULL + DROP FUNCTION MakeValidGeographyFromGeometry + +IF OBJECT_ID('MakeValidGeographyFromText') IS NOT NULL + DROP FUNCTION MakeValidGeographyFromText + +IF OBJECT_ID('VacuousGeographyToGeometry') IS NOT NULL + DROP FUNCTION VacuousGeographyToGeometry + +-- LRS Geometry +IF OBJECT_ID('LRS_ClipGeometrySegment') IS NOT NULL + DROP FUNCTION LRS_ClipGeometrySegment + +IF OBJECT_ID('LRS_ConvertToLrsGeom') IS NOT NULL +DROP FUNCTION LRS_ConvertToLrsGeom + +IF OBJECT_ID('LRS_GetEndMeasure') IS NOT NULL + DROP FUNCTION LRS_GetEndMeasure + +IF OBJECT_ID('LRS_GetMergePosition') IS NOT NULL + DROP FUNCTION LRS_GetMergePosition + +IF OBJECT_ID('LRS_GetStartMeasure') IS NOT NULL + DROP FUNCTION LRS_GetStartMeasure + +IF OBJECT_ID('LRS_InterpolateBetweenGeom') IS NOT NULL + DROP FUNCTION LRS_InterpolateBetweenGeom + +IF OBJECT_ID('LRS_IsConnected') IS NOT NULL + DROP FUNCTION LRS_IsConnected + +IF OBJECT_ID('LRS_IsValidPoint') IS NOT NULL + DROP FUNCTION LRS_IsValidPoint + +IF OBJECT_ID('LRS_LocatePointAlongGeom') IS NOT NULL + DROP FUNCTION LRS_LocatePointAlongGeom + +IF OBJECT_ID('LRS_MergeGeometrySegments') IS NOT NULL + DROP FUNCTION LRS_MergeGeometrySegments + +IF OBJECT_ID('LRS_MergeAndResetGeometrySegments') IS NOT NULL + DROP FUNCTION LRS_MergeAndResetGeometrySegments + +IF OBJECT_ID('LRS_OffsetGeometrySegments') IS NOT NULL + DROP FUNCTION LRS_OffsetGeometrySegments + +IF OBJECT_ID('LRS_PopulateGeometryMeasures') IS NOT NULL + DROP FUNCTION LRS_PopulateGeometryMeasures + +IF OBJECT_ID('LRS_ResetMeasure') IS NOT NULL + DROP FUNCTION LRS_ResetMeasure + +IF OBJECT_ID('LRS_ReverseLinearGeometry') IS NOT NULL + DROP FUNCTION LRS_ReverseLinearGeometry + +IF OBJECT_ID('LRS_ScaleGeometrySegment') IS NOT NULL + DROP FUNCTION LRS_ScaleGeometrySegment + +IF OBJECT_ID('LRS_SplitGeometrySegment') IS NOT NULL + DROP PROCEDURE LRS_SplitGeometrySegment + +IF OBJECT_ID('LRS_TranslateMeasure') IS NOT NULL + DROP FUNCTION LRS_TranslateMeasure + +IF OBJECT_ID('LRS_ValidateLRSGeometry') IS NOT NULL + DROP FUNCTION LRS_ValidateLRSGeometry + +-- Utility Functions +IF OBJECT_ID('Util_PolygonToLine') IS NOT NULL + DROP FUNCTION Util_PolygonToLine + +IF OBJECT_ID('Util_RemoveDuplicateVertices') IS NOT NULL + DROP FUNCTION Util_RemoveDuplicateVertices + +IF OBJECT_ID('Util_Extract') IS NOT NULL + DROP FUNCTION Util_Extract + +-- Drop the types... +IF TYPE_ID('Projection') IS NOT NULL + DROP TYPE Projection + +IF TYPE_ID('AffineTransform') IS NOT NULL + DROP TYPE AffineTransform + +-- Drop the assembly... +DROP ASSEMBLY IF EXISTS SQLSpatialTools diff --git a/SQL Scripts/aggregate_example.sql b/src/SQL Scripts/aggregate_example.sql similarity index 98% rename from SQL Scripts/aggregate_example.sql rename to src/SQL Scripts/aggregate_example.sql index 4ab178d..1902fb0 100644 --- a/SQL Scripts/aggregate_example.sql +++ b/src/SQL Scripts/aggregate_example.sql @@ -1,7 +1,7 @@ --- Script assumes that current database contains zipcodes table with columns: code, state, shape_geom, shape_geog - -select state, dbo.GeometryEnvelopeAggregate(shape_geom) from zipcodes group by state; - --- For every state return its polygon by performing an union of all zip area belonging to that state. --- Also buffer result by 0.5 meters to avoid tiny cracks along the borders of zip areas. +-- Script assumes that current database contains zipcodes table with columns: code, state, shape_geom, shape_geog + +select state, dbo.GeometryEnvelopeAggregate(shape_geom) from zipcodes group by state; + +-- For every state return its polygon by performing an union of all zip area belonging to that state. +-- Also buffer result by 0.5 meters to avoid tiny cracks along the borders of zip areas. select state, dbo.GeographyUnionAggregate(shape_geog).STBuffer(0.5) from zipcodes group by state; \ No newline at end of file diff --git a/src/SQL Scripts/lrs_geometry_example.sql b/src/SQL Scripts/lrs_geometry_example.sql new file mode 100644 index 0000000..0e4079b --- /dev/null +++ b/src/SQL Scripts/lrs_geometry_example.sql @@ -0,0 +1,176 @@ +-- Script that shows examples on exposed LRS Functions in SQLSpatialTools Library +-- LRS Geometric Functions +DECLARE @geom geometry; +DECLARE @geom1 geometry +DECLARE @geom2 geometry +DECLARE @srid INT = 4326; +DECLARE @distance FLOAT; +DECLARE @measure FLOAT; +DECLARE @offset FLOAT = 2; +DECLARE @tolerance FLOAT = 0.5; +DECLARE @startMeasure FLOAT = 15.0; +DECLARE @endMeasure FLOAT = 20.0; + +SET @geom = GEOMETRY::STGeomFromText('LINESTRING (20 1 NULL 10, 25 1 NULL 25 )', @srid); + +-- 1. ClipGeometrySegement Function +SELECT 'Clipped Segment' AS 'FunctionInfo' + ,[dbo].[LRS_ClipGeometrySegment](@geom, @startMeasure, @endMeasure, @tolerance) AS 'Geometry' + ,[dbo].[LRS_ClipGeometrySegment](@geom, @startMeasure, @endMeasure, @tolerance).ToString() AS 'Geometry in String' + +-- 2. Get Start Measure +SELECT 'Start Measure' AS 'FunctionInfo' + ,[dbo].[LRS_GetStartMeasure](@geom) AS 'Measure' + +-- 3. Get Merge Position +SET @geom1 = GEOMETRY::STGeomFromText('LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 0 0)', @srid); +SET @geom2 = GEOMETRY::STGeomFromText('LINESTRING(5 5 0 0, 2 2 0 0)', @srid); +SET @tolerance = 0.5; + +SELECT 'Merge Position' AS 'FunctionInfo' + ,[dbo].LRS_GetMergePosition(@geom1, @geom2, @tolerance) AS 'IsConnected'; + +-- 4. Get End Measure +SELECT 'End Measure' AS 'FunctionInfo' + ,[dbo].[LRS_GetEndMeasure](@geom) AS 'Measure' + +-- 5. Interpolate points between Geom +SET @geom1 = GEOMETRY::STGeomFromText('POINT(0 0 0 0)', @srid); +SET @geom2 = GEOMETRY::STGeomFromText('POINT(10 0 0 10)', @srid); + +SET @measure = 5; + +SELECT 'Interpolate Points' AS 'FunctionInfo' + ,[dbo].[LRS_InterpolateBetweenGeom](@geom1, @geom2, @measure) AS 'Geometry' + ,[dbo].[LRS_InterpolateBetweenGeom](@geom1, @geom2, @measure).ToString() AS 'Geometry in String'; + +-- 6. Is Spatially Connected +SET @geom1 = GEOMETRY::STGeomFromText('LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 0 0)', @srid); +SET @geom2 = GEOMETRY::STGeomFromText('LINESTRING(5 5 0 0, 2 2 0 0)', @srid); +SET @tolerance = 0.5; + +SELECT 'Is Spatially Connected' AS 'FunctionInfo' + ,[dbo].[LRS_IsConnected](@geom1, @geom2, @tolerance) AS 'IsConnected'; + +-- 7. IsValid LRS Point +SET @geom = GEOMETRY::STGeomFromText('POINT(0 0 0)', @srid); + +SELECT 'Is Valid LRS Point' AS 'FunctionInfo' + ,[dbo].[LRS_IsValidPoint](@geom) AS 'IsValidLRSPoint'; + +-- 8. Locate Point Along the Geometry Segment +SET @geom = GEOMETRY::STGeomFromText('LINESTRING (0 0 0 0, 10 0 0 10)', @srid); +SET @measure = 5.0; + +SELECT 'Point to Locate' AS 'FunctionInfo' + ,[dbo].[LRS_LocatePointAlongGeom](@geom, @measure) AS 'Geometry' + ,[dbo].[LRS_LocatePointAlongGeom](@geom, @measure).ToString() AS 'Geometry in String'; + +-- 9. Merge two Geometry Segments to one Geometry Segment. +SET @geom1 = GEOMETRY::STGeomFromText('LINESTRING (10 1 NULL 10, 25 1 NULL 25)', @srid); +SET @geom2 = GEOMETRY::STGeomFromText('LINESTRING (30 1 NULL 30, 40 1 NULL 40 )', @srid); + +SELECT 'Merge Geometry Segments' AS 'FunctionInfo' + ,[dbo].[LRS_MergeGeometrySegments](@geom1, @geom2, @tolerance) AS 'Geometry' + ,[dbo].[LRS_MergeGeometrySegments](@geom1, @geom2, @tolerance).ToString() AS 'Geometry in String'; + +-- 10. Merge two Geometry Segments to one Geometry Segment and reset there measures +SET @geom1 = GEOMETRY::STGeomFromText('LINESTRING (10 15 9, 10 14 12, 10 10 20)', @srid); +SET @geom2 = GEOMETRY::STGeomFromText('LINESTRING (9.5 14.5 100, 10 18 10, 10 10 5)', @srid); + +SELECT 'Merge and Reset Geometry Segments' AS 'FunctionInfo' + ,[dbo].[LRS_MergeAndResetGeometrySegments](@geom1, @geom2, @tolerance) AS 'Geometry' + ,[dbo].[LRS_MergeAndResetGeometrySegments](@geom1, @geom2, @tolerance).ToString() AS 'Geometry in String'; + +-- 11. OffsetGeometrySegment Function +SET @geom = GEOMETRY::STGeomFromText('LINESTRING(5 10 0, 20 5 30.628, 35 10 61.257, 55 10 100)', @srid); +SET @startMeasure = 0; +SET @endMeasure = 61.5; +SET @offset = 2; +SELECT 'Offset Segment' AS 'FunctionInfo' + ,[dbo].[LRS_OffsetGeometrySegments](@geom, @startMeasure, @endMeasure, @offset, @tolerance) AS 'Geometry' + ,[dbo].[LRS_OffsetGeometrySegments](@geom, @startMeasure, @endMeasure, @offset, @tolerance).ToString() AS 'Geometry in String' + +-- 12. Populate geometry measures. +SET @startMeasure = 10; +SET @endMeasure = 40; +SET @geom = GEOMETRY::STGeomFromText('LINESTRING (10 1 10 100, 15 1 10 NULL, 20 1 10 NULL, 25 1 10 250 )', @srid); + +SELECT 'Populate Geometric Measures' AS 'FunctionInfo' + ,[dbo].[LRS_PopulateGeometryMeasures](@geom, @startMeasure, @endMeasure) AS 'Geometry' + ,[dbo].[LRS_PopulateGeometryMeasures](@geom, @startMeasure, @endMeasure).ToString() AS 'Geometry in String'; + +-- 13. Convert To Lrs Geometry +SET @startMeasure = 10; +SET @endMeasure = 40; +SET @geom = GEOMETRY::STGeomFromText('LINESTRING (10 1 , 15 1, 20 1, 25 1)', @srid); +SELECT 'Convert Lrs geometry' AS 'FunctionInfo' + ,[dbo].[LRS_ConvertToLrsGeom](@geom, @startMeasure, @endMeasure) AS 'Geometry' + ,[dbo].[LRS_ConvertToLrsGeom](@geom, @startMeasure, @endMeasure).ToString() AS 'Geometry in String'; + +-- 14. Reset Measure +SET @geom = GEOMETRY::STGeomFromText('LINESTRING (1 1 0 10, 5 5 0 25)', @srid); + +SELECT 'Reset Measure' AS 'FunctionInfo' + ,[dbo].[LRS_ResetMeasure](@geom) AS 'Geometry' + ,@geom.ToString() AS 'Input Line' + ,[dbo].[LRS_ResetMeasure](@geom).ToString() AS 'Geometry in String' + +-- 15. Reverse Line String +SET @geom = GEOMETRY::STGeomFromText('LINESTRING (1 1 0 0, 5 5 0 0)', @srid); + +SELECT 'Reverse Linear Geometry' AS 'FunctionInfo' + ,[dbo].[LRS_ReverseLinearGeometry](@geom) AS 'Geometry' + ,@geom.ToString() AS 'Input Line' + ,[dbo].[LRS_ReverseLinearGeometry](@geom).ToString() AS 'Geometry in String' + +-- 16. Scale Geometry Measures +SET @geom = GEOMETRY::STGeomFromText('LINESTRING (2 2 0 6, 2 4 0 7, 8 4 0 8)', @srid); +SET @startMeasure = 10; +SET @endMeasure = 40; +SET @measure = 5; + +SELECT 'Scale Geometry Measures' AS 'FunctionInfo' + ,[dbo].[LRS_ScaleGeometrySegment](@geom, @startMeasure, @endMeasure, @measure) AS 'Geometry' + ,@geom.ToString() AS 'Input Line' + ,[dbo].[LRS_ScaleGeometrySegment](@geom, @startMeasure, @endMeasure, @measure).ToString() AS 'Geometry in String' + +-- 17. Split Geometry Segment +SET @geom = GEOMETRY::STGeomFromText('LINESTRING (10 1 NULL 10, 25 1 NULL 25)', @srid); +SET @measure = 15; +EXECUTE [dbo].[LRS_SplitGeometrySegment] + @geom + ,@measure + ,@geom1 OUTPUT + ,@geom2 OUTPUT + +SELECT 'Split Line Segment' AS 'FunctionInfo' + ,@geom.ToString() AS 'Input Line' + ,@geom1.ToString() AS 'Line Segment 1' + ,@geom2.ToString() AS 'Line Segment 2' + +-- 18. Translate Linear Geometry Measure +SET @geom = GEOMETRY::STGeomFromText('LINESTRING (1 1 0 10, 5 5 0 20)', @srid); +SET @measure = 5; + +SELECT 'Translate Measures' AS 'FunctionInfo' + ,[dbo].[LRS_TranslateMeasure](@geom, @measure) AS 'Geometry' + ,@geom.ToString() AS 'Input Line' + ,[dbo].[LRS_TranslateMeasure](@geom, @measure).ToString() AS 'Geometry in String' + +-- 19. Validate LRS Segment +SET @geom1 = GEOMETRY::STGeomFromText('LINESTRING (1 1 0 0, 5 5 0 0)', @srid); +SET @geom2 = GEOMETRY::STGeomFromText('LINESTRING (2 2 0, 2 4 2, 8 4 8, 12 4 12, 12 10 29, 8 10 22, 5 14 27)', @srid); +SET @geom = GEOMETRY::STGeomFromText('LINESTRING (2 2, 2 4, 8 4)', @srid); + +SELECT 'Validate LRS Segment' AS 'FunctionInfo' + ,@geom1.ToString() AS 'Input Geom Segment' + ,[dbo].[LRS_ValidateLRSGeometry](@geom1) AS 'Valid State' +UNION +SELECT 'Validate LRS Segment' AS 'FunctionInfo' + ,@geom2.ToString() AS 'Input Geom Segment' + ,[dbo].[LRS_ValidateLRSGeometry](@geom2) AS 'Valid State' +UNION +SELECT 'Validate LRS Segment' AS 'FunctionInfo' + ,@geom.ToString() AS 'Input Geom Segment' + ,[dbo].[LRS_ValidateLRSGeometry](@geom) AS 'Valid State'; diff --git a/SQL Scripts/makevalid_example.sql b/src/SQL Scripts/makevalid_example.sql similarity index 62% rename from SQL Scripts/makevalid_example.sql rename to src/SQL Scripts/makevalid_example.sql index cf8b267..4af377a 100644 --- a/SQL Scripts/makevalid_example.sql +++ b/src/SQL Scripts/makevalid_example.sql @@ -1,8 +1,8 @@ --- Simple bow-tie polygon -declare @geog varchar(max) = 'POLYGON ((0 0, 10 2, 10 0, 0 2, 0 0))'; - -select dbo.IsValidGeographyFromText(@geog2, 4326); --- returns 0 without throwing an exception - -select dbo.MakeValidGeographyFromText(@geog2, 4326); +-- Simple bow-tie polygon +declare @geog varchar(max) = 'POLYGON ((0 0, 10 2, 10 0, 0 2, 0 0))'; + +select dbo.IsValidGeographyFromText(@geog, 4326); +-- returns 0 without throwing an exception + +select dbo.MakeValidGeographyFromText(@geog, 4326); -- returns multipolygon of two triangles \ No newline at end of file diff --git a/SQL Scripts/projection_example.sql b/src/SQL Scripts/projection_example.sql similarity index 98% rename from SQL Scripts/projection_example.sql rename to src/SQL Scripts/projection_example.sql index 417c6bc..bbf7f6c 100644 --- a/SQL Scripts/projection_example.sql +++ b/src/SQL Scripts/projection_example.sql @@ -1,7 +1,7 @@ --- Project point and linestring using Albers Equal Area projection -declare @albers Projection -set @albers = Projection::AlbersEqualArea(0, 0, 0, 60) - -select @albers.Project('POINT (45 30)').ToString() -select @albers.Unproject(@albers.Project('LINESTRING (10 0, 10 10)')).ToString() +-- Project point and linestring using Albers Equal Area projection +declare @albers Projection +set @albers = Projection::AlbersEqualArea(0, 0, 0, 60) + +select @albers.Project('POINT (45 30)').ToString() +select @albers.Unproject(@albers.Project('LINESTRING (10 0, 10 10)')).ToString() select @albers.ToString() \ No newline at end of file diff --git a/SQL Scripts/transform_example.sql b/src/SQL Scripts/transform_example.sql similarity index 98% rename from SQL Scripts/transform_example.sql rename to src/SQL Scripts/transform_example.sql index 626c122..ebc5445 100644 --- a/SQL Scripts/transform_example.sql +++ b/src/SQL Scripts/transform_example.sql @@ -1,11 +1,11 @@ --- Rotate line by 30 degrees counter-clockwise around (0 0) -select AffineTransform::Rotate(45).Apply('LINESTRING (5 0, 10 0)').ToString() --- Returns: LINESTRING (3.5355339059327378 3.5355339059327373, 7.0710678118654755 7.0710678118654746) - --- Decrease line size 5 times -select AffineTransform::Scale(0.2, 0.2).Apply('LINESTRING (5 0, 10 0)').ToString() --- Returns: LINESTRING (1 0, 2 0) - --- Move line down by 2 units -select AffineTransform::Translate(0, -2).Apply('LINESTRING (5 0, 10 0)').ToString() +-- Rotate line by 30 degrees counter-clockwise around (0 0) +select AffineTransform::Rotate(45).Apply('LINESTRING (5 0, 10 0)').ToString() +-- Returns: LINESTRING (3.5355339059327378 3.5355339059327373, 7.0710678118654755 7.0710678118654746) + +-- Decrease line size 5 times +select AffineTransform::Scale(0.2, 0.2).Apply('LINESTRING (5 0, 10 0)').ToString() +-- Returns: LINESTRING (1 0, 2 0) + +-- Move line down by 2 units +select AffineTransform::Translate(0, -2).Apply('LINESTRING (5 0, 10 0)').ToString() -- Returns: LINESTRING (5 -2, 10 -2) \ No newline at end of file diff --git a/src/SQL Scripts/util_geometry_example.sql b/src/SQL Scripts/util_geometry_example.sql new file mode 100644 index 0000000..c2790b4 --- /dev/null +++ b/src/SQL Scripts/util_geometry_example.sql @@ -0,0 +1,36 @@ +-- Script that shows examples on exposed Util Functions in SQLSpatialTools Library +-- Util Geometric Functions +DECLARE @geom geometry; +DECLARE @geom1 geometry +DECLARE @geom2 geometry +DECLARE @srid INT = 4326; + +-- 1. Utility Function - Polygon To Line +SET @geom = GEOMETRY::STGeomFromText('POLYGON((-5 -5, -5 5, 5 5, 5 -5, -5 -5), (0 0, 3 0, 3 3, 0 3, 0 0))', @srid); +SET @geom1 = GEOMETRY::STGeomFromText('CURVEPOLYGON((-122.3 47, 122.3 -47, 125.7 -49, 121 -38, -122.3 47)) ', @srid); +SET @geom2 = GEOMETRY::STGeomFromText('MULTIPOLYGON(((1 1, 1 1, 1 1, 1 1)),((1 1, 3 1, 3 3, 1 3, 1 1)))', @srid); + +SELECT 'Polygon To Line' AS 'FunctionInfo' + ,@geom.ToString() AS 'Input Geom Segment' + ,[dbo].[Util_PolygonToLine](@geom).ToString() AS 'Converted Line' +UNION ALL +SELECT 'Polygon To Line' AS 'FunctionInfo' + ,@geom1.ToString() AS 'Input Geom Segment' + ,[dbo].[Util_PolygonToLine](@geom1).ToString() AS 'Converted Line' +UNION ALL +SELECT 'Polygon To Line' AS 'FunctionInfo' + ,@geom2.ToString() AS 'Input Geom Segment' + ,[dbo].[Util_PolygonToLine](@geom2).ToString() AS 'Converted Line'; + +-- 2. Utility Function - Extract +SELECT 'Extract' AS 'FunctionInfo' + ,@geom.ToString() AS 'Input Geom Segment' + ,[dbo].[Util_Extract](@geom, 1, 2).ToString() AS 'Extracted Geom' + UNION ALL +SELECT 'Extract' AS 'FunctionInfo' + ,@geom1.ToString() AS 'Input Geom Segment' + ,[dbo].[Util_Extract](@geom, 1, 1).ToString() AS 'Extracted Geom' +UNION ALL +SELECT 'Extract' AS 'FunctionInfo' + ,@geom2.ToString() AS 'Input Geom Segment' + ,[dbo].[Util_Extract](@geom, 1, 2).ToString() AS 'Extracted Geom'; \ No newline at end of file diff --git a/SQLSpatialTools.csproj b/src/SQLSpatialTools.csproj similarity index 57% rename from SQLSpatialTools.csproj rename to src/SQLSpatialTools.csproj index 0ea0e38..1d9f1c5 100644 --- a/SQLSpatialTools.csproj +++ b/src/SQLSpatialTools.csproj @@ -1,188 +1,240 @@ - - - - Debug - AnyCPU - 9.0.30729 - 2.0 - {09428C16-7DAE-4C28-A853-E4F04DD0BEDD} - Library - Properties - SQLSpatialTools - SQLSpatialTools - v3.5 - 512 - - - 4.0 - - - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - false - true - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - AllRules.ruleset - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - AllRules.ruleset - - - - False - .\Microsoft.SqlServer.Types.dll - - - - 3.5 - - - 3.5 - - - 3.5 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - - - - - PreserveNewest - - - PreserveNewest - - - - - False - .NET Framework 3.5 SP1 Client Profile - false - - - False - .NET Framework 3.5 SP1 - true - - - False - Windows Installer 3.1 - true - - - - + + + + 10.0 + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {09428C16-7DAE-4C28-A853-E4F04DD0BEDD} + Library + Properties + SQLSpatialTools + SQLSpatialTools + v3.5 + 512 + + + 4.0 + + + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + true + full + false + ..\output\lib + ..\output\intermediate\lib + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + + + full + true + ..\output\lib + ..\output\intermediate\lib + TRACE + prompt + 4 + AllRules.ruleset + true + + + true + + + ..\SpatialTools.pfx + + + + False + ..\packages\Microsoft.SqlServer.Types.14.0.1016.290\lib\net40\Microsoft.SqlServer.Types.dll + + + + 3.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + true + + + False + Windows Installer 3.1 + true + + + + + PreserveNewest + + + + + Always + + + + + + This project references NuGet package(s) that are missing on this computer. %0AUse NuGet Package Restore to download them. %0AFor more information, see http://go.microsoft.com/fwlink/?LinkID=322105. %0AThe missing file is {0}. + + + + + if exist "$(ProjectDir)obj" rd "$(ProjectDir)obj" /S /Q +powershell.exe -ExecutionPolicy Unrestricted [io.file]::WriteAllText('$(TargetDir)SQL Scripts\Register.sql', ((gc '$(TargetDir)SQL Scripts\Register.sql') -replace 'DLLPath', '$(TargetPath)' -join \"`r`n\")) +powershell.exe -ExecutionPolicy Unrestricted [io.file]::WriteAllText('$(TargetDir)SQL Scripts\RegisterAndValidate.sql', ((gc '$(TargetDir)SQL Scripts\RegisterAndValidate.sql') -replace 'ScriptDirPath', '$(TargetDir)SQL Scripts' -join \"`r`n\")) +exit 0 + + \ No newline at end of file diff --git a/Sinks/DensifyGeographySink.cs b/src/Sinks/Geography/DensifyGeographySink.cs similarity index 59% rename from Sinks/DensifyGeographySink.cs rename to src/Sinks/Geography/DensifyGeographySink.cs index d2219c3..26a6890 100644 --- a/Sinks/DensifyGeographySink.cs +++ b/src/Sinks/Geography/DensifyGeographySink.cs @@ -1,122 +1,126 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -using System; -using Microsoft.SqlServer.Types; - -namespace SQLSpatialTools -{ - /// - /// This class is used for generating a new geography object where additional points are inserted - /// along every line in such a way that the angle between two consecutive points does not - /// exceed a prescribed angle. The points are generated between the unit vectors that correspond - /// to the line's start and end along the great-circle arc on the unit sphere . This follows the - /// definition of geodetic lines in SQL Server. - /// - public class DensifyGeographySink : IGeographySink - { - // Minimum angle. If the user specifies a smaller angle, the angle will be set to this minimum. - public static readonly double MinAngle = 0.000001; - - // Maximum angular difference in degrees between two consecutive points in the "densified" line. - private readonly double _angle; - - // Previous point added. - private Vector3 _startPoint; - - private readonly IGeographySink _sink; - - // Constructor. - public DensifyGeographySink(IGeographySink sink, double angle) - { - if (sink == null) - throw new ArgumentNullException("sink"); - _sink = sink; - - if (angle < MinAngle) - _angle = MinAngle; - else - _angle = angle; - } - - #region IGeographySink Members - - public void AddLine(double latitude, double longitude, double? z, double? m) - { - // Transforming from geodetic coordinates to a unit vector. - Vector3 endPoint = Util.SphericalDegToCartesian(latitude, longitude); - - double angle = endPoint.Angle(_startPoint); - if (angle > MinAngle) - { - // _startPoint and endPoint are the unit vectors that correspond to the input - // start and end points. In their 3D space we operate in a local coordinate system - // where _startPoint is the x axis and the xy plane contains endPoint. Every - // point is now generated from the previous one by a fixed rotation in the local - // xy plane, and converted back to geodetic coordinates. - - // Construct the local z and y axes. - Vector3 zAxis = (_startPoint + endPoint).CrossProduct(_startPoint - endPoint).Unitize(); - Vector3 yAxis = (_startPoint).CrossProduct(zAxis); - - // Calculating how many points we need. - int count = Convert.ToInt32(Math.Ceiling(angle / Util.ToRadians(_angle))); - - // Scaling the angle so that points are equaly placed. - double exactAngle = angle / count; - - double cosine = Math.Cos(exactAngle); - double sine = Math.Sin(exactAngle); - - // Setting the first x and y points in our local coordinate system. - double x = cosine; - double y = sine; - - for (int i = 0; i < count - 1; i++) - { - Vector3 newPoint = (_startPoint * x + yAxis * y).Unitize(); - - // Adding the point. - _sink.AddLine(Util.LatitudeDeg(newPoint), Util.LongitudeDeg(newPoint), null, null); - - // Rotating to get next point. - double r = x * cosine - y * sine; - y = x * sine + y * cosine; - x = r; - } - } - _sink.AddLine(latitude, longitude, z, m); - - // Remembering last point we added. - _startPoint = endPoint; - } - - public void BeginFigure(double latitude, double longitude, double? z, double? m) - { - // Starting the figure, remembering the vector that corresponds to the first point. - _startPoint = Util.SphericalDegToCartesian(latitude, longitude); - _sink.BeginFigure(latitude, longitude, z, m); - } - - public void BeginGeography(OpenGisGeographyType type) - { - _sink.BeginGeography(type); - } - - public void EndFigure() - { - _sink.EndFigure(); - } - - public void EndGeography() - { - _sink.EndGeography(); - } - - public void SetSrid(int srid) - { - _sink.SetSrid(srid); - } - - #endregion - } +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Types; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.Sinks.Geography +{ + /// + /// This class is used for generating a new geography object where additional points are inserted + /// along every line in such a way that the angle between two consecutive points does not + /// exceed a prescribed angle. The points are generated between the unit vectors that correspond + /// to the line's start and end along the great-circle arc on the unit sphere . This follows the + /// definition of geodetic lines in SQL Server. + /// + public class DensifyGeographySink : IGeographySink110 + { + // Minimum angle. If the user specifies a smaller angle, the angle will be set to this minimum. + private const double MinAngle = 0.000001; + + // Maximum angular difference in degrees between two consecutive points in the "densified" line. + private readonly double _angle; + + // Previous point added. + private Vector3 _startPoint; + + private readonly IGeographySink110 _sink; + + // Constructor. + public DensifyGeographySink(IGeographySink110 sink, double angle) + { + _sink = sink ?? throw new ArgumentNullException(nameof(sink), "sink"); + + _angle = angle < MinAngle ? MinAngle : angle; + } + + #region IGeographySink Members + + public void AddLine(double latitude, double longitude, double? z, double? m) + { + // Transforming from geodetic coordinates to a unit vector. + var endPoint = SpatialUtil.SphericalDegToCartesian(latitude, longitude); + + var angle = endPoint.Angle(_startPoint); + if (angle > MinAngle) + { + // _startPoint and endPoint are the unit vectors that correspond to the input + // start and end points. In their 3D space we operate in a local coordinate system + // where _startPoint is the x axis and the xy plane contains endPoint. Every + // point is now generated from the previous one by a fixed rotation in the local + // xy plane, and converted back to geodetic coordinates. + + // Construct the local z and y axes. + var zAxis = (_startPoint + endPoint).CrossProduct(_startPoint - endPoint).Unitize(); + var yAxis = (_startPoint).CrossProduct(zAxis); + + // Calculating how many points we need. + var count = Convert.ToInt32(Math.Ceiling(angle / SpatialUtil.ToRadians(_angle))); + + // Scaling the angle so that points are equally placed. + var exactAngle = angle / count; + + var cosine = Math.Cos(exactAngle); + var sine = Math.Sin(exactAngle); + + // Setting the first x and y points in our local coordinate system. + var x = cosine; + var y = sine; + + for (var i = 0; i < count - 1; i++) + { + var newPoint = (_startPoint * x + yAxis * y).Unitize(); + + // Adding the point. + _sink.AddLine(SpatialUtil.LatitudeDeg(newPoint), SpatialUtil.LongitudeDeg(newPoint), null, null); + + // Rotating to get next point. + var r = x * cosine - y * sine; + y = x * sine + y * cosine; + x = r; + } + } + _sink.AddLine(latitude, longitude, z, m); + + // Remembering last point we added. + _startPoint = endPoint; + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void BeginFigure(double latitude, double longitude, double? z, double? m) + { + // Starting the figure, remembering the vector that corresponds to the first point. + _startPoint = SpatialUtil.SphericalDegToCartesian(latitude, longitude); + _sink.BeginFigure(latitude, longitude, z, m); + } + + public void BeginGeography(OpenGisGeographyType type) + { + _sink.BeginGeography(type); + } + + public void EndFigure() + { + _sink.EndFigure(); + } + + public void EndGeography() + { + _sink.EndGeography(); + } + + public void SetSrid(int srid) + { + _sink.SetSrid(srid); + } + + #endregion + } } \ No newline at end of file diff --git a/src/Sinks/Geography/GeographyFilters.cs b/src/Sinks/Geography/GeographyFilters.cs new file mode 100644 index 0000000..d3c6614 --- /dev/null +++ b/src/Sinks/Geography/GeographyFilters.cs @@ -0,0 +1,367 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Types; + +namespace SQLSpatialTools.Sinks.Geography +{ + public class GeographyEmptyShapeFilter : IGeographySink110 + { + private readonly IGeographySink110 _sink; + private readonly Queue _types = new Queue(); + private bool _root = true; + + public GeographyEmptyShapeFilter(IGeographySink110 sink) + { + _sink = sink; + } + + public void SetSrid(int srid) + { + _sink.SetSrid(srid); + } + + public void BeginGeography(OpenGisGeographyType type) + { + if (_root) + { + _root = false; + _sink.BeginGeography(type); + } + else + { + _types.Enqueue(type); + } + } + + public void EndGeography() + { + if (_types.Count > 0) + _types.Dequeue(); + else + _sink.EndGeography(); + } + + public void BeginFigure(double latitude, double longitude, double? z, double? m) + { + while (_types.Count > 0) + _sink.BeginGeography(_types.Dequeue()); + _sink.BeginFigure(latitude, longitude, z, m); + } + + public void AddLine(double latitude, double longitude, double? z, double? m) + { + _sink.AddLine(latitude, longitude, z, m); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + _sink.EndFigure(); + } + } + + public class GeographyPointFilter : IGeographySink110 + { + private readonly IGeographySink110 _sink; + private int _depth; + private bool _root = true; + + public GeographyPointFilter(IGeographySink110 sink) + { + _sink = sink; + } + + public void SetSrid(int srid) + { + _sink.SetSrid(srid); + } + + public void BeginGeography(OpenGisGeographyType type) + { + if (type == OpenGisGeographyType.Point || type == OpenGisGeographyType.MultiPoint) + { + if (_root) + { + _root = false; + _sink.BeginGeography(OpenGisGeographyType.GeometryCollection); + _sink.EndGeography(); + } + _depth++; + } + else + { + _root = false; + _sink.BeginGeography(type); + } + } + + public void EndGeography() + { + if (_depth > 0) + _depth--; + else + _sink.EndGeography(); + } + + public void BeginFigure(double latitude, double longitude, double? z, double? m) + { + if (_depth == 0) + _sink.BeginFigure(latitude, longitude, z, m); + } + + public void AddLine(double latitude, double longitude, double? z, double? m) + { + if (_depth == 0) + _sink.AddLine(latitude, longitude, z, m); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + if (_depth == 0) + _sink.EndFigure(); + } + } + + public class GeographyShortLineStringFilter : IGeographySink110 + { + private readonly IGeographySink110 _sink; + private readonly double _tolerance; + private int _srid; + private bool _insideLineString; + private readonly List _figure = new List(); + + public GeographyShortLineStringFilter(IGeographySink110 sink, double tolerance) + { + _sink = sink; + _tolerance = tolerance; + } + + public void SetSrid(int srid) + { + _srid = srid; + _sink.SetSrid(srid); + } + + public void BeginGeography(OpenGisGeographyType type) + { + _sink.BeginGeography(type); + _insideLineString = type == OpenGisGeographyType.LineString; + } + + public void EndGeography() + { + _sink.EndGeography(); + } + + public void BeginFigure(double latitude, double longitude, double? z, double? m) + { + if (_insideLineString) + { + _figure.Clear(); + _figure.Add(new Vertex(latitude, longitude, z, m)); + } + else + { + _sink.BeginFigure(latitude, longitude, z, m); + } + } + + public void AddLine(double latitude, double longitude, double? z, double? m) + { + if (_insideLineString) + { + _figure.Add(new Vertex(latitude, longitude, z, m)); + } + else + { + _sink.AddLine(latitude, longitude, z, m); + } + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + if (_insideLineString) + { + if (!IsShortLineString()) + { + PopulateFigure(_sink); + } + } + else + { + _sink.EndFigure(); + } + } + + private bool IsShortLineString() + { + try + { + SqlGeographyBuilder b = new SqlGeographyBuilder(); + b.SetSrid(_srid); + b.BeginGeography(OpenGisGeographyType.LineString); + PopulateFigure(b); + b.EndGeography(); + SqlGeography g = b.ConstructedGeography; + return g.STLength().Value < _tolerance; + } + catch (FormatException) { } + catch (ArgumentException) { } + return false; + } + + private void PopulateFigure(IGeographySink110 sink) + { + _figure[0].BeginFigure(sink); + for (int i = 1; i < _figure.Count; i++) + _figure[i].AddLine(sink); + sink.EndFigure(); + } + } + + public class GeographyThinRingFilter : IGeographySink110 + { + private readonly IGeographySink110 _sink; + private readonly double _tolerance; + private bool _insidePolygon; + private int _srid; + private readonly List _figure = new List(); + + public GeographyThinRingFilter(IGeographySink110 sink, double tolerance) + { + _sink = sink; + _tolerance = tolerance; + } + + public void SetSrid(int srid) + { + _srid = srid; + _sink.SetSrid(srid); + } + + public void BeginGeography(OpenGisGeographyType type) + { + _sink.BeginGeography(type); + _insidePolygon = type == OpenGisGeographyType.Polygon; + } + + public void EndGeography() + { + _sink.EndGeography(); + } + + public void BeginFigure(double latitude, double longitude, double? z, double? m) + { + if (_insidePolygon) + { + _figure.Clear(); + _figure.Add(new Vertex(latitude, longitude, z, m)); + } + else + { + _sink.BeginFigure(latitude, longitude, z, m); + } + } + + public void AddLine(double latitude, double longitude, double? z, double? m) + { + if (_insidePolygon) + { + _figure.Add(new Vertex(latitude, longitude, z, m)); + } + else + { + _sink.AddLine(latitude, longitude, z, m); + } + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + if (_insidePolygon) + { + if (!IsThinRing()) + { + PopulateFigure(_sink, false); + } + } + else + { + _sink.EndFigure(); + } + } + + private bool IsThinRing() + { + SqlGeography poly = RingToPolygon(true); + if (poly == null) + { + // ring was not valid, try with different orientation + poly = RingToPolygon(false); + if (poly == null) + { + // if both orientations are invalid, we are dealing with very thin ring + // so just return true + return true; + } + } + return poly.STArea().Value < _tolerance * poly.STLength().Value; + } + + private SqlGeography RingToPolygon(bool reverse) + { + try + { + SqlGeographyBuilder b = new SqlGeographyBuilder(); + b.SetSrid(_srid); + b.BeginGeography(OpenGisGeographyType.Polygon); + PopulateFigure(b, reverse); + b.EndGeography(); + return b.ConstructedGeography; + } + catch (FormatException) { } + catch (ArgumentException) { } + return null; + } + + private void PopulateFigure(IGeographySink110 sink, bool reverse) + { + if (reverse) + { + _figure[_figure.Count - 1].BeginFigure(sink); + for (int i = _figure.Count - 2; i >= 0; i--) + _figure[i].AddLine(sink); + } + else + { + _figure[0].BeginFigure(sink); + for (int i = 1; i < _figure.Count; i++) + _figure[i].AddLine(sink); + } + sink.EndFigure(); + } + } +} \ No newline at end of file diff --git a/Sinks/LocateAlongGeographySink.cs b/src/Sinks/Geography/LocateAlongGeographySink.cs similarity index 65% rename from Sinks/LocateAlongGeographySink.cs rename to src/Sinks/Geography/LocateAlongGeographySink.cs index d7aba14..2b9b4a1 100644 --- a/Sinks/LocateAlongGeographySink.cs +++ b/src/Sinks/Geography/LocateAlongGeographySink.cs @@ -1,101 +1,107 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -using Microsoft.SqlServer.Types; -using System; - -namespace SQLSpatialTools -{ - /** - * This class implements a geography sink that finds a point along a geography linestring instance and pipes - * it to another sink. - */ - class LocateAlongGeographySink : IGeographySink - { - - double _distance; // The running count of how much further we have to go. - SqlGeography _lastPoint; // The last point in the LineString we have passed. - SqlGeography _foundPoint; // This is the point we're looking for, assuming it isn't null, we're done. - int _srid; // The _srid we are working in. - SqlGeographyBuilder _target; // Where we place our result. - - // We target another builder, to which we will send a point representing the point we find. - // We also take a distance, which is the point along the input linestring we will travel. - // Note that we only operate on LineString instances: anything else will throw an exception. - public LocateAlongGeographySink(double distance, SqlGeographyBuilder target) - { - _target = target; - _distance = distance; - } - - // Save the SRID for later - public void SetSrid(int srid) - { - _srid = srid; - } - - // Start the geography. Throw if it isn't a LineString. - public void BeginGeography(OpenGisGeographyType type) - { - if (type != OpenGisGeographyType.LineString) - throw new ArgumentException("This operation may only be executed on LineString instances."); - } - - // Start the figure. Note that since we only operate on LineStrings, this should only be executed - // once. - public void BeginFigure(double latitude, double longitude, double? z, double? m) - { - // Memorize the point. - _lastPoint = SqlGeography.Point(latitude, longitude, _srid); - } - - // This is where the real work is done. - public void AddLine(double latitude, double longitude, double? z, double? m) - { - // If we've already found a point, then we're done. We just need to keep ignoring these - // pesky calls. - if (_foundPoint != null) return; - - // Make a point for our current position. - SqlGeography thisPoint = SqlGeography.Point(latitude, longitude, _srid); - - // is the found point between this point and the last, or past this point? - double length = thisPoint.STDistance(_lastPoint).Value; - if (length < _distance) - { - // it's past this point---just step along the line - _distance -= length; - _lastPoint = thisPoint; - } - else - { - // now we need to do the hard work and find the point in between these two - _foundPoint = Functions.InterpolateBetweenGeog(_lastPoint, thisPoint, _distance); - } - } - - // This is a NOP. - public void EndFigure() - { - } - - // When we end, we'll make all of our output calls to our target. - // Here's also where we catch whether we've run off the end of our LineString. - public void EndGeography() - { - if (_foundPoint != null) - { - // We could use a simple point constructor, but by targetting another sink we can use this - // class in a pipeline. - _target.SetSrid(_srid); - _target.BeginGeography(OpenGisGeographyType.Point); - _target.BeginFigure(_foundPoint.Lat.Value, _foundPoint.Long.Value); - _target.EndFigure(); - _target.EndGeography(); - } - else - { - throw new ArgumentException("Distance provided is greated then the length of the LineString."); - } - } - } -} +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.Sinks.Geography +{ + /// + /// This class implements a geography sink that finds a point along a geography linestring instance and pipes + /// it to another sink. + /// + internal class LocateAlongGeographySink : IGeographySink110 + { + private double _distance; // The running count of how much further we have to go. + private SqlGeography _lastPoint; // The last point in the LineString we have passed. + private SqlGeography _foundPoint; // This is the point we're looking for, assuming it isn't null, we're done. + private int _srid; // The _srid we are working in. + private readonly SqlGeographyBuilder _target; // Where we place our result. + + // We target another builder, to which we will send a point representing the point we find. + // We also take a distance, which is the point along the input linestring we will travel. + // Note that we only operate on LineString instances: anything else will throw an exception. + public LocateAlongGeographySink(double distance, SqlGeographyBuilder target) + { + _target = target; + _distance = distance; + } + + // Save the SRID for later + public void SetSrid(int srid) + { + _srid = srid; + } + + // Start the geography. Throw if it isn't a LineString. + public void BeginGeography(OpenGisGeographyType type) + { + if (type != OpenGisGeographyType.LineString) + throw new ArgumentException("This operation may only be executed on LineString instances."); + } + + // Start the figure. Note that since we only operate on LineStrings, this should only be executed + // once. + public void BeginFigure(double latitude, double longitude, double? z, double? m) + { + // Memorize the point. + _lastPoint = SqlGeography.Point(latitude, longitude, _srid); + } + + // This is where the real work is done. + public void AddLine(double latitude, double longitude, double? z, double? m) + { + // If we've already found a point, then we're done. We just need to keep ignoring these + // pesky calls. + if (_foundPoint != null) return; + + // Make a point for our current position. + var thisPoint = SqlGeography.Point(latitude, longitude, _srid); + + // is the found point between this point and the last, or past this point? + var length = thisPoint.STDistance(_lastPoint).Value; + if (length < _distance) + { + // it's past this point---just step along the line + _distance -= length; + _lastPoint = thisPoint; + } + else + { + // now we need to do the hard work and find the point in between these two + _foundPoint = Functions.General.Geography.InterpolateBetweenGeog(_lastPoint, thisPoint, _distance); + } + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + // This is a NOP. + public void EndFigure() + { + } + + // When we end, we'll make all of our output calls to our target. + // Here's also where we catch whether we've run off the end of our LineString. + public void EndGeography() + { + if (_foundPoint != null) + { + // We could use a simple point constructor, but by targeting another sink we can use this + // class in a pipeline. + _target.SetSrid(_srid); + _target.BeginGeography(OpenGisGeographyType.Point); + _target.BeginFigure(_foundPoint.Lat.Value, _foundPoint.Long.Value); + _target.EndFigure(); + _target.EndGeography(); + } + else + { + throw new ArgumentException("Distance provided is greater then the length of the LineString."); + } + } + } +} diff --git a/src/Sinks/Geography/Projector.cs b/src/Sinks/Geography/Projector.cs new file mode 100644 index 0000000..87ae11f --- /dev/null +++ b/src/Sinks/Geography/Projector.cs @@ -0,0 +1,62 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Types.SQL; + +namespace SQLSpatialTools.Sinks.Geography +{ + /// + /// This class projects a geography segment based on specified project to a geometry segment. + /// + public sealed class Projector : IGeographySink110 + { + private readonly SqlProjection _projection; + private readonly IGeometrySink110 _sink; + + public Projector(SqlProjection projection, IGeometrySink110 sink) + { + _projection = projection; + _sink = sink; + } + + public void BeginGeography(OpenGisGeographyType type) + { + _sink.BeginGeometry((OpenGisGeometryType)type); + } + + public void EndGeography() + { + _sink.EndGeometry(); + } + + public void BeginFigure(double latitude, double longitude, double? z, double? m) + { + _projection.ProjectPoint(latitude, longitude, out var x, out var y); + _sink.BeginFigure(x, y, z, m); + } + + public void AddLine(double latitude, double longitude, double? z, double? m) + { + _projection.ProjectPoint(latitude, longitude, out var x, out var y); + _sink.AddLine(x, y, z, m); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + _sink.EndFigure(); + } + + public void SetSrid(int srid) + { + _sink.SetSrid(srid); + } + } +} \ No newline at end of file diff --git a/src/Sinks/Geography/VacuousGeographyToGeometrySink.cs b/src/Sinks/Geography/VacuousGeographyToGeometrySink.cs new file mode 100644 index 0000000..5a1b861 --- /dev/null +++ b/src/Sinks/Geography/VacuousGeographyToGeometrySink.cs @@ -0,0 +1,62 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.Sinks.Geography +{ + /// + /// This class implements a completely trivial conversion from geography to geometry, simply taking each + /// point(lat, long) --> (y, x). The class takes a target geometry sink, as well as the target SRID to + /// assign to the results. + /// + public class VacuousGeographyToGeometrySink : IGeographySink110 + { + private readonly IGeometrySink110 _target; + private readonly int _targetSrid; + + public VacuousGeographyToGeometrySink(int targetSrid, IGeometrySink110 target) + { + _target = target; + _targetSrid = targetSrid; + } + + public void AddLine(double latitude, double longitude, double? z, double? m) + { + _target.AddLine(longitude, latitude, z, m); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void BeginFigure(double latitude, double longitude, double? z, double? m) + { + _target.BeginFigure(longitude, latitude, z, m); + } + + public void BeginGeography(OpenGisGeographyType type) + { + // Convert geography to geometry types... + _target.BeginGeometry((OpenGisGeometryType) type); + } + + public void EndFigure() + { + _target.EndFigure(); + } + + public void EndGeography() + { + _target.EndGeometry(); + } + + public void SetSrid(int srid) + { + _target.SetSrid(_targetSrid); + } + } +} diff --git a/src/Sinks/Geometry/BuidLRSMultiLineSink.cs b/src/Sinks/Geometry/BuidLRSMultiLineSink.cs new file mode 100644 index 0000000..b620fb5 --- /dev/null +++ b/src/Sinks/Geometry/BuidLRSMultiLineSink.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Utility; +using System; +using System.Text; + +namespace SQLSpatialTools +{ + /// + /// This class implements a geometry sink that builds LRS multiline. + /// Second segment measure is updated with offset difference. + /// + class BuildLRSMultiLineSink : IGeometrySink110 + { + private LRSLine CurrentLine; + public LRSMultiLine MultiLine; + private int Srid; + + public void ScaleMeasure(double offsetMeasure) + { + MultiLine.ScaleMeasure(offsetMeasure); + } + public void TranslateMeasure(double offsetMeasure) + { + MultiLine.TranslateMeasure(offsetMeasure); + } + /// + /// Initializes a new instance of the class. + /// + public BuildLRSMultiLineSink() + { + MultiLine = new LRSMultiLine(); + } + + public void SetSrid(int srid) + { + Srid = srid; + } + + // Start the geometry. + public void BeginGeometry(OpenGisGeometryType type) + { + if (type == OpenGisGeometryType.LineString) + { + CurrentLine = new LRSLine(); + CurrentLine.SetSrid(Srid); + } + } + + // Start the figure. + public void BeginFigure(double x, double y, double? z, double? m) + { + CurrentLine.AddPoint(new LRSPoint(x, y, z, m)); + } + + // This is where the real work is done. + public void AddLine(double x, double y, double? z, double? m) + { + CurrentLine.AddPoint(new LRSPoint(x, y, z, m)); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + // This is a NO-OP + public void EndFigure() + { + MultiLine.AddLine(CurrentLine); + } + + // This is a NO-OP + public void EndGeometry() + { + } + public SqlGeometry ToSqlGeometry() + { + return MultiLine.ToSqlGeometry(); + } + } +} diff --git a/src/Sinks/Geometry/BuildLRSMultiLineSink.cs b/src/Sinks/Geometry/BuildLRSMultiLineSink.cs new file mode 100644 index 0000000..5d80b1e --- /dev/null +++ b/src/Sinks/Geometry/BuildLRSMultiLineSink.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Types; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that builds LRS multiline. + /// Second segment measure is updated with offset difference. + /// + internal class BuildLRSMultiLineSink : IGeometrySink110 + { + private int _srid; + private readonly bool _doUpdateM; + private readonly double _offsetM; + private LRSLine _currentLine; + public LRSMultiLine MultiLine; + + public BuildLRSMultiLineSink(bool doUpdateM, double? offsetM) + { + _doUpdateM = doUpdateM; + _offsetM = offsetM ?? 0; + } + + // Initialize MultiLine and sets srid. + public void SetSrid(int srid) + { + MultiLine = new LRSMultiLine(srid); + _srid = srid; + } + + // Start the geometry. + public void BeginGeometry(OpenGisGeometryType type) + { + if (type == OpenGisGeometryType.LineString) + _currentLine = new LRSLine(_srid); + } + + // Just add the points to the current line. + public void BeginFigure(double x, double y, double? z, double? m) + { + var currentM = _doUpdateM ? m + _offsetM : m; + _currentLine.AddPoint(x, y, z, currentM); + } + + // Just add the points to the current line. + public void AddLine(double x, double y, double? z, double? m) + { + var currentM = _doUpdateM ? m + _offsetM : m; + _currentLine.AddPoint(x, y, z, currentM); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + // Add the current line to the MULTILINESTRING collection + public void EndFigure() + { + MultiLine.AddLine(_currentLine); + } + + // This is a NO-OP + public void EndGeometry() + { + } + } +} diff --git a/src/Sinks/Geometry/BuildMultiLineFromLinesSink.cs b/src/Sinks/Geometry/BuildMultiLineFromLinesSink.cs new file mode 100644 index 0000000..0eac5e5 --- /dev/null +++ b/src/Sinks/Geometry/BuildMultiLineFromLinesSink.cs @@ -0,0 +1,84 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Utility; +using System; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that builds Multiline from Line String + /// + internal class BuildMultiLineFromLinesSink : IGeometrySink110 + { + private readonly SqlGeometryBuilder _target; + private bool _isFirstPoint; + private int _linesCount; + + /// + /// Initializes a new instance of the class. + /// + /// The target. + /// The lines count. + public BuildMultiLineFromLinesSink(SqlGeometryBuilder target, int linesCount) + { + _target = target; + _linesCount = linesCount; + _isFirstPoint = true; + } + + public void SetSrid(int srid) + { + _target.SetSrid(srid); + } + + // Start the geometry. + public void BeginGeometry(OpenGisGeometryType type) + { + if (type == OpenGisGeometryType.Point) + return; + + if (type != OpenGisGeometryType.LineString) + SpatialExtensions.ThrowException(ErrorMessage.LineStringCompatible); + + if (_isFirstPoint) + { + _isFirstPoint = false; + _target.BeginGeometry(OpenGisGeometryType.MultiLineString); + } + _target.BeginGeometry(type); + _linesCount--; + } + + public void BeginFigure(double x, double y, double? z, double? m) + { + _target.BeginFigure(x, y, z, m); + } + + public void AddLine(double x, double y, double? z, double? m) + { + _target.AddLine(x, y, z, m); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + _target.EndFigure(); + } + + public void EndGeometry() + { + _target.EndGeometry(); + + // end of multi line + if (_linesCount == 0) + _target.EndGeometry(); + } + } +} diff --git a/src/Sinks/Geometry/ClipMGeometrySegmentSink.cs b/src/Sinks/Geometry/ClipMGeometrySegmentSink.cs new file mode 100644 index 0000000..c49d58c --- /dev/null +++ b/src/Sinks/Geometry/ClipMGeometrySegmentSink.cs @@ -0,0 +1,272 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Utility; +using System; +using Ext = SQLSpatialTools.Utility.SpatialExtensions; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that clips a line segment based on measure. + /// + internal class ClipMGeometrySegmentSink : IGeometrySink110 + { + // Where we place our result + private readonly SqlGeometryBuilder _target; + private readonly double _tolerance; + private readonly bool _retainClipMeasure; + + private readonly double _clipStartMeasure; + private readonly double _clipEndMeasure; + private double _previousX; + private double _previousY; + private double _previousM; + private bool _started; + private bool _finished; + + // The _srid we are working in. + private int _srid; + + /// + /// We target another builder, to which we will send a point representing the point we find. + /// We also take a distance, which is the point along the input linestring we will travel. + /// Note that we only operate on LineString instances: anything else will throw an exception. + /// + /// Start Measure to be clipped + /// End Measure to be clipped + /// SqlGeometry builder + /// tolerance value + /// Flag to retain ClipMeasure values + public ClipMGeometrySegmentSink(double startMeasure, double endMeasure, SqlGeometryBuilder target, double tolerance, bool retainClipMeasure = false) + { + _target = target; + _clipStartMeasure = startMeasure; + _clipEndMeasure = endMeasure; + _tolerance = tolerance; + _retainClipMeasure = retainClipMeasure; + } + + // Save the SRID for later + public void SetSrid(int srid) + { + _srid = srid; + _target.SetSrid(_srid); + } + + // Start the geometry. Throw if it isn't a LineString. + public void BeginGeometry(OpenGisGeometryType type) + { + if (type != OpenGisGeometryType.LineString) + throw new ArgumentException("This operation may only be executed on LineString instances."); + + _target.BeginGeometry(OpenGisGeometryType.LineString); + } + + public void BeginFigure(double x, double y, double? z, double? m) + { + // Add the first shape point if either the start or end measure matches the clip start and end measure + if (m.EqualsTo(_clipStartMeasure) || m.EqualsTo(_clipEndMeasure)) + { + _target.BeginFigure(x, y, z, m); + _started = true; + } + + UpdateLastPoint(x, y, m); + } + + // This is where the real work is done. + public void AddLine(double x, double y, double? z, double? m) + { + // To unify code for ascending and descending measures - clipPointMeasure + double? clipPointMeasure = GetClipPointMeasure(m); + double newX, newY; + + // If current measure is between start measure and end measure, + // we should add segment to the result linestring + if (m.IsWithinRange(_clipStartMeasure, _clipEndMeasure)) + { + // if the geometry is started, just add the point to line + if (_started) + { + _target.AddLine(x, y, z, m); + } + // Else we need to begin the geom figure first + else + { + var isShapePoint = false; + // if clip point is shape point measure then add the point without computation + if (m.EqualsTo(clipPointMeasure)) + { + _target.BeginFigure(x, y, null, m); + isShapePoint = true; + } + else + { + ComputePointCoordinates(clipPointMeasure, m, x, y, out newX, out newY); + + // if computed point is within tolerance of last point then begin figure with last point + if (Ext.IsWithinTolerance(_previousX, _previousY, newX, newY, _tolerance)) + _target.BeginFigure(_previousX, _previousY, null, _retainClipMeasure ? _clipStartMeasure : _previousM); + + // check with current point against new computed point + else if (Ext.IsWithinTolerance(x, y, newX, newY, _tolerance)) + { + _target.BeginFigure(x, y, null, m); + isShapePoint = true; + } + + // else begin figure with clipped point + else + _target.BeginFigure(newX, newY, null, clipPointMeasure); + } + + _started = true; + if (_clipStartMeasure.EqualsTo(_clipEndMeasure) || isShapePoint) + { + UpdateLastPoint(x, y, m); + return; + } + + _target.AddLine(x, y, z, m); + } + } + // We may still need to add last segment, + // if current point is the first one after we passed range of interest + else + { + if (!_started) + { + var isShapePoint = false; + if (clipPointMeasure.IsWithinRange(m, _previousM)) + { + ComputePointCoordinates(clipPointMeasure, m, x, y, out newX, out newY); + + // if computed point is within tolerance of last point then begin figure with last point + if (Ext.IsWithinTolerance(_previousX, _previousY, newX, newY, _tolerance)) + _target.BeginFigure(_previousX, _previousY, null, _retainClipMeasure ? clipPointMeasure : _previousM); + // check with current point against new computed point + else if (Ext.IsWithinTolerance(x, y, newX, newY, _tolerance)) + { + _target.BeginFigure(x, y, null, _retainClipMeasure ? clipPointMeasure : m); + isShapePoint = true; + } + // else begin figure with clipped point + else + _target.BeginFigure(newX, newY, null, clipPointMeasure); + + _started = true; + if (_clipStartMeasure.EqualsTo(_clipEndMeasure) || isShapePoint) + { + UpdateLastPoint(x, y, m); + return; + } + } + } + if (_started && !_finished) + { + // re calculate clip point measure as it can be changed from above condition. + clipPointMeasure = GetClipPointMeasure(m); + + if (clipPointMeasure.IsWithinRange(m, _previousM)) + { + ComputePointCoordinates(clipPointMeasure, m, x, y, out newX, out newY); + + var isWithinLastPoint = Ext.IsWithinTolerance(_previousX, _previousY, newX, newY, _tolerance); + var isWithinCurrentPoint = Ext.IsWithinTolerance(x, y, newX, newY, _tolerance); + + // if computed point is within tolerance of last point then skip + if (!isWithinLastPoint) + { + // if within current point then add current point + if (isWithinCurrentPoint) + _target.AddLine(x, y, null, _retainClipMeasure ? clipPointMeasure : m); + // else add computed point + else + _target.AddLine(newX, newY, null, clipPointMeasure); + } + + _finished = true; + } + } + } + + // re-assign the current co-ordinates to match for next iteration. + UpdateLastPoint(x, y, m); + } + + private void UpdateLastPoint(double x, double y, double? m) + { + _previousX = x; + _previousY = y; + _previousM = m ?? 0; + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + // This is a NOP. + public void EndFigure() + { + _target.EndFigure(); + } + + // When we end, we'll make all of our output calls to our target. + // Here's also where we catch whether we've run off the end of our LineString. + public void EndGeometry() + { + _target.EndGeometry(); + _started = false; + _finished = false; + _previousX = default(double); + _previousY = default(double); + _previousM = default(double); + } + + /// + /// Gets the clip start or end measure. + /// + /// The current point measure. + /// Start or End Clip Measure + private double GetClipPointMeasure(double? currentPointMeasure) + { + double clipPointMeasure; + // increasing measures + if(_previousM < currentPointMeasure) + clipPointMeasure = _started ? Math.Max(_clipStartMeasure, _clipEndMeasure) : Math.Min(_clipStartMeasure, _clipEndMeasure); + // decreasing measures + else + clipPointMeasure = _started ? Math.Min(_clipStartMeasure, _clipEndMeasure) : Math.Max(_clipStartMeasure, _clipEndMeasure); + + return clipPointMeasure; + } + + /// + /// Computes the point coordinates. + /// + /// The compute point measure. + /// The current point measure. + /// The current x coordinate. + /// The current y coordinate. + /// The new x. + /// The new y. + private void ComputePointCoordinates(double? computePointMeasure, double? currentPointMeasure, + double currentXCoordinate, double currentYCoordinate, out double newX, out double newY) + { + newX = 0.0; + newY = 0.0; + if (!currentPointMeasure.HasValue || !computePointMeasure.HasValue) + return; + + var currentM = (double) currentPointMeasure; + // The fraction of the way from start to end. + var fraction = ((double) computePointMeasure - _previousM) / (currentM - _previousM); + newX = (_previousX * (1 - fraction)) + (currentXCoordinate * fraction); + newY = (_previousY * (1 - fraction)) + (currentYCoordinate * fraction); + } + } +} diff --git a/src/Sinks/Geometry/ConvertXYZ2XYMGeometrySink.cs b/src/Sinks/Geometry/ConvertXYZ2XYMGeometrySink.cs new file mode 100644 index 0000000..eef80fc --- /dev/null +++ b/src/Sinks/Geometry/ConvertXYZ2XYMGeometrySink.cs @@ -0,0 +1,40 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// Converts Z co-ordinate as measure. + /// + internal class ConvertXYZ2XYMGeometrySink : SqlGeometryBuilder + { + public override void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + if (m1.HasValue || m2.HasValue) + throw new ArgumentException(ErrorMessage.WKT3DOnly); + + base.AddCircularArc(x1, y1, null, z1, x2, y2, null, z2); + } + + public override void AddLine(double x, double y, double? z, double? m) + { + if (m.HasValue) + throw new ArgumentException(ErrorMessage.WKT3DOnly); + + base.AddLine(x, y, null, z); + } + + public override void BeginFigure(double x, double y, double? z, double? m) + { + if (m.HasValue) + throw new ArgumentException(ErrorMessage.WKT3DOnly); + + base.BeginFigure(x, y, null, z); + } + } +} \ No newline at end of file diff --git a/src/Sinks/Geometry/ExtractPolygonFromLineGeometrySink.cs b/src/Sinks/Geometry/ExtractPolygonFromLineGeometrySink.cs new file mode 100644 index 0000000..49e0331 --- /dev/null +++ b/src/Sinks/Geometry/ExtractPolygonFromLineGeometrySink.cs @@ -0,0 +1,118 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Types; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that extracts the polygon based upon the linestring. + /// Second segment measure is updated with offset difference. + /// + internal class ExtractPolygonFromLineGeometrySink : IGeometrySink110 + { + private readonly SqlGeometryBuilder _target; + private List _points; + private int _srid; + private bool _doReverse; + private bool _isCircularString; + + public ExtractPolygonFromLineGeometrySink(SqlGeometryBuilder geomBuilder, bool doReverse) + { + _target = geomBuilder; + _points = new List(); + _doReverse = doReverse; + } + + // Initialize MultiLine and sets srid. + public void SetSrid(int srid) + { + _srid = srid; + _target.SetSrid(_srid); + } + + // Start the geometry. + public void BeginGeometry(OpenGisGeometryType type) + { + _isCircularString = type == OpenGisGeometryType.CircularString; + } + + // Just add the points to the current line. + public void BeginFigure(double x, double y, double? z, double? m) + { + _points.Add(new LRSPoint(x, y, z, m, _srid)); + } + + // Just add the points to the current line. + public void AddLine(double x, double y, double? z, double? m) + { + _points.Add(new LRSPoint(x, y, z, m, _srid)); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + _points.Add(new LRSPoint(x1, y1, z1, m1, _srid)); + _points.Add(new LRSPoint(x2, y2, z2, m2, _srid)); + } + + // Add the current line to the MULTILINESTRING collection + public void EndFigure() + { + // no-op + } + + // This is a NO-OP + public void EndGeometry() + { + + var first = true; + if (_doReverse) + _points.Reverse(); + + if (_isCircularString) + { + _target.BeginGeometry(OpenGisGeometryType.CurvePolygon); + + for (var iterator = 0; iterator < _points.Count; iterator++) + { + var point = _points[iterator]; + if (first) + { + _target.BeginFigure(point.X, point.Y, point.Z, point.M); + first = false; + } + else + { + var nextPoint = _points[iterator + 1]; + _target.AddCircularArc(point.X, point.Y, point.Z, point.M, nextPoint.X, nextPoint.Y, nextPoint.Z, nextPoint.M); + iterator++; + } + } + } + else + { + _target.BeginGeometry(OpenGisGeometryType.Polygon); + + foreach (var point in _points) + { + if (first) + { + _target.BeginFigure(point.X, point.Y, point.Z, point.M); + first = false; + } + else + { + _target.AddLine(point.X, point.Y, point.Z, point.M); + } + } + } + + _target.EndFigure(); + _target.EndGeometry(); + } + } +} diff --git a/src/Sinks/Geometry/GISTypeCheckGeometrySink.cs b/src/Sinks/Geometry/GISTypeCheckGeometrySink.cs new file mode 100644 index 0000000..d5f9287 --- /dev/null +++ b/src/Sinks/Geometry/GISTypeCheckGeometrySink.cs @@ -0,0 +1,82 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that checks whether the geometry collection is of supported types. + /// + internal class GISTypeCheckGeometrySink : IGeometrySink110 + { + private bool _isSupportedType; + private readonly OpenGisGeometryType[] _supportedTypes; + + public GISTypeCheckGeometrySink(OpenGisGeometryType[] supportedTypes) + { + _isSupportedType = true; + _supportedTypes = supportedTypes; + } + + /// + /// Returns true if input type is of specified supported types list. + /// + /// + public bool IsCompatible() + { + return _isSupportedType; + } + + // This is a NOP. + public void SetSrid(int srid) + { + } + + /// + /// Loop through each geometry types in geom collection and validate if the type is either POINT, LINESTRING, MULTILINESTRING + /// + /// Geometry Type + public void BeginGeometry(OpenGisGeometryType type) + { + if (type == OpenGisGeometryType.GeometryCollection) + return; + + // check if the type is of the supported types + if (_isSupportedType && !(type.Contains(_supportedTypes))) + { + _isSupportedType = false; + } + } + + // This is a NOP. + public void BeginFigure(double x, double y, double? z, double? m) + { + + } + + // This is a NOP. + public void AddLine(double x, double y, double? z, double? m) + { + + } + + // This is a NOP. + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + } + + // This is a NOP. + public void EndFigure() + { + } + + // This is a NOP. + public void EndGeometry() + { + + } + } +} diff --git a/src/Sinks/Geometry/GeometryFilters.cs b/src/Sinks/Geometry/GeometryFilters.cs new file mode 100644 index 0000000..bbbeea7 --- /dev/null +++ b/src/Sinks/Geometry/GeometryFilters.cs @@ -0,0 +1,340 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Types; + +namespace SQLSpatialTools.Sinks.Geometry +{ + + public class GeometryEmptyShapeFilter : IGeometrySink110 + { + private readonly IGeometrySink110 _sink; + private readonly Queue _types = new Queue(); + private bool _root = true; + + public GeometryEmptyShapeFilter(IGeometrySink110 sink) + { + _sink = sink; + } + + public void SetSrid(int srid) + { + _sink.SetSrid(srid); + } + + public void BeginGeometry(OpenGisGeometryType type) + { + if (_root) + { + _root = false; + _sink.BeginGeometry(type); + } + else + { + _types.Enqueue(type); + } + } + + public void EndGeometry() + { + if (_types.Count > 0) + _types.Dequeue(); + else + _sink.EndGeometry(); + } + + public void BeginFigure(double x, double y, double? z, double? m) + { + while (_types.Count > 0) + _sink.BeginGeometry(_types.Dequeue()); + _sink.BeginFigure(x, y, z, m); + } + + public void AddLine(double x, double y, double? z, double? m) + { + _sink.AddLine(x, y, z, m); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + _sink.EndFigure(); + } + } + + public class GeometryPointFilter : IGeometrySink110 + { + private readonly IGeometrySink110 _sink; + private int _depth; + private bool _root = true; + + public GeometryPointFilter(IGeometrySink110 sink) + { + _sink = sink; + } + + public void SetSrid(int srid) + { + _sink.SetSrid(srid); + } + + public void BeginGeometry(OpenGisGeometryType type) + { + if (type == OpenGisGeometryType.Point || type == OpenGisGeometryType.MultiPoint) + { + if (_root) + { + _root = false; + _sink.BeginGeometry(OpenGisGeometryType.GeometryCollection); + _sink.EndGeometry(); + } + _depth++; + } + else + { + _sink.BeginGeometry(type); + } + } + + public void EndGeometry() + { + if (_depth > 0) + _depth--; + else + _sink.EndGeometry(); + } + + public void BeginFigure(double x, double y, double? z, double? m) + { + if (_depth == 0) + _sink.BeginFigure(x, y, z, m); + } + + public void AddLine(double x, double y, double? z, double? m) + { + _sink.AddLine(x, y, z, m); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + if (_depth == 0) + _sink.EndFigure(); + } + } + + public class GeometryShortLineStringFilter : IGeometrySink110 + { + private readonly IGeometrySink110 _sink; + private readonly double _tolerance; + private int _srid; + private bool _insideLineString; + private readonly List _figure = new List(); + + public GeometryShortLineStringFilter(IGeometrySink110 sink, double tolerance) + { + _sink = sink; + _tolerance = tolerance; + } + + public void SetSrid(int srid) + { + _srid = srid; + _sink.SetSrid(srid); + } + + public void BeginGeometry(OpenGisGeometryType type) + { + _sink.BeginGeometry(type); + _insideLineString = type == OpenGisGeometryType.LineString; + } + + public void EndGeometry() + { + _sink.EndGeometry(); + } + + public void BeginFigure(double x, double y, double? z, double? m) + { + if (_insideLineString) + { + _figure.Clear(); + _figure.Add(new Vertex(x, y, z, m)); + } + else + { + _sink.BeginFigure(x, y, z, m); + } + } + + public void AddLine(double x, double y, double? z, double? m) + { + if (_insideLineString) + { + _figure.Add(new Vertex(x, y, z, m)); + } + else + { + _sink.AddLine(x, y, z, m); + } + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + if (_insideLineString) + { + if (!IsShortLineString()) + { + PopulateFigure(_sink); + } + } + else + { + _sink.EndFigure(); + } + } + + private bool IsShortLineString() + { + try + { + SqlGeometryBuilder b = new SqlGeometryBuilder(); + b.SetSrid(_srid); + b.BeginGeometry(OpenGisGeometryType.LineString); + PopulateFigure(b); + b.EndGeometry(); + return b.ConstructedGeometry.STLength().Value < _tolerance; + } + catch (ArgumentException) { } + catch (FormatException) { } + return true; + } + + private void PopulateFigure(IGeometrySink110 sink) + { + _figure[0].BeginFigure(sink); + for (int i = 1; i < _figure.Count; i++) + _figure[i].AddLine(sink); + sink.EndFigure(); + } + } + + public class GeometryThinRingFilter : IGeometrySink110 + { + private readonly IGeometrySink110 _sink; + private readonly double _tolerance; + private bool _insidePolygon; + private int _srid; + private readonly List _figure = new List(); + + public GeometryThinRingFilter(IGeometrySink110 sink, double tolerance) + { + _sink = sink; + _tolerance = tolerance; + } + + public void SetSrid(int srid) + { + _srid = srid; + _sink.SetSrid(srid); + } + + public void BeginGeometry(OpenGisGeometryType type) + { + _sink.BeginGeometry(type); + _insidePolygon = type == OpenGisGeometryType.Polygon; + } + + public void EndGeometry() + { + _sink.EndGeometry(); + } + + public void BeginFigure(double x, double y, double? z, double? m) + { + if (_insidePolygon) + { + _figure.Clear(); + _figure.Add(new Vertex(x, y, z, m)); + } + else + { + _sink.BeginFigure(x, y, z, m); + } + } + + public void AddLine(double x, double y, double? z, double? m) + { + if (_insidePolygon) + { + _figure.Add(new Vertex(x, y, z, m)); + } + else + { + _sink.AddLine(x, y, z, m); + } + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + if (_insidePolygon) + { + if (!IsThinRing()) + { + PopulateFigure(_sink); + } + } + else + { + _sink.EndFigure(); + } + } + + private bool IsThinRing() + { + try + { + var builder = new SqlGeometryBuilder(); + builder.SetSrid(_srid); + builder.BeginGeometry(OpenGisGeometryType.Polygon); + PopulateFigure(builder); + builder.EndGeometry(); + var poly = builder.ConstructedGeometry.MakeValid(); + return poly.STArea().Value < _tolerance * poly.STLength().Value; + } + catch (ArgumentException) { } + catch (FormatException) { } + return true; + } + + private void PopulateFigure(IGeometrySink110 sink) + { + _figure[0].BeginFigure(sink); + for (int i = 1; i < _figure.Count; i++) + _figure[i].AddLine(sink); + sink.EndFigure(); + } + } +} \ No newline at end of file diff --git a/Sinks/GeometryToPointGeographySink.cs b/src/Sinks/Geometry/GeometryToPointGeographySink.cs similarity index 58% rename from Sinks/GeometryToPointGeographySink.cs rename to src/Sinks/Geometry/GeometryToPointGeographySink.cs index 02f8cea..bd9aaa1 100644 --- a/Sinks/GeometryToPointGeographySink.cs +++ b/src/Sinks/Geometry/GeometryToPointGeographySink.cs @@ -1,64 +1,70 @@ -//------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. -//------------------------------------------------------------------------------ -using System; -using Microsoft.SqlServer.Types; - -namespace SQLSpatialTools -{ - // - // Sink which extracts points from a geometry instance and forwards them to a geography sink - // - public sealed class GeometryToPointGeographySink : IGeometrySink - { - private readonly IGeographySink _sink; - private int _count; - - public GeometryToPointGeographySink(IGeographySink sink) - { - _sink = sink; - _count = 0; - } - - public void BeginGeometry(OpenGisGeometryType type) - { - if (_count == 0) - { - _sink.BeginGeography(OpenGisGeographyType.MultiPoint); - } - _count++; - } - - public void EndGeometry() - { - _count--; - if (_count == 0) - { - _sink.EndGeography(); - } - } - - public void BeginFigure(double x, double y, double? z, double? m) - { - _sink.BeginGeography(OpenGisGeographyType.Point); - _sink.BeginFigure(y, x, z, m); - _sink.EndFigure(); - _sink.EndGeography(); - } - - public void AddLine(double x, double y, double? z, double? m) - { - BeginFigure(x, y, z, m); - } - - public void EndFigure() - { - // we ignore these calls - } - - public void SetSrid(int srid) - { - _sink.SetSrid(srid); - } - } +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// Sink which extracts points from a geometry instance and forwards them to a geography sink. + /// + public sealed class GeometryToPointGeographySink : IGeometrySink110 + { + private readonly IGeographySink110 _sink; + private int _count; + + public GeometryToPointGeographySink(IGeographySink110 sink) + { + _sink = sink; + _count = 0; + } + + public void BeginGeometry(OpenGisGeometryType type) + { + if (_count == 0) + { + _sink.BeginGeography(OpenGisGeographyType.MultiPoint); + } + _count++; + } + + public void EndGeometry() + { + _count--; + if (_count == 0) + { + _sink.EndGeography(); + } + } + + public void BeginFigure(double x, double y, double? z, double? m) + { + _sink.BeginGeography(OpenGisGeographyType.Point); + _sink.BeginFigure(y, x, z, m); + _sink.EndFigure(); + _sink.EndGeography(); + } + + public void AddLine(double x, double y, double? z, double? m) + { + BeginFigure(x, y, z, m); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + // we ignore these calls + } + + public void SetSrid(int srid) + { + _sink.SetSrid(srid); + } + } } \ No newline at end of file diff --git a/Sinks/GeometryTransformer.cs b/src/Sinks/Geometry/GeometryTransformer.cs similarity index 55% rename from Sinks/GeometryTransformer.cs rename to src/Sinks/Geometry/GeometryTransformer.cs index 5afa5e5..f247360 100644 --- a/Sinks/GeometryTransformer.cs +++ b/src/Sinks/Geometry/GeometryTransformer.cs @@ -1,50 +1,56 @@ -//------------------------------------------------------------------------------ -// Copyright (c) 2008 Microsoft Corporation. -//------------------------------------------------------------------------------ - -using Microsoft.SqlServer.Types; - -namespace SQLSpatialTools -{ - public sealed class GeometryTransformer : IGeometrySink - { - readonly IGeometrySink _sink; - readonly AffineTransform _transform; - - public GeometryTransformer(IGeometrySink sink, AffineTransform transform) - { - _sink = sink; - _transform = transform; - } - - public void SetSrid(int srid) - { - _sink.SetSrid(srid); - } - - public void BeginGeometry(OpenGisGeometryType type) - { - _sink.BeginGeometry(type); - } - - public void BeginFigure(double x, double y, double? z, double? m) - { - _sink.BeginFigure(_transform.GetX(x, y), _transform.GetY(x, y), z, m); - } - - public void AddLine(double x, double y, double? z, double? m) - { - _sink.AddLine(_transform.GetX(x, y), _transform.GetY(x, y), z, m); - } - - public void EndFigure() - { - _sink.EndFigure(); - } - - public void EndGeometry() - { - _sink.EndGeometry(); - } - } +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Types.SQL; + +namespace SQLSpatialTools.Sinks.Geometry +{ + public sealed class GeometryTransformer : IGeometrySink110 + { + private readonly IGeometrySink110 _sink; + private readonly AffineTransform _transform; + + public GeometryTransformer(IGeometrySink110 sink, AffineTransform transform) + { + _sink = sink; + _transform = transform; + } + + public void SetSrid(int srid) + { + _sink.SetSrid(srid); + } + + public void BeginGeometry(OpenGisGeometryType type) + { + _sink.BeginGeometry(type); + } + + public void BeginFigure(double x, double y, double? z, double? m) + { + _sink.BeginFigure(_transform.GetX(x, y), _transform.GetY(x, y), z, m); + } + + public void AddLine(double x, double y, double? z, double? m) + { + _sink.AddLine(_transform.GetX(x, y), _transform.GetY(x, y), z, m); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new System.Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + _sink.EndFigure(); + } + + public void EndGeometry() + { + _sink.EndGeometry(); + } + } } \ No newline at end of file diff --git a/src/Sinks/Geometry/LineStringMergeGeometrySink.cs b/src/Sinks/Geometry/LineStringMergeGeometrySink.cs new file mode 100644 index 0000000..0a59436 --- /dev/null +++ b/src/Sinks/Geometry/LineStringMergeGeometrySink.cs @@ -0,0 +1,95 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; +using static SQLSpatialTools.Utility.SQLTypeConversions; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// Class implements a geometry sink that builds a line string by reducing from the _geometry in the range of points represented by indexes + /// + internal class LineStringMergeGeometrySink : IGeometrySink110 + { + private readonly SqlGeometryBuilder _target; /* builder reference to store the reduced range of points */ + private readonly bool _isFirstSegment; /* represent the _geometry would be the first part of the resultant geometry */ + private readonly int _numPoints; /* counter indicates the index of the coordinates present in the _geometry */ + private int _indexCounter; /* counter indicates the index of the coordinates present in the _geometry */ + + public LineStringMergeGeometrySink(SqlGeometryBuilder target, bool isFirstSegment, Numeric numPoints) + { + _target = target; + _isFirstSegment = isFirstSegment; + _numPoints = numPoints; + } + + /// + /// Persisting the srid of the first geometry and using the + /// + /// + public void SetSrid(int srid) + { + if (_isFirstSegment) + _target.SetSrid(srid); + } + + /// + /// Loop through each geometry types in geom collection and validate if the type is LINESTRING + /// + /// Geometry Type + public void BeginGeometry(OpenGisGeometryType type) + { + if (type == OpenGisGeometryType.LineString) + { + if (_isFirstSegment) + _target.BeginGeometry(type); + } + else + throw new System.Exception("Line string is the only supported type"); + } + + public void BeginFigure(double x, double y, double? z, double? m) + { + ++_indexCounter; + if (_isFirstSegment) + _target.BeginFigure(x, y, z, m); + else + _target.AddLine(x, y, z, m); + + } + + public void AddLine(double x, double y, double? z, double? m) + { + ++_indexCounter; + // skip merging point for fist segment + if (!(_isFirstSegment && _numPoints == _indexCounter)) + _target.AddLine(x, y, z, m); + + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new System.Exception("Circular arc not yet implemented"); + + } + + /// + /// End the figure, if its last point + /// + public void EndFigure() + { + if (!_isFirstSegment) + _target.EndFigure(); + } + + /// + /// End geometry construction + /// + public void EndGeometry() + { + if (!_isFirstSegment) + _target.EndGeometry(); + } + } +} diff --git a/Sinks/LocateAlongGeometrySink.cs b/src/Sinks/Geometry/LocateAlongGeometrySink.cs similarity index 59% rename from Sinks/LocateAlongGeometrySink.cs rename to src/Sinks/Geometry/LocateAlongGeometrySink.cs index aca262f..af8d008 100644 --- a/Sinks/LocateAlongGeometrySink.cs +++ b/src/Sinks/Geometry/LocateAlongGeometrySink.cs @@ -1,102 +1,112 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -using Microsoft.SqlServer.Types; -using System; - -namespace SQLSpatialTools -{ - /** - * This class implements a geometry sink that finds a point along a geography linestring instance and pipes - * it to another sink. - */ - class LocateAlongGeometrySink : IGeometrySink - { - - double _distance; // The running count of how much further we have to go. - SqlGeometry _lastPoint; // The last point in the LineString we have passed. - SqlGeometry _foundPoint; // This is the point we're looking for, assuming it isn't null, we're done. - int _srid; // The _srid we are working in. - SqlGeometryBuilder _target; // Where we place our result. - - // We target another builder, to which we will send a point representing the point we find. - // We also take a distance, which is the point along the input linestring we will travel. - // Note that we only operate on LineString instances: anything else will throw an exception. - public LocateAlongGeometrySink(double distance, SqlGeometryBuilder target) - { - _target = target; - _distance = distance; - } - - // Save the SRID for later - public void SetSrid(int srid) - { - _srid = srid; - } - - // Start the geometry. Throw if it isn't a LineString. - public void BeginGeometry(OpenGisGeometryType type) - { - if (type != OpenGisGeometryType.LineString) - throw new ArgumentException("This operation may only be executed on LineString instances."); - } - - // Start the figure. Note that since we only operate on LineStrings, this should only be executed - // once. - public void BeginFigure(double latitude, double longitude, double? z, double? m) - { - // Memorize the point. - _lastPoint = SqlGeometry.Point(latitude, longitude, _srid); - } - - // This is where the real work is done. - public void AddLine(double latitude, double longitude, double? z, double? m) - { - // If we've already found a point, then we're done. We just need to keep ignoring these - // pesky calls. - if (_foundPoint != null) return; - - // Make a point for our current position. - SqlGeometry thisPoint = SqlGeometry.Point(latitude, longitude, _srid); - - // is the found point between this point and the last, or past this point? - double length = thisPoint.STDistance(_lastPoint).Value; - if (length < _distance) - { - // it's past this point---just step along the line - _distance -= length; - _lastPoint = thisPoint; - } - else - { - // now we need to do the hard work and find the point in between these two - _foundPoint = Functions.InterpolateBetweenGeom(_lastPoint, thisPoint, _distance); - } - } - - // This is a NOP. - public void EndFigure() - { - } - - // When we end, we'll make all of our output calls to our target. - // Here's also where we catch whether we've run off the end of our LineString. - public void EndGeometry() - { - if (_foundPoint != null) - { - // We could use a simple point constructor, but by targetting another sink we can use this - // class in a pipeline. - _target.SetSrid(_srid); - _target.BeginGeometry(OpenGisGeometryType.Point); - _target.BeginFigure(_foundPoint.STX.Value, _foundPoint.STY.Value); - _target.EndFigure(); - _target.EndGeometry(); - } - else - { - throw new ArgumentException("Distance provided is greated then the length of the LineString."); - } - } - - } -} +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that finds a point along a geometry linestring instance and pipes + /// it to another sink. + /// + internal class LocateAlongGeometrySink : IGeometrySink110 + { + private double _distance; // The running count of how much further we have to go. + private SqlGeometry _lastPoint; // The last point in the LineString we have passed. + private SqlGeometry _foundPoint; // This is the point we're looking for, assuming it isn't null, we're done. + private int _srid; // The _srid we are working in. + private readonly SqlGeometryBuilder _target; // Where we place our result. + + // We target another builder, to which we will send a point representing the point we find. + // We also take a distance, which is the point along the input linestring we will travel. + // Note that we only operate on LineString instances: anything else will throw an exception. + public LocateAlongGeometrySink(double distance, SqlGeometryBuilder target) + { + _target = target; + _distance = distance; + } + + // Save the SRID for later + public void SetSrid(int srid) + { + _srid = srid; + } + + // Start the geometry. Throw if it isn't a LineString. + public void BeginGeometry(OpenGisGeometryType type) + { + if (type != OpenGisGeometryType.LineString) + throw new ArgumentException("This operation may only be executed on LineString instances."); + } + + // Start the figure. Note that since we only operate on LineStrings, this should only be executed + // once. + public void BeginFigure(double x, double y, double? z, double? m) + { + // Memorize the point. + _lastPoint = SqlGeometry.Point(x, y, _srid); + if (_distance.EqualsTo(0)) + _foundPoint = _lastPoint; + } + + // This is where the real work is done. + public void AddLine(double x, double y, double? z, double? m) + { + // If we've already found a point, then we're done. We just need to keep ignoring these + // pesky calls. + if (_foundPoint != null) + return; + + // Make a point for our current position. + SqlGeometry thisPoint = SqlGeometry.Point(x, y, _srid); + + // is the found point between this point and the last, or past this point? + double length = thisPoint.STDistance(_lastPoint).Value; + if (length < _distance) + { + // it's past this point---just step along the line + _distance -= length; + _lastPoint = thisPoint; + } + else + { + // now we need to do the hard work and find the point in between these two + _foundPoint = Functions.General.Geometry.InterpolateBetweenGeom(_lastPoint, thisPoint, _distance); + } + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + // This is a NOP. + public void EndFigure() + { + } + + // When we end, we'll make all of our output calls to our target. + // Here's also where we catch whether we've run off the end of our LineString. + public void EndGeometry() + { + if (_foundPoint != null) + { + // We could use a simple point constructor, but by targeting another sink we can use this + // class in a pipeline. + _target.SetSrid(_srid); + _target.BeginGeometry(OpenGisGeometryType.Point); + _target.BeginFigure(_foundPoint.STX.Value, _foundPoint.STY.Value); + _target.EndFigure(); + _target.EndGeometry(); + } + else + { + throw new ArgumentException("Distance provided is greater than the length of the LineString."); + } + } + + } +} diff --git a/src/Sinks/Geometry/LocateMAlongGeometrySink.cs b/src/Sinks/Geometry/LocateMAlongGeometrySink.cs new file mode 100644 index 0000000..a3356cd --- /dev/null +++ b/src/Sinks/Geometry/LocateMAlongGeometrySink.cs @@ -0,0 +1,133 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; +using System; +using SQLSpatialTools.Utility; +using Ext = SQLSpatialTools.Utility.SpatialExtensions; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that finds a point along a geometry linestring instance and pipes + /// it to another sink. + /// + internal class LocateMAlongGeometrySink : IGeometrySink110 + { + private readonly double _measure; // The running count of how much further we have to go. + private readonly double _tolerance; // The tolerance. + private readonly SqlGeometryBuilder _target; // Where we place our result. + private int _srid; // The srid we are working in. + private SqlGeometry _lastPoint; // The last point in the LineString we have passed. + private SqlGeometry _foundPoint; // This is the point we're looking for, assuming it isn't null, we're done. + public bool IsPointDerived; + public bool IsShapePoint; + + // We target another builder, to which we will send a point representing the point we find. + // We also take a measure, which is the point along the input linestring we will travel to. + // Note that we only operate on LineString instances: anything else will throw an exception. + public LocateMAlongGeometrySink(double measure, SqlGeometryBuilder target, double tolerance = Constants.Tolerance) + { + _target = target; + _measure = measure; + _tolerance = tolerance; + IsPointDerived = false; + } + + // Save the SRID for later + public void SetSrid(int srid) + { + _srid = srid; + } + + // Start the geometry. Throw if it isn't a LineString. + public void BeginGeometry(OpenGisGeometryType type) + { + } + + // Start the figure. Note that since we only operate on LineStrings, this should only be executed + // once. + public void BeginFigure(double x, double y, double? z, double? m) + { + if (_foundPoint != null) + return; + + // Memorize the point. + _lastPoint = CheckShapePointAndGet(m, Ext.GetPoint(x, y, z, m, _srid)); + } + + // This is where the real work is done. + public void AddLine(double x, double y, double? z, double? m) + { + // If we've already found a point, then we're done. We just need to keep ignoring these + // pesky calls. + if (_foundPoint != null) + return; + + // Make a point for our current position. + var thisPoint = CheckShapePointAndGet(m, Ext.GetPoint(x, y, z, m, _srid)); + + // is the found point between this point and the last, or past this point? + if (m != null && _measure.IsWithinRange(_lastPoint.M.Value, m.Value)) + { + // now we need to do the hard work and find the point in between these two + _foundPoint = Functions.LRS.Geometry.InterpolateBetweenGeom(_lastPoint, thisPoint, _measure); + if (_lastPoint.IsWithinTolerance(_foundPoint, _tolerance)) + { + _foundPoint = _lastPoint; + IsShapePoint = true; + } + else if (thisPoint.IsWithinTolerance(_foundPoint, _tolerance)) + { + _foundPoint = thisPoint; + IsShapePoint = true; + } + } + else + { + // it's past this point---just step along the line + _lastPoint = thisPoint; + } + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + /// + /// Checks if the point is a shape point. + /// + /// The m. + /// The geometry. + /// + private SqlGeometry CheckShapePointAndGet(double? m, SqlGeometry geometry) + { + IsShapePoint = m.EqualsTo(_measure); + if (IsShapePoint) + _foundPoint = geometry; + return geometry; + } + + // This is a NOP. + public void EndFigure() + { + } + + // When we end, we'll make all of our output calls to our target. + // Here's also where we catch whether we've run off the end of our LineString. + public void EndGeometry() + { + if (_foundPoint == null || IsPointDerived) return; + // We could use a simple point constructor, but by targeting another sink we can use this + // class in a pipeline. + _target.SetSrid(_srid); + _target.BeginGeometry(OpenGisGeometryType.Point); + _target.BeginFigure(_foundPoint.STX.Value, _foundPoint.STY.Value, _foundPoint.Z.IsNull ? (double?)null : _foundPoint.Z.Value, _foundPoint.M.Value); + _target.EndFigure(); + _target.EndGeometry(); + IsPointDerived = true; + } + } +} diff --git a/src/Sinks/Geometry/MultiplyMeasureGeometrySink.cs b/src/Sinks/Geometry/MultiplyMeasureGeometrySink.cs new file mode 100644 index 0000000..2568b79 --- /dev/null +++ b/src/Sinks/Geometry/MultiplyMeasureGeometrySink.cs @@ -0,0 +1,78 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; +using System; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that multiplies the measure values of input geometry. + /// + internal class MultiplyMeasureGeometrySink : IGeometrySink110 + { + private readonly SqlGeometryBuilder _target; + private readonly double _scaleMeasure; + + /// + /// Loop through each geometry types LINESTRING and MULTILINESTRING and scales the measure by given magnitude. + /// + /// + /// + public MultiplyMeasureGeometrySink(SqlGeometryBuilder target, double scaleMeasure) + { + _target = target; + _scaleMeasure = scaleMeasure; + } + + // Just pass through target. + public void SetSrid(int srid) + { + _target.SetSrid(srid); + } + + // Just pass through target. + public void BeginGeometry(OpenGisGeometryType type) + { + _target.BeginGeometry(type); + } + + // Just add the points with updated measure. + public void BeginFigure(double x, double y, double? z, double? m) + { + _target.BeginFigure(x, y, z, GetUpdatedMeasure(m)); + } + + // Just add the points with updated measure. + public void AddLine(double x, double y, double? z, double? m) + { + _target.AddLine(x, y, z, GetUpdatedMeasure(m)); + } + + // Just pass through target. + public void EndFigure() + { + _target.EndFigure(); + } + + // Just pass through target. + public void EndGeometry() + { + _target.EndGeometry(); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + private double? GetUpdatedMeasure(double? m) + { + double? measure = null; + if (m.HasValue) + measure = (double)m * _scaleMeasure; + return measure; + } + } +} diff --git a/src/Sinks/Geometry/PopulateGeometryMeasuresSink.cs b/src/Sinks/Geometry/PopulateGeometryMeasuresSink.cs new file mode 100644 index 0000000..382a2d9 --- /dev/null +++ b/src/Sinks/Geometry/PopulateGeometryMeasuresSink.cs @@ -0,0 +1,131 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Types; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that populate measures for each point in a geometry . + /// + internal class PopulateGeometryMeasuresSink : IGeometrySink110 + { + private SqlGeometry _lastPoint; + private SqlGeometry _thisPoint; + + private LRSMultiLine _lines; + private LRSLine _currentLine; + + private readonly double _startMeasure; + private readonly double _endMeasure; + private readonly double _totalLength; + + private bool _isMultiLine; + private int _lineCounter; + private int _srid; // The _srid we are working in. + private double _currentLength; + private double _currentPointM; + private SqlGeometry _target; // Where we place our result. + + /// + /// Gets the constructed geometry. + /// + /// + public SqlGeometry GetConstructedGeom() + { + return _target; + } + + // We target another builder, to which we will send a point representing the point we find. + // We also take a distance, which is the point along the input linestring we will travel. + // Note that we only operate on LineString instances: anything else will throw an exception. + public PopulateGeometryMeasuresSink(double startMeasure, double endMeasure, double length) + { + _startMeasure = startMeasure; + _endMeasure = endMeasure; + _totalLength = length; + _isMultiLine = false; + _lineCounter = 0; + _currentPointM = startMeasure; + } + + // Initialize MultiLine and sets srid. + public void SetSrid(int srid) + { + _lines = new LRSMultiLine(srid); + _srid = srid; + } + + // Start geometry and check if the type is of the supported types + public void BeginGeometry(OpenGisGeometryType type) + { + if (type == OpenGisGeometryType.MultiLineString) + _isMultiLine = true; + else if (type == OpenGisGeometryType.LineString) + _lineCounter++; + } + + + // This operates on LineStrings, multi linestring + public void BeginFigure(double x, double y, double? z, double? m) + { + _currentLine = new LRSLine(_srid); + _currentLine.AddPoint(x, y, null, _currentPointM); + + // Memorize the starting point. + _lastPoint = SqlGeometry.Point(x, y, _srid); + } + + // This is where the real work is done. + public void AddLine(double x, double y, double? z, double? m) + { + _thisPoint = SqlGeometry.Point(x, y, _srid); + _currentLength += _lastPoint.STDistance(_thisPoint).Value; + _currentLine.AddPoint(x, y, null, GetCurrentMeasure()); + + // reset the last point with the current point. + _lastPoint = _thisPoint; + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + // This is a NOP. + public void EndFigure() + { + + } + + // When we end, we'll make all of our output calls to our target. + public void EndGeometry() + { + // if not multi line then add the current line to the collection. + if (!_isMultiLine) + _lines.AddLine(_currentLine); + + // if line counter is 0 then it is multiline + // if 1 then it is linestring + if (_lineCounter == 0 || !_isMultiLine) + { + _target = _lines.ToSqlGeometry(); + } + else + { + _lines.AddLine(_currentLine); + // reset the line counter so that the child line strings chaining is done and return to base multiline type + _lineCounter--; + } + } + + private double GetCurrentMeasure() + { + _currentPointM = _startMeasure + (_currentLength / _totalLength) * (_endMeasure - _startMeasure); + return _currentPointM; + } + } +} diff --git a/src/Sinks/Geometry/ResetMGeometrySink.cs b/src/Sinks/Geometry/ResetMGeometrySink.cs new file mode 100644 index 0000000..be53140 --- /dev/null +++ b/src/Sinks/Geometry/ResetMGeometrySink.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; +using System; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that resets M value to null. + /// + internal class ResetMGeometrySink : IGeometrySink110 + { + private readonly SqlGeometryBuilder _target; // Where we place our result. + + public ResetMGeometrySink(SqlGeometryBuilder target) + { + _target = target; + } + + /// + /// Save the SRID for later + /// + /// Spatial Reference Identifier + public void SetSrid(int srid) + { + _target.SetSrid(srid); + } + + /// + /// Start the geometry + /// + /// Geometry Type + public void BeginGeometry(OpenGisGeometryType type) + { + _target.BeginGeometry(type); + } + + /// + /// Start the figure. + /// Note that since we only operate on LineStrings, Multilinestring and Point these should only be executed. + /// + /// + /// + /// + /// + public void BeginFigure(double x, double y, double? z, double? m) + { + _target.BeginFigure(x, y, z, null); + } + + public void AddLine(double x, double y, double? z, double? m) + { + _target.AddLine(x, y, z, null); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + _target.EndFigure(); + } + + public void EndGeometry() + { + _target.EndGeometry(); + } + } +} \ No newline at end of file diff --git a/src/Sinks/Geometry/ReverseAndTranslateGeometrySink.cs b/src/Sinks/Geometry/ReverseAndTranslateGeometrySink.cs new file mode 100644 index 0000000..56b35dd --- /dev/null +++ b/src/Sinks/Geometry/ReverseAndTranslateGeometrySink.cs @@ -0,0 +1,101 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Types; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that reverses the input geometry and translate the measure + /// + internal class ReverseAndTranslateGeometrySink : IGeometrySink110 + { + private SqlGeometryBuilder _target; + private readonly double _translateMeasure; + private LRSMultiLine _lines; + private LRSLine _currentLine; + private bool _isMultiLine; + private int _lineCounter; + private int _srid; + + /// + /// Loop through each geometry types LINESTRING and MULTILINESTRING and reverse and translate measure it accordingly. + /// + /// + /// + public ReverseAndTranslateGeometrySink(SqlGeometryBuilder target, double translateMeasure) + { + _target = target; + _translateMeasure = translateMeasure; + _isMultiLine = false; + _lineCounter = 0; + } + + // Initialize MultiLine and sets srid. + public void SetSrid(int srid) + { + _lines = new LRSMultiLine(srid); + _srid = srid; + } + + // Check for types and begin geometry accordingly. + public void BeginGeometry(OpenGisGeometryType type) + { + if (type == OpenGisGeometryType.MultiLineString) + _isMultiLine = true; + if (type == OpenGisGeometryType.LineString) + _lineCounter++; + } + + // Just add the points to the current line segment. + public void BeginFigure(double x, double y, double? z, double? m) + { + _currentLine = new LRSLine(_srid); + _currentLine.AddPoint(x, y, z, m); + } + + // Just add the points to the current line segment. + public void AddLine(double x, double y, double? z, double? m) + { + _currentLine.AddPoint(x, y, z, m); + } + + // Reverse the points at the end of figure. + public void EndFigure() + { + _currentLine.ReversePoints(); + } + + // This is where real work is done. + public void EndGeometry() + { + // if not multi line then add the current line to the collection. + if (!_isMultiLine) + _lines.AddLine(_currentLine); + + // if line counter is 0 then it is multiline + // if 1 then it is linestring + if (_lineCounter == 0 || !_isMultiLine) + { + // reverse the line before constructing the geometry + _lines.ReversLines(); + _lines.TranslateMeasure(_translateMeasure); + _lines.ToSqlGeometry(ref _target); + } + else + { + _lines.AddLine(_currentLine); + // reset the line counter so that the child line strings chaining is done and return to base multiline type + _lineCounter--; + } + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + } +} diff --git a/src/Sinks/Geometry/ReverseLinearGeometrySink.cs b/src/Sinks/Geometry/ReverseLinearGeometrySink.cs new file mode 100644 index 0000000..f390d63 --- /dev/null +++ b/src/Sinks/Geometry/ReverseLinearGeometrySink.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Types; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that reverses the input geometry. + /// + internal class ReverseLinearGeometrySink : IGeometrySink110 + { + private SqlGeometryBuilder _target; + private LRSMultiLine _lines; + private LRSLine _currentLine; + private bool _isMultiLine; + private int _lineCounter; + private int _srid; + + /// + /// Loop through each geometry types LINESTRING and MULTILINESTRING and reverse it accordingly. + /// + /// + public ReverseLinearGeometrySink(SqlGeometryBuilder target) + { + _target = target; + _isMultiLine = false; + _lineCounter = 0; + } + + // Initialize MultiLine and sets srid. + public void SetSrid(int srid) + { + _lines = new LRSMultiLine(srid); + _srid = srid; + } + + // Check for types and begin geometry accordingly. + public void BeginGeometry(OpenGisGeometryType type) + { + // check if the type is of the supported types + if (type == OpenGisGeometryType.MultiLineString) + _isMultiLine = true; + + if (type == OpenGisGeometryType.LineString) + _lineCounter++; + } + + // Just add the points to the current line segment. + public void BeginFigure(double x, double y, double? z, double? m) + { + _currentLine = new LRSLine(_srid); + _currentLine.AddPoint(x, y, z, m); + } + + // Just add the points to the current line segment. + public void AddLine(double x, double y, double? z, double? m) + { + _currentLine.AddPoint(x, y, z, m); + } + + // Reverse the points at the end of figure. + public void EndFigure() + { + _currentLine.ReversePoints(); + } + + // This is where real work is done. + public void EndGeometry() + { + // if not multi line then add the current line to the collection. + if (!_isMultiLine) + _lines.AddLine(_currentLine); + + // if line counter is 0 then it is multiline + // if 1 then it is linestring + if (_lineCounter == 0 || !_isMultiLine) + { + // reverse the line before constructing the geometry + _lines.ReversLines(); + _lines.ToSqlGeometry(ref _target); + } + else + { + _lines.AddLine(_currentLine); + // reset the line counter so that the child line strings chaining is done and return to base multiline type + _lineCounter--; + } + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + } +} diff --git a/src/Sinks/Geometry/ScaleGeometrySink.cs b/src/Sinks/Geometry/ScaleGeometrySink.cs new file mode 100644 index 0000000..2692d69 --- /dev/null +++ b/src/Sinks/Geometry/ScaleGeometrySink.cs @@ -0,0 +1,92 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that scales the measure values of input geometry. + /// + internal class ScaleGeometrySink : IGeometrySink110 + { + private readonly SqlGeometryBuilder _target; + private readonly double _startMeasure, _endMeasure, _newStartMeasure, _newEndMeasure, _shiftMeasure; + private bool _isPoint; + + /// + /// Loop through each geometry types LINESTRING and MULTILINESTRING and translates its measure. + /// + /// + /// + /// + /// + /// + /// + public ScaleGeometrySink(SqlGeometryBuilder target, double startMeasure, double endMeasure, double newStartMeasure, double newEndMeasure, double shiftMeasure) + { + _target = target; + _startMeasure = startMeasure; + _endMeasure = endMeasure; + _newStartMeasure = newStartMeasure; + _newEndMeasure = newEndMeasure; + _shiftMeasure = shiftMeasure; + } + + // Just pass through target. + public void SetSrid(int srid) + { + _target.SetSrid(srid); + } + + // Just pass through target. + public void BeginGeometry(OpenGisGeometryType type) + { + if (type == OpenGisGeometryType.Point) + _isPoint = true; + + _target.BeginGeometry(type); + } + + // Just add the points with updated measure. + public void BeginFigure(double x, double y, double? z, double? m) + { + if (_isPoint) + _target.BeginFigure(x, y, z, _newEndMeasure + _shiftMeasure); + else + _target.BeginFigure(x, y, z, GetUpdatedMeasure(m)); + } + + // Just add the points with updated measure. + public void AddLine(double x, double y, double? z, double? m) + { + _target.AddLine(x, y, z, GetUpdatedMeasure(m)); + } + + // Just pass through target. + public void EndFigure() + { + _target.EndFigure(); + } + + // Just pass through target. + public void EndGeometry() + { + _target.EndGeometry(); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + private double GetUpdatedMeasure(double? m) + { + var mValue = m ?? 0.0; + return ((mValue - _startMeasure) * ((_newEndMeasure - _newStartMeasure) / (_endMeasure - _startMeasure))) + + _newStartMeasure + _shiftMeasure; + } + } +} diff --git a/Sinks/ShiftGeometrySink.cs b/src/Sinks/Geometry/ShiftGeometrySink.cs similarity index 63% rename from Sinks/ShiftGeometrySink.cs rename to src/Sinks/Geometry/ShiftGeometrySink.cs index 2ccc48b..c5f404f 100644 --- a/Sinks/ShiftGeometrySink.cs +++ b/src/Sinks/Geometry/ShiftGeometrySink.cs @@ -1,62 +1,69 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -using Microsoft.SqlServer.Types; - -namespace SQLSpatialTools -{ - /** - * This class implements a geometry sink that will shift an input geometry by a given amount in the x and - * y directions. It directs its output to another sink, and can therefore be used in a pipeline if desired. - */ - public class ShiftGeometrySink : IGeometrySink - { - private readonly IGeometrySink _target; // the target sink - private readonly double _xShift; // How much to shift in the x direction. - private readonly double _yShift; // How much to shift in the y direction. - - // We take an amount to shift in the x and y directions, as well as a target sink, to which - // we will pipe our result. - public ShiftGeometrySink(double xShift, double yShift, IGeometrySink target) - { - _target = target; - _xShift = xShift; - _yShift = yShift; - } - - // Just pass through without change. - public void SetSrid(int srid) - { - _target.SetSrid(srid); - } - - // Just pass through without change. - public void BeginGeometry(OpenGisGeometryType type) - { - _target.BeginGeometry(type); - } - - // Each BeginFigure call will just move the start point by the required amount. - public void BeginFigure(double x, double y, double? z, double? m) - { - _target.BeginFigure(x + _xShift, y + _yShift, z, m); - } - - // Each AddLine call will just move the endpoint by the required amount. - public void AddLine(double x, double y, double? z, double? m) - { - _target.AddLine(x + _xShift, y + _yShift, z, m); - } - - // Just pass through without change. - public void EndFigure() - { - _target.EndFigure(); - } - - // Just pass through without change. - public void EndGeometry() - { - _target.EndGeometry(); - } - } -} +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that will shift an input geometry by a given amount in the x and + /// y directions. It directs its output to another sink, and can therefore be used in a pipeline if desired. + /// + public class ShiftGeometrySink : IGeometrySink110 + { + private readonly IGeometrySink110 _target; // the target sink + private readonly double _xShift; // How much to shift in the x direction. + private readonly double _yShift; // How much to shift in the y direction. + + // We take an amount to shift in the x and y directions, as well as a target sink, to which + // we will pipe our result. + public ShiftGeometrySink(double xShift, double yShift, IGeometrySink110 target) + { + _target = target; + _xShift = xShift; + _yShift = yShift; + } + + // Just pass through without change. + public void SetSrid(int srid) + { + _target.SetSrid(srid); + } + + // Just pass through without change. + public void BeginGeometry(OpenGisGeometryType type) + { + _target.BeginGeometry(type); + } + + // Each BeginFigure call will just move the start point by the required amount. + public void BeginFigure(double x, double y, double? z, double? m) + { + _target.BeginFigure(x + _xShift, y + _yShift, z, m); + } + + // Each AddLine call will just move the endpoint by the required amount. + public void AddLine(double x, double y, double? z, double? m) + { + _target.AddLine(x + _xShift, y + _yShift, z, m); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new System.Exception("AddCircularArc is not implemented yet in this class"); + } + + // Just pass through without change. + public void EndFigure() + { + _target.EndFigure(); + } + + // Just pass through without change. + public void EndGeometry() + { + _target.EndGeometry(); + } + } +} diff --git a/src/Sinks/Geometry/SplitGeometrySegmentSink.cs b/src/Sinks/Geometry/SplitGeometrySegmentSink.cs new file mode 100644 index 0000000..d70bbd1 --- /dev/null +++ b/src/Sinks/Geometry/SplitGeometrySegmentSink.cs @@ -0,0 +1,209 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Types; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that split the geometry LINESTRING and MULTILINESTRING into two segments based on split point. + /// + internal class SplitGeometrySegmentSink : IGeometrySink110 + { + private readonly double _splitPointMeasure; + private readonly SqlGeometry _splitPoint; + + public SqlGeometry Segment1; // Where we place our result. + public SqlGeometry Segment2; // Where we place our result. + + private LRSMultiLine _segment1; + private LRSMultiLine _segment2; + private LRSLine _currentLineForSegment1; + private LRSLine _currentLineForSegment2; + + private int _srid, _lineCounter; + private bool _isMultiLine; + private bool _splitPointReached; + private double _lastM; + + // Initialize Split Geom Sink with split point + public SplitGeometrySegmentSink(SqlGeometry splitPoint) + { + _splitPoint = splitPoint; + _isMultiLine = false; + _lineCounter = 0; + _splitPointMeasure = splitPoint.HasM ? splitPoint.M.Value : 0; + } + + // Save the SRID for later + public void SetSrid(int srid) + { + _segment1 = new LRSMultiLine(srid); + _segment2 = new LRSMultiLine(srid); + _srid = srid; + } + + private bool IsEqualToSplitMeasure(double? currentMeasure) + { + return currentMeasure.EqualsTo(_splitPoint.M.Value); + } + + // Start the geometry. + public void BeginGeometry(OpenGisGeometryType type) + { + if (type == OpenGisGeometryType.MultiLineString) + _isMultiLine = true; + if (type == OpenGisGeometryType.LineString) + _lineCounter++; + } + + // Start the figure. + public void BeginFigure(double x, double y, double? z, double? m) + { + _currentLineForSegment1 = new LRSLine(_srid); + _currentLineForSegment2 = new LRSLine(_srid); + + // just add it to the second segment once split point is reached + if (m != null && (_splitPointReached && m.Value.NotEqualsTo(_splitPointMeasure))) + { + _currentLineForSegment2.AddPoint(x, y, z, m); + return; + } + + if (m < _splitPointMeasure) + _currentLineForSegment1.AddPoint(x, y, null, m); + else if (m > _splitPointMeasure || IsEqualToSplitMeasure(m)) + { + _currentLineForSegment2.AddPoint(x, y, null, m); + _splitPointReached = IsEqualToSplitMeasure(m); + } + + if (m != null) _lastM = (double) m; + } + + // This is where the real work is done. + public void AddLine(double x, double y, double? z, double? m) + { + // just add it to the second segment once split point is reached + if (m != null && (_splitPointReached && m.Value.NotEqualsTo(_splitPointMeasure))) + { + _currentLineForSegment2.AddPoint(x, y, z, m); + return; + } + + // If current measure is less than split measure; then add it to the first segment. + if (m < _splitPointMeasure) + { + _currentLineForSegment1.AddPoint(x, y, z, m); + } + + // split measure in between last point measure and current point measure. + else if (_splitPointMeasure > _lastM && _splitPointMeasure < m) + { + _currentLineForSegment1.AddPoint(_splitPoint); + _currentLineForSegment2.AddPoint(_splitPoint); + _currentLineForSegment2.AddPoint(x, y, z, m); + _splitPointReached = true; + } + + // if current measure is equal to split measure; then it is a shape point + else if (m != null && (IsEqualToSplitMeasure(m) && _lastM.NotEqualsTo(m.Value))) + { + _currentLineForSegment1.AddPoint(x, y, z, m); + _currentLineForSegment2.AddPoint(x, y, z, m); + _splitPointReached = true; + } + + // If current measure is greater than split measure; then add it to the second segment. + else if (m > _splitPointMeasure) + { + _currentLineForSegment2.AddPoint(x, y, z, m); + } + + // reassign current measure to last measure + if (m != null) _lastM = (double) m; + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + // This is a NOP. + public void EndFigure() + { + } + + // add segments to target + public void EndGeometry() + { + // if not multi line then add the current line to the collection. + if (!_isMultiLine) + { + _segment1.AddLine(_currentLineForSegment1); + _segment2.AddLine(_currentLineForSegment2); + } + + // if line counter is 0 then it is multiline + // if 1 then it is linestring + if (_lineCounter == 0 || !_isMultiLine) + { + Segment1 = _segment1.ToSqlGeometry(); + + // TODO:: Messy logic to be meet as per Oracle; need to be re-factored + // if second is a multi line + // end measure of first line > end measure of last line + // then consider only first line + // by locating the point + if (!_segment1.IsEmpty && _segment2.IsMultiLine) + { + var endSegmentEndM = _segment2.GetLastLine().GetEndPointM(); + var startSegmentStartM = _segment2.GetFirstLine().GetEndPointM(); + + if (startSegmentStartM > endSegmentEndM) + { + var trimmedLine = _segment2.GetFirstLine(); + var newLRSLine = new LRSLine(_srid); + + // add points up to end segment measure + foreach (var point in trimmedLine) + { + if (point.M < endSegmentEndM) + newLRSLine.AddPoint(point); + } + + // add the end point + if (endSegmentEndM.EqualsTo(_splitPointMeasure)) + newLRSLine.AddPoint(_splitPoint); + else + newLRSLine.AddPoint(trimmedLine.LocatePoint(endSegmentEndM, newLRSLine.GetEndPoint())); + + Segment2 = newLRSLine.ToSqlGeometry(); + } + // if end segment measure is equal to split measure; then return the split alone for second segment + else if (endSegmentEndM.EqualsTo(_splitPointMeasure)) + Segment2 = _splitPoint; + else + Segment2 = _segment2.ToSqlGeometry(); + } + else + Segment2 = _segment2.ToSqlGeometry(); + } + else + { + if (_currentLineForSegment1.IsLine) + _segment1.AddLine(_currentLineForSegment1); + + if (_currentLineForSegment2.IsLine) + _segment2.AddLine(_currentLineForSegment2); + + // reset the line counter so that the child line strings chaining is done and return to base multiline type + _lineCounter--; + } + } + } +} diff --git a/src/Sinks/Geometry/TranslateMeasureGeometrySink.cs b/src/Sinks/Geometry/TranslateMeasureGeometrySink.cs new file mode 100644 index 0000000..50479cd --- /dev/null +++ b/src/Sinks/Geometry/TranslateMeasureGeometrySink.cs @@ -0,0 +1,75 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a geometry sink that translates the measure values of input geometry. + /// + internal class TranslateMeasureGeometrySink : IGeometrySink110 + { + private readonly SqlGeometryBuilder _target; + private readonly double _translateMeasure; + + /// + /// Loop through each geometry types LINESTRING and MULTILINESTRING and translates its measure. + /// + /// + /// + public TranslateMeasureGeometrySink(SqlGeometryBuilder target, double translateMeasure) + { + _target = target; + _translateMeasure = translateMeasure; + } + + // Just pass through target. + public void SetSrid(int srid) + { + _target.SetSrid(srid); + } + + // Just pass through target. + public void BeginGeometry(OpenGisGeometryType type) + { + _target.BeginGeometry(type); + } + + // Just add the points with updated measure. + public void BeginFigure(double x, double y, double? z, double? m) + { + _target.BeginFigure(x, y, z, GetUpdatedMeasure(m)); + } + + // Just add the points with updated measure. + public void AddLine(double x, double y, double? z, double? m) + { + _target.AddLine(x, y, z, GetUpdatedMeasure(m)); + } + + // Just pass through target. + public void EndFigure() + { + _target.EndFigure(); + } + + // Just pass through target. + public void EndGeometry() + { + _target.EndGeometry(); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + private double GetUpdatedMeasure(double? m) + { + return m + _translateMeasure ?? _translateMeasure; + } + } +} diff --git a/src/Sinks/Geometry/Unprojector.cs b/src/Sinks/Geometry/Unprojector.cs new file mode 100644 index 0000000..f186432 --- /dev/null +++ b/src/Sinks/Geometry/Unprojector.cs @@ -0,0 +1,59 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Types.SQL; + +namespace SQLSpatialTools.Sinks.Geometry +{ + public sealed class UnProjector : IGeometrySink110 + { + private readonly SqlProjection _projection; + private readonly IGeographySink110 _sink; + + public UnProjector(SqlProjection projection, IGeographySink110 sink, int newSrid) + { + _projection = projection; + _sink = sink; + _sink.SetSrid(newSrid); + } + + public void BeginGeometry(OpenGisGeometryType type) + { + _sink.BeginGeography((OpenGisGeographyType)type); + } + + public void EndGeometry() + { + _sink.EndGeography(); + } + + public void BeginFigure(double x, double y, double? z, double? m) + { + _projection.UnprojectPoint(x, y, out double latitude, out double longitude); + _sink.BeginFigure(latitude, longitude, z, m); + } + + public void AddLine(double x, double y, double? z, double? m) + { + _projection.UnprojectPoint(x, y, out double latitude, out double longitude); + _sink.AddLine(latitude, longitude, z, m); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new System.Exception("AddCircularArc is not implemented yet in this class"); + } + + public void EndFigure() + { + _sink.EndFigure(); + } + + public void SetSrid(int srid) + { + // Input argument not used since a new srid is defined in the constructor. + } + } +} \ No newline at end of file diff --git a/src/Sinks/Geometry/VacuousGeometryToGeographySink.cs b/src/Sinks/Geometry/VacuousGeometryToGeographySink.cs new file mode 100644 index 0000000..978a76b --- /dev/null +++ b/src/Sinks/Geometry/VacuousGeometryToGeographySink.cs @@ -0,0 +1,62 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.Sinks.Geometry +{ + /// + /// This class implements a completely trivial conversion from geometry to geography, simply taking each + /// point(x, y) --> (long, lat). The class takes a target geography sink, as well as the target SRID to + /// assign to the results. + /// + public class VacuousGeometryToGeographySink : IGeometrySink110 + { + private readonly IGeographySink110 _target; + private readonly int _targetSrid; + + public VacuousGeometryToGeographySink(int targetSrid, IGeographySink110 target) + { + _target = target; + _targetSrid = targetSrid; + } + + public void AddLine(double x, double y, double? z, double? m) + { + _target.AddLine(y, x, z, m); + } + + public void AddCircularArc(double x1, double y1, double? z1, double? m1, double x2, double y2, double? z2, double? m2) + { + throw new Exception("AddCircularArc is not implemented yet in this class"); + } + + public void BeginFigure(double x, double y, double? z, double? m) + { + _target.BeginFigure(y, x, z, m); + } + + public void BeginGeometry(OpenGisGeometryType type) + { + // Convert geography to geometry types... + _target.BeginGeography((OpenGisGeographyType) type); + } + + public void EndFigure() + { + _target.EndFigure(); + } + + public void EndGeometry() + { + _target.EndGeography(); + } + + public void SetSrid(int srid) + { + _target.SetSrid(_targetSrid); + } + } +} diff --git a/src/Types/LRSEnumerator.cs b/src/Types/LRSEnumerator.cs new file mode 100644 index 0000000..b9c9b69 --- /dev/null +++ b/src/Types/LRSEnumerator.cs @@ -0,0 +1,57 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace SQLSpatialTools.Types +{ + /// + /// Enumerator for LRS Types + /// + /// + /// + internal class LRSEnumerator : IEnumerator + { + private readonly List _listOfItems; + + // Enumerators are positioned before the first element + // until the first MoveNext() call. + private int _position = -1; + + public LRSEnumerator(List list) + { + _listOfItems = list; + } + + public bool MoveNext() + { + _position++; + return (_position < _listOfItems.Count); + } + + public void Reset() + { + _position = -1; + } + + object IEnumerator.Current => Current; + + public T Current + { + get + { + try + { + return _listOfItems[_position]; + } + catch (ArgumentOutOfRangeException) + { + throw new InvalidOperationException(); + } + } + } + } +} \ No newline at end of file diff --git a/src/Types/LRSLine.cs b/src/Types/LRSLine.cs new file mode 100644 index 0000000..d30720c --- /dev/null +++ b/src/Types/LRSLine.cs @@ -0,0 +1,559 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.Types +{ + /// + /// Data structure to capture LINESTRING geometry type. + /// + internal class LRSLine : IEnumerable + { + private List _points; + private string _wkt; + internal int SRID; + internal bool IsInRange; + internal bool IsCompletelyInRange; + + /// + /// Initializes a new instance of the class. + /// + /// The srid. + internal LRSLine(int srid) + { + SRID = srid; + _points = new List(); + } + + /// + /// Gets a value indicating whether this instance is empty or not. + /// + /// + /// true if this instance is multi line; otherwise, false. + /// + internal bool IsEmpty => !_points.Any() || _points.Count == 0; + + /// + /// Gets a value indicating whether this instance has points. + /// + /// + /// true if this instance has points; otherwise, false. + /// + public bool HasPoints => _points != null && _points.Any(); + + /// + /// Gets the number of POINT in this LINESTRING. + /// + /// + /// The count. + /// + public int Count => _points.Any() ? _points.Count : 0; + + /// + /// Determines whether this instance is LINESTRING. + /// + /// + /// true if this instance is line; otherwise, false. + /// + internal bool IsLine => _points.Any() && _points.Count > 1; + + /// + /// Determines whether this instance is POINT. + /// + /// + /// true if this instance is point; otherwise, false. + /// + internal bool IsPoint => _points.Any() && _points.Count == 1; + + /// + /// Gets the length. + /// + /// + /// The length. + /// + internal double Length { get; private set; } + + #region Add Points + + /// + /// Adds the point. + /// + /// The l rs point. + internal void AddPoint(LRSPoint lrsPoint) + { + UpdateLength(lrsPoint); + _points.Add(lrsPoint); + } + + /// + /// Adds multiple points. + /// + /// List of lrs point. + internal void AddPoint(List lrsPoints) + { + lrsPoints.ForEach(AddPoint); + } + + /// + /// Adds the point. + /// + /// The lrs points. + internal void AddPoint(params LRSPoint[] points) + { + foreach (var lrsPoint in points) + { + AddPoint(lrsPoint); + } + } + + /// + /// Adds the point. + /// + /// The SqlGeometry point. + internal void AddPoint(SqlGeometry pointGeometry) + { + AddPoint(new LRSPoint(pointGeometry)); + } + + /// + /// Adds the point. + /// + /// The x. + /// The y. + /// The z. + /// The m. + internal void AddPoint(double x, double y, double? z, double? m) + { + AddPoint(new LRSPoint(x, y, z, m, SRID)); + } + + /// + /// Updates the length. + /// + /// The current point. + private void UpdateLength(LRSPoint currentPoint) + { + if (!_points.Any() || _points.Count <= 0) return; + var previousPoint = _points.Last(); + Length += previousPoint.GetDistance(currentPoint); + } + + #endregion + + #region Point Manipulation + + /// + /// Reverses the points of the line. + /// + internal void ReversePoints() + { + _points.Reverse(); + } + + /// + /// Gets the start point. + /// + /// + internal LRSPoint GetStartPoint() + { + return _points.Any() ? _points.First() : null; + } + + /// + /// Gets the start point measure. + /// + /// + internal double GetStartPointM() + { + var currentPoint = GetStartPoint().M; + return currentPoint ?? 0; + } + + /// + /// Gets the end point. + /// + /// + internal LRSPoint GetEndPoint() + { + return _points.Any() ? _points.Last() : null; + } + + /// + /// Gets the end point m. + /// + /// + internal double GetEndPointM() + { + var currentPoint = GetEndPoint().M; + return currentPoint ?? 0; + } + + /// + /// Locates the point. + /// + /// The measure. + /// + /// + internal LRSPoint LocatePoint(double measure, LRSPoint firstPoint = null) + { + var startPoint = firstPoint ?? GetStartPoint(); + var endPoint = GetEndPoint(); + + if (startPoint.M == null || endPoint.M == null) return null; + + var fraction = (measure - startPoint.M.Value) / (endPoint.M.Value - startPoint.M.Value); + var newX = (startPoint.X * (1 - fraction)) + (endPoint.X * fraction); + var newY = (startPoint.Y * (1 - fraction)) + (endPoint.Y * fraction); + + return new LRSPoint(newX, newY, null, measure, SRID); + } + + /// + /// Gets the point at m. + /// + /// + internal LRSPoint GetPointAtM(double measure) + { + return _points.FirstOrDefault(e => e.M.EqualsTo(measure)); + } + + /// + /// Scale the existing measure of geometry by multiplying existing measure with offsetMeasure + /// + /// + internal void ScaleMeasure(double offsetMeasure) + { + _points.ForEach(point => point.ScaleMeasure(offsetMeasure)); + } + + /// + /// Sum the existing measure with the offsetMeasure + /// + /// + internal void TranslateMeasure(double offsetMeasure) + { + _points.ForEach(point => point.TranslateMeasure(offsetMeasure)); + } + + /// + /// Removes the collinear points. + /// + internal void RemoveCollinearPoints() + { + // If the input segment has only two points; then there is no way of collinear + // so its no-op + if (_points.Count <= 2) + return; + + var pointCounter = 0; + var pointsToRemove = new List(); + + foreach (var point in this) + { + // ignore first point and last point + if (pointCounter == 0 || pointCounter == _points.Count - 1) + { + pointCounter++; + continue; + } + + // previous point is A + var previousPoint = _points[pointCounter - 1]; + // slope AB + var slopeAB = previousPoint.Slope; + var slopeABType = previousPoint.SlopeType; + + // current point is B, slope is BC + var slopeBC = point.Slope; + var slopeBCType = point.SlopeType; + + // next point is C + var nextPoint = _points[pointCounter + 1]; + // calculate AC slope + var slopeAC = previousPoint.GetSlope(nextPoint, out var slopeACType); + + // for each point previous and next point slope is compared + // if slope of AB = BC = CA + var isABBCSlopeEqual = slopeAB.HasValue && slopeBC.HasValue + ? slopeAB.Value.EqualsTo(slopeBC.Value) + : slopeABType == slopeBCType; + var isBCACSlopeEqual = slopeBC.HasValue && slopeAC.HasValue + ? slopeBC.Value.EqualsTo(slopeAC.Value) + : slopeBCType == slopeACType; + + if (isABBCSlopeEqual && isBCACSlopeEqual) + pointsToRemove.Add(point); + pointCounter++; + } + + // remove the collinear points + RemovePoints(pointsToRemove); + } + + /// + /// Removes the points. + /// + /// The points to remove. + internal void RemovePoints(List pointsToRemove) + { + pointsToRemove.ForEach(pointToRemove => _points.Remove(pointToRemove)); + } + + /// + /// Calculates the slope. + /// + internal void CalculateSlope() + { + var pointIterator = 1; + foreach (var point in _points) + { + // last point; next point is the first point + point.CalculateSlope(pointIterator == _points.Count ? _points.First() : _points[pointIterator]); + + pointIterator++; + } + } + + #endregion + + #region Data Structure Conversions + + /// + /// Converts to WKT format. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + if (IsEmpty) + { + _wkt = "LINESTRING EMPTY"; + return _wkt; + } + + var wktBuilder = new StringBuilder(); + + if (IsLine) + wktBuilder.Append("LINESTRING ("); + else if (IsPoint) + wktBuilder.Append("POINT ("); + + var pointIterator = 1; + + foreach (var point in _points) + { + wktBuilder.AppendFormat("{0} {1} {2}", point.X, point.Y, point.M); + if (pointIterator != _points.Count) + wktBuilder.Append(", "); + pointIterator++; + } + wktBuilder.Append(")"); + _wkt = wktBuilder.ToString(); + + return _wkt; + } + + /// + /// Converts to SqlGeometry. + /// + /// + internal SqlGeometry ToSqlGeometry() + { + var geomBuilder = new SqlGeometryBuilder(); + return ToSqlGeometry(ref geomBuilder); + } + + /// + /// Method returns the SqlGeometry form of the MULTILINESTRING + /// + /// Reference SqlGeometryBuilder to be used for building Geometry. + /// SqlGeometry + internal SqlGeometry ToSqlGeometry(ref SqlGeometryBuilder geomBuilder) + { + // ignore if the line has only one point. + if (_points.Count < 2) + return GetStartPoint().ToSqlGeometry(ref geomBuilder); + BuildSqlGeometry(ref geomBuilder); + return geomBuilder.ConstructedGeometry; + } + + /// + /// Builds the SqlGeometry form LINESTRING or POINT + /// + /// The geom builder. + /// + internal void BuildSqlGeometry(ref SqlGeometryBuilder geomBuilder, bool isFromMultiLine = false) + { + if (!isFromMultiLine) + geomBuilder.SetSrid(SRID); + geomBuilder.BeginGeometry(OpenGisGeometryType.LineString); + var pointIterator = 1; + foreach (var point in _points) + { + if (pointIterator == 1) + geomBuilder.BeginFigure(point.X, point.Y, point.Z, point.M); + else + geomBuilder.AddLine(point.X, point.Y, point.Z, point.M); + pointIterator++; + } + geomBuilder.EndFigure(); + geomBuilder.EndGeometry(); + } + + #endregion + + #region Modules for Offset Angle Calculation + /// + /// Calculate offset bearings for all points. + /// + private void CalculateOffsetBearing() + { + var pointCount = _points.Count; + for (var i = 0; i < pointCount; i++) + { + var currentPoint = _points[i]; + if (i != pointCount - 1) + { + var nextPoint = _points[i + 1]; + currentPoint.SetOffsetBearing(nextPoint); + } + else + { + currentPoint.OffsetBearing = null; + } + } + } + + /// + /// Calculate offset angle between points + /// + /// + private void CalculateOffsetAngle(bool isNegativeOffset) + { + var pointCount = _points.Count; + for (var i = 0; i < pointCount; i++) + { + var currentPoint = _points[i]; + // first point + if (i == 0) + { + _points[i].SetOffsetAngle(null, isNegativeOffset); + } + // except first point + else + { + var previousPoint = _points[i - 1]; + currentPoint.SetOffsetAngle(previousPoint, isNegativeOffset); + } + } + } + + /// + /// Calculate offset distance between points + /// + /// Offset value + private void CalculateOffset(double offset) + { + var pointCount = _points.Count; + for (var i = 0; i < pointCount; i++) + { + var currentPoint = _points[i]; + // if not last point + if (i != pointCount - 1) + { + // other points point + currentPoint.SetOffsetDistance(offset); + } + else + { + // for the last point specified offset is offset distance. + // if decrease offset distance should be negative + currentPoint.OffsetDistance = Math.Abs(offset); + } + } + } + + /// + /// Compute a parallel line to input line segment + /// + /// Offset Value + /// The tolerance. + /// Parallel Line + internal LRSLine ComputeParallelLine(double offset, double tolerance) + { + CalculateOffsetBearing(); + CalculateOffsetAngle(offset < 0); + CalculateOffset(offset); + + var parallelLine = new LRSLine(SRID); + + // compute parallel point and add additional vertices + foreach (var point in _points) + { + parallelLine.AddPoint(point.GetAndPopulateParallelPoints(offset, tolerance, ref _points)); + } + + return parallelLine; + } + + /// + /// Populates the measures. + /// + /// Length of the segment. + /// Length of the current. + /// The start measure. + /// The end measure. + /// + internal void PopulateMeasures(double segmentLength, ref double currentLength, double startMeasure, double endMeasure) + { + var pointCount = _points.Count; + for (var i = 0; i < pointCount; i++) + { + if (i == pointCount - 1) + _points[i].M = GetEndPointM(); + else if (i == 0) + _points[i].M = GetStartPointM(); + else + { + var currentPoint = _points[i]; + currentPoint.ReCalculateMeasure(_points[i - 1], ref currentLength, segmentLength, startMeasure, endMeasure); + } + } + } + + #endregion + + #region Enumeration + + /// + /// Returns an enumerator that iterates through each POINT in a LINESTRING. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Gets the enumerator. + /// + /// + public LRSEnumerator GetEnumerator() + { + return new LRSEnumerator(_points); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Types/LRSMultiLine.cs b/src/Types/LRSMultiLine.cs new file mode 100644 index 0000000..29132dc --- /dev/null +++ b/src/Types/LRSMultiLine.cs @@ -0,0 +1,419 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.Types +{ + /// + /// Data structure to capture MULTILINESTRING geometry type. + /// + internal class LRSMultiLine : IEnumerable + { + private readonly List _lines; + internal readonly int SRID; + private string _wkt; + + /// + /// Initializes a new instance of the class. + /// + /// The srid. + internal LRSMultiLine(int srid) + { + SRID = srid; + _lines = new List(); + } + + /// + /// Gets a value indicating whether this instance is empty or not. + /// + /// + /// true if this instance is multi line; otherwise, false. + /// + internal bool IsEmpty => !_lines.Any() || _lines.Count == 0; + + /// + /// Gets the number of line segments in the MULTILINESTRING. + /// + /// + /// The count. + /// + internal int Count => _lines.Any() ? _lines.Count : 0; + + /// + /// Gets a value indicating whether this instance is MULTILINESTRING. + /// + /// + /// true if this instance is a multi line; otherwise, false. + /// + internal bool IsMultiLine => Count > 1; + + /// + /// Gets a value indicating whether this instance is LINESTRING. + /// + /// + /// true if this instance is a line string; otherwise, false. + /// + internal bool IsLine => Count == 1; + + /// + /// Gets a value indicating whether this instance is a 2 POINT LINESTRING. + /// + /// + /// true if this instance is a 2 point line string; otherwise, false. + /// + internal bool Is2PointLine => IsLine && GetFirstLine().Count == 2; + + /// + /// Gets the length. + /// + /// + /// The length. + /// + internal double Length { get { return _lines.Any() ? _lines.Sum(line => line.Length) : 0; } } + + #region Add Lines + + /// + /// Adds the line. + /// + /// The LRS line. + internal void AddLine(LRSLine line) + { + if (line != null && line.HasPoints && line.IsLine) + _lines.Add(line); + } + + /// + /// Adds multiple lines. + /// + /// The line list. + internal void AddLines(List lineList) + { + if (lineList != null && lineList.Any()) + lineList.ForEach(AddLine); + } + + /// + /// Adds LRS Multi lines. + /// + /// The line list. + internal void Add(LRSMultiLine lrsMultiLine) + { + if (lrsMultiLine?._lines != null && lrsMultiLine._lines.Any()) + AddLines(lrsMultiLine._lines); + } + + #endregion + + #region Line Manipulation + + /// + /// Scale the existing measure of geometry by multiplying existing measure with offsetMeasure + /// + /// + internal void ScaleMeasure(double offsetMeasure) + { + _lines.ForEach(line => line.ScaleMeasure(offsetMeasure)); + } + + /// + /// Sum the existing measure with the offsetMeasure + /// + /// + internal void TranslateMeasure(double offsetMeasure) + { + _lines.ForEach(line => line.TranslateMeasure(offsetMeasure)); + } + + /// + /// Reverses only the LINESTRING segments order in a MULTILINESTRING + /// + internal void ReversLines() + { + _lines.Reverse(); + } + + /// + /// Removes the collinear points. + /// + internal void RemoveCollinearPoints() + { + // First calculate the slope to remove collinear points. + CalculateSlope(); + _lines.ForEach(line => line.RemoveCollinearPoints()); + } + + /// + /// Calculates the slope. + /// + internal void CalculateSlope() + { + _lines.ForEach(line => line.CalculateSlope()); + } + + /// + /// Computes the offset. + /// + /// The offset. + /// The tolerance. + /// + internal LRSMultiLine ComputeOffset(double offset, double tolerance) + { + var parallelMultiLine = new LRSMultiLine(SRID); + _lines.ForEach(line => parallelMultiLine._lines.Add(line.ComputeParallelLine(offset, tolerance))); + return parallelMultiLine; + } + + /// + /// Populates the measures. + /// + /// The start m. + /// The end m. + internal void PopulateMeasures(double? startM, double? endM) + { + var startMeasure = startM ?? 0; + var endMeasure = endM ?? Length; + double currentLength = 0; + + _lines.ForEach(line => + { + line.PopulateMeasures(Length, ref currentLength, startMeasure, endMeasure); + }); + + } + + /// + /// Reverse both LINESTRING segment and its POINTS + /// + internal void ReverseLinesAndPoints() + { + ReversLines(); + _lines.ForEach(line => line.ReversePoints()); + } + + /// + /// Gets the first LINESTRING in a MULTILINESTRING + /// + /// + internal LRSLine GetFirstLine() + { + return _lines.Any() ? _lines.First() : null; + } + + /// + /// Gets the last LINESTRING in a MULTILINESTRING. + /// + /// + internal LRSLine GetLastLine() + { + return _lines.Any() ? _lines.Last() : null; + } + + /// + /// Gets the start POINT in a MULTILINESTRING + /// + /// + internal LRSPoint GetStartPoint() + { + return _lines.Any() ? _lines.First().GetStartPoint() : null; + } + + /// + /// Gets the start POINT measure in a MULTILINESTRING + /// + /// + internal double? GetStartPointM() + { + return GetStartPoint()?.M; + } + + /// + /// Gets the end POINT in a MULTILINESTRING + /// + /// + internal LRSPoint GetEndPoint() + { + return _lines.Any() ? _lines.Last().GetEndPoint() : null; + } + + /// + /// Gets the end POINT measure in a MULTILINESTRING + /// + /// + internal double? GetEndPointM() + { + return GetEndPoint()?.M; + } + + /// + /// Gets the POINT in a MULTILINESTRING at a specified measure + /// + /// + internal LRSPoint GetPointAtM(double measure) + { + if (!_lines.Any()) return null; + + foreach (var line in _lines) + { + var point = line.GetPointAtM(measure); + if (point != null) + return point; + } + return null; + } + + /// + /// Removes the first. + /// + /// + internal void RemoveFirst() + { + if (!_lines.Any()) return; + _lines.RemoveAt(0); + } + + /// + /// Removes the last. + /// + /// + internal void RemoveLast() + { + if (!_lines.Any()) return; + _lines.RemoveAt(_lines.Count - 1); + } + + #endregion + + #region Data Structure Conversion + + /// + /// Converts to WKT format. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + if (IsEmpty) + { + _wkt = string.Empty; + return "MULTILINESTRING EMPTY"; + } + + if(IsLine) + return GetFirstLine().ToString(); + + var wktBuilder = new StringBuilder(); + if (IsMultiLine) + wktBuilder.Append("MULTILINESTRING ("); + + var lineIterator = 1; + + foreach (var line in _lines) + { + if (line.IsLine) + wktBuilder.Append(line.ToString().Replace("LINESTRING ", string.Empty)); + + if (lineIterator != _lines.Count) + wktBuilder.Append(", "); + lineIterator++; + } + + if (IsMultiLine) + wktBuilder.Append(")"); + + _wkt = wktBuilder.ToString(); + + return _wkt; + } + + /// + /// Method returns the SqlGeometry form of the MULTILINESTRING + /// + /// SqlGeometry + internal SqlGeometry ToSqlGeometry() + { + var geomBuilder = new SqlGeometryBuilder(); + return ToSqlGeometry(ref geomBuilder); + } + + /// + /// Method returns the SqlGeometry form of the MULTILINESTRING + /// + /// Reference SqlGeometryBuilder to be used for building Geometry. + /// SqlGeometry + internal SqlGeometry ToSqlGeometry(ref SqlGeometryBuilder geomBuilder) + { + if (IsEmpty) + return SqlGeometry.Null; + + return BuildSqlGeometry(ref geomBuilder) ? geomBuilder.ConstructedGeometry : null; + } + + /// + /// Method builds the SqlGeometry form of the MULTILINESTRING through reference GeometryBuilder. + /// + /// Reference SqlGeometryBuilder to be used for building Geometry. + internal bool BuildSqlGeometry(ref SqlGeometryBuilder geomBuilder) + { + var isBuildDone = false; + if (IsEmpty) + return false; + + if (geomBuilder == null) + geomBuilder = new SqlGeometryBuilder(); + + if (IsMultiLine) + { + geomBuilder.SetSrid(SRID); + geomBuilder.BeginGeometry(OpenGisGeometryType.MultiLineString); + isBuildDone = true; + } + + // ignore points + foreach (var line in _lines.Where(line => line.IsLine).ToList()) + { + line.BuildSqlGeometry(ref geomBuilder, IsMultiLine); + isBuildDone = true; + } + + if (IsMultiLine) + geomBuilder.EndGeometry(); + + return isBuildDone; + } + + + #endregion + + #region Enumeration + + /// + /// Gets the enumerator. + /// + /// + public LRSEnumerator GetEnumerator() + { + return new LRSEnumerator(_lines); + } + + /// + /// Returns an enumerator that iterates through each LINESTRING in a MULTILINESTRING. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Types/LRSPoint.cs b/src/Types/LRSPoint.cs new file mode 100644 index 0000000..1068e01 --- /dev/null +++ b/src/Types/LRSPoint.cs @@ -0,0 +1,606 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.Types +{ + /// + /// Data structure to capture POINT geometry type. + /// + internal class LRSPoint + { + // Fields. + private string _wkt; + internal readonly double X; + internal readonly double Y; + internal double? Z, M; + private readonly int _srid; + internal double? Slope; + private double _angle; + internal SlopeValue SlopeType; + + internal double? OffsetBearing; + private double _offsetAngle; + internal double OffsetDistance; + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The x. + /// The y. + /// The z. + /// The m. + /// The srid. + internal LRSPoint(double x, double y, double? z, double? m, int srid) + { + X = x; + Y = y; + Z = m.HasValue ? z : null; + M = m ?? z; + _srid = srid; + } + + /// + /// Initializes a new instance of the class. + /// + /// The SQL geometry. + internal LRSPoint(SqlGeometry sqlGeometry) + { + if (sqlGeometry.IsNullOrEmpty() || !sqlGeometry.IsPoint()) + return; + + X = sqlGeometry.STX.Value; + Y = sqlGeometry.STY.Value; + Z = sqlGeometry.HasZ ? sqlGeometry.Z.Value : (double?)null; + M = sqlGeometry.HasM ? sqlGeometry.M.Value : (double?)null; + _srid = (int)sqlGeometry.STSrid; + } + + #endregion + + #region Point Manipulation + + /// + /// Translating measure of LRSPoint + /// + /// + internal void TranslateMeasure(double offsetMeasure) + { + M += offsetMeasure; + } + + /// + /// Scaling Measure of LRSPoint + /// + /// + internal void ScaleMeasure(double offsetMeasure) + { + M *= offsetMeasure; + } + + /// + /// Gets the distance from point. + /// + /// The next point. + /// Offset Point. + internal double GetDistance(LRSPoint nextPoint) + { + return SpatialExtensions.GetDistance(X, Y, nextPoint.X, nextPoint.Y); + } + + /// + /// Determines whether X, Y co-ordinates of current and second point is within tolerance + /// + /// The second point. + /// The tolerance. + /// + /// true if X, Y co-ordinates of current and second point is within tolerance; otherwise, false. + /// + internal bool IsXYWithinTolerance(LRSPoint secondPoint, double tolerance) + { + return (Math.Abs(X - secondPoint.X) <= tolerance && Math.Abs(Y - secondPoint.Y) <= tolerance); + } + + /// + /// Re calculate the measure. + /// + /// The previous point. + /// + /// The total length. + /// The start measure. + /// The end measure. + internal void ReCalculateMeasure(LRSPoint previousPoint, ref double currentLength, double totalLength, double startMeasure, double endMeasure) + { + currentLength += GetDistance(previousPoint); + M = startMeasure + (currentLength / totalLength) * (endMeasure - startMeasure); + } + + /// + /// Gets the previous point. + /// + /// The points. + /// The current point. + /// + internal static LRSPoint GetPreviousPoint(ref List points, LRSPoint currentPoint) + { + var index = points.IndexOf(currentPoint); + // return null if index is -1; null for first point + return index > 0 ? points[index - 1] : null; + } + + /// + /// Gets the next point. + /// + /// The points. + /// The current point. + /// + internal static LRSPoint GetNextPoint(ref List points, LRSPoint currentPoint) + { + var index = points.IndexOf(currentPoint); + // return null if index is -1 or last index + return index >= 0 && index < points.Count - 1 ? points[index + 1] : null; + } + + #endregion + + #region Operator Overloading + + /// + /// Implements the operator -. + /// + /// LRS Point 1. + /// LRS Point 2. + /// The result of the operator. + public static LRSPoint operator -(LRSPoint a, LRSPoint b) + { + return new LRSPoint(b.X - a.X, b.Y - a.Y, null, null, a._srid); + } + + /// + /// Implements the operator ==. + /// + /// LRS Point 1. + /// LRS Point 2. + /// The result of the operator. + + public static bool operator ==(LRSPoint a, LRSPoint b) + { + if (ReferenceEquals(b, null) && ReferenceEquals(a, null)) + return true; + if (ReferenceEquals(b, null) || ReferenceEquals(a, null)) + return false; + return a.X.EqualsTo(b.X) && a.Y.EqualsTo(b.Y) && EqualityComparer.Default.Equals(a.M, b.M); + } + + /// + /// Implements the operator !=. + /// + /// LRS Point 1. + /// LRS Point 2. + /// The result of the operator. + public static bool operator !=(LRSPoint a, LRSPoint b) + { + return !(a == b); + } + + /// + /// Determines whether the specified , is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public override bool Equals(object obj) + { + var point = obj as LRSPoint; + return point != null && + X.EqualsTo(point.X) && + Y.EqualsTo(point.Y) && + EqualityComparer.Default.Equals(M, point.M); + } + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + // ReSharper disable NonReadonlyMemberInGetHashCode + public override int GetHashCode() + { + var hashCode = 1911090832; + hashCode = hashCode * 1521134295 + X.GetHashCode(); + hashCode = hashCode * 1521134295 + Y.GetHashCode(); + hashCode = hashCode * 1521134295 + EqualityComparer.Default.GetHashCode(Z); + hashCode = hashCode * 1521134295 + EqualityComparer.Default.GetHashCode(M); + return hashCode; + } + // ReSharper restore NonReadonlyMemberInGetHashCode + + #endregion + + #region Data Structure Conversions + + /// + /// Converts to WKT format. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + _wkt = $"POINT ({X} {Y} {M})"; + + return _wkt; + } + + /// + /// Converts to SqlGeometry. + /// + /// + internal SqlGeometry ToSqlGeometry() + { + var geomBuilder = new SqlGeometryBuilder(); + return ToSqlGeometry(ref geomBuilder); + } + + /// + /// Converts to SqlGeometry. + /// + /// The geometry builder. + /// + internal SqlGeometry ToSqlGeometry(ref SqlGeometryBuilder geometryBuilder) + { + geometryBuilder.SetSrid(_srid); + geometryBuilder.BeginGeometry(OpenGisGeometryType.Point); + geometryBuilder.BeginFigure(X, Y, Z, M); + geometryBuilder.EndFigure(); + geometryBuilder.EndGeometry(); + return geometryBuilder.ConstructedGeometry; + } + + #endregion + + #region Parallel Point Computation + + /// + /// Gets the offset point. + /// + /// The next point. + /// Offset Point. + private LRSPoint GetOffsetPoint(LRSPoint nextPoint) + { + return this - nextPoint; + } + + /// + /// Gets the arc to tangent. + /// + /// The next point. + /// In Radian + private double GetAtanInRadian(LRSPoint nextPoint) + { + var offsetPoint = GetOffsetPoint(nextPoint); + return Math.Atan2(offsetPoint.Y, offsetPoint.X); + } + + /// + /// Gets the atan2 in degrees. + /// This does angle correct when atan2 value is negative + /// + /// The point1. + /// The point2. + /// Atan2 in degrees + private double GetAtanInDegree(LRSPoint point1, LRSPoint point2) + { + var atan = point1.GetAtanInRadian(point2); + return SpatialUtil.ToDegrees(atan <= 0 ? (2 * Math.PI) + atan : atan); + } + + /// + /// Gets the first point radian. + /// + /// The previous point. + /// The middle point. + /// + private double GetFirstPointRadian(LRSPoint previousPoint, LRSPoint middlePoint) + { + var atan = GetAtanInDegree(middlePoint, previousPoint); + atan = 90 - atan; + atan = atan <= 0 ? 360 + atan : atan; + + return SpatialUtil.ToRadians(360 - atan); + } + + /// + /// Gets the second point radian. + /// + /// The next point. + /// The middle point. + /// + private double GetSecondPointRadian(LRSPoint nextPoint, LRSPoint middlePoint) + { + var atan = GetAtanInDegree(middlePoint, nextPoint); + atan = 90 - atan; + atan = atan <= 0 ? 360 + atan : atan; + + return SpatialUtil.ToRadians(180 - atan); + } + + /// + /// Gets the deviation angle of 3 points. + /// + /// The point a. + /// The point o. + /// The point b. + /// if set to true [is negative offset]. + /// + private double GetAOBAngle(LRSPoint pointA, LRSPoint pointO, LRSPoint pointB, bool isNegativeOffset) + { + const double angleCorrection = Math.PI / 2; + const double angleConversion = 2 * Math.PI; + + var atanAo = pointA.GetAtanInRadian(pointO) + angleCorrection; + var atanBo = pointB.GetAtanInRadian(pointO) + angleCorrection; + + // angle conversion + atanAo = SpatialUtil.ToDegrees(atanAo <= 0 ? angleConversion + atanAo : atanAo); + atanBo = SpatialUtil.ToDegrees(atanBo <= 0 ? angleConversion + atanBo : atanBo); + + var deviationAngle = + 360 - (atanAo > atanBo + ? 360 - (atanAo - atanBo) + : atanBo - atanAo); + + // for positive offset; offset curve will be to the left of input geom; + // so for positive deviation angle the computed angle should be subtracted from 360 + // for negative offset; offset curve will be to the right of input geom + return isNegativeOffset ? deviationAngle : 360 - deviationAngle; + } + + /// + /// Sets the offset bearing. + /// + /// The next point. + internal void SetOffsetBearing(LRSPoint nextPoint) + { + if (nextPoint != null) + OffsetBearing = CalculateOffsetBearing(nextPoint); + } + + /// + /// Calculates the offset bearing. + /// + /// The next point. + private double CalculateOffsetBearing(LRSPoint nextPoint) + { + _angle = SpatialUtil.ToDegrees(GetAtanInRadian(nextPoint)); + return (90 - _angle + 360) % 360; + } + + /// + /// Sets the offset angle. + /// + /// The current point. + /// Is Offset is Negative + internal void SetOffsetAngle(LRSPoint previousPoint, bool isNegativeOffset) + { + _offsetAngle = CalculateOffsetAngle(previousPoint, isNegativeOffset); + } + + /// + /// Calculates the offset angle. + /// + /// The current point. + /// Is Offset is Negative + private double CalculateOffsetAngle(LRSPoint previousPoint, bool isNegativeOffset) + { + double offsetAngle = 0; + + var previousPointOffsetBearing = previousPoint?.OffsetBearing; + + // Left + if (!isNegativeOffset) + { + if (OffsetBearing == null) + { + if (previousPointOffsetBearing != null) offsetAngle = (double)previousPointOffsetBearing - 90; + } + else if (previousPointOffsetBearing == null) + offsetAngle = (double)OffsetBearing - 90; + else + //(360 + b1.OffsetBearing - ((360 - ((b2.OffsetBearing + 180) - b1.OffsetBearing)) / 2)) % 360 + offsetAngle = ( + 360 + (double)OffsetBearing - + ( + ( + 360 - ( + ((double)previousPointOffsetBearing + 180) - (double)OffsetBearing + ) + ) / 2 + ) + ) % 360; + } + // Right + else + { + + if (OffsetBearing == null) + { + if (previousPointOffsetBearing != null) offsetAngle = (double)previousPointOffsetBearing + 90; + } + else if (previousPointOffsetBearing == null) + offsetAngle = (double)OffsetBearing + 90; + else + // (b1.OffsetBearing + ((((b2.OffsetBearing + 180) - b1.OffsetBearing)) / 2)) % 360 + offsetAngle = ((double)OffsetBearing + (((((double)previousPointOffsetBearing + 180) - (double)OffsetBearing)) / 2)) % 360; + } + + return offsetAngle; + } + + /// + /// Sets the offset distance. + /// + /// The offset. + internal void SetOffsetDistance(double offset) + { + var offsetBearing = OffsetBearing ?? default(double); + // offset / (SIN(RADIANS(((OffsetBearing - OffsetAngleLeft) + 360) % 360))) + OffsetDistance = CalculateOffsetDistance(offset, offsetBearing, _offsetAngle); + } + + /// + /// Calculates the offset distance. + /// + /// The offset. + /// + /// + private static double CalculateOffsetDistance(double offset, double offsetBearing, double offsetAngle) + { + // offset / (SIN(RADIANS(((OffsetBearing - OffsetAngleLeft) + 360) % 360))) + var denominator = (Math.Sin(SpatialUtil.ToRadians(((offsetBearing - offsetAngle) + 360) % 360))); + return offset / denominator; + } + + /// + /// Gets the parallel point. + /// + /// Point parallel to the current point. + private LRSPoint GetParallelPoint() + { + var newX = X + (OffsetDistance * Math.Cos(SpatialUtil.ToRadians(90 - _offsetAngle))); + var newY = Y + (OffsetDistance * Math.Sin(SpatialUtil.ToRadians(90 - _offsetAngle))); + + return new LRSPoint( + newX, + newY, + null, + M, + _srid + ); + } + + /// + /// Compute and populate parallel points on bend lines. + /// + /// The offset. + /// The points. + /// + /// Point parallel to the current point. + internal List GetAndPopulateParallelPoints(double offset, double tolerance, ref List points) + { + // list to capture additional vertices. + var lrsPoints = new List(); + + // parallel point to the current point + var parallelPoint = GetParallelPoint(); + + // get previous and next point + var previousPoint = GetPreviousPoint(ref points, this); + var nextPoint = GetNextPoint(ref points, this); + + // offset distance between parallel point and input point + var diffInDistance = Math.Round(parallelPoint.GetDistance(this), 5); + // offset distance difference between parallel point and input point + var offsetDiff = Math.Abs(Math.Abs(diffInDistance) - Math.Abs(offset)); + + if (offsetDiff <= tolerance || previousPoint == null || nextPoint == null) + { + lrsPoints.Add(parallelPoint); + } + else + { + var negativeOffset = offset < 0; + var deviationAngle = GetAOBAngle(previousPoint, this, nextPoint, negativeOffset); + + if (deviationAngle <= 90) + { + var firsPointRadian = GetFirstPointRadian(previousPoint, this); + var nextPointRadian = GetSecondPointRadian(nextPoint, this); + + // first point + var firstPointX = X + (offset * Math.Cos(firsPointRadian)); + var firstPointY = Y + (offset * Math.Sin(firsPointRadian)); + var firstPoint = new LRSPoint(firstPointX, firstPointY, null, M, _srid); + + // second point + var secondPointX = X + (offset * Math.Cos(nextPointRadian)); + var secondPointY = Y + (offset * Math.Sin(nextPointRadian)); + var secondPoint = new LRSPoint(secondPointX, secondPointY, null, M, _srid); + + // if computed first point is within tolerance of second point then add only first point + if (firstPoint.GetDistance(secondPoint) <= tolerance) + { + lrsPoints.Add(firstPoint); + } + else + { + // add first point + lrsPoints.Add(firstPoint); + + // compute middle point + var fraction = Math.Abs(offset / OffsetDistance); + var middleX = (X * (1 - fraction)) + (parallelPoint.X * fraction); + var middleY = (Y * (1 - fraction)) + (parallelPoint.Y * fraction); + var middlePoint = new LRSPoint(middleX, middleY, null, M, _srid); + + // if not within tolerance add middle point + if (firstPoint.GetDistance(middlePoint) > tolerance) + lrsPoints.Add(middlePoint); + + // add second point + lrsPoints.Add(secondPoint); + } + } + else + lrsPoints.Add(parallelPoint); + } + + return lrsPoints; + } + + /// + /// Calculates the slope. + /// + /// The next LRS point. + internal void CalculateSlope(LRSPoint nextLRSPoint) + { + Slope = GetSlope(nextLRSPoint, out SlopeType); + } + + /// + /// Calculates the slope. + /// + /// The next LRS point. + /// + internal double? GetSlope(LRSPoint nextLRSPoint, out SlopeValue slopeValue) + { + slopeValue = SlopeValue.None; + var xDifference = nextLRSPoint.X - X; + var yDifference = nextLRSPoint.Y - Y; + + if (xDifference.EqualsTo(0)) + { + slopeValue = yDifference > 0 ? SlopeValue.PositiveInfinity : SlopeValue.NegativeInfinity; + return null; + } + else if (yDifference.EqualsTo(0)) + { + slopeValue = xDifference > 0 ? SlopeValue.PositiveZero : SlopeValue.NegativeZero; + return null; + } + + return yDifference / xDifference; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Types/SQL/AffineTransform.cs b/src/Types/SQL/AffineTransform.cs new file mode 100644 index 0000000..7dd94d9 --- /dev/null +++ b/src/Types/SQL/AffineTransform.cs @@ -0,0 +1,115 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.IO; +using System.Data.SqlTypes; +using Microsoft.SqlServer.Server; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Utility; +using SQLSpatialTools.Sinks.Geometry; + +namespace SQLSpatialTools.Types.SQL +{ + [Serializable] + [SqlUserDefinedType(Format.UserDefined, IsByteOrdered = false, MaxByteSize = -1, IsFixedLength = false)] + public sealed class AffineTransform : INullable, IBinarySerialize + { + private double _ax, _bx, _cx; + private double _ay, _by, _cy; + + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public static AffineTransform Translate(double cx, double cy) + { + var transform = new AffineTransform {_ax = 1, _by = 1, _cx = cx, _cy = cy}; + return transform; + } + + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public static AffineTransform Rotate(double angleDeg) + { + var angle = SpatialUtil.ToRadians(angleDeg); + var transform = new AffineTransform {_ax = Math.Cos(angle), _ay = Math.Sin(angle)}; + transform._bx = -transform._ay; + transform._by = transform._ax; + return transform; + } + + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public static AffineTransform Scale(double sx, double sy) + { + var transform = new AffineTransform {_ax = sx, _by = sy}; + return transform; + } + + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public SqlGeometry Apply(SqlGeometry geometry) + { + var builder = new SqlGeometryBuilder(); + geometry.Populate(new GeometryTransformer(builder, this)); + return builder.ConstructedGeometry; + } + + public double GetX(double x, double y) + { + return _ax * x + _bx * y + _cx; + } + + public double GetY(double x, double y) + { + return _ay * x + _by * y + _cy; + } + + public void Read(BinaryReader r) + { + _ax = r.ReadDouble(); + _bx = r.ReadDouble(); + _cx = r.ReadDouble(); + _ay = r.ReadDouble(); + _by = r.ReadDouble(); + _cy = r.ReadDouble(); + } + + public void Write(BinaryWriter binaryWriter) + { + binaryWriter.Write(_ax); + binaryWriter.Write(_bx); + binaryWriter.Write(_cx); + binaryWriter.Write(_ay); + binaryWriter.Write(_by); + binaryWriter.Write(_cy); + } + + public bool IsNull => false; + + public static AffineTransform Null + { + [SqlMethod(IsDeterministic = true, IsPrecise = true)] + get => null; + } + + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public static AffineTransform Parse(SqlString str) + { + var transform = new AffineTransform(); + + var args = str.ToString().Split(' '); + transform._ax = double.Parse(args[0]); + transform._bx = double.Parse(args[1]); + transform._cx = double.Parse(args[2]); + transform._ay = double.Parse(args[3]); + transform._by = double.Parse(args[4]); + transform._cy = double.Parse(args[5]); + + return transform; + } + + [return: SqlFacet(MaxSize = -1)] + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public override string ToString() + { + return _ax + " " + _bx + " " + _cx + " " + _ay + " " + _by + " " + _cy; + } + } +} \ No newline at end of file diff --git a/src/Types/SQL/SqlProjection.cs b/src/Types/SQL/SqlProjection.cs new file mode 100644 index 0000000..8058b35 --- /dev/null +++ b/src/Types/SQL/SqlProjection.cs @@ -0,0 +1,278 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Data.SqlTypes; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using Microsoft.SqlServer.Server; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Projections; +using SQLSpatialTools.Sinks.Geography; +using SQLSpatialTools.Sinks.Geometry; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.Types.SQL +{ + [Serializable] + [SqlUserDefinedType(Format.UserDefined, IsByteOrdered = false, MaxByteSize = -1, IsFixedLength = false)] + public sealed class SqlProjection : INullable, IBinarySerialize + { + private Projection _projection; + + public SqlProjection() + { + } + + internal SqlProjection(Projection projection) + { + Debug.Assert(projection != null); + _projection = projection; + } + + public static SqlProjection Null + { + [SqlMethod(IsDeterministic = true, IsPrecise = true)] + get => new SqlProjection(); + } + + // TODO use WKT projection description format + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public static SqlProjection Parse(SqlString s) + { + if (s.Value.Equals("NULL")) + return Null; + + var a = s.Value.Split(' '); + var cons = Type.GetType(a[0])?.GetConstructor(new[] { typeof(Dictionary) }); + return cons != null ? new SqlProjection((Projection) cons.Invoke(new object[] {Projection.ParseParameters(a[1])})) : Null; + } + + // TODO use WKT projection description format + [return: SqlFacet(MaxSize = -1)] + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public override string ToString() + { + if (_projection == null) + { + return "NULL"; + } + else + { + return _projection.GetType().FullName + " " + _projection.Parameters; + } + } + + public bool IsNull + { + [SqlMethod(IsDeterministic = true, IsPrecise = true)] + get => _projection == null; + } + + public void Read(BinaryReader r) + { + if (r == null) return; + + var name = r.ReadString(); + if (string.IsNullOrEmpty(name)) + { + _projection = null; + } + else + { + var cons = Type.GetType(name)?.GetConstructor(new[] { typeof(Dictionary) }); + if (cons != null) + _projection = (Projection) cons.Invoke(new object[] {Projection.ParseParameters(r.ReadString())}); + } + } + + public void Write(BinaryWriter w) + { + if (w == null) return; + if (_projection == null) + { + w.Write(""); + } + else + { + w.Write(_projection.GetType().FullName ?? throw new InvalidOperationException()); + w.Write(_projection.Parameters); + } + } + + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public static SqlProjection AlbersEqualArea(double longitude0, double latitude0, double parallel1, double parallel2) + { + var parameters = new Dictionary + { + ["longitude0"] = longitude0, + ["latitude0"] = latitude0, + ["parallel1"] = parallel1, + ["parallel2"] = parallel2 + }; + return new SqlProjection(new AlbersEqualAreaProjection(parameters)); + } + + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public static SqlProjection Equirectangular(double longitude0, double parallel) + { + var parameters = new Dictionary {["longitude0"] = longitude0, ["parallel"] = parallel}; + return new SqlProjection(new EquirectangularProjection(parameters)); + } + + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public static SqlProjection LambertConformalConic(double longitude0, double latitude, double fi1, double fi2) + { + var parameters = new Dictionary + { + ["longitude0"] = longitude0, ["latitude0"] = latitude, ["fi1"] = fi1, ["fi2"] = fi2 + }; + return new SqlProjection(new LambertConformalConicProjection(parameters)); + } + + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public static SqlProjection Mercator(double longitude0) + { + var parameters = new Dictionary {["longitude0"] = longitude0}; + return new SqlProjection(new MercatorProjection(parameters)); + } + + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public static SqlProjection ObliqueMercator(double longitude0, double fi1, double lambda1, double fi2, double lambda2) + { + var parameters = new Dictionary + { + ["longitude0"] = longitude0, + ["fi1"] = fi1, + ["lambda1"] = lambda1, + ["fi2"] = fi2, + ["lambda2"] = lambda2 + }; + return new SqlProjection(new ObliqueMercatorProjection(parameters)); + } + + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public static SqlProjection TransverseMercator(double longitude0) + { + var parameters = new Dictionary {["longitude0"] = longitude0}; + return new SqlProjection(new TransverseMercatorProjection(parameters)); + } + + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public static SqlProjection Gnomonic(double longitude, double latitude) + { + var parameters = new Dictionary + { + ["longitude0"] = 0, ["longitude1"] = longitude, ["latitude1"] = latitude + }; + return new SqlProjection(new GnomonicProjection(parameters)); + } + + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + private static void ThrowIfArgumentNull(INullable argument, string name) + { + if (argument == null || argument.IsNull) + { + throw new ArgumentNullException(name); + } + } + + // INSTANCE METHODS AND FIELDS + + // Projects geography onto geometry. + // Returns valid projected SqlGeometry object. + // Constructed geometry will have the same SRID as geography. + // + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public SqlGeometry Project(SqlGeography geography) + { + ThrowIfArgumentNull(geography, "geography"); + var builder = new SqlGeometryBuilder(); + geography.Populate(new Projector(this, builder)); + return builder.ConstructedGeometry; + } + + // Unprojects geometry producing geography. + // Returns a valid unprojected SqlGeography object. + // + // SRID taken from the geometry object may not be valid for geography, + // in which case a new SRID must be given. + // + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public SqlGeography UnprojectWithSRID(SqlGeometry geometry, SqlInt32 newSrid) + { + ThrowIfArgumentNull(geometry, "geometry"); + ThrowIfArgumentNull(newSrid, "newSrid"); + var builder = new SqlGeographyBuilder(); + geometry.Populate(new UnProjector(this, builder, newSrid.Value)); + return builder.ConstructedGeography; + } + + // Unprojects geometry producing geography. + // This method assumes that the SRID of geometry object is valid for geography. + // Returns a valid unprojected SqlGeography object. + // + [SqlMethod(IsDeterministic = true, IsPrecise = false)] + public SqlGeography Unproject(SqlGeometry geometry) + { + ThrowIfArgumentNull(geometry, "geometry"); + var builder = new SqlGeographyBuilder(); + geometry.Populate(new UnProjector(this, builder, geometry.STSrid.Value)); // NOTE srid will be reused + return builder.ConstructedGeography; + } + + // Longitude and latitude are in degrees. + // Input Latitude must be in range [-90, 90]. + // Input Longitude must be in range [-15069, 15069]. + // + public void ProjectPoint(double latitudeDeg, double longitudeDeg, out double x, out double y) + { + var latitude = MathX.InputLat(latitudeDeg, 90, "latitude"); + var longitude = MathX.NormalizeLongitudeRad(MathX.InputLong(longitudeDeg, 15069, "longitude") - _projection.CentralLongitudeRad); + + _projection.Project(latitude, longitude, out x, out y); + + if (double.IsNaN(x)) + { + throw new ArgumentOutOfRangeException(nameof(x)); + } + if (double.IsNaN(y)) + { + throw new ArgumentOutOfRangeException(nameof(y)); + } + } + + // Longitude and latitude are in degrees. + // Output Latitude will be in range [-90, 90]. + // Output Longitude will be in range [-180, 180]. + // + public void UnprojectPoint(double x, double y, out double latitudeDeg, out double longitudeDeg) + { + if (double.IsNaN(x)) + { + throw new ArgumentException(Resource.InputCoordinateIsNaN, nameof(x)); + } + if (double.IsNaN(y)) + { + throw new ArgumentException(Resource.InputCoordinateIsNaN, nameof(y)); + } + + _projection.Unproject(x, y, out var latitude, out var longitude); + + if (double.IsNaN(latitude) || latitude < -Math.PI / 2 || latitude > Math.PI / 2) + { + throw new ArgumentOutOfRangeException(nameof(latitude), string.Format(CultureInfo.InvariantCulture, Resource.OutputLatitudeIsOutOfRange, latitude)); + } + if (double.IsNaN(longitude) || longitude < -Math.PI || longitude > Math.PI) + { + throw new ArgumentOutOfRangeException(nameof(longitude), string.Format(CultureInfo.InvariantCulture, Resource.OutputLongitudeIsOutOfRange, longitude)); + } + + latitudeDeg = MathX.Clamp(90, SpatialUtil.ToDegrees(latitude)); + longitudeDeg = MathX.NormalizeLongitudeDeg(SpatialUtil.ToDegrees(longitude + _projection.CentralLongitudeRad)); + } + } +} \ No newline at end of file diff --git a/Utility/Vector3.cs b/src/Types/Vector3.cs similarity index 68% rename from Utility/Vector3.cs rename to src/Types/Vector3.cs index 699c9e7..6ec3020 100644 --- a/Utility/Vector3.cs +++ b/src/Types/Vector3.cs @@ -1,98 +1,101 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -using System; - -namespace SQLSpatialTools -{ - internal class Vector3 - { - // Fields. - public readonly double x, y, z; - - // Constructors. - public Vector3(double x, double y, double z) - { - this.x = x; - this.y = y; - this.z = z; - } - - // Addition. - public static Vector3 operator +(Vector3 a, Vector3 b) - { - return new Vector3(a.x + b.x, a.y + b.y, a.z + b.z); - } - - // Subtraction. - public static Vector3 operator -(Vector3 a, Vector3 b) - { - return new Vector3(a.x - b.x, a.y - b.y, a.z - b.z); - } - - // Multiplication. - public static Vector3 operator *(Vector3 vector, double a) - { - return new Vector3(vector.x * a, vector.y * a, vector.z * a); - } - - // Division. - public static Vector3 operator /(Vector3 v, double a) - { - return v * (1 / a); - } - - // Unit vector with the same direction. - public Vector3 Unitize() - { - return this / VectorLength(); - } - - // Dot product. - public static double operator *(Vector3 a, Vector3 b) - { - return b.x * a.x + b.y * a.y + b.z * a.z; - } - - // The square if the length. - public double LengthSquared() - { - return this * this; - } - - // The length of the vector. - public double VectorLength() - { - return Math.Sqrt(LengthSquared()); - } - - // Squared distance between vectors a and b. - public double DistanceSquared(Vector3 a) - { - return (this - a) * (this - a); - } - - // Distance between vectors a and b. - public double Distance(Vector3 a) - { - return Math.Sqrt(DistanceSquared(a)); - } - - // Cross product between vectors a and b. - public Vector3 CrossProduct(Vector3 a) - { - return new Vector3(y * a.z - z * a.y, z * a.x - x * a.z, x * a.y - y * a.x); - } - - // Angle in radians between vectors a and b. - public double Angle(Vector3 a) - { - return 2 * Math.Asin(this.Distance(a) / (2 * a.VectorLength())); - } - - // Angle in degrees between vectors a and b. - public double AngleInDegrees(Vector3 a) - { - return Util.ToDegrees(this.Angle(a)); - } - } +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.Types +{ + internal class Vector3 + { + // Fields. + public readonly double X, Y, Z; + + // Constructors. + public Vector3(double x, double y, double z) + { + X = x; + Y = y; + Z = z; + } + + // Addition. + public static Vector3 operator +(Vector3 a, Vector3 b) + { + return new Vector3(a.X + b.X, a.Y + b.Y, a.Z + b.Z); + } + + // Subtraction. + public static Vector3 operator -(Vector3 a, Vector3 b) + { + return new Vector3(a.X - b.X, a.Y - b.Y, a.Z - b.Z); + } + + // Multiplication. + public static Vector3 operator *(Vector3 vector, double a) + { + return new Vector3(vector.X * a, vector.Y * a, vector.Z * a); + } + + // Division. + public static Vector3 operator /(Vector3 v, double a) + { + return v * (1 / a); + } + + // Unit vector with the same direction. + public Vector3 Unitize() + { + return this / VectorLength(); + } + + // Dot product. + public static double operator *(Vector3 a, Vector3 b) + { + return b.X * a.X + b.Y * a.Y + b.Z * a.Z; + } + + // The square if the length. + public double LengthSquared() + { + return this * this; + } + + // The length of the vector. + public double VectorLength() + { + return Math.Sqrt(LengthSquared()); + } + + // Squared distance between vectors a and b. + public double DistanceSquared(Vector3 a) + { + return (this - a) * (this - a); + } + + // Distance between vectors a and b. + public double Distance(Vector3 a) + { + return Math.Sqrt(DistanceSquared(a)); + } + + // Cross product between vectors a and b. + public Vector3 CrossProduct(Vector3 a) + { + return new Vector3(Y * a.Z - Z * a.Y, Z * a.X - X * a.Z, X * a.Y - Y * a.X); + } + + // Angle in radians between vectors a and b. + public double Angle(Vector3 a) + { + return 2 * Math.Asin(Distance(a) / (2 * a.VectorLength())); + } + + // Angle in degrees between vectors a and b. + public double AngleInDegrees(Vector3 a) + { + return SpatialUtil.ToDegrees(Angle(a)); + } + } } \ No newline at end of file diff --git a/src/Types/Vertex.cs b/src/Types/Vertex.cs new file mode 100644 index 0000000..a2ee836 --- /dev/null +++ b/src/Types/Vertex.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.SqlServer.Types; + +namespace SQLSpatialTools.Types +{ + internal struct Vertex + { + private readonly double _x; + private readonly double _y; + private readonly double? _z; + private readonly double? _m; + + public Vertex(double x, double y, double? z, double? m) + { + _x = x; + _y = y; + _z = z; + _m = m; + } + + public void BeginFigure(IGeometrySink110 sink) { sink.BeginFigure(_x, _y, _z, _m); } + public void AddLine(IGeometrySink110 sink) { sink.AddLine(_x, _y, _z, _m); } + + public void BeginFigure(IGeographySink110 sink) { sink.BeginFigure(_x, _y, _z, _m); } + public void AddLine(IGeographySink110 sink) { sink.AddLine(_x, _y, _z, _m); } + } +} \ No newline at end of file diff --git a/src/Utility/EnumsAndConstants.cs b/src/Utility/EnumsAndConstants.cs new file mode 100644 index 0000000..df2ba07 --- /dev/null +++ b/src/Utility/EnumsAndConstants.cs @@ -0,0 +1,155 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; + +namespace SQLSpatialTools.Utility +{ + /// + /// Enum for Open Geo spatial Consortium Type Names + /// + public enum OGCType + { + [StringValue("Point")] + Point, + + [StringValue("LineString")] + LineString, + + [StringValue("CircularString")] + CircularString, + + [StringValue("CompoundCurve")] + CompoundCurve, + + [StringValue("Polygon")] + Polygon, + + [StringValue("CurvePolygon")] + CurvePolygon, + + [StringValue("GeometryCollection")] + GeometryCollection, + + [StringValue("MultiPoint")] + MultiPoint, + + [StringValue("MultiLineString")] + MultiLineString, + + [StringValue("MultiPolygon")] + MultiPolygon + } + + public enum DimensionalInfo + { + None, + [StringValue("2 Dimensional point, with x and y")] + Dim2D, + [StringValue("2 Dimensional point, with x, y and measure")] + Dim2DWithMeasure, + [StringValue("3 Dimensional point, with x, y and z")] + Dim3D, + [StringValue("3 Dimensional point, with x, y, z with measure")] + Dim3DWithMeasure + } + + public static class Constants + { + public const int DefaultSRID = 4326; + + // 1 cm tolerance in most SRIDs + public const double Tolerance = 0.5; + + // Point sql char format. + public const string PointSqlCharFormat = "POINT({0} {1} {2} {3})"; + } + + public enum SlopeValue + { + None, + PositiveZero, + NegativeZero, + PositiveInfinity, + NegativeInfinity + } + + /// + /// This attribute is used to represent a string value + /// for a value in an enum. + /// + public sealed class StringValueAttribute : Attribute + { + /// + /// Holds the string value for a value in an enum. + /// + public string StringValue { get; } + + /// + /// Constructor used to initialize a StringValue Attribute + /// + /// + public StringValueAttribute(string value) + { + StringValue = value; + } + } + + public enum LRSErrorCodes + { + [StringValue("Valid LRS segment")] + ValidLRS = 1, + + [StringValue("Invalid LRS segment")] + InvalidLRS = 13331, + + [StringValue("Invalid LRS point")] + InvalidLRSPoint = 13332, + + [StringValue("Invalid LRS measure")] + InvalidLRSMeasure = 13333, + + [StringValue("LRS segments not connected")] + LRSSegmentNotConnected = 13334, + + [StringValue("LRS segment is not defined")] + LRSSegmentNotDefined = 13335 + } + + /// + /// Progression of Geom Segment based on measure. + /// + public enum LinearMeasureProgress : short + { + None = 0, + Increasing = 1, + Decreasing = 2 + } + + /// + /// Merging position of Geom Segments + /// + public enum MergePosition + { + None = 0, + StartStart = 1, + StartEnd = 2, + EndStart = 3, + EndEnd = 4, + BothEnds = 5, + CrossEnds = 6 + } + + /// + /// Merge Input Segments Geometry type. + /// + public enum MergeInputType + { + None = 0, + LSLS = 1, + LSMLS = 2, + MLSLS = 3, + MLSMLS = 4 + } +} diff --git a/src/Utility/ErrorMessage.cs b/src/Utility/ErrorMessage.cs new file mode 100644 index 0000000..3e633fd --- /dev/null +++ b/src/Utility/ErrorMessage.cs @@ -0,0 +1,27 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +namespace SQLSpatialTools.Utility +{ + public static class ErrorMessage + { + public const string LineStringCompatible = "LINESTRING is currently the only spatial type supported."; + public const string LRSCompatible = "POINT, LINESTRING or MULTILINE STRING is currently the only spatial type supported."; + public const string LineOrMultiLineStringCompatible = "LINESTRING or MULTILINE STRING is currently the only spatial type supported."; + public const string LineOrPointCompatible = "LINESTRING or POINT is currently the only spatial type supported."; + public const string MultiLineStringCompatible = "MULTILINE STRING is currently the only spatial type supported."; + public const string PointCompatible = "Start and End geometry must be a point."; + public const string SRIDCompatible = @"SRID's of geography\geometry objects doesn't match."; + public const string MeasureRange = "Measure not within range."; + public const string LinearMeasureRange = "The given measure for linear referencing was out of range."; + public const string WKT3DOnly = "Input WKT should only have three dimensions!"; + public const string LinearGeometryMeasureMustBeInRange = "{0} is not within the measure range {1} : {2} of the linear geometry."; + public const string DistanceMustBeBetweenTwoPoints = "The distance value provided exceeds the distance between the two points."; + public const string DistanceMustBePositive = "The distance must be positive."; + public const string TwoDimensionalCoordinates = "Cannot operate on 2 Dimensional co-ordinates without measure values."; + public const string InvalidElementIndex = "Invalid index for element to be extracted."; + public const string InvalidSubElementIndex = "Invalid index for sub-element to be extracted."; + public const string InvalidGeometry = "Invalid geometry"; + } +} diff --git a/src/Utility/SQLTypeConversions.cs b/src/Utility/SQLTypeConversions.cs new file mode 100644 index 0000000..52123c7 --- /dev/null +++ b/src/Utility/SQLTypeConversions.cs @@ -0,0 +1,31 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System.Data.SqlTypes; + +namespace SQLSpatialTools.Utility +{ + internal static class SQLTypeConversions + { + public class Numeric + { + private readonly int _value; + + private Numeric(int value) + { + _value = value; + } + + public static implicit operator int(Numeric v) + { + return v._value; + } + + public static implicit operator Numeric(SqlInt32 value) + { + return new Numeric((int)value); + } + } + } +} \ No newline at end of file diff --git a/src/Utility/SpatialExtensions.cs b/src/Utility/SpatialExtensions.cs new file mode 100644 index 0000000..d085f47 --- /dev/null +++ b/src/Utility/SpatialExtensions.cs @@ -0,0 +1,1332 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Data.SqlTypes; +using System.Linq; +using Microsoft.SqlServer.Types; +using System.Globalization; +using SQLSpatialTools.Sinks.Geometry; +using SQLSpatialTools.Types; +using System.Text.RegularExpressions; +using System.Text; +using System.Collections.Generic; + +namespace SQLSpatialTools.Utility +{ + public static class SpatialExtensions + { + private const string _geomTypeRegexString = @"^(?\w+)?\s?\("; + private const string _polygonSegmentRegex = @"POLYGON\s*?\((?.*)\)"; + private const string _multipolygonSegmentRegex = @"MULTIPOLYGON\s*?\((?.*)\)"; + private const string _segmentMatchRegex = @"\(.*?\)"; + + #region OGC Type Checks + + /// + /// Check if Geometry is of type point, + /// it returns true, if the geometry is of type point + /// Instead of checking the STGeometryType directly, this utility method parses the points of the geometry, which make sure it returns true, even if the geometry contains invalid co-ordinates + /// + /// + /// + public static bool CheckGeomPoint(this SqlGeometry sqlGeometry) + { + try + { + return GetPoint(sqlGeometry).STGeometryType().Compare(OGCType.Point.GetString()); + } + catch (SqlNullValueException) + { + return false; + } + } + + /// + /// Check if Geometry is Point + /// + /// + /// + public static bool IsPoint(this SqlGeometry sqlGeometry) + { + return sqlGeometry.STGeometryType().Compare(OGCType.Point.GetString()); + } + + /// + /// Check if Geometry is Point; Works on invalid types too. + /// + /// Geometry in WKT format + /// + public static bool IsPoint(this string wktGeometry) + { + return Regex.IsMatch(wktGeometry, @"^POINT\s*\(.*\)", RegexOptions.IgnoreCase); + } + + /// + /// Check if Geography type is Point + /// + /// SQL Geography + /// + public static bool IsPoint(this SqlGeography sqlGeography) + { + return sqlGeography.STGeometryType().Compare(OGCType.Point.GetString()); + } + + /// + /// Check if Geometry is LineString + /// + /// + /// true; false + public static bool IsLineString(this SqlGeometry sqlGeometry) + { + return sqlGeometry.STGeometryType().Compare(OGCType.LineString.GetString()); + } + + /// + /// Check if Geometry is CircularString + /// + /// + /// true; false + public static bool IsCircularString(this SqlGeometry sqlGeometry) + { + return sqlGeometry.STGeometryType().Compare(OGCType.CircularString.GetString()); + } + + /// + /// Check if Geometry is CompoundCurve + /// + /// + /// true; false + public static bool IsCompoundCurve(this SqlGeometry sqlGeometry) + { + return sqlGeometry.STGeometryType().Compare(OGCType.CompoundCurve.GetString()); + } + + /// + /// Check if Geometry is Polygon + /// + /// + /// true; false + public static bool IsPolygon(this SqlGeometry sqlGeometry) + { + return sqlGeometry.STGeometryType().Compare(OGCType.Polygon.GetString()); + } + + /// + /// Check if Geometry is Polygon + /// + /// + /// true; false + public static bool IsPolygon(this SqlGeometry sqlGeometry, bool checkValid = true) + { + if (checkValid) + return sqlGeometry.IsPolygon(); + return GetGeomText(sqlGeometry.ToString()).Equals("POLYGON"); + } + + /// + /// Check if Geometry is CurvePolygon + /// + /// + /// true; false + public static bool IsCurvePolygon(this SqlGeometry sqlGeometry) + { + return sqlGeometry.STGeometryType().Compare(OGCType.CurvePolygon.GetString()); + } + + /// + /// Check if Geometry is CurvePolygon + /// + /// + /// true; false + public static bool IsCurvePolygon(this SqlGeometry sqlGeometry, bool checkValid = true) + { + if (checkValid) + return sqlGeometry.IsCurvePolygon(); + return GetGeomText(sqlGeometry.ToString()).Equals("CURVEPOLYGON"); + } + + /// + /// Check if Geometry is GeometryCollection + /// + /// + /// true; false + public static bool IsGeometryCollection(this SqlGeometry sqlGeometry) + { + return sqlGeometry.STGeometryType().Compare(OGCType.GeometryCollection.GetString()); + } + + /// + /// Check if Geometry is MultiPoint + /// + /// + /// true; false + public static bool IsMultiPoint(this SqlGeometry sqlGeometry) + { + return sqlGeometry.STGeometryType().Compare(OGCType.MultiPoint.GetString()); + } + + /// + /// Check if Geometry is MultiLineString + /// + /// + /// true; false + public static bool IsMultiLineString(this SqlGeometry sqlGeometry) + { + return sqlGeometry.STGeometryType().Compare(OGCType.MultiLineString.GetString()); + } + + /// + /// Check if Geometry is MultiPolygon + /// + /// + /// true; false + public static bool IsMultiPolygon(this SqlGeometry sqlGeometry) + { + return sqlGeometry.STGeometryType().Compare(OGCType.MultiPolygon.GetString()); + } + + /// + /// Check if Geometry is MultiPolygon + /// + /// + /// true; false + public static bool IsMultiPolygon(this SqlGeometry sqlGeometry, bool checkValid = true) + { + if (checkValid) + return sqlGeometry.IsMultiPolygon(); + return GetGeomText(sqlGeometry.ToString()).Equals("MULTIPOLYGON"); + } + + /// + /// Get geometry type. + /// + /// Geometry text + /// Geometry type + public static string GetGeomText(this string geomText) + { + var match = Regex.Match(geomText, _geomTypeRegexString); + return match.Groups["type"].Value.ToUpper(); + } + + #endregion + + #region Measures Tolerance + + /// + /// Get start point measure of Geometry + /// + /// Input Geometry + /// + public static double GetStartPointMeasure(this SqlGeometry sqlGeometry) + { + return sqlGeometry.STStartPoint().M.IsNull + ? 0 + : sqlGeometry.STStartPoint().M.Value; + } + + /// + /// Get end point measure of Geometry + /// + /// Input Geometry + /// + public static double GetEndPointMeasure(this SqlGeometry sqlGeometry) + { + return sqlGeometry.STEndPoint().M.IsNull + ? sqlGeometry.STLength().Value + : sqlGeometry.STEndPoint().M.Value; + } + + /// + /// Determines whether the geometry segment is not null or empty. + /// + /// The SQL geometry. + /// + /// true if [is not null or empty] [the specified SQL geometry]; otherwise, false. + /// + public static bool IsNotNullOrEmpty(this SqlGeometry sqlGeometry) + { + return !sqlGeometry.IsNullOrEmpty(); + } + + /// + /// Determines whether the geometry segment is null or empty. + /// + /// The SQL geometry. + /// + /// true if [is null or empty] [the specified SQL geometry]; otherwise, false. + /// + public static bool IsNullOrEmpty(this SqlGeometry sqlGeometry) + { + if (sqlGeometry == null) + return true; + return (bool)(sqlGeometry.IsNull || sqlGeometry.STIsEmpty()); + } + + /// + /// for checking whether geometry measure is increasing or decreasing + /// + /// + /// + public static bool STHasLinearMeasure(this SqlGeometry geometry) + { + if (geometry.IsNullOrEmpty() || !geometry.STIsValid()) + return false; + + var numPoints = geometry.STNumPoints(); + var measureProgress = geometry.STLinearMeasureProgress(); + + var previousM = 0.0; + for (var iterator = 1; iterator <= numPoints; iterator++) + { + var currentPoint = geometry.STPointN(iterator); + if (!currentPoint.HasM) + return false; + + if (iterator == 1) + { + previousM = currentPoint.M.Value; + continue; + } + + switch (measureProgress) + { + case LinearMeasureProgress.Increasing when previousM > currentPoint.M: + case LinearMeasureProgress.Decreasing when previousM < currentPoint.M: + return false; + default: + previousM = currentPoint.M.Value; + break; + } + } + + return true; + } + + /// + /// To check whether start measure value is equal to End Measure value + /// + /// + /// equal measure value + public static bool STHasEqualStartAndEndMeasure(this SqlGeometry geometry) + { + return Math.Abs(geometry.GetStartPointMeasure() - geometry.GetEndPointMeasure()) < 0.0000001; + } + + /// + /// Get the linear progression of the geometry based on measure value; whether increasing or decreasing. + /// + /// The input SqlGeometry. + /// Increasing or Decreasing + public static LinearMeasureProgress STLinearMeasureProgress(this SqlGeometry geometry) + { + if (geometry.IsNullOrEmpty()) + return LinearMeasureProgress.None; + + if (geometry.IsPoint()) + return LinearMeasureProgress.Increasing; + + var startPoint = geometry.STStartPoint(); + var endPoint = geometry.STEndPoint(); + if (!geometry.STStartPoint().HasM || !geometry.STEndPoint().HasM) + return LinearMeasureProgress.None; + + return (endPoint.M - startPoint.M > 0 ? LinearMeasureProgress.Increasing : LinearMeasureProgress.Decreasing); + } + + /// + /// Gets the distance between two x,y co-ordinates. + /// + /// The x1. + /// The y1. + /// The x2. + /// The y2. + /// + public static double GetDistance(double x1, double y1, double x2, double y2) + { + var xCoordinateDiff = Math.Pow(Math.Abs(x2 - x1), 2); + var yCoordinateDiff = Math.Pow(Math.Abs(y2 - y1), 2); + return Math.Sqrt(xCoordinateDiff + yCoordinateDiff); + } + + /// + /// Determines whether the specified distance is within tolerance. + /// + /// The distance. + /// The tolerance. + /// + /// true if the specified distance is tolerable; otherwise, false. + /// + public static bool IsTolerable(this double distance, double tolerance) + { + var digitToCompare = distance; + var decDigits = tolerance.GetPrecisionLength(); + + if (decDigits > 0) + digitToCompare = Math.Round(distance, decDigits); + + return digitToCompare <= tolerance; + } + + /// + /// Gets the precision length + /// + /// The decimal digit. + /// + public static int GetPrecisionLength(this double decimalDigit) + { + var decimalStr = decimalDigit.ToString(CultureInfo.CurrentCulture); + var decimalIndex = decimalStr.IndexOf(".", StringComparison.CurrentCulture); + + return decimalIndex > 0 ? decimalStr.Substring(decimalIndex).Length : 0; + } + + /// + /// Determines whether the x y distance between start and point is tolerable. + /// + /// The start point. + /// The end point. + /// The tolerance. + /// + /// true if the specified start and end point distance is tolerable; otherwise, false. + /// + public static bool IsWithinTolerance(this SqlGeometry startPoint, SqlGeometry endPoint, double tolerance) + { + return IsXYWithinRange(startPoint, endPoint, tolerance); + } + + /// + /// Determines whether the x y distance between 2 x,y points is within tolerance + /// + /// The x1. + /// The y1. + /// The x2. + /// The y2. + /// The tolerance. + /// + /// true if distance between 2 points are within tolerance; otherwise, false. + /// + public static bool IsWithinTolerance(double x1, double y1, double x2, double y2, double tolerance) + { + return IsXYWithinRange(x1, y1, x2, y2, tolerance); + } + + #endregion + + #region Range Validations + + /// + /// Check whether the measure falls withing the start and end measure. + /// + /// + /// + /// + /// + public static bool IsWithinRange(this double? currentMeasure, double? startMeasure, double? endMeasure) + { + if (currentMeasure.HasValue && startMeasure.HasValue && endMeasure.HasValue) + return IsWithinRange(currentMeasure.Value, startMeasure.Value, endMeasure.Value); + return false; + } + + /// + /// Check whether the measure falls withing the start and end measure. + /// + /// + /// + /// + /// + public static bool IsWithinRange(this double currentMeasure, double startMeasure, double endMeasure) + { + return ( + // if line segment measure is increasing start ----> end + (currentMeasure >= startMeasure && currentMeasure <= endMeasure) + || + // if line segment measure is increasing start <---- end + (currentMeasure >= endMeasure && currentMeasure <= startMeasure) + ); + } + + /// + /// Check whether the measure falls withing the start and end measure of geometry. + /// + /// + /// + /// + public static bool IsWithinRange(this double currentMeasure, SqlGeometry sqlGeometry) + { + var startMeasure = sqlGeometry.GetStartPointMeasure(); + var endMeasure = sqlGeometry.GetEndPointMeasure(); + return IsWithinRange(currentMeasure, startMeasure, endMeasure); + } + + /// + /// Check whether the measure falls between start and end geometry points + /// + /// + /// + /// + /// + public static bool IsWithinRange(this double currentMeasure, SqlGeometry startPoint, SqlGeometry endPoint) + { + var startMeasure = startPoint.GetStartPointMeasure(); + var endMeasure = endPoint.GetEndPointMeasure(); + return IsWithinRange(currentMeasure, startMeasure, endMeasure); + } + + /// + /// Checks whether difference of X and Y point between source and point to compare is within tolerable range + /// + /// + /// + /// + /// + public static bool IsXYWithinRange(this SqlGeometry sourcePoint, SqlGeometry pointToCompare, double tolerance = 0.0F) + { + return Math.Abs(sourcePoint.STX.Value - pointToCompare.STX.Value) <= tolerance && + Math.Abs(sourcePoint.STY.Value - pointToCompare.STY.Value) <= tolerance; + } + + /// + /// Determines whether [is x y within range] [the specified x1]. + /// + /// The x1. + /// The y1. + /// The x2. + /// The y2. + /// The tolerance. + /// + /// true if [is x y within range] [the specified x1]; otherwise, false. + /// + public static bool IsXYWithinRange(double x1, double y1, double x2, double y2, double tolerance = 0.0F) + { + return Math.Abs(x1 - x2) <= tolerance && + Math.Abs(y1 - y2) <= tolerance; + } + + /// + /// Determines whether the input start or end measure matches the start or end point measure of geometry. + /// + /// The geom start measure. + /// The geom end measure. + /// The input start measure. + /// The input end measure. + /// + /// true if input measure matches end or start point measure of geometry; otherwise, false. + /// + public static bool IsExtremeMeasuresMatch(double geomStartMeasure, double geomEndMeasure, double inputStartMeasure, double inputEndMeasure) + { + var inputStartCheck1 = inputStartMeasure.Equals(geomStartMeasure); + var inputStartCheck2 = inputStartMeasure.Equals(geomEndMeasure); + + var inputEndCheck1 = inputEndMeasure.Equals(geomStartMeasure); + var inputEndCheck2 = inputEndMeasure.Equals(geomEndMeasure); + + return (inputStartCheck1 || inputStartCheck2) || (inputEndCheck1 || inputEndCheck2); + } + + /// + /// + /// Get offset measure between the two geometry. + /// + /// Subtracts end measure of first geometry against the first measure of second geometry. + /// + /// + /// First Geometry + /// Second Geometry + /// Difference in measure + public static double? GetOffset(this SqlGeometry sqlGeometry1, SqlGeometry sqlGeometry2) + { + return sqlGeometry1.GetEndPointMeasure() - sqlGeometry2.GetStartPointMeasure(); + } + + /// + /// Get offset measure between the two point geometry. + /// Subtracts end measure of first point and second point. + /// + /// First Point + /// Second Point + /// Difference in measure + public static double GetPointOffset(this SqlGeometry point1, SqlGeometry point2) + { + return point1.M.Value - point2.M.Value; + } + + #endregion + + #region Sql Types Comparison + + /// + /// Compares sql string for equality. + /// + /// SQL string + /// Target OGC type string to compare + /// + public static bool Compare(this SqlString sqlString, string targetString) + { + if (string.IsNullOrEmpty(targetString)) + return false; + + var convertString = sqlString.ToString(); + + return !string.IsNullOrEmpty(convertString) && convertString.ToLowerInvariant().Equals(targetString.ToLowerInvariant(), StringComparison.CurrentCulture); + } + + #endregion + + #region Enum Attribute Extension + + /// + /// Will get the string value for a given enums value, this will + /// only work if you assign the StringValue attribute to + /// the items in your enum. + /// + /// + /// + public static string GetString(this OGCType value) + { + return GetStringAttributeValue(value); + } + + /// + /// Will get the string value for a given enums value, this will + /// only work if you assign the StringValue attribute to + /// the items in your enum. + /// + /// + /// + public static string GetString(this DimensionalInfo value) + { + return GetStringAttributeValue(value); + } + + /// + /// Will get the string value for a given enums value, this will + /// only work if you assign the StringValue attribute to + /// the items in your enum. + /// + /// + /// + public static string GetString(this LRSErrorCodes value) + { + return GetStringAttributeValue(value); + } + + /// + /// Gets the string attribute value. + /// + /// + /// The value. + /// + private static string GetStringAttributeValue(this T value) + { + // Get the type + var type = value.GetType(); + + // Get field info for this type + var fieldInfo = type.GetField(value.ToString()); + + // Get the string value attributes + // Return the first if there was a match. + return fieldInfo.GetCustomAttributes(typeof(StringValueAttribute), false) + is StringValueAttribute[] attributes && attributes.Length > 0 ? attributes[0].StringValue : null; + } + + /// + /// Get the int value of enum + /// + /// The linear measure progress. + /// Returns the implicit integer value of respective enum + public static short Value(this LinearMeasureProgress linearMeasureProgress) + { + return (short)linearMeasureProgress; + } + + /// + /// Get the string value for the specified LRS Error Code. + /// + /// The LRS error codes. + /// String Value of LRS Error Code. + public static string Value(this LRSErrorCodes lrsErrorCodes) + { + return lrsErrorCodes == LRSErrorCodes.ValidLRS ? "TRUE" : ((short)lrsErrorCodes).ToString(CultureInfo.CurrentCulture); + } + + #endregion + + #region Exception Handling + + /// + /// Throws Argument Exception if the element index to be extracted is greater than the input geometry. + /// + internal static void ThrowInvalidElementIndex() + { + throw new ArgumentException(ErrorMessage.InvalidElementIndex); + } + + /// + /// Throws Argument Exception if the sub-element index to be extracted is greater than the input geometry. + /// + internal static void ThrowInvalidSubElementIndex() + { + throw new ArgumentException(ErrorMessage.InvalidSubElementIndex); + } + + /// + /// Throws Argument Exception if the geometry is not valid + /// + internal static void ThrowIfInvalidGeometry() + { + throw new ArgumentException(ErrorMessage.InvalidGeometry); + } + /// + /// Throw if input geometry is not a LRS Geometry collection POINT, LINESTRING or MULTILINESTRING. + /// + /// Input Sql Geometry + internal static void ThrowIfNotLRSType(params SqlGeometry[] sqlGeometries) + { + if (sqlGeometries.Any(geom => !geom.IsLRSType())) + throw new ArgumentException(ErrorMessage.LRSCompatible); + } + + /// + /// Throw if input geometry is not a Point. + /// + /// Input Sql Geometry + /// Sql Geometries + internal static void ThrowIfNotPoint(SqlGeometry sqlGeometry, params SqlGeometry[] sqlGeometries) + { + var geometries = (new[] { sqlGeometry }).Concat(sqlGeometries); + if (geometries.Any(geom => !geom.IsOfSupportedTypes(OpenGisGeometryType.Point))) + throw new ArgumentException(ErrorMessage.PointCompatible); + } + + /// + /// Throw if input geometry is not a line string. + /// + /// Input Sql Geometry + /// Sql Geometries + internal static void ThrowIfNotLine(SqlGeometry sqlGeometry, params SqlGeometry[] sqlGeometries) + { + var geometries = (new[] { sqlGeometry }).Concat(sqlGeometries); + if (geometries.Any(geom => !geom.IsOfSupportedTypes(OpenGisGeometryType.LineString))) + throw new ArgumentException(ErrorMessage.LineStringCompatible); + } + + /// + /// Throw if input geometry is not a line string or multiline string. + /// + /// Input Sql Geometry + /// Sql Geometries + internal static void ThrowIfNotLineOrMultiLine(SqlGeometry sqlGeometry, params SqlGeometry[] sqlGeometries) + { + var geometries = (new[] { sqlGeometry }).Concat(sqlGeometries); + if (geometries.Any(geom => !geom.IsOfSupportedTypes(OpenGisGeometryType.LineString, OpenGisGeometryType.MultiLineString))) + throw new ArgumentException(ErrorMessage.LineOrMultiLineStringCompatible); + } + + /// + /// Throw if SRIDs of two geometries doesn't match. + /// + /// Source Sql Geometry + /// Target Sql Geometry + internal static void ThrowIfSRIDDoesNotMatch(SqlGeometry sourceGeometry, SqlGeometry targetGeometry) + { + if (!sourceGeometry.STSrid.Equals(targetGeometry.STSrid)) + throw new ArgumentException(ErrorMessage.SRIDCompatible); + } + + /// + /// Throw if measure is not withing the range of two geometries. + /// + /// Measure + /// Start Point Sql Geometry + /// End Point Sql Geometry + internal static void ThrowIfMeasureIsNotInRange(double measure, SqlGeometry startPoint, SqlGeometry endPoint) + { + if (!measure.IsWithinRange(startPoint, endPoint)) + throw new ArgumentException(ErrorMessage.MeasureRange); + } + + /// + /// Throw if measure is not linear for LRS System. + /// + /// SQL Geometry + internal static void ThrowIfMeasureNotLinear(SqlGeometry inputGeometry) + { + if (!inputGeometry.STHasLinearMeasure()) + throw new ArgumentException(ErrorMessage.LinearMeasureRange); + } + + /// + /// Throw if measure is not withing the range of two geometries. + /// + /// Measure + /// Input Sql Geometry + internal static void ThrowIfMeasureIsNotInRange(double measure, SqlGeometry sqlGeometry) + { + if (!measure.IsWithinRange(sqlGeometry)) + ThrowLRSError(LRSErrorCodes.InvalidLRSMeasure); + } + + /// + /// Check if the geometry collection is of POINT, LINESTRING, MULTILINESTRING. + /// + /// + /// + public static bool IsLRSType(this SqlGeometry geometry) + { + return geometry.IsOfSupportedTypes(OpenGisGeometryType.Point, OpenGisGeometryType.LineString, OpenGisGeometryType.MultiLineString); + } + + /// + /// Check if the input geometry is of the supported specified type + /// + /// + /// + /// + /// + public static bool IsOfSupportedTypes(this SqlGeometry geometry, OpenGisGeometryType supportedType, params OpenGisGeometryType[] additionalSupportedTypes) + { + var geomSink = new GISTypeCheckGeometrySink((new[] { supportedType }).Concat(additionalSupportedTypes).ToArray()); + geometry.Populate(geomSink); + return geomSink.IsCompatible(); + } + + /// + /// Throws ArgumentException based on message format and parameters + /// + /// Message format + /// Arguments to be appended with format + public static void ThrowException(string messageFormat, params object[] args) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, messageFormat, args)); + } + + /// + /// Throws LRS Error based on error code. + /// + /// LRS Error Code + public static void ThrowLRSError(LRSErrorCodes lrsErrorCode) + { + var message = string.Format(CultureInfo.CurrentCulture, "{0} - {1}", (int)lrsErrorCode, lrsErrorCode.GetString()); + throw new ArgumentException(message); + } + + #endregion + + #region Dimensions + + /// + /// Returns true if the input type is in the list of supported types. + /// + /// + /// + /// + public static bool Contains(this OpenGisGeometryType type, OpenGisGeometryType[] geometryTypes) + { + foreach (var geom in geometryTypes) + { + if (type.Equals(geom)) + return true; + } + + return false; + } + /// + /// Checks if both the segments are in same direction. either increasing or decrease based on measures. + /// + /// Geometry 1 + /// Geometry 2 + /// Returns true if both Geom Segments are in same direction + public static bool STSameDirection(this SqlGeometry geometrySegment1, SqlGeometry geometrySegment2) + { + return geometrySegment1.STLinearMeasureProgress() == geometrySegment2.STLinearMeasureProgress(); + } + + /// + /// Gets the dimension info of input geometry + /// + /// The SQL geometry. + /// Dimensional Info 2D, 2DM, 3D or 3DM + public static DimensionalInfo STGetDimension(this SqlGeometry sqlGeometry) + { + // return object + var dmDimensionalInfo = DimensionalInfo.None; + + // STNumPoint can be performed only on valid geometries. + if (!sqlGeometry.STIsValid() || sqlGeometry.STNumPoints() <= 0) + { + dmDimensionalInfo = DimensionalInfo.None; + } + else + { + var firstPoint = sqlGeometry.STPointN(1); + if (firstPoint.Z.IsNull && firstPoint.M.IsNull) + dmDimensionalInfo = DimensionalInfo.Dim2D; + + else if (firstPoint.Z.IsNull && !firstPoint.M.IsNull) + dmDimensionalInfo = DimensionalInfo.Dim2DWithMeasure; + + else if (!firstPoint.Z.IsNull && firstPoint.M.IsNull) + dmDimensionalInfo = DimensionalInfo.Dim3D; + + else if (!firstPoint.Z.IsNull && !firstPoint.M.IsNull) + dmDimensionalInfo = DimensionalInfo.Dim3DWithMeasure; + } + + return dmDimensionalInfo; + } + + // + /// Gets the dimension info of input geometry + /// Here no way to infer 2D with measure value + /// + /// The SQL geometry in wkt format. + /// Dimensional Info 2D, 2DM, 3D or 3DM + public static DimensionalInfo GetDimension(this string wktGeometry) + { + if (string.IsNullOrEmpty(wktGeometry)) + return DimensionalInfo.None; + + var match = Regex.Match(wktGeometry, @"\w+.*?\((?.*?)[\,\)]"); + + if(match.Success) + { + var dimensions = match.Groups["content"].Value.Split(' '); + switch(dimensions.Count()) + { + case 2: + return DimensionalInfo.Dim2D; + case 3: + return DimensionalInfo.Dim3D; + case 4: + return DimensionalInfo.Dim3DWithMeasure; + default: + return DimensionalInfo.None; + } + } + + return DimensionalInfo.None; + } + + /// + /// Validate and convert to LRS dimension + /// + /// Input SQL Geometry + internal static void ValidateLRSDimensions(ref SqlGeometry sqlGeometry) + { + var dimension = sqlGeometry.STGetDimension(); + + switch (dimension) + { + case DimensionalInfo.Dim3DWithMeasure: + case DimensionalInfo.Dim2DWithMeasure: + return; + // if dimension is of x, y and z + // need to convert third z co-ordinate to M for LRS + case DimensionalInfo.Dim3D: + sqlGeometry = sqlGeometry.ConvertTo2DimensionWithMeasure(); + break; + case DimensionalInfo.Dim2D: + throw new ArgumentException(ErrorMessage.TwoDimensionalCoordinates); + // skip for invalid types where Dimensional information can't be inferred + default: + return; + } + } + + #endregion + + #region Others + + /// + /// Checks if two double values are equal. + /// + /// The left. + /// The right. + /// + public static bool EqualsTo(this double left, double right) + { + return Math.Abs(left - right) < double.Epsilon; + } + + /// + /// Checks if two double values are equal. + /// + /// The left. + /// The right. + /// + public static bool EqualsTo(this double? left, double right) + { + var leftValue = left ?? 0.0; + return EqualsTo(leftValue, right); + } + + /// + /// Checks if two double values are equal. + /// + /// The left. + /// The right. + /// + public static bool EqualsTo(this double left, double? right) + { + var rightValue = right ?? 0.0; + return EqualsTo(left, rightValue); + } + + /// + /// Checks if two double values are equal. + /// + /// The left. + /// The right. + /// + public static bool EqualsTo(this double? left, double? right) + { + var leftValue = left ?? 0.0; + var rightValue = right ?? 0.0; + return EqualsTo(leftValue, rightValue); + } + + /// + /// Checks if two double values are not equal. + /// + /// The left. + /// The right. + /// + public static bool NotEqualsTo(this double left, double right) + { + return !EqualsTo(left, right); + } + + /// + /// Convert Sql geometry with x,y,z to x,y,m + /// + /// Sql Geometry + /// + internal static SqlGeometry ConvertTo2DimensionWithMeasure(this SqlGeometry sqlGeometry) + { + var sqlBuilder = new ConvertXYZ2XYMGeometrySink(); + sqlGeometry.Populate(sqlBuilder); + return sqlBuilder.ConstructedGeometry; + } + + /// + /// Construct SqlGeometry Point from x, y, z, m values + /// + /// x Coordinate + /// y Coordinate + /// z Coordinate + /// Measure + /// Spatial Reference Identifier; Default is 4326 + /// Sql Point Geometry + public static SqlGeometry GetPoint(double x, double y, double? z, double? m, int srid = Constants.DefaultSRID) + { + var zCoordinate = z == null ? "NULL" : z.ToString(); + var geometry = string.Format(CultureInfo.CurrentCulture, Constants.PointSqlCharFormat, x, y, zCoordinate, m); + return SqlGeometry.STPointFromText(new SqlChars(geometry), srid); + } + + /// + /// Gets the point. + /// + /// The x. + /// The y. + /// The z. + /// The m. + /// The srid. + /// + private static SqlGeometry GetPoint(SqlDouble x, SqlDouble y, SqlDouble z, SqlDouble m, SqlInt32 srid) + { + double? zCoordinate = z.IsNull ? (double?)null : z.Value; + double? mValue = m.IsNull ? (double?)null : m.Value; + return GetPoint((double)x, (double)y, zCoordinate, mValue, (int)srid); + } + + /// + /// Gets the point. + /// + /// The SQL geometry. + /// + public static SqlGeometry GetPoint(SqlGeometry sqlGeometry) + { + return GetPoint(sqlGeometry.STX, sqlGeometry.STY, sqlGeometry.Z, sqlGeometry.M, sqlGeometry.STSrid); + } + + /// + /// Gets the LRS multi line. + /// + /// The SQL geometry. + /// if set to true [do update m]. + /// The offset m. + /// + internal static LRSMultiLine GetLRSMultiLine(this SqlGeometry sqlGeometry, bool doUpdateM, double? offsetM) + { + ThrowIfNotLineOrMultiLine(sqlGeometry); + // populate the input segment + var lrsBuilder = new BuildLRSMultiLineSink(doUpdateM, offsetM); + sqlGeometry.Populate(lrsBuilder); + return lrsBuilder.MultiLine; + } + + /// + /// Gets the LRS multi line from Sql Geometry. + /// + /// The SQL geometry. + /// + internal static LRSMultiLine GetLRSMultiLine(this SqlGeometry sqlGeometry) + { + return sqlGeometry.GetLRSMultiLine(false, 0); + } + + /// + /// Gets the point with updated m. + /// + /// The SQL geometry. + /// The updated measure. + /// + public static SqlGeometry GetPointWithUpdatedM(SqlGeometry sqlGeometry, double updatedMeasure) + { + return GetPoint(sqlGeometry.STX, sqlGeometry.STY, sqlGeometry.Z, updatedMeasure, sqlGeometry.STSrid); + } + + /// + /// Gets the type of the merge. + /// + /// The geometry1. + /// The geometry2. + /// + public static MergeInputType GetMergeType(this SqlGeometry geometry1, SqlGeometry geometry2) + { + var isGeom1LineString = geometry1.IsLineString(); + var isGeom1MultiLineString = geometry1.IsMultiLineString(); + + var isGeom2LineString = geometry2.IsLineString(); + var isGeom2MultiLineString = geometry2.IsMultiLineString(); + + if (isGeom1LineString && isGeom2LineString) + return MergeInputType.LSLS; + + if (isGeom1MultiLineString && isGeom2MultiLineString) + return MergeInputType.MLSMLS; + + if (isGeom1LineString && isGeom2MultiLineString) + return MergeInputType.LSMLS; + + if (isGeom1MultiLineString && isGeom2LineString) + return MergeInputType.MLSLS; + + return MergeInputType.None; + } + + /// + /// Converts WKT string to SqlGeometry object. + /// + /// geometry in WKT representation + /// Spatial reference identifier; Default for SQL Server 4326 + /// SqlGeometry + public static SqlGeometry GetGeom(this string geomWKT, int srid = Constants.DefaultSRID) + { + return string.IsNullOrEmpty(geomWKT) ? null : SqlGeometry.STGeomFromText(new SqlChars(geomWKT), srid); + } + + /// + /// Converts WKT string to SqlGeometry object. + /// + /// geometry in WKT representation + /// Spatial reference identifier + /// SqlGeometry + public static SqlGeometry GetGeom(this string geomWKT, SqlInt32 srid) + { + return GetGeom(geomWKT, (int)srid); + } + + /// + /// Convert WKT string to SqlGeography object. + /// + /// geography in WKT string representation + /// Spatial reference identifier; Default for SQL Server 4326 + /// SqlGeography + public static SqlGeography GetGeog(this string geogString, int srid = Constants.DefaultSRID) + { + return SqlGeography.STGeomFromText(new SqlChars(geogString), srid); + } + + /// + /// Converts Polygon to Multi Linestring or LineString + /// Does only based on string matching and replacement. + /// + /// Input SqlGeometry + /// + public static string GetLineWKTFromPolygon(this SqlGeometry geometry) + { + var inputText = geometry.ToString(); + Match match = Regex.Match(inputText, _polygonSegmentRegex, RegexOptions.IgnoreCase); + + if (match.Success) + { + var content = match.Groups["segments"].Value; + var segments = Regex.Matches(content, _segmentMatchRegex); + if (segments.Count == 1) + return $"LINESTRING {content}"; + + if (segments.Count > 1) + return $"MULTILINESTRING ({content})"; + + return "LINESTRING EMPTY"; + } + return inputText; + } + + /// + /// Converts MultiPolygon to Multi linestring + /// Does only based on string replacement + /// + /// Input SqlGeometry + /// + public static string GetLineWKTFromMultiPolygon(this SqlGeometry geometry) + { + var inputText = geometry.ToString(); + Match match = Regex.Match(inputText, _multipolygonSegmentRegex, RegexOptions.IgnoreCase); + + if (match.Success) + { + var content = match.Groups["segments"].Value; + content = content.Replace("((", "(").Replace("))", ")"); + return $"MULTILINESTRING ({content})"; + } + return inputText; + } + + /// + /// Converts CurvePolygon to Line representation. + /// Does only based on string matching and replacement. + /// + /// Input SqlGeometry + /// WKT format + public static string GetLineWKTFromCurvePolygon(this SqlGeometry geometry) + { + var geomText = geometry.ToString(); + + var patternCurvePolygon = @"CURVEPOLYGON\s*\((?.*)\)"; + var regexCurvePolygon = new Regex(patternCurvePolygon, RegexOptions.IgnoreCase); + + var patternCompoundCurve = @"COMPOUNDCURVE\s*\((?.*?\)\)\,?)"; + var regexCompoundCurve = new Regex(patternCompoundCurve, RegexOptions.IgnoreCase); + + var match = regexCurvePolygon.Match(geomText); + var curvePolygonContent = match.Groups["content"].Value; + + var geomWKTBuilder = new StringBuilder(); + var ccIterator = 1; + + // compound curve + var ccMatches = regexCompoundCurve.Matches(curvePolygonContent); + + // within compound curve + if (ccMatches.Count > 0) + { + // start of multi curve + geomWKTBuilder.Append("GEOMETRYCOLLECTION("); + + // for each compound curves + foreach (Match ccMatch in ccMatches) + { + geomWKTBuilder.Append("COMPOUNDCURVE("); + + GetCCSubComponent(ccMatch.Groups["content"].Value, ref geomWKTBuilder); + + geomWKTBuilder.Append(")"); + if (ccIterator != ccMatches.Count) + geomWKTBuilder.Append(", "); + ccIterator++; + } + + // end of multi curve + geomWKTBuilder.Append(")"); + } + else + { + GetMCSubComponent(curvePolygonContent, ref geomWKTBuilder); + } + + return geomWKTBuilder.ToString(); + } + + /// + /// Get WKT of a Compound Curve segment + /// + /// Sub component + /// String builder + private static void GetCCSubComponent(string textToMatch, ref StringBuilder wktBuilder) + { + var patternSubComponents = @"(?\w+)?\s*\((?.*?\d)\s*?\)"; + var regexSubComponents = new Regex(patternSubComponents, RegexOptions.IgnoreCase); + var subIterator = 1; + // inner contents of compound curve + var ccContents = regexSubComponents.Matches(textToMatch); + + foreach (Match ccContent in ccContents) + { + var geomType = ccContent.Groups["type"].Value.ToUpper(); + var content = ccContent.Groups["content"].Value; + + wktBuilder.Append($"{geomType}({content})"); + if (subIterator != ccContents.Count) + wktBuilder.Append(", "); + subIterator++; + } + } + + /// + /// Get WKT of a Multi Curve segment + /// Since SQL Server doesn't support Multi Curve it is converted to GeometryCollection. + /// + /// Sub component + /// String builder + private static void GetMCSubComponent(string textToMatch, ref StringBuilder wktBuilder) + { + var patternSubComponents = @"(?\w+)?\s*\((?.*?\d)\s*?\)"; + var regexSubComponents = new Regex(patternSubComponents, RegexOptions.IgnoreCase); + var subIterator = 1; + // inner contents of compound curve + var ccContents = regexSubComponents.Matches(textToMatch); + + var types = new List>(); + + foreach (Match ccContent in ccContents) + { + var type = ccContent.Groups["type"]; + var geomType = type != null && !string.IsNullOrEmpty(type.Value) ? type.Value.ToUpper() : "LINESTRING"; + var content = ccContent.Groups["content"].Value; + types.Add(new KeyValuePair(geomType, content)); + } + + var isMultiLine = types.Count(e => e.Key == "LINESTRING") == types.Count && types.Count > 1; + + if (types.Count > 1 && !isMultiLine) + wktBuilder.Append("GEOMETRYCOLLECTION("); + + if (isMultiLine) + wktBuilder.Append("MULTILINESTRING("); + + foreach (var entry in types) + { + if (isMultiLine) + wktBuilder.Append($"({entry.Value})"); + else + wktBuilder.Append($"{entry.Key}({entry.Value})"); + + if (subIterator != types.Count) + wktBuilder.Append(", "); + subIterator++; + } + + if (types.Count > 1 || isMultiLine) + wktBuilder.Append(")"); + } + + #endregion + } +} diff --git a/Utility/Util.cs b/src/Utility/SpatialUtil.cs similarity index 67% rename from Utility/Util.cs rename to src/Utility/SpatialUtil.cs index 81c8bf4..c09149c 100644 --- a/Utility/Util.cs +++ b/src/Utility/SpatialUtil.cs @@ -1,83 +1,86 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -using System; -using Microsoft.SqlServer.Types; - -namespace SQLSpatialTools -{ - /** - * This class contains functions that are meant to be used internally in this library. - */ - class Util - { - // Convert a SqlGeography to an X,Y,Z vector. - public static Vector3 GeographicToCartesian(SqlGeography point) - { - return SphericalDegToCartesian(point.Lat.Value, point.Long.Value); - } - - // Convert an X,Y,Z vector to a SqlGeography - public static SqlGeography CartesianToGeographic(Vector3 point, int srid) - { - return SqlGeography.Point(LatitudeDeg(point), LongitudeDeg(point), srid); - } - - // Convert a Lat/Long in degrees to an X,Y,Z vector. - public static Vector3 SphericalDegToCartesian(double latitudeDeg, double longitudeDeg) - { - if (Math.Abs(latitudeDeg) > 90) - throw new ArgumentOutOfRangeException("|latitudeDeg| > 90"); - - double latitudeRad = ToRadians(latitudeDeg); - double longitudeRad = ToRadians(longitudeDeg); - double r = Math.Cos(latitudeRad); - return new Vector3(r * Math.Cos(longitudeRad), r * Math.Sin(longitudeRad), Math.Sin(latitudeRad)); - } - - // Convert a Lat/Long in radians to an X,Y,Z vector. - public static Vector3 SphericalRadToCartesian(double latitudeRad, double longitudeRad) - { - if (Math.Abs(latitudeRad) > Math.PI / 2) - throw new ArgumentOutOfRangeException("|latitudeRad| > PI / 2"); - - double r = Math.Cos(latitudeRad); - return new Vector3(r * Math.Cos(longitudeRad), r * Math.Sin(longitudeRad), Math.Sin(latitudeRad)); - } - - // Returns longitude in radians given the vector. - public static double Longitude(Vector3 p) - { - return Math.Atan2(p.y, p.x); - } - - // Returns longitude in degrees given the vector. - public static double LongitudeDeg(Vector3 p) - { - return ToDegrees(Longitude(p)); - } - - // Returns latitude in radians given the vector. - public static double Latitude(Vector3 p) - { - return Math.Atan2(p.z, Math.Sqrt(p.x * p.x + p.y * p.y)); - } - - // Returns latitude in degrees given the vector. - public static double LatitudeDeg(Vector3 p) - { - return ToDegrees(Latitude(p)); - } - - // Converts degrees to radians. - public static double ToRadians(double a) - { - return a / 180 * Math.PI; - } - - // Converts radians to degrees. - public static double ToDegrees(double a) - { - return a * 180 / Math.PI; - } - } +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; +using SQLSpatialTools.Types; + +namespace SQLSpatialTools.Utility +{ + /// + /// This class contains functions that are meant to be used internally in this library. + /// + internal static class SpatialUtil + { + // Convert a SqlGeography to an X,Y,Z vector. + public static Vector3 GeographicToCartesian(SqlGeography point) + { + return SphericalDegToCartesian(point.Lat.Value, point.Long.Value); + } + + // Convert an X,Y,Z vector to a SqlGeography + public static SqlGeography CartesianToGeographic(Vector3 point, int srid) + { + return SqlGeography.Point(LatitudeDeg(point), LongitudeDeg(point), srid); + } + + // Convert a Lat/Long in degrees to an X,Y,Z vector. + public static Vector3 SphericalDegToCartesian(double latitudeDeg, double longitudeDeg) + { + if (Math.Abs(latitudeDeg) > 90) + throw new ArgumentOutOfRangeException(nameof(latitudeDeg),"|latitudeDeg| > 90"); + + var latitudeRad = ToRadians(latitudeDeg); + var longitudeRad = ToRadians(longitudeDeg); + var r = Math.Cos(latitudeRad); + return new Vector3(r * Math.Cos(longitudeRad), r * Math.Sin(longitudeRad), Math.Sin(latitudeRad)); + } + + // Convert a Lat/Long in radians to an X,Y,Z vector. + public static Vector3 SphericalRadToCartesian(double latitudeRad, double longitudeRad) + { + if (Math.Abs(latitudeRad) > Math.PI / 2) + throw new ArgumentOutOfRangeException(nameof(latitudeRad), "|latitudeRad| > PI / 2"); + + var r = Math.Cos(latitudeRad); + return new Vector3(r * Math.Cos(longitudeRad), r * Math.Sin(longitudeRad), Math.Sin(latitudeRad)); + } + + // Returns longitude in radians given the vector. + public static double Longitude(Vector3 p) + { + return Math.Atan2(p.Y, p.X); + } + + // Returns longitude in degrees given the vector. + public static double LongitudeDeg(Vector3 p) + { + return ToDegrees(Longitude(p)); + } + + // Returns latitude in radians given the vector. + public static double Latitude(Vector3 p) + { + return Math.Atan2(p.Z, Math.Sqrt(p.X * p.X + p.Y * p.Y)); + } + + // Returns latitude in degrees given the vector. + public static double LatitudeDeg(Vector3 p) + { + return ToDegrees(Latitude(p)); + } + + // Converts degrees to radians. + public static double ToRadians(double a) + { + return a / 180 * Math.PI; + } + + // Converts radians to degrees. + public static double ToDegrees(double a) + { + return a * 180 / Math.PI; + } + } } \ No newline at end of file diff --git a/src/packages.config b/src/packages.config new file mode 100644 index 0000000..393940a --- /dev/null +++ b/src/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/unittest-extension/BaseUnitTest.cs b/unittest-extension/BaseUnitTest.cs new file mode 100644 index 0000000..80ebfe5 --- /dev/null +++ b/unittest-extension/BaseUnitTest.cs @@ -0,0 +1,25 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace SQLSpatialTools.UnitTests.Extension +{ + public class BaseUnitTest + { + protected TestLogger Logger; + + [TestInitialize] + public void Initialize() + { + Logger = new TestLogger(TestContext); + } + + /// + /// Gets or sets the test context which provides + /// information about and functionality for the current test run. + /// + public TestContext TestContext { get; set; } + } +} \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/unittest-extension/Properties/AssemblyInfo.cs similarity index 59% rename from Properties/AssemblyInfo.cs rename to unittest-extension/Properties/AssemblyInfo.cs index 774cbe3..f0951b7 100644 --- a/Properties/AssemblyInfo.cs +++ b/unittest-extension/Properties/AssemblyInfo.cs @@ -1,36 +1,35 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("SQLSpatialTools")] -[assembly: AssemblyDescription("A collection of tools for use with the SQL Server spatial functionality.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("SQLSpatialTools")] -[assembly: AssemblyCopyright("Copyright (c) Microsoft Corporation. All rights reserved.")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("ee2e7fb3-3d63-4eb8-b0a7-8b089f5e2cf3")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.1.0.0")] -[assembly: AssemblyFileVersion("0.1.0.0")] +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SQLSpatialTools.UnitTests.Extension")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SQLSpatialTools.UnitTests.Extension")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("98efe72e-e303-4bde-b86f-5f704db3f5be")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.3.0")] +[assembly: AssemblyFileVersion("1.0.3.0")] diff --git a/unittest-extension/SQLSpatialTools.UnitTests.Extension.csproj b/unittest-extension/SQLSpatialTools.UnitTests.Extension.csproj new file mode 100644 index 0000000..8eb6c9b --- /dev/null +++ b/unittest-extension/SQLSpatialTools.UnitTests.Extension.csproj @@ -0,0 +1,76 @@ + + + + + + Debug + AnyCPU + {98EFE72E-E303-4BDE-B86F-5F704DB3F5BE} + Library + Properties + SQLSpatialTools.UnitTests.Extension + SQLSpatialTools.UnitTests.Extension + v4.5 + 512 + true + + + true + full + false + ..\output\unittest-extension\ + ..\output\intermediate\unittest-extension\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + ..\output\unittest-extension\ + ..\output\intermediate\unittest-extension\ + TRACE + prompt + 4 + + + true + + + ..\SpatialTools.pfx + + + + ..\packages\MSTest.TestFramework.1.4.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + ..\packages\MSTest.TestFramework.1.4.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. %0AUse NuGet Package Restore to download them. %0AFor more information, see http://go.microsoft.com/fwlink/?LinkID=322105. %0AThe missing file is {0}. + + + + + + + + if exist "$(ProjectDir)obj" rd "$(ProjectDir)obj" /S /Q +exit 0 + + \ No newline at end of file diff --git a/unittest-extension/TestExtension.cs b/unittest-extension/TestExtension.cs new file mode 100644 index 0000000..16bcf51 --- /dev/null +++ b/unittest-extension/TestExtension.cs @@ -0,0 +1,194 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Data.SqlTypes; +using System.Diagnostics; +using System.Text; +using System.Text.RegularExpressions; +using MST = Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Globalization; + +namespace SQLSpatialTools.UnitTests.Extension +{ + public static class SqlAssert + { + public static void IsTrue(SqlBoolean sqlBoolean) + { + MST.Assert.IsTrue((bool)sqlBoolean); + } + + public static void IsFalse(SqlBoolean sqlBoolean) + { + MST.Assert.IsFalse((bool)sqlBoolean); + } + + public static void AreEqual(SqlDouble sqlDouble, double targetValue) + { + MST.Assert.AreEqual(Math.Round((double)sqlDouble, 4), Math.Round(targetValue, 4)); + } + + public static string GetResult(this bool result) + { + return result ? "Passed" : "Failed"; + } + } + + public static class TestExtension + { + private const string DecimalPointMatch = @"\.0([\s\,\)])"; + + /// + /// Trim null values in the input geometry WKT. + /// + /// input geometry in WKT + /// Null trimmed geom text + public static string TrimNullValue(this string inputGeom) + { + return Regex.Replace(inputGeom, @"\s*null\s*", " ", RegexOptions.IgnoreCase); + } + + /// + /// Trims the decimal precision. + /// + /// The input geom. + /// + private static string RoundOffDecimalPrecision(this string inputGeom) + { + var output = inputGeom; + //var matches = Regex.Matches(inputGeom, @"(\d+\.\d{5,16})", RegexOptions.Compiled); + var matches = Regex.Matches(inputGeom, @"(\d+\.\d+)", RegexOptions.Compiled); + + foreach (Match match in matches) + { + var inputStr = match.Groups[1].Value; + if (double.TryParse(inputStr, out var trimValue)) + { + output = output.Replace(inputStr, Math.Round(trimValue, 4).ToString(CultureInfo.InvariantCulture)); + } + } + + return output; + } + + /// + /// Compare the two results after converting to lower and trimming space. + /// + /// + /// + /// + public static bool Compare(this string firstResult, string secondResult) + { + // check for null words and assign null + if (!string.IsNullOrEmpty(firstResult)) + firstResult = firstResult.ToLower(CultureInfo.CurrentCulture).Trim().Equals("null", StringComparison.CurrentCulture) ? null : firstResult.Trim(); + + if (!string.IsNullOrEmpty(secondResult)) + secondResult = secondResult.ToLower(CultureInfo.CurrentCulture).Trim().Equals("null", StringComparison.CurrentCulture) ? null : secondResult.Trim(); + + if (string.IsNullOrEmpty(firstResult) && string.IsNullOrEmpty(secondResult)) + return true; + if (string.IsNullOrEmpty(firstResult) || string.IsNullOrEmpty(secondResult)) + return false; + + firstResult = firstResult.RoundOffDecimalPrecision(); + secondResult = secondResult.RoundOffDecimalPrecision(); + firstResult = Regex.Replace(firstResult, @"\s+", string.Empty).ToLower(CultureInfo.CurrentCulture); + secondResult = Regex.Replace(secondResult, @"\s+", string.Empty).ToLower(CultureInfo.CurrentCulture); + return firstResult.Equals(secondResult, StringComparison.CurrentCulture); + } + + /// + /// Trims the decimal points in input WKT geometry. + /// + /// + /// + public static string TrimDecimalPoints(this string inputGeomWKT) + { + if (!string.IsNullOrEmpty(inputGeomWKT)) + return Regex.Replace(inputGeomWKT, DecimalPointMatch, "$1"); + return inputGeomWKT; + } + + /// + /// Escape single quotation in input query. + /// + /// + /// + public static string EscapeQueryString(this string query) + { + return query?.Replace("'", "''"); + } + } + + public class TestLogger + { + private readonly MST.TestContext _testContext; + + public TestLogger(MST.TestContext testContext) + { + _testContext = testContext; + } + + public void Log(string msgFormat, params object[] args) + { + _testContext.WriteLine(string.Format(CultureInfo.CurrentCulture, msgFormat, args)); + } + + public void LogLine(string msgFormat, params object[] args) + { + var message = new StringBuilder(); + message.AppendLine(); + if (args != null && args.Length > 0) + message.AppendFormat(CultureInfo.CurrentCulture, msgFormat, args); + else + message.Append(msgFormat); + + _testContext.WriteLine(message.ToString()); + } + + public void LogError(Exception ex, string errorMessage = "", params object[] args) + { + var message = new StringBuilder(); + var trace = new StackTrace(ex, true); + var frame = trace.GetFrame(0); + message.AppendLine(); + if (!string.IsNullOrWhiteSpace(errorMessage)) + { + if (args != null && args.Length > 0) + message.AppendFormat(CultureInfo.CurrentCulture, errorMessage, args); + else + message.Append(errorMessage); + } + + message.AppendLine(); + if (frame != null) + { + message.AppendFormat(CultureInfo.CurrentCulture, "Error module: {0}", frame.GetMethod().Name); + message.AppendLine(); + message.AppendFormat(CultureInfo.CurrentCulture, "File Name: {0}", frame.GetFileName()); + message.AppendLine(); + message.AppendFormat(CultureInfo.CurrentCulture, "Line Number: {0}", frame.GetFileLineNumber()); + message.AppendLine(); + } + message.AppendFormat(CultureInfo.CurrentCulture, "Exception: {0}", ex.Message); + message.AppendLine(); + if (ex.StackTrace != null) + { + message.AppendFormat(CultureInfo.CurrentCulture, "Stack trace: {0}", ex.StackTrace); + message.AppendLine(); + } + + if (ex.InnerException != null) + { + message.AppendFormat(CultureInfo.CurrentCulture, "Inner Exception: {0}", ex.InnerException.Message); + message.AppendLine(); + if (ex.InnerException.StackTrace != null) + message.AppendFormat(CultureInfo.CurrentCulture, "Inner Stack trace: {0}", ex.InnerException.StackTrace); + } + + _testContext.WriteLine(message.ToString()); + } + } +} diff --git a/unittest-extension/packages.config b/unittest-extension/packages.config new file mode 100644 index 0000000..06aa648 --- /dev/null +++ b/unittest-extension/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/unittest/Functions/GeneralFunctionTests.cs b/unittest/Functions/GeneralFunctionTests.cs new file mode 100644 index 0000000..db952be --- /dev/null +++ b/unittest/Functions/GeneralFunctionTests.cs @@ -0,0 +1,407 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SQLSpatialTools.Functions.General; +using SQLSpatialTools.UnitTests.Extension; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.UnitTests.Functions +{ + public static class GeneralFunctionTests + { + [TestClass] + public class GeometryTests : BaseUnitTest + { + [TestMethod] + public void FilterArtifactsGeometryTest() + { + var geom = "GEOMETRYCOLLECTION(LINESTRING EMPTY, LINESTRING (1 1, 3 5), POINT (1 1), POLYGON ((-1 -1, -1 -5, -5 -5, -5 -1, -1 -1)))".GetGeom(); + + // Empty line and point should be removed + // short line should be removed - tolerance length + var shortLineTolerance = 5; + // Polygon inner ring with area < tolerance * polygon length + var polygonAreaTolerance = 1.5; + + Logger.LogLine("Input Geometry: {0}", geom); + Logger.Log("Filtering input geometry; removing empty line string"); + Logger.Log("points, short line of tolerance: {0}, Polygon with inner ring area tolerance: {1}", shortLineTolerance, polygonAreaTolerance); + var expectedGeom = "GEOMETRYCOLLECTION EMPTY".GetGeom(); + var filteredGeom = Geometry.FilterArtifactsGeometry(geom, true, true, shortLineTolerance, polygonAreaTolerance); + Logger.Log("Expected converted geom: {0}", expectedGeom); + Logger.Log("Obtained converted geom: {0}", filteredGeom); + SqlAssert.IsTrue(filteredGeom.STEquals(expectedGeom)); + } + + [TestMethod] + public void GeomFromXYMTextTest() + { + var geomWKT = "LINESTRING (0 0 3 4, 10 0 3 4)"; + Logger.LogLine("Converting input Geom with 3 dimension and measure : {0}", geomWKT); + try + { + Geometry.GeomFromXYMText(geomWKT, Constants.DefaultSRID); + } + catch (ArgumentException e) + { + Assert.AreEqual(e.Message, ErrorMessage.WKT3DOnly); + TestContext.WriteLine(ErrorMessage.WKT3DOnly); + } + + geomWKT = "LINESTRING (0 0 3, 10 0 4)"; + Logger.LogLine("Converting input Geom with 3 dimension and measure : {0}", geomWKT); + var expectedGeom = "LINESTRING(0 0 NULL 3, 10 0 NULL 4)".GetGeom(); + var convertedGeom = Geometry.GeomFromXYMText(geomWKT, Constants.DefaultSRID); + Logger.Log("Expected converted geom: {0}", expectedGeom); + Logger.Log("Obtained converted geom: {0}", convertedGeom); + SqlAssert.IsTrue(convertedGeom.STEquals(expectedGeom)); + } + + [TestMethod] + public void InterpolateBetweenGeomTest() + { + var geom1 = "POINT(0 0 0 0)".GetGeom(); + var geom2 = "POINT(10 0 0 10)".GetGeom(); + var returnPoint = "POINT (5 0 NULL 5)".GetGeom(); + const int distance = 5; + Logger.LogLine("Input Point 1:{0} Point 2:{1}", geom1, geom2); + Logger.Log("Interpolating at a distance of {0}", geom1, geom2, distance); + Logger.LogLine("Expected Point: {0}", returnPoint); + var sqlGeometry = Geometry.InterpolateBetweenGeom(geom1, geom2, distance); + Logger.Log("Obtained Point: {0}", sqlGeometry.ToString()); + SqlAssert.IsTrue(sqlGeometry.STEquals(returnPoint)); + + try + { + geom1 = "LINESTRING(0 0 0 0, 1 1 1 1)".GetGeom(); + Geometry.InterpolateBetweenGeom(geom1, geom2, distance); + } + catch (ArgumentException) + { + } + + try + { + geom1 = "POINT(0 0 0 0)".GetGeom(); + geom2 = "POINT(0 0 0 0)".GetGeom(0); + Geometry.InterpolateBetweenGeom(geom1, geom2, distance); + } + catch (ArgumentException) + { + } + + try + { + geom1 = "POINT(0 0 0 0)".GetGeom(); + geom2 = "POINT(0 0 0 1)".GetGeom(); + Geometry.InterpolateBetweenGeom(geom1, geom2, 10); + } + catch (ArgumentException) + { + } + + try + { + geom1 = "POINT(0 0 0 0)".GetGeom(); + geom2 = "POINT(0 0 0 1)".GetGeom(); + Geometry.InterpolateBetweenGeom(geom1, geom2, -5); + } + catch (ArgumentException) + { + } + } + + [TestMethod] + public void LocatePointAlongGeomTest() + { + var geom = "LINESTRING (0 0, 10 0)".GetGeom(); + Logger.Log("Input Geom : {0}", geom.ToString()); + var returnPoint = "POINT (5 0)".GetGeom(); + var distance = 5; + + Logger.LogLine("Locating a point at distance of {0} Measure", distance); + var locatedPoint = Geometry.LocatePointAlongGeom(geom, distance); + Logger.Log("Expected point: {0}", returnPoint); + Logger.Log("Located point: {0} at distance of {1} Measure", locatedPoint, distance); + SqlAssert.IsTrue(locatedPoint.STEquals(returnPoint)); + + geom = "LINESTRING (0 0 0 5, 10 0 0 10)".GetGeom(); + returnPoint = "POINT (0 0 0 5)".GetGeom(); + locatedPoint = Geometry.LocatePointAlongGeom(geom, 0); + SqlAssert.IsTrue(locatedPoint.STEquals(returnPoint)); + + try + { + Geometry.LocatePointAlongGeom(geom, 15); + } + catch (ArgumentException) { } + + try + { + geom = "POINT (0 0 0 0)".GetGeom(); + Geometry.LocatePointAlongGeom(geom, 15); + } + catch (ArgumentException) { } + } + + [TestMethod] + public void MakeValidForGeographyTest() + { + var geometry = "CURVEPOLYGON (CIRCULARSTRING (0 -4, 4 0, 0 4, -4 0, 0 -4))".GetGeom(); + var retGeom = Geometry.MakeValidForGeography(geometry); + Logger.LogLine("Executing Make Valid: {0}", geometry); + SqlAssert.IsTrue(retGeom.STEquals(retGeom)); + + geometry = "LINESTRING(0 2, 1 1, 1 0, 1 1, 2 2)".GetGeom(); + Logger.LogLine("Executing Make Valid: {0}", geometry); + var expectedGeom = "MULTILINESTRING ((7.1054273576010019E-15 2, 1 1, 2 2), (1 1, 1 7.1054273576010019E-15))".GetGeom(); + retGeom = Geometry.MakeValidForGeography(geometry); + Logger.Log("Expected Geom: {0}", expectedGeom); + Logger.Log("Obtained Geom: {0}", retGeom); + SqlAssert.IsTrue(retGeom.STEquals(expectedGeom)); + } + + [TestMethod] + public void ReverseLinestringTest() + { + var geom = "LINESTRING (1 1, 5 5)".GetGeom(); + Logger.Log("Input Geom : {0}", geom.ToString()); + + var endPoint = "POINT (5 5 0 0)".GetGeom(); + var reversedLineSegment = Geometry.ReverseLinestring(geom); + Logger.Log("Reversed Line string : {0}", reversedLineSegment.ToString()); + SqlAssert.IsTrue(reversedLineSegment.STStartPoint().STEquals(endPoint)); + + try + { + geom = "POINT (1 1)".GetGeom(); + Geometry.ReverseLinestring(geom); + } + catch (ArgumentException) + { + } + } + + [TestMethod] + public void ShiftGeometryTest() + { + // Point + var geom = "POINT(0 1)".GetGeom(); + var shiftPoint = "POINT (4 5)".GetGeom(); + double xShift = 4, yShift = 4; + Logger.LogLine("Input Point: {0}", geom); + Logger.Log("Expected Point: {0}", shiftPoint); + var shiftedGeom = Geometry.ShiftGeometry(geom, xShift, yShift); + Logger.Log("Obtained Point: {0}", shiftedGeom); + SqlAssert.IsTrue(shiftedGeom.STEquals(shiftPoint)); + + // Simple Line String + geom = "LINESTRING (1 1, 4 4)".GetGeom(); + shiftPoint = "LINESTRING (11 11, 14 14)".GetGeom(); + xShift = 10; + yShift = 10; + Logger.LogLine("Input Geom: {0}", geom); + Logger.Log("Expected Geom: {0}", shiftPoint); + shiftedGeom = Geometry.ShiftGeometry(geom, xShift, yShift); + Logger.Log("Obtained Point: {0}", shiftedGeom); + SqlAssert.IsTrue(shiftedGeom.STEquals(shiftPoint)); + + // Line String with multiple points + geom = "LINESTRING (1 1, 2 3, -1 -3, 4 -3, -2 1)".GetGeom(); + shiftPoint = "LINESTRING (11 11, 12 13, 9 7, 14 7, 8 11)".GetGeom(); + Logger.LogLine("Input Geom: {0}", geom); + Logger.Log("Expected Geom: {0}", shiftPoint); + shiftedGeom = Geometry.ShiftGeometry(geom, xShift, yShift); + Logger.Log("Obtained Point: {0}", shiftedGeom); + SqlAssert.IsTrue(shiftedGeom.STEquals(shiftPoint)); + + // Multi Line String + geom = "MULTILINESTRING ((1 1, 2 3), (-1 -3, 4 -3, -2 1))".GetGeom(); + shiftPoint = "MULTILINESTRING ((11 11, 12 13), (9 7, 14 7, 8 11))".GetGeom(); + Logger.LogLine("Input Geom: {0}", geom); + Logger.Log("Expected Geom: {0}", shiftPoint); + shiftedGeom = Geometry.ShiftGeometry(geom, xShift, yShift); + Logger.Log("Obtained Point: {0}", shiftedGeom); + SqlAssert.IsTrue(shiftedGeom.STEquals(shiftPoint)); + + // Polygon + geom = "POLYGON((1 1, 3 3, 3 1, 1 1))".GetGeom(); + shiftPoint = "POLYGON ((11 11, 13 13, 13 11, 11 11))".GetGeom(); + Logger.LogLine("Input Geom: {0}", geom); + Logger.Log("Expected Geom: {0}", shiftPoint); + shiftedGeom = Geometry.ShiftGeometry(geom, xShift, yShift); + Logger.Log("Obtained Point: {0}", shiftedGeom); + SqlAssert.IsTrue(shiftedGeom.STEquals(shiftPoint)); + } + + [TestMethod] + public void VacuousGeometryToGeographyTest() + { + var geom = "LINESTRING (0 0 1 1, 2 2 1 1)".GetGeom(); + var expectedGeog = "LINESTRING (0 0, 2 2)".GetGeog(); + Logger.LogLine("Input Geometry: {0}", geom); + Logger.Log("Expected Geography: {0}", expectedGeog); + var obtainedGeog = Geometry.VacuousGeometryToGeography(geom, Constants.DefaultSRID); + Logger.Log("Obtained Geography: {0}", obtainedGeog); + SqlAssert.IsTrue(obtainedGeog.STEquals(expectedGeog)); + } + } + + [TestClass] + public class GeographyTests : BaseUnitTest + { + [TestMethod] + public void ConvexHullGeographyFromTextTest() + { + var geomText = "LINESTRING(-122.360 47.656, -122.343 47.656)"; + var expectedGeog = "LINESTRING (-122.343 47.655999999999992, -122.36 47.655999999999992)".GetGeog(); + Logger.LogLine("Input Geometry: {0}", geomText); + var result = Geography.ConvexHullGeographyFromText(geomText, Constants.DefaultSRID); + Logger.LogLine("Expected result: {0}", expectedGeog); + Logger.LogLine("Obtained result: {0}", result); + SqlAssert.IsTrue(result.STEquals(expectedGeog)); + } + + [TestMethod] + public void ConvexHullGeographyTest() + { + var geog = "LINESTRING(-122.360 47.656, -122.343 47.656)".GetGeog(); + var expectedGeog = "LINESTRING (-122.343 47.655999999999992, -122.36 47.655999999999992)".GetGeog(); + Logger.LogLine("Input Geometry: {0}", geog); + var result = Geography.ConvexHullGeography(geog); + Logger.LogLine("Expected result: {0}", expectedGeog); + Logger.LogLine("Obtained result: {0}", result); + SqlAssert.IsTrue(result.STEquals(expectedGeog)); + } + + [TestMethod] + public void DensifyGeographyTest() + { + var geog = "LINESTRING(-5 0, 5 0)".GetGeog(); + var expectedGeog = "LINESTRING (-5 0, -3 0, -1.0000000000000004 0, 0.99999999999999956 0, 2.9999999999999978 0, 5 0)".GetGeog(); + Logger.LogLine("Input Geometry: {0}", geog); + var result = Geography.DensifyGeography(geog, 2.0); + Logger.LogLine("Expected result: {0}", expectedGeog); + Logger.LogLine("Obtained result: {0}", result); + SqlAssert.IsTrue(result.STEquals(expectedGeog)); + } + + [TestMethod] + public void FilterArtifactsGeographyTest() + { + var geog = "GEOMETRYCOLLECTION(LINESTRING EMPTY, LINESTRING (1 1, 3 5), POINT (1 1), POLYGON ((-1 -1, -1 -5, -5 -5, -5 -1, -1 -1)))".GetGeog(); + + // Empty line and point should be removed + // short line should be removed - tolerance length + const double shortLineTolerance = 500000.0F; + // Polygon inner ring with area < tolerance * polygon length + const double polygonAreaTolerance = 150000.0F; + + Logger.LogLine("Input Geography: {0}", geog); + Logger.Log("Filtering input geometry; removing empty line string"); + Logger.Log("points, short line of tolerance: {0}, Polygon with inner ring area tolerance: {1}", shortLineTolerance, polygonAreaTolerance); + var expectedGeog = "GEOMETRYCOLLECTION EMPTY".GetGeog(); + var filteredGeog = Geography.FilterArtifactsGeography(geog, true, true, shortLineTolerance, polygonAreaTolerance); + Logger.Log("Expected converted geog: {0}", expectedGeog); + Logger.Log("Obtained converted geog: {0}", filteredGeog); + SqlAssert.IsTrue(filteredGeog.STEquals(expectedGeog)); + } + + [TestMethod] + public void InterpolateBetweenGeogTest() + { + var geog1 = "POINT(0 0 0 0)".GetGeog(); + var geog2 = "POINT(10 0 0 10)".GetGeog(); + var returnPoint = "POINT (4.7441999536520428E-05 0)".GetGeog(); + const int distance = 5; + Logger.LogLine("Input Point 1:{0} Point 2:{1}", geog1, geog2); + Logger.Log("Interpolating at a distance of {0}", geog1, geog2, distance); + Logger.LogLine("Expected Point: {0}", returnPoint); + var sqlGeography = Geography.InterpolateBetweenGeog(geog1, geog2, distance); + Logger.Log("Obtained Point: {0}", sqlGeography.ToString()); + SqlAssert.IsTrue(sqlGeography.STEquals(returnPoint)); + } + + [TestMethod] + public void IsValidGeographyFromGeometryTest() + { + var geom = "LINESTRING (0 0 1 1, 2 2 1 1)".GetGeom(); + Logger.LogLine("Input Geography: {0}", geom); + var result = Geography.IsValidGeographyFromGeometry(geom); + Logger.LogLine("Expected result: true, Obtained result: {0}", result); + SqlAssert.IsTrue(result); + } + + [TestMethod] + public void IsValidGeographyFromTextTest() + { + var geogText = "CURVEPOLYGON (CIRCULARSTRING (0 -4, 4 0, 0 4, -4 0, 0 -4)"; + Logger.LogLine("Input Geography: {0}", geogText); + var result = Geography.IsValidGeographyFromText(geogText, Constants.DefaultSRID); + Logger.LogLine("Expected result: false, Obtained result: {0}", result); + SqlAssert.IsFalse(result); + + geogText = "CURVEPOLYGON (CIRCULARSTRING (0 -4, 4 0, 0 4, -4 0, 0 -4))"; + Logger.LogLine("Input Geography: {0}", geogText); + result = Geography.IsValidGeographyFromText(geogText, Constants.DefaultSRID); + Logger.LogLine("Expected result: false, Obtained result: {0}", result); + SqlAssert.IsTrue(result); + } + + [TestMethod] + public void LocatePointAlongGeogTest() + { + var geog = "LINESTRING (0 0, 10 0)".GetGeog(); + Logger.Log("Input Geom : {0}", geog.ToString()); + var returnPoint = "POINT (4.7441999536520428E-05 0)".GetGeog(); + var distance = 5; + + Logger.LogLine("Locating a point at distance of {0} Measure", distance); + var locatedPoint = Geography.LocatePointAlongGeog(geog, distance); + Logger.Log("Expected point: {0}", returnPoint); + Logger.Log("Located point: {0} at distance of {1} Measure", locatedPoint, distance); + SqlAssert.IsTrue(locatedPoint.STEquals(returnPoint)); + } + + [TestMethod] + public void MakeValidGeographyFromGeometryTest() + { + var geom = "LINESTRING(-122.360 47.656, -122.343 47.656)".GetGeom(); + var expectedGeog = "LINESTRING (-122.343 47.655999999999992, -122.36 47.655999999999992)".GetGeog(); + Logger.LogLine("Input Geometry: {0}", geom); + var result = Geography.MakeValidGeographyFromGeometry(geom); + Logger.LogLine("Expected result: {0}", expectedGeog); + Logger.LogLine("Obtained result: {0}", result); + SqlAssert.IsTrue(result.STEquals(expectedGeog)); + } + + [TestMethod] + public void MakeValidGeographyFromTextTest() + { + var geomText = "LINESTRING(-122.360 47.656, -122.343 47.656)"; + var expectedGeog = "LINESTRING (-122.343 47.655999999999992, -122.36 47.655999999999992)".GetGeog(); + Logger.LogLine("Input Geometry: {0}", geomText); + var result = Geography.MakeValidGeographyFromText(geomText, Constants.DefaultSRID); + Logger.LogLine("Expected result: {0}", expectedGeog); + Logger.LogLine("Obtained result: {0}", result); + SqlAssert.IsTrue(result.STEquals(expectedGeog)); + } + + [TestMethod] + public void VacuousGeographyToGeometry() + { + var geog = "LINESTRING (0 0 1 1, 2 2 1 1)".GetGeog(); + var expectedGeom = "LINESTRING (0 0, 2 2)".GetGeom(); + Logger.LogLine("Input Geometry: {0}", geog); + Logger.Log("Expected Geography: {0}", expectedGeom); + var obtainedGeom = Geography.VacuousGeographyToGeometry(geog, Constants.DefaultSRID); + Logger.Log("Obtained Geography: {0}", obtainedGeom); + SqlAssert.IsTrue(obtainedGeom.STEquals(expectedGeom)); + } + + } + } +} \ No newline at end of file diff --git a/unittest/Functions/LRSFunctionTests.cs b/unittest/Functions/LRSFunctionTests.cs new file mode 100644 index 0000000..c397456 --- /dev/null +++ b/unittest/Functions/LRSFunctionTests.cs @@ -0,0 +1,1135 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.SqlServer.Types; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SQLSpatialTools.Functions.LRS; +using SQLSpatialTools.Sinks.Geometry; +using SQLSpatialTools.UnitTests.Extension; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.UnitTests.Functions +{ + [TestClass] + public class LRSFunctionTests : BaseUnitTest + { + [TestMethod] + public void ClipMultiLineTest() + { + double startMeasure = 1; + double endMeasure = -5; + var geom = "MULTILINESTRING((1 1 1, 2 2 2, 3 3 3), (4 4 4, 5.125 5.125 5.125, 6 6 6), (10 10 10, 11.25 11.25 11.25, 12 12 12))".GetGeom(); + var expected = "POINT (1 1 1)".GetGeom(); + var result = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + LogClipGeomSegments(startMeasure, endMeasure, geom, result, expected); + + startMeasure = 2; + endMeasure = 10; + geom = "MULTILINESTRING((1 1 1, 2 2 2, 3 3 3), (4 4 4, 5.125 5.125 5.125, 6 6 6), (10 10 10, 11.25 11.25 11.25, 12 12 12))".GetGeom(); + expected = "MULTILINESTRING ((2.0 2.0 2.0, 3.0 3.0 3.0), (4.0 4.0 4.0, 5.125 5.125 5.125, 6.0 6.0 6.0))".GetGeom(); + result = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + LogClipGeomSegments(startMeasure, endMeasure, geom, result, expected); + + startMeasure = 2; + endMeasure = 11.25; + geom = "MULTILINESTRING((1 1 1, 2 2 2, 3 3 3), (4 4 4, 5.125 5.125 5.125, 6 6 6), (10 10 10, 11.25 11.25 11.25, 12 12 12))".GetGeom(); + expected = "MULTILINESTRING ((2 2 NULL 2, 2 2 NULL 2, 3 3 NULL 3), (4 4 NULL 4, 5.125 5.125 NULL 5.125, 6 6 NULL 6), (10 10 NULL 10, 11.25 11.25 NULL 11.25))".GetGeom(); + result = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + LogClipGeomSegments(startMeasure, endMeasure, geom, result, expected); + + startMeasure = 2.6; + endMeasure = 11; + geom = "MULTILINESTRING((1 1 1, 2 2 2, 3 3 3), (4 4 4, 5.125 5.125 5.125, 6 6 6), (10 10 10, 11.25 11.25 11.25, 12 12 12))".GetGeom(); + expected = "MULTILINESTRING ((4 4 NULL 4, 5.125 5.125 NULL 5.125, 6 6 NULL 6), (10 10 NULL 10, 11.25 11.25 NULL 11.25))".GetGeom(); + result = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + LogClipGeomSegments(startMeasure, endMeasure, geom, result, expected); + + startMeasure = 2.6; + endMeasure = 10; + geom = "MULTILINESTRING((1 1 1, 2 2 2, 3 3 3), (4 4 4, 5.125 5.125 5.125, 6 6 6), (10 10 10, 11.25 11.25 11.25, 12 12 12))".GetGeom(); + expected = "LINESTRING (4 4 NULL 4, 5.125 5.125 NULL 5.125, 6 6 NULL 6)".GetGeom(); + result = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + LogClipGeomSegments(startMeasure, endMeasure, geom, result, expected); + } + + [TestMethod] + public void ClipGeometrySegmentExtensionTest() + { + double startMeasure = 0; + double endMeasure = 500; + var geom = "LINESTRING(200000 100000 0, 200500 100000 500)".GetGeom(); + var expected = "LINESTRING (200000 100000 NULL 0, 200500 100000 NULL 500)".GetGeom(); + var result = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + LogClipGeomSegments(startMeasure, endMeasure, geom, result, expected); + + startMeasure = 10; + endMeasure = 5; + geom = "LINESTRING(5 10 10, 20 5 30.628, 35 10 61.257, 55 10 100)".GetGeom(); + result = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + expected = "POINT (5 10 NULL 10)".GetGeom(); + LogClipGeomSegments(startMeasure, endMeasure, geom, result, expected); + + startMeasure = 5; + endMeasure = 110; + result = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + expected = "LINESTRING (5 10 NULL 10, 20 5 NULL 30.628, 35 10 NULL 61.257, 55 10 NULL 100)".GetGeom(); + LogClipGeomSegments(startMeasure, endMeasure, geom, result, expected); + + // both measures less than start measure of input geom + startMeasure = 5; + endMeasure = 9; + result = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + LogClipGeomSegments(startMeasure, endMeasure, geom, result, null); + + // both measures greater than end measure of input geom + startMeasure = 110; + endMeasure = 120; + result = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + LogClipGeomSegments(startMeasure, endMeasure, geom, result, null); + + startMeasure = 10; + endMeasure = 110; + result = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + expected = "LINESTRING (5 10 NULL 10, 20 5 NULL 30.628, 35 10 NULL 61.257, 55 10 NULL 100)".GetGeom(); + LogClipGeomSegments(startMeasure, endMeasure, geom, result, expected); + + startMeasure = 15; + endMeasure = 90; + result = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + expected = "LINESTRING (8.63583478766725 8.7880550707775846 NULL 15, 20 5 NULL 30.628, 35 10 NULL 61.257, 49.837777146839429 10 NULL 90)".GetGeom(); + LogClipGeomSegments(startMeasure, endMeasure, geom, result, expected); + + startMeasure = 100; + endMeasure = 200; + result = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + expected = "POINT (55 10 NULL 100)".GetGeom(); + LogClipGeomSegments(startMeasure, endMeasure, geom, result, expected); + + startMeasure = 61.257F; + endMeasure = 200; + result = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + expected = "LINESTRING (35 10 NULL 61.257, 55 10 NULL 100)".GetGeom(); + LogClipGeomSegments(startMeasure, endMeasure, geom, result, expected); + + startMeasure = 50; + endMeasure = 100; + result = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + expected = "LINESTRING (29.487087400829282 8.1623624669430939 NULL 50, 35 10 NULL 61.257, 55 10 NULL 100)".GetGeom(); + LogClipGeomSegments(startMeasure, endMeasure, geom, result, expected); + } + + [TestMethod] + public void ClipGeometrySegmentTest() + { + var geom = "MULTILINESTRING((100 100, 200 200), (3 4, 7 8, 10 10))".GetGeom(); + Logger.Log("Input Geom : {0}", geom.ToString()); + try + { + Geometry.ClipGeometrySegment(geom, 15, 20); + } + catch (ArgumentException e) + { + Assert.AreEqual(e.Message, ErrorMessage.TwoDimensionalCoordinates); + TestContext.WriteLine(ErrorMessage.TwoDimensionalCoordinates); + } + + // line string with null z value + geom = "LINESTRING (10 1 NULL 10, 25 1 NULL 25 )".GetGeom(); + Logger.LogLine("Input Geom : {0}", geom.ToString()); + + // when measures are out of range; clip segment is null + var startMeasure = 5; + var endMeasure = 7; + Logger.Log("Clip input geom with a Start Measure: {0} and End Measure: {1}", startMeasure, endMeasure); + var clippedGeom = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + Assert.IsNull(clippedGeom); + + // measure in between range. + startMeasure = 15; + endMeasure = 27; + Logger.LogLine("Clip input geom with a Start Measure: {0} and End Measure: {1}", startMeasure, endMeasure); + var retGeom = "LINESTRING(15 1 NULL 15, 25 1 NULL 25)".GetGeom(); + clippedGeom = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + SqlAssert.IsTrue(retGeom.STIsValid()); + SqlAssert.IsTrue(clippedGeom.STEquals(retGeom)); + + // From start to 5 point measure + startMeasure = 10; + endMeasure = 15; + Logger.LogLine("Clip input geom with a Start Measure: {0} and End Measure: {1}", startMeasure, endMeasure); + + retGeom = "LINESTRING (10 1 NULL 10, 15 1 NULL 15 )".GetGeom(); + clippedGeom = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + Logger.Log("Clipped Geom: {0}", clippedGeom.ToString()); + SqlAssert.IsTrue(retGeom.STIsValid()); + SqlAssert.IsTrue(clippedGeom.STEquals(retGeom)); + + // From 15 to 20 + startMeasure = 15; + endMeasure = 20; + Logger.LogLine("Clip input geom with a Start Measure: {0} and End Measure: {1}", startMeasure, endMeasure); + + retGeom = "LINESTRING (15 1 NULL 15, 20 1 NULL 20 )".GetGeom(); + clippedGeom = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + Logger.Log("Clipped Geom: {0}", clippedGeom.ToString()); + SqlAssert.IsTrue(retGeom.STIsValid()); + SqlAssert.IsTrue(clippedGeom.STEquals(retGeom)); + + // From 20 to 25 + startMeasure = 20; + endMeasure = 25; + Logger.LogLine("Clip input geom with a Start Measure: {0} and End Measure: {1}", startMeasure, endMeasure); + + retGeom = "LINESTRING (20 1 NULL 20, 25 1 NULL 25 )".GetGeom(); + clippedGeom = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + Logger.Log("Clipped Geom: {0}", clippedGeom.ToString()); + SqlAssert.IsTrue(retGeom.STIsValid()); + SqlAssert.IsTrue(clippedGeom.STEquals(retGeom)); + + startMeasure = 5; + endMeasure = 10; + geom = "LINESTRING(2 2 0, 2 4 2, 8 4 8, 12 4 12, 12 10 0, 8 10 22, 5 14 27)".GetGeom(); + retGeom = "LINESTRING (5 4 NULL 5, 8 4 NULL 8, 10 4 NULL 10)".GetGeom(); + clippedGeom = Geometry.ClipGeometrySegment(geom, startMeasure, endMeasure); + Logger.Log("Clipped Geom: {0}", clippedGeom.ToString()); + SqlAssert.IsTrue(retGeom.STIsValid()); + SqlAssert.IsTrue(clippedGeom.STEquals(retGeom)); + } + + [TestMethod] + public void ConvertToLrsGeomTest() + { + // 4 point line string + var geom = "LINESTRING (10 4 , 20 7 , 30 9 )".GetGeom(); + Logger.Log("Input Geom : {0}", geom.ToString()); + + Logger.Log("ConvertToLrs Geom with null Start and End Measure"); + var ConvertedLrsGeom = Geometry.ConvertToLrsGeom(geom,9,null); + Logger.Log("ConvertToLrs Geom : {0}", ConvertedLrsGeom ); + + // As per requirement; + // the default value of start point is 0 when null is specified + // the default value of end point is cartographic length of the segment when null is specified + // if the start or end measure anything is null then it returns null value + + //SqlAssert.AreEqual(ConvertedLrsGeom.GetStartPointMeasure(), 10.0F); + //SqlAssert.AreEqual(ConvertedLrsGeom.GetEndPointMeasure(), 1.0F); + } + + [TestMethod] + public void GetEndMeasureTest() + { + var endMeasureValue = 14.0F; + var geom = $"POINT(5.5 5 0 {endMeasureValue})".GetGeom(); + var endMeasure = Geometry.GetEndMeasure(geom); + SqlAssert.AreEqual(endMeasure, endMeasureValue); + + endMeasureValue = 10.0F; + geom = $"LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 1000 {endMeasureValue})".GetGeom(); + endMeasure = Geometry.GetEndMeasure(geom); + SqlAssert.AreEqual(endMeasure, endMeasureValue); + + endMeasureValue = 100.000999450684F; + geom = $"MULTILINESTRING((0 0 0 0, 1 1 0 0), (3 2 0 null, 5 5 2 {endMeasureValue}))".GetGeom(); + endMeasure = Geometry.GetEndMeasure(geom); + SqlAssert.AreEqual(endMeasure, endMeasureValue); + + try + { + geom = ("POLYGON((0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 1000, 0 0 0 0))").GetGeom(); + Geometry.GetEndMeasure(geom); + Assert.Fail("Method GetGeomSegmentEndMeasureTest should not accept polygon geometric structure"); + } + catch (ArgumentException e) + { + Assert.AreEqual(e.Message, ErrorMessage.LRSCompatible); + TestContext.WriteLine(ErrorMessage.LRSCompatible); + } + + endMeasureValue = 10.0F; + geom = "GEOMETRYCOLLECTION(LINESTRING(1 1 4, 3 5 6), MULTILINESTRING((-1 -1 0, 1 -5 5, -5 5 10), (-5 -1 5, -1 -1 10)), POINT(5 6 10))".GetGeom(); + endMeasure = Geometry.GetEndMeasure(geom); + SqlAssert.AreEqual(endMeasure, endMeasureValue); + } + + [TestMethod] + public void GetStartMeasureTest() + { + var startMeasureValue = 14.0F; + var geom = $"POINT(5.5 5 1000 {startMeasureValue})".GetGeom(); + var startMeasure = Geometry.GetStartMeasure(geom); + SqlAssert.AreEqual(startMeasure, startMeasureValue); + + startMeasureValue = 10.0F; + geom = $"LINESTRING(0 0 0 {startMeasureValue}, 1 1 0 0, 3 4 0 0, 5.5 5 1000 0)".GetGeom(); + startMeasure = Geometry.GetStartMeasure(geom); + SqlAssert.AreEqual(startMeasure, startMeasureValue); + + startMeasureValue = 100.000999450684F; + geom = string.Format("MULTILINESTRING((0 0 0 {0}, 1 1 0 0), (3 2 0 null, 5 5 2 {0}))", startMeasureValue).GetGeom(); + startMeasure = Geometry.GetStartMeasure(geom); + SqlAssert.AreEqual(startMeasure, startMeasureValue); + + try + { + geom = ("POLYGON((0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 1000, 0 0 0 0))").GetGeom(); + Geometry.GetStartMeasure(geom); + Assert.Fail("Method GetGeomSegmentStartMeasure should not accept polygon geometric structure"); + } + catch (ArgumentException e) + { + Assert.AreEqual(e.Message, ErrorMessage.LRSCompatible); + TestContext.WriteLine(ErrorMessage.LRSCompatible); + } + + startMeasureValue = 4.0F; + geom = "GEOMETRYCOLLECTION(LINESTRING(1 1 4, 3 5 6), MULTILINESTRING((-1 -1 0, 1 -5 5, -5 5 10), (-5 -1 5, -1 -1 10)), POINT(5 6 10))".GetGeom(); + startMeasure = Geometry.GetStartMeasure(geom); + SqlAssert.AreEqual(startMeasure, startMeasureValue); + } + + [TestMethod] + public void InterpolateBetweenGeomTest() + { + var geom1 = "POINT(0 0 0 0)".GetGeom(); + var geom2 = "POINT(10 0 0 10)".GetGeom(); + var returnPoint = "POINT (5 0 NULL 5)".GetGeom(); + const int distance = 5; + Logger.LogLine("Input Point 1:{0} Point 2:{1}", geom1, geom2); + Logger.Log("Interpolating at a distance of {0}", geom1, geom2, distance); + Logger.LogLine("Expected Point: {0}", returnPoint.ToString()); + var sqlGeometry = Geometry.InterpolateBetweenGeom(geom1, geom2, distance); + Logger.Log("Obtained Point: {0}", sqlGeometry.ToString()); + SqlAssert.IsTrue(sqlGeometry.STEquals(returnPoint)); + } + + [TestMethod] + public void IsConnectedTest() + { + // test cases without considering tolerance values + var geom1 = "LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 0 0)".GetGeom(); + var geom2 = "LINESTRING(0 0 0 0, 2 2 0 0)".GetGeom(); + var tolerance = Constants.Tolerance; + var result = Geometry.IsConnected(geom1, geom2, tolerance); + SqlAssert.IsTrue(result); + + // Different SRIDs Failure Test + try + { + geom2 = "LINESTRING(0 0 0 0, 2 2 0 0)".GetGeom(10); + Geometry.IsConnected(geom1, geom2, tolerance); + } + catch (ArgumentException e) + { + Assert.AreEqual(e.Message, ErrorMessage.SRIDCompatible); + TestContext.WriteLine(ErrorMessage.SRIDCompatible); + } + + geom1 = "LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 0 0)".GetGeom(); + geom2 = "LINESTRING(2 2 0 0, 0 0 0 0)".GetGeom(); + tolerance = 0; + result = Geometry.IsConnected(geom1, geom2, tolerance); + SqlAssert.IsTrue(result); + + geom1 = "LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 0 0)".GetGeom(); + geom2 = "LINESTRING(5.5 5 0 0, 2 2 0 0)".GetGeom(); + tolerance = 0; + result = Geometry.IsConnected(geom1, geom2, tolerance); + SqlAssert.IsTrue(result); + + geom1 = "LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 0 0)".GetGeom(); + geom2 = "LINESTRING(2 2 9 0, 5.5 5 0 0)".GetGeom(); + tolerance = 0; + result = Geometry.IsConnected(geom1, geom2, tolerance); + SqlAssert.IsTrue(result); + + // test cases with tolerance values considered + // here point difference is not considered; rather x2-x1 and y2-y1 is considered for tolerance + geom1 = "LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 0 0)".GetGeom(); + geom2 = "LINESTRING(0.5 0.5 0 0, 2 2 0 0)".GetGeom(); + tolerance = 0.5; + result = Geometry.IsConnected(geom1, geom2, tolerance); + SqlAssert.IsTrue(result); + + geom1 = "LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 0 0)".GetGeom(); + geom2 = "LINESTRING(0.5 0 0 0, 2 2 0 0)".GetGeom(); + tolerance = 0.5; + result = Geometry.IsConnected(geom1, geom2, tolerance); + SqlAssert.IsTrue(result); + + geom1 = "LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 0 0)".GetGeom(); + geom2 = "LINESTRING(1.5 1.5 0 0, 2 2 0 0)".GetGeom(); + tolerance = 0.5; + result = Geometry.IsConnected(geom1, geom2, tolerance); + SqlAssert.IsFalse(result); + + geom1 = "LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 0 0)".GetGeom(); + geom2 = "LINESTRING(0 0.5 0 0, 2 2 0 0)".GetGeom(); + tolerance = 0.5; + result = Geometry.IsConnected(geom1, geom2, tolerance); + SqlAssert.IsTrue(result); + + geom1 = "LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 0 0)".GetGeom(); + geom2 = "LINESTRING(2 2 0 0, 0.1 0.6 0 0)".GetGeom(); + tolerance = 0.5; + result = Geometry.IsConnected(geom1, geom2, tolerance); + SqlAssert.IsFalse(result); + + geom1 = "LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 0 0)".GetGeom(); + geom2 = "LINESTRING(2 2 0 0, 0.6 0.1 0 0)".GetGeom(); + tolerance = 1; + result = Geometry.IsConnected(geom1, geom2, tolerance); + SqlAssert.IsTrue(result); + + geom1 = "LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 0 0)".GetGeom(); + geom2 = "LINESTRING(5 5 0 0, 2 2 0 0)".GetGeom(); + tolerance = 0.5; + result = Geometry.IsConnected(geom1, geom2, tolerance); + SqlAssert.IsTrue(result); + + geom1 = "LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 0 0)".GetGeom(); + geom2 = "LINESTRING(2 2 9 0, 6 4.9 0 0)".GetGeom(); + tolerance = 0.5; + result = Geometry.IsConnected(geom1, geom2, tolerance); + SqlAssert.IsTrue(result); + + geom1 = "POINT(0 0 NULL 0)".GetGeom(); + geom2 = "POINT(0 0 NULL 0)".GetGeom(); + SqlAssert.IsTrue(Geometry.IsConnected(geom1, geom2, tolerance)); + } + + [TestMethod] + public void IsValidPoint() + { + var geom = "LINESTRING(0 0 0 0, 1 1 0 0, 3 4 0 0, 5.5 5 0 0)".GetGeom(); + SqlAssert.IsFalse(Geometry.IsValidPoint(geom)); + + geom = "POINT(0 0 NULL 0)".GetGeom(); + SqlAssert.IsTrue(Geometry.IsValidPoint(geom)); + + geom = "POINT(0 0 0 0)".GetGeom(); + SqlAssert.IsTrue(Geometry.IsValidPoint(geom)); + + geom = "POINT(0 0 1)".GetGeom(); + SqlAssert.IsTrue(Geometry.IsValidPoint(geom)); + + geom = "POINT(0 0 0)".GetGeom(); + SqlAssert.IsTrue(Geometry.IsValidPoint(geom)); + + geom = "POINT(0 0)".GetGeom(); + SqlAssert.IsFalse(Geometry.IsValidPoint(geom)); + } + + [TestMethod] + public void LocatePointAlongGeomTest() + { + var geom = "LINESTRING (0 0 0 0, 10 0 0 10)".GetGeom(); + Logger.Log("Input Geom : {0}", geom.ToString()); + var returnPoint = "POINT (5 0 NULL 5)".GetGeom(); + const int distance = 5; + + var locatedPoint = Geometry.LocatePointAlongGeom(geom, distance); + Logger.Log("Located point : {0} at distance of {1} Measure", locatedPoint.ToString(), distance); + + SqlAssert.IsTrue(locatedPoint.STEquals(returnPoint)); + + geom = "POINT(0 0 5)".GetGeom(); + Geometry.LocatePointAlongGeom(geom, distance); + } + + [TestMethod] + public void MergeGeometrySegmentsTest() + { + var geom1 = "MULTILINESTRING((100 100 0, 200 200 100), (3 4 0, 7 8 4, 10 10 6))".GetGeom(); + var geomBuilder = new SqlGeometryBuilder(); + try + { + var segment1 = new LineStringMergeGeometrySink(geomBuilder, true, geom1.STNumPoints()); + geom1.Populate(segment1); + } + catch (Exception) + { + // ignored + } + + try + { + geom1 = "POINT(1 1 1 1)".GetGeom(); + geomBuilder = new SqlGeometryBuilder(); + var segment1 = new BuildMultiLineFromLinesSink(geomBuilder, 2); + geom1.Populate(segment1); + } + catch (Exception) + { + // ignored + } + + try + { + geom1 = "POLYGON((1 1, 3 3, 3 1, 1 1))".GetGeom(); + geomBuilder = new SqlGeometryBuilder(); + var segment1 = new BuildMultiLineFromLinesSink(geomBuilder, 2); + geom1.Populate(segment1); + } + catch (Exception) + { + // ignored + } + + geom1 = "MULTILINESTRING((100 100 0, 200 200 100), (3 4 0, 7 8 4, 10 10 6))".GetGeom(); + var geom2 = "MULTILINESTRING((11 2 0, 12 4 2, 15 5 4), (5 4 4, 6 8 6, 9 11 8))".GetGeom(); + Logger.Log("Input Geom 1: {0}", geom1.ToString()); + Logger.Log("Input Geom 2: {0}", geom2.ToString()); + try + { + Geometry.MergeGeometrySegments(geom1, geom2); + } + catch (ArgumentException e) + { + Assert.AreEqual(e.Message, ErrorMessage.LineStringCompatible); + TestContext.WriteLine(ErrorMessage.LineStringCompatible); + } + + geom1 = "LINESTRING(10 1 NULL 10, 25 1 NULL 25)".GetGeom(); + geom2 = "MULTILINESTRING((11 2 0, 12 4 2, 15 5 4), (5 4 4, 6 8 6, 9 11 8))".GetGeom(); + Logger.LogLine("Input Geom 1: {0}", geom1.ToString()); + Logger.Log("Input Geom 2: {0}", geom2.ToString()); + try + { + Geometry.MergeGeometrySegments(geom1, geom2); + } + catch (ArgumentException e) + { + Assert.AreEqual(e.Message, ErrorMessage.LineStringCompatible); + TestContext.WriteLine(ErrorMessage.LineStringCompatible); + } + + geom1 = "MULTILINESTRING((11 2 0, 12 4 2, 15 5 4), (5 4 4, 6 8 6, 9 11 8))".GetGeom(); + geom2 = "LINESTRING(10 1 NULL 10, 25 1 NULL 25)".GetGeom(); + Logger.LogLine("Input Geom 1: {0}", geom1.ToString()); + Logger.Log("Input Geom 2: {0}", geom2.ToString()); + try + { + Geometry.MergeGeometrySegments(geom1, geom2); + } + catch (ArgumentException e) + { + Assert.AreEqual(e.Message, ErrorMessage.LineStringCompatible); + TestContext.WriteLine(ErrorMessage.LineStringCompatible); + } + + // offset between geometries : 0 + geom1 = "LINESTRING (10 1 NULL 10, 25 1 NULL 25 )".GetGeom(); + geom2 = "LINESTRING (25 1 NULL 25, 40 1 NULL 40 )".GetGeom(); + Logger.LogLine("Input Geom 1: {0}", geom1.ToString()); + Logger.Log("Input Geom 2: {0}", geom2.ToString()); + + var mergedGeom = "LINESTRING (10 1 NULL 10, 25 1 NULL 25, 40 1 NULL 40)".GetGeom(); + var retGeom = Geometry.MergeGeometrySegments(geom1, geom2); + Logger.Log("Merged Geom: {0}", retGeom.ToString()); + SqlAssert.IsTrue(retGeom.STEquals(mergedGeom)); + + // offset between geometries: 5 + geom1 = "LINESTRING (10 1 NULL 10, 25 1 NULL 25 )".GetGeom(); + geom2 = "LINESTRING (30 1 NULL 30, 40 1 NULL 40 )".GetGeom(); + Logger.LogLine("Input Geom 1: {0}", geom1.ToString()); + Logger.Log("Input Geom 2: {0}", geom2.ToString()); + + mergedGeom = "MULTILINESTRING ((10 1 NULL 10, 25 1 NULL 25), (30 1 NULL 30, 40 1 NULL 40))".GetGeom(); + retGeom = Geometry.MergeGeometrySegments(geom1, geom2); + Logger.Log("Expected Geom: {0}", mergedGeom.ToString()); + Logger.Log("Merged Geom: {0}", retGeom.ToString()); + SqlAssert.IsTrue(retGeom.STEquals(mergedGeom)); + SqlAssert.IsTrue(retGeom.GetEndPointMeasure().Equals(mergedGeom.GetEndPointMeasure())); + } + + [TestMethod] + public void OffsetGeometrySegmentTest() + { + var geom = "LINESTRING(214250 104000 0, 214750 104050 502.494)".GetGeom(); + var offsetGeom = "LINESTRING (214249.80099256197 104001.99007438042 0, 214749.80099256197 104051.99007438042 502.494)".GetGeom(); + var startMeasure = 0; + var endMeasure = 502.494; + var offset = 2; + var tolerance = 0.5; + Logger.LogLine("Input Line : {0}", geom.ToString()); + var result = Geometry.OffsetGeometrySegment(geom, startMeasure, endMeasure, offset, tolerance); + Logger.Log("Expected Line : {0}", offsetGeom.ToString()); + Logger.Log("Offset Line : {0}", result.ToString()); + SqlAssert.IsTrue(offsetGeom.STEquals(result)); + + geom = "LINESTRING (5 10 10, 20 5 30.628, 35 10 61.257, 55 10 100)".GetGeom(); + offsetGeom = "LINESTRING (28.854631868795604 10.059729063044122 NULL 50, 34.675444679663244 12 NULL 61.594145747453567, 55 12 NULL 100)".GetGeom(); + startMeasure = 50; + endMeasure = 100.5; + offset = 2; + tolerance = 0.5; + Logger.LogLine("Input Line : {0}", geom.ToString()); + result = Geometry.OffsetGeometrySegment(geom, startMeasure, endMeasure, offset, tolerance); + Logger.Log("Expected Line : {0}", offsetGeom.ToString()); + Logger.Log("Offset Line : {0}", result.ToString()); + SqlAssert.IsTrue(offsetGeom.STEquals(result)); + } + + [TestMethod] + public void OffsetGeometryWithBendLinesTest() + { + var geom = "LINESTRING (5 10 10, 20 10 30.628, 20 14 61.257, 5 14 100)".GetGeom(); + var offsetGeom = "LINESTRING (7.0867894471005748 11.412588624412061 NULL 12, 20 7.10818510677892 NULL 36.2224547311131, 34.675444679663244 12 NULL 61.257, 54.483777714683939 12 NULL 99)".GetGeom(); + var startMeasure = 10; + var endMeasure = 100; + var offset = -2; + var tolerance = 0.5; + Logger.LogLine("Input Line : {0}", geom.ToString()); + var result = Geometry.OffsetGeometrySegment(geom, startMeasure, endMeasure, offset, tolerance); + Logger.Log("Expected Line : {0}", offsetGeom.ToString()); + Logger.Log("Offset Line : {0}", result.ToString()); + } + + [TestMethod] + public void OffsetGeometrySegmentCollinearPointsTest() + { + var geom = "LINESTRING (218000 104375 0, 218000 104875 500, 218000 105000 625)".GetGeom(); + var offsetGeom = "LINESTRING (217998 104375 0, 217998 105000 625)".GetGeom(); + var startMeasure = 0; + var endMeasure = 625; + var offset = 2; + var tolerance = 0.5; + Logger.LogLine("Input Line : {0}", geom); + Logger.Log("Expected Offset Line : {0}", offsetGeom); + var result = Geometry.OffsetGeometrySegment(geom, startMeasure, endMeasure, offset, tolerance); + Logger.Log("Obtained Offset Line : {0}", result); + SqlAssert.IsTrue(offsetGeom.STEquals(result)); + + geom = "LINESTRING (2 2 0, 2 4 4, 2 6 6, 2 10 10, 2 12 12, 2 13 13, 2 17 17)".GetGeom(); + offsetGeom = "LINESTRING (0 2.0000000000000004 0, 0 13 13)".GetGeom(); + startMeasure = 0; + endMeasure = 13; + offset = 2; + tolerance = 0.5; + Logger.LogLine("Input Line : {0}", geom); + Logger.Log("Expected Offset Line : {0}", offsetGeom); + result = Geometry.OffsetGeometrySegment(geom, startMeasure, endMeasure, offset, tolerance); + Logger.Log("Obtained Offset Line : {0}", result); + SqlAssert.IsTrue(offsetGeom.STEquals(result)); + + geom = "LINESTRING(2 2 0, 4 4 4, 6 6 6, 10 5 8)".GetGeom(); + offsetGeom = "LINESTRING (0.58578643762690508 3.4142135623730949 NULL 0, 4.5857864376269051 7.4142135623730949 NULL 6)".GetGeom(); + startMeasure = 0; + endMeasure = 6; + offset = 2; + tolerance = 0.5; + Logger.LogLine("Input Line : {0}", geom.ToString()); + Logger.Log("Expected Offset Line : {0}", offsetGeom); + result = Geometry.OffsetGeometrySegment(geom, startMeasure, endMeasure, offset, tolerance); + Logger.Log("Obtained Offset Line : {0}", result); + SqlAssert.IsTrue(offsetGeom.STEquals(result)); + } + + [TestMethod] + public void PopulateGeometryMeasuresTest() + { + // 4 point line string + var geom = "LINESTRING (10 1 10 100, 15 1 10 NULL, 20 1 10 NULL, 25 1 10 250 )".GetGeom(); + Logger.Log("Input Geom : {0}", geom.ToString()); + + Logger.Log("Populating Geom with null Start and End Measure"); + var populatedGeometry = Geometry.PopulateGeometryMeasures(geom, null, null); + Logger.Log("Populated Geom : {0}", populatedGeometry.ToString()); + + // As per requirement; + // the default value of start point is 0 when null is specified + // the default value of end point is cartographic length of the segment when null is specified + SqlAssert.AreEqual(populatedGeometry.GetStartPointMeasure(), 0.0F); + SqlAssert.AreEqual(populatedGeometry.GetEndPointMeasure(), 15.0F); + + double startMeasure = 10; + double endMeasure = 40; + // if the start, end measure would be non null, then this function overrides the 'M' value that has been passed + Logger.Log("Populating Geom with Start Measure: {0} and End Measure: {1}", startMeasure, endMeasure); + populatedGeometry = Geometry.PopulateGeometryMeasures(geom, startMeasure, endMeasure); + Logger.Log("Populated Geom : {0}", populatedGeometry.ToString()); + Assert.AreEqual(populatedGeometry.GetStartPointMeasure(), startMeasure); + SqlAssert.AreEqual(populatedGeometry.STPointN(2).M, 20.0F); + SqlAssert.AreEqual(populatedGeometry.STPointN(3).M, 30.0F); + Assert.AreEqual(populatedGeometry.GetEndPointMeasure(), endMeasure); + } + + [TestMethod] + public void ResetMeasureTest() + { // line string with null z value + var geom = "LINESTRING (0 0 NULL 10, 10 0 NULL 20)".GetGeom(); + Logger.LogLine("Input Geom : {0}", geom.ToString()); + var expectedGeom = "LINESTRING (0 0 NULL, 10 0 NULL)".GetGeom(); + + var measureResetGeom = Geometry.ResetMeasure(geom); + Logger.Log("Expected Geom: {0}", expectedGeom); + Logger.Log("Obtained Geom: {0}", measureResetGeom); + + SqlAssert.AreEqual(geom.GetStartPointMeasure(), 10); + SqlAssert.AreEqual(geom.GetEndPointMeasure(), 20); + SqlAssert.IsTrue(measureResetGeom.STEquals(expectedGeom)); + SqlAssert.IsTrue(measureResetGeom.STStartPoint().M.IsNull); + SqlAssert.IsTrue(measureResetGeom.STEndPoint().M.IsNull); + + geom = "LINESTRING (11.235 25.987 NULL 116.124, 16.78 30.897 NULL 206.35)".GetGeom(); + Logger.LogLine("Input Geom : {0}", geom.ToString()); + expectedGeom = "LINESTRING (11.235 25.987, 16.78 30.897)".GetGeom(); + + measureResetGeom = Geometry.ResetMeasure(geom); + Logger.Log("Expected Geom: {0}", expectedGeom); + Logger.Log("Obtained Geom: {0}", measureResetGeom); + + SqlAssert.AreEqual(geom.GetStartPointMeasure(), 116.124); + SqlAssert.AreEqual(geom.GetEndPointMeasure(), 206.35); + SqlAssert.IsTrue(measureResetGeom.STEquals(expectedGeom)); + SqlAssert.IsTrue(measureResetGeom.STStartPoint().M.IsNull); + SqlAssert.IsTrue(measureResetGeom.STEndPoint().M.IsNull); + } + + [TestMethod] + public void ReverseLinearGeometryTest() + { + // Check for Multi Line + var geom = "MULTILINESTRING((1 1 1,2 2 2),(3 3 3, 5 5 7))".GetGeom(); + var reversedStartPoint = "POINT (5 5 7)".GetGeom(); + Logger.LogLine("Input Geom : {0}", geom.ToString()); + var reversedGeom = Geometry.ReverseLinearGeometry(geom); + Logger.Log("Reversed Geom : {0}", reversedGeom); + SqlAssert.IsTrue(reversedGeom.STStartPoint().STEquals(reversedStartPoint)); + + // Multi Line with 5 line segments + geom = "MULTILINESTRING((1 1 1,2 2 2, 3 3 3),(4 4 4, 5 5 5, 6 6 6), (8 8 8, 9 9 9, 10 10 10), (11 11 11, 12 12 12, 13 13 13, 14 14 14))".GetGeom(); + reversedStartPoint = "POINT (14 14 14)".GetGeom(); + Logger.LogLine("Input Geom : {0}", geom.ToString()); + reversedGeom = Geometry.ReverseLinearGeometry(geom); + Logger.Log("Reversed Geom : {0}", reversedGeom); + SqlAssert.IsTrue(reversedGeom.STStartPoint().STEquals(reversedStartPoint)); + + // Check for Line String + geom = " LINESTRING(0 0 0, 10 0 40)".GetGeom(); + reversedStartPoint = "POINT (10 0 40)".GetGeom(); + Logger.LogLine("Input Geom : {0}", geom.ToString()); + reversedGeom = Geometry.ReverseLinearGeometry(geom); + Logger.Log("Reversed Geom : {0}", reversedGeom); + SqlAssert.IsTrue(reversedGeom.STStartPoint().STEquals(reversedStartPoint)); + + // Check for Point + geom = " POINT(10 0 40)".GetGeom(); + reversedStartPoint = "POINT (10 0 40)".GetGeom(); + Logger.LogLine("Input Geom : {0}", geom.ToString()); + reversedGeom = Geometry.ReverseLinearGeometry(geom); + Logger.Log("Reversed Geom : {0}", reversedGeom); + SqlAssert.IsTrue(reversedGeom.STStartPoint().STEquals(reversedStartPoint)); + } + + [TestMethod] + public void ReverseTranslateMeasureGeometryTest() + { + // Check for Multi Line + var geom = "MULTILINESTRING((1 1 1,2 2 2),(3 3 3, 5 5 7))".GetGeom(); + var offsetMeasure = 2; + var reversedStartPoint = "POINT (5 5 9)".GetGeom(); + Logger.LogLine("Input Geom : {0}", geom.ToString()); + var reversedGeom = Geometry.ReverseAndTranslateGeometry(geom, offsetMeasure); + Logger.Log("Reversed Geom : {0}", reversedGeom); + SqlAssert.IsTrue(reversedGeom.STStartPoint().STEquals(reversedStartPoint)); + + // Multi Line with 5 line segments + geom = "MULTILINESTRING((1 1 1,2 2 2, 3 3 3),(4 4 4, 5 5 5, 6 6 6), (8 8 8, 9 9 9, 10 10 10), (11 11 11, 12 12 12, 13 13 13, 14 14 14))".GetGeom(); + reversedStartPoint = "POINT (14 14 16)".GetGeom(); + Logger.LogLine("Input Geom : {0}", geom.ToString()); + reversedGeom = Geometry.ReverseAndTranslateGeometry(geom, offsetMeasure); + Logger.Log("Reversed Geom : {0}", reversedGeom); + SqlAssert.IsTrue(reversedGeom.STStartPoint().STEquals(reversedStartPoint)); + + // Check for Line String + geom = " LINESTRING(0 0 0, 10 0 40)".GetGeom(); + reversedStartPoint = "POINT (10 0 42)".GetGeom(); + Logger.LogLine("Input Geom : {0}", geom.ToString()); + reversedGeom = Geometry.ReverseAndTranslateGeometry(geom, offsetMeasure); + Logger.Log("Reversed Geom : {0}", reversedGeom); + SqlAssert.IsTrue(reversedGeom.STStartPoint().STEquals(reversedStartPoint)); + + // Check for Point + geom = " POINT(10 0 40)".GetGeom(); + reversedStartPoint = "POINT (10 0 42)".GetGeom(); + Logger.LogLine("Input Geom : {0}", geom.ToString()); + reversedGeom = Geometry.ReverseAndTranslateGeometry(geom, offsetMeasure); + Logger.Log("Reversed Geom : {0}", reversedGeom); + SqlAssert.IsTrue(reversedGeom.STStartPoint().STEquals(reversedStartPoint)); + } + + [TestMethod] + public void MultiplyGeometryMeasureTest() + { + Logger.LogLine("Multiply Geometry Measures."); + // Check for Multi Line + var geom = "MULTILINESTRING((1 1 1,2 2 2),(3 3 3, 5 5 7))".GetGeom(); + var scaleMeasure = -2; + var scaledStartPoint = "POINT (1 1 2)".GetGeom(); + Logger.LogLine("Scale Measure : {0}", scaleMeasure); + Logger.Log("Input Geom : {0}", geom.ToString()); + var scaledGeom = Geometry.MultiplyGeometryMeasures(geom, scaleMeasure); + Logger.Log("Reversed Geom : {0}", scaledGeom); + SqlAssert.IsTrue(scaledGeom.STStartPoint().STEquals(scaledStartPoint)); + + // Check for Line String + scaleMeasure = -1; + Logger.LogLine("Scale Measure : {0}", scaleMeasure); + + geom = " LINESTRING(0 0 0, 10 0 40)".GetGeom(); + scaledStartPoint = "POINT (0 0 0)".GetGeom(); + Logger.LogLine("Input Geom : {0}", geom.ToString()); + scaledGeom = Geometry.MultiplyGeometryMeasures(geom, scaleMeasure); + Logger.Log("Reversed Geom : {0}", scaledGeom); + SqlAssert.IsTrue(scaledGeom.STStartPoint().STEquals(scaledStartPoint)); + + // Check for Point + scaleMeasure = 5; + Logger.LogLine("Scale Measure : {0}", scaleMeasure); + + geom = " POINT(10 0 40)".GetGeom(); + scaledStartPoint = "POINT (10 0 200)".GetGeom(); + Logger.LogLine("Input Geom : {0}", geom.ToString()); + scaledGeom = Geometry.MultiplyGeometryMeasures(geom, scaleMeasure); + Logger.Log("Reversed Geom : {0}", scaledGeom); + SqlAssert.IsTrue(scaledGeom.STStartPoint().STEquals(scaledStartPoint)); + } + + [TestMethod] + public void SplitGeometryExperimentalTest() + { + var geom = "MULTILINESTRING ((2 2 2, 2 4 4), (8 4 8, 12 4 12, 12 10 29))".GetGeom(); + Logger.LogLine("Input Geom : {0}\n-------------------------------------\n", geom.ToString()); + + DoSplitTest(1, geom); + DoSplitTest(2, geom); + DoSplitTest(3, geom); + DoSplitTest(4, geom); + DoSplitTest(5, geom); + DoSplitTest(8, geom); + DoSplitTest(12, geom); + DoSplitTest(15, geom); + DoSplitTest(29, geom); + DoSplitTest(30, geom); + + geom = "POINT(2 2 7)".GetGeom(); + Logger.LogLine("Input Geom : {0}\n-------------------------------------\n", geom.ToString()); + + DoSplitTest(1, geom); + DoSplitTest(7, geom); + DoSplitTest(8, geom); + + geom = "LINESTRING (2 2 2, 2 4 4, 8 4 8, 12 4 12, 12 10 29)".GetGeom(); + Logger.LogLine("Input Geom : {0}\n-------------------------------------\n", geom.ToString()); + + DoSplitTest(1, geom); + DoSplitTest(2, geom); + DoSplitTest(3, geom); + DoSplitTest(4, geom); + DoSplitTest(5, geom); + DoSplitTest(8, geom); + DoSplitTest(12, geom); + DoSplitTest(15, geom); + DoSplitTest(29, geom); + DoSplitTest(30, geom); + } + + [TestMethod] + public void ScaleGeometryMeasureTest() + { + var geom = "LINESTRING (2 2 0 6, 2 4 0 2, 8 4 0 8)".GetGeom(); + var shiftMeasure = 2; + try + { + Geometry.ScaleGeometrySegment(geom, 5, 25, shiftMeasure); + Assert.Fail("Exception not thrown when not linear."); + } + catch (ArgumentException) + { + // ignored + } + + geom = "LINESTRING (2 2 0 6, 2 4 0 7, 8 4 0 8)".GetGeom(); + var result = Geometry.ScaleGeometrySegment(geom, 7, 5, shiftMeasure); + var expected = "LINESTRING (2 2 0 9, 2 4 0 8, 8 4 0 7)".GetGeom(); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log("Expected : {0}", expected); + Logger.Log("Result : {0}", result); + SqlAssert.IsTrue(expected.STEquals(result)); + + geom = "LINESTRING (2 2 0 6, 2 4 0 7, 8 4 0 8)".GetGeom(); + result = Geometry.ScaleGeometrySegment(geom, -7, -5, -5); + expected = "LINESTRING (2 2 0 -12, 2 4 0 -11, 8 4 0 -10)".GetGeom(); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log("Expected : {0}", expected); + Logger.Log("Result : {0}", result); + SqlAssert.IsTrue(expected.STEquals(result)); + + geom = "MULTILINESTRING((1 1 1,2 2 2, 3 3 3),(4 4 4, 5 5 5, 6 6 6), (8 8 8, 9 9 9, 10 10 10), (11 11 11, 12 12 12, 13 13 13, 14 14 14))".GetGeom(); + result = Geometry.ScaleGeometrySegment(geom, 5, 20, shiftMeasure); + expected = "MULTILINESTRING ((1 1 0 2, 2 2 0 4, 3 3 0 5), (4 4 0 6, 5 5 0 7, 6 6 0 8), (8 8 0 10, 9 9 0 11, 10 10 0 12), (11 11 0 13, 12 12 0 14, 13 13 0 15, 14 14 0 16))".GetGeom(); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log("Expected : {0}", expected); + Logger.Log("Result : {0}", result); + SqlAssert.IsTrue(expected.STEquals(result)); + + geom = "POINT(2 4 6)".GetGeom(); + shiftMeasure = 5; + result = Geometry.ScaleGeometrySegment(geom, 0, 25, shiftMeasure); + expected = "POINT (2 4 30)".GetGeom(); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log("Expected : {0}", expected); + Logger.Log("Result : {0}", result); + SqlAssert.IsTrue(expected.STEquals(result)); + + geom = "POINT(2 4 6)".GetGeom(); + shiftMeasure = 5; + result = Geometry.ScaleGeometrySegment(geom, 25, 0, shiftMeasure); + expected = "POINT (2 4 5)".GetGeom(); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log("Expected : {0}", expected); + Logger.Log("Result : {0}", result); + SqlAssert.IsTrue(expected.STEquals(result)); + } + + [TestMethod] + public void TranslateMeasureGeometryTest() + { + var geom = "LINESTRING (2 2 6, 2 4 2, 8 4 8)".GetGeom(); + const int translateMeasure = 2; + var result = Geometry.TranslateMeasure(geom, translateMeasure); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log("Result : {0}", result); + + geom = "MULTILINESTRING((1 1 1,2 2 2, 3 3 3),(4 4 4, 5 5 5, 6 6 6), (8 8 8, 9 9 9, 10 10 10), (11 11 11, 12 12 12, 13 13 13, 14 14 14))".GetGeom(); + result = Geometry.TranslateMeasure(geom, translateMeasure); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log("Result : {0}", result); + + geom = "POINT(2 4 6)".GetGeom(); + result = Geometry.TranslateMeasure(geom, translateMeasure); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log("Result : {0}", result); + } + + [TestMethod] + public void ValidateLRSGeometryTest() + { + var geom = "GEOMETRYCOLLECTION(LINESTRING(1 1 1, 3 5 2))".GetGeom(); + var result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual(LRSErrorCodes.InvalidLRS.Value(), result); + + geom = "POINT(5 6)".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual(LRSErrorCodes.InvalidLRS.Value(), result); + + geom = "LINESTRING(1 1, 3 5)".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual(LRSErrorCodes.InvalidLRS.Value(), result); + + geom = "MULTILINESTRING ((2 2, 2 4), (8 4, 12 4))".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual(LRSErrorCodes.InvalidLRS.Value(), result); + + geom = "LINESTRING (2 2 0, 2 4 2, 8 4 8, 12 4 12, 12 10 29, 8 10 22, 5 14 27)".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual(LRSErrorCodes.InvalidLRSMeasure.Value(), result); + + geom = "LINESTRING (2 2 6, 2 4 2, 8 4 8)".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual(LRSErrorCodes.InvalidLRSMeasure.Value(), result); + + geom = "LINESTRING (2 2 12, 2 4 2, 8 4 8)".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual(LRSErrorCodes.InvalidLRSMeasure.Value(), result); + + geom = "MULTILINESTRING ((2 2 2, 2 4 0), (8 4 8, 12 4 12, 12 10 29))".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual(LRSErrorCodes.InvalidLRSMeasure.Value(), result); + + geom = "MULTILINESTRING ((2 2 2, 2 4 4), (8 4 4, 12 4 2, 12 10 29))".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual(LRSErrorCodes.InvalidLRSMeasure.Value(), result); + + geom = "MULTILINESTRING((2 2 2, 2 4 4), (8 4 2, 12 4 4, 12 10 6))".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual(LRSErrorCodes.InvalidLRSMeasure.Value(), result); + + geom = "MULTILINESTRING((2 2 2, 2 4 4), (8 4 4, 12 4 4, 12 10 6))".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual(LRSErrorCodes.ValidLRS.Value(), result); + + geom = "MULTILINESTRING((2 2 2, 2 4 2), (8 4 4, 12 4 4, 12 10 6))".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual(LRSErrorCodes.ValidLRS.Value(), result); + + // For the below TRUE is seen in Oracle; but in SQL the geom is Invalid hence we are returning invalid in this case. + geom = "MULTILINESTRING((2 2 2, 2 2 2), (2 2 2, 2 2 2, 2 2 2))".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual(LRSErrorCodes.InvalidLRS.Value(), result); + + // Valid cases + geom = "POINT(5 6 5)".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual("TRUE", result); + + geom = "LINESTRING(1 1 3, 3 5 5)".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual("TRUE", result); + + geom = "MULTILINESTRING ((2 2 1, 2 4 4), (8 4 5, 12 4 6))".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual("TRUE", result); + + geom = "LINESTRING (2 2 0, 2 4 2, 8 4 8, 12 4 12, 12 10 18, 8 10 22, 5 14 27)".GetGeom(); + result = Geometry.ValidateLRSGeometry(geom); + Logger.LogLine("Input : {0}", geom.ToString()); + Logger.Log(result); + Assert.AreEqual("TRUE", result); + } + + /// + /// Logs the clip geom segments. + /// + /// The start measure. + /// The end measure. + /// The geom. + /// The result. + /// The expected. + private void LogClipGeomSegments(double startMeasure, double endMeasure, SqlGeometry geom, SqlGeometry result, SqlGeometry expected) + { + Logger.LogLine("Input Clipped at measure : {0}, End Measure : {1}", startMeasure, endMeasure); + Logger.Log("Input Geom : {0}", geom.ToString()); + Logger.Log("Expected : {0}", expected?.ToString()); + Logger.Log("Clipped : {0}", result?.ToString()); + + if (expected == null) + SqlAssert.IsTrue(result == null); + else + SqlAssert.IsTrue(expected.STEquals(result)); + } + + [TestMethod] + public void GetMergePositionTest() + { + //EndStart connected + var geom1 = "LINESTRING (1 1 10, 55 55 690)".GetGeom(); + var geom2 = "MULTILINESTRING ((54.7 55 690, 100 100 3488), (121 124 4000, 200 201 5000))".GetGeom(); + var tolerance = 0.3; + var result = Geometry.GetMergePosition(geom1, geom2, tolerance); + Assert.AreEqual(result, MergePosition.EndStart.ToString()); + + //StartStart connected + geom1 = "MULTILINESTRING ((1 1 10, 55 55 690), (60 61 700, 71 72 800))".GetGeom(); + geom2 = "MULTILINESTRING ((1 0.8 100, 100 100 3488), (200 201 5000, 300 301 5005))".GetGeom(); + tolerance = 0.3; + result = Geometry.GetMergePosition(geom1, geom2, tolerance); + Assert.AreEqual(result, MergePosition.StartStart.ToString()); + + //StartEnd connected + geom1 = "LINESTRING (1 1 10, 55 55 690)".GetGeom(); + geom2 = "LINESTRING (5 5 690, 0.71 1 1045)".GetGeom(); + tolerance = 0.3; + result = Geometry.GetMergePosition(geom1, geom2, tolerance); + Assert.AreEqual(result, MergePosition.StartEnd.ToString()); + + //StartEnd connected + geom1 = "LINESTRING (1 1 10, 55 55 690)".GetGeom(); + geom2 = "POINT (1 1 100)".GetGeom(); + tolerance = 0.3; + result = Geometry.GetMergePosition(geom1, geom2, tolerance); + Assert.AreEqual(result, MergePosition.StartEnd.ToString()); + + //EndEnd connected + geom1 = "LINESTRING (1 1 10, 55 55 690)".GetGeom(); + geom2 = "LINESTRING (5 5 690, 54.9111 55 4555)".GetGeom(); + tolerance = 0.3; + result = Geometry.GetMergePosition(geom1, geom2, tolerance); + Assert.AreEqual(result, MergePosition.EndEnd.ToString()); + + //BothEnds connected + geom1 = "LINESTRING (1 1 10, 44 50 45, 55 55 690)".GetGeom(); + geom2 = "LINESTRING (1 1 690, 55 54.71 3488)".GetGeom(); + tolerance = 0.3; + result = Geometry.GetMergePosition(geom1, geom2, tolerance); + Assert.AreEqual(result, MergePosition.BothEnds.ToString()); + + //StartEnd connected + geom1 = "LINESTRING (1 1 10, 55 55 690)".GetGeom(); + geom2 = "LINESTRING (55 5 5690, 1 0.87 3488)".GetGeom(); + tolerance = 0.3; + result = Geometry.GetMergePosition(geom1, geom2, tolerance); + Assert.AreEqual(result, MergePosition.StartEnd.ToString()); + + //Not connected + geom1 = "LINESTRING (1 1 10, 55 55 690)".GetGeom(); + geom2 = "LINESTRING (5 5 690, 100 100 3488)".GetGeom(); + tolerance = 0.3; + result = Geometry.GetMergePosition(geom1, geom2, tolerance); + Assert.AreEqual("false", result); + + //CrossEnds connected + geom1 = "LINESTRING (1 1 10, 55 55 690)".GetGeom(); + geom2 = "LINESTRING (55 55 5690, 1 0.87 3488)".GetGeom(); + tolerance = 0.3; + result = Geometry.GetMergePosition(geom1, geom2, tolerance); + Assert.AreEqual(result, MergePosition.CrossEnds.ToString()); + } + + /// + /// Does the split test. + /// + /// The measure. + /// The geom. + private void DoSplitTest(double measure, SqlGeometry geom) + { + try + { + Logger.LogLine("Splitting for measure Geom : {0}", measure); + Geometry.SplitGeometrySegment(geom, measure, out var geomSegment1, out var geomSegment2); + Logger.Log("Segment 1 Geom : {0}", geomSegment1); + Logger.Log("Segment 2 Geom : {0}", geomSegment2); + } + catch (Exception ex) + { + Logger.Log("Error : {0}", ex.Message); + } + } + } +} diff --git a/unittest/Functions/UtilFunctionTests.cs b/unittest/Functions/UtilFunctionTests.cs new file mode 100644 index 0000000..c4685cc --- /dev/null +++ b/unittest/Functions/UtilFunctionTests.cs @@ -0,0 +1,557 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SQLSpatialTools.Functions.Util; +using SQLSpatialTools.UnitTests.Extension; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.UnitTests.Functions +{ + [TestClass] + public class UtilFunctionTests + { + [TestMethod] + public void ExtractPointTest() + { + var geom = "POINT(1 1 1)".GetGeom(); + var expected = "POINT(1 1 1)".GetGeom(); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1))); + + try + { + Geometry.ExtractGeometry(geom, 4, 2); + Assert.Fail("Should through exception : Invalid index for element to be extracted."); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for element to be extracted.", ex.Message); + } + + try + { + Geometry.ExtractGeometry(geom, 1, 2); + Assert.Fail("Should through exception : Invalid index for sub-element to be extracted."); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for sub-element to be extracted.", ex.Message); + } + + geom = "MULTIPOINT((1 1 1), (2 2 2), (3 3 3), (4 4 4))".GetGeom(); + expected = geom; + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1))); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1, 0))); + + geom = "MULTIPOINT((1 1 1), (2 2 2), (3 3 3), (4 4 4))".GetGeom(); + expected = "POINT(1 1 1)".GetGeom(); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1, 1))); + + expected = "POINT(3 3 3)".GetGeom(); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1, 3))); + + try + { + Geometry.ExtractGeometry(geom, 2, 3); + Assert.Fail("Should through exception : invalid index for element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for element to be extracted.", ex.Message); + } + + try + { + Geometry.ExtractGeometry(geom, 1, 5); + Assert.Fail("Should through exception : invalid index for sub-element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for sub-element to be extracted.", ex.Message); + } + } + + [TestMethod] + public void ExtractLineStringTest() + { + // LINESTRING + var geom = "LINESTRING(1 1 1, 2 2 2)".GetGeom(); + var expected = "LINESTRING(1 1 1, 2 2 2)".GetGeom(); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1))); + + try + { + Geometry.ExtractGeometry(geom, 2); + Assert.Fail("Should through exception : invalid index for element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for element to be extracted.", ex.Message); + } + + try + { + Geometry.ExtractGeometry(geom, 1, 2); + Assert.Fail("Should through exception : invalid index for sub-element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for sub-element to be extracted.", ex.Message); + } + + // CIRCULARSTRING + geom = "CIRCULARSTRING(1 1, 2 0, -1 1)".GetGeom(); + expected = "CIRCULARSTRING(1 1, 2 0, -1 1)".GetGeom(); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1))); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1, 0))); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1, 1))); + + try + { + Geometry.ExtractGeometry(geom, 2); + Assert.Fail("Should through exception : invalid index for element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for element to be extracted.", ex.Message); + } + + try + { + Geometry.ExtractGeometry(geom, 1, 2); + Assert.Fail("Should through exception : invalid index for sub-element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for sub-element to be extracted.", ex.Message); + } + + // MULTILINESTRING + geom = "MULTILINESTRING((1 1 1, 2 2 2), (3 3 3, 4 4 4))".GetGeom(); + expected = "LINESTRING(1 1 1, 2 2 2)".GetGeom(); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1))); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1, 0))); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1, 1))); + + expected = "LINESTRING(3 3 3, 4 4 4)".GetGeom(); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 2))); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 2, 0))); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 2, 1))); + + try + { + Geometry.ExtractGeometry(geom, 3, 2); + Assert.Fail("Should through exception : invalid index for element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for element to be extracted.", ex.Message); + } + + try + { + Geometry.ExtractGeometry(geom, 1, 2); + Assert.Fail("Should through exception : invalid index for sub-element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for sub-element to be extracted.", ex.Message); + } + } + + [TestMethod] + public void ExtractPolygonTest() + { + // Single Polygon - Sub index is to extract the inner rings + var geom = "POLYGON((-5 -5, -5 5, 5 5, 5 -5, -5 -5), (0 0, 3 0, 3 3, 0 3, 0 0))".GetGeom(); + var expected = "POLYGON((-5 -5, -5 5, 5 5, 5 -5, -5 -5), (0 0, 3 0, 3 3, 0 3, 0 0))".GetGeom(); + var obtainedGeom = Geometry.ExtractGeometry(geom, 1); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + geom = "POLYGON((-5 -5, -5 5, 5 5, 5 -5, -5 -5), (0 0, 3 0, 3 3, 0 3, 0 0))".GetGeom(); + expected = "POLYGON((-5 -5, -5 5, 5 5, 5 -5, -5 -5))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 1); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + geom = "POLYGON((-5 -5, -5 5, 5 5, 5 -5, -5 -5), (0 0, 3 0, 3 3, 0 3, 0 0))".GetGeom(); + expected = "POLYGON((0 0, 0 3, 3 3, 3 0, 0 0))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 2); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + // here for the interior ring; the polygon is rotated; so checking the points + SqlAssert.IsTrue(expected.STPointN(2).STEquals(obtainedGeom.STPointN(2))); + + try + { + Geometry.ExtractGeometry(geom, 2, 3); + Assert.Fail("Should through exception : invalid index for element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for element to be extracted.", ex.Message); + } + + try + { + Geometry.ExtractGeometry(geom, 1, 3); + Assert.Fail("Should through exception : invalid index for sub-element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for sub-element to be extracted.", ex.Message); + } + + // Multi Polygon + geom = "MULTIPOLYGON(((1 1, 1 -1, -1 -1, -1 1, 1 1)),((1 1, 3 1, 3 3, 1 1)))".GetGeom(); + expected = "POLYGON((1 1, 1 -1, -1 -1, -1 1, 1 1))".GetGeom(); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1))); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1, 0))); + + expected = "POLYGON((1 1, 3 1, 3 3, 1 1))".GetGeom(); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 2))); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 2, 0))); + + try + { + Geometry.ExtractGeometry(geom, 3); + Assert.Fail("Should through exception : invalid index for element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for element to be extracted.", ex.Message); + } + + geom = "MULTIPOLYGON(((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 1, 1 1)), ((9 9, 9 10, 10 9, 9 9)))".GetGeom(); + expected = "POLYGON((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 1, 1 1))".GetGeom(); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1))); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1, 0))); + + expected = "POLYGON((0 0, 0 3, 3 3, 3 0, 0 0))".GetGeom(); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1, 1))); + + expected = "POLYGON((1 1, 1 2, 2 1, 1 1))".GetGeom(); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 1, 2))); + + try + { + Geometry.ExtractGeometry(geom, 1, 3); + Assert.Fail("Should through exception : invalid index for sub-element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for sub-element to be extracted.", ex.Message); + } + + expected = "POLYGON((9 9, 9 10, 10 9, 9 9))".GetGeom(); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 2, 0))); + SqlAssert.IsTrue(expected.STEquals(Geometry.ExtractGeometry(geom, 2, 1))); + + try + { + Geometry.ExtractGeometry(geom, 2, 2); + Assert.Fail("Should through exception : invalid index for sub-element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for sub-element to be extracted.", ex.Message); + } + } + + [TestMethod] + public void ExtractCurvePolygonTest() + { + var geom = "CURVEPOLYGON (CIRCULARSTRING (3 3, 4 9, 2 3, 0 0, 3 3), (1 1, 2 2, 2 1, 1 1))".GetGeom(); + var expected = "CURVEPOLYGON (CIRCULARSTRING (3 3, 4 9, 2 3, 0 0, 3 3), (1 1, 2 2, 2 1, 1 1))".GetGeom(); + var obtainedGeom = Geometry.ExtractGeometry(geom, 1); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 0); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + expected = "CURVEPOLYGON (CIRCULARSTRING (3 3, 4 9, 2 3, 0 0, 3 3))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 1); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + expected = "POLYGON ((1 1, 2 1, 2 2, 1 1))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 2); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + // here for the interior ring; the curve polygon is rotated; so checking the points + SqlAssert.IsTrue(expected.STPointN(2).STEquals(obtainedGeom.STPointN(2))); + + geom = "CURVEPOLYGON ((0 1, 0.5 0.5, 1 0, 0.8 0.8, 0 1), CIRCULARSTRING(0.8 0.4, 0.6 0.6, 0.2 0.9, 0.7 0.7, 0.8 0.4))".GetGeom(); + expected = "POLYGON ((0 1, 0.5 0.5, 1 0, 0.8 0.8, 0 1))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 1); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + // here for the exterior ring; the curve polygon shouldn't be rotated; so checking the points + SqlAssert.IsTrue(expected.STPointN(2).STEquals(obtainedGeom.STPointN(2))); + + expected = "CURVEPOLYGON(CIRCULARSTRING(0.8 0.4, 0.7 0.7, 0.2 0.9, 0.6 0.6, 0.8 0.4))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 2); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + geom = "CURVEPOLYGON ((0 1, 0.5 0.5, 1 0, 0.8 0.8, 0 1), (0.8 0.4, 0.6 0.6, 0.2 0.9, 0.7 0.7, 0.8 0.4))".GetGeom(); + expected = "POLYGON ((0 1, 0.5 0.5, 1 0, 0.8 0.8, 0 1))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 1); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + // here for the exterior ring; the curve polygon shouldn't be rotated; so checking the points + SqlAssert.IsTrue(expected.STPointN(2).STEquals(obtainedGeom.STPointN(2))); + + expected = "POLYGON ((0.8 0.4, 0.7 0.7, 0.2 0.9, 0.6 0.6, 0.8 0.4))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 2); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + try + { + Geometry.ExtractGeometry(geom, 2, 0); + Assert.Fail("Should through exception : invalid index for element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for element to be extracted.", ex.Message); + } + + try + { + Geometry.ExtractGeometry(geom, 1, 3); + Assert.Fail("Should through exception : invalid index for sub-element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for sub-element to be extracted.", ex.Message); + } + + geom = "CURVEPOLYGON(COMPOUNDCURVE(CIRCULARSTRING (1 0, 0.7 0.7, 0 1), (0 1, 1 0)))".GetGeom(); + expected = "CURVEPOLYGON(COMPOUNDCURVE(CIRCULARSTRING (1 0, 0.7 0.7, 0 1), (0 1, 1 0)))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 0); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + expected = "COMPOUNDCURVE(CIRCULARSTRING (1 0, 0.7 0.7, 0 1), (0 1, 1 0))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 1); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + try + { + Geometry.ExtractGeometry(geom, 2, 0); + Assert.Fail("Should through exception : invalid index for element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for element to be extracted.", ex.Message); + } + + geom = "CURVEPOLYGON(COMPOUNDCURVE(CIRCULARSTRING(1 3, 3 5, 4 7, 7 3, 1 3)), COMPOUNDCURVE(CIRCULARSTRING(1 3, 3 2, 5 6, 6 3, 1 3)))".GetGeom(); + expected = "CURVEPOLYGON(COMPOUNDCURVE(CIRCULARSTRING(1 3, 3 5, 4 7, 7 3, 1 3)), COMPOUNDCURVE(CIRCULARSTRING(1 3, 3 2, 5 6, 6 3, 1 3)))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 0); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + expected = "COMPOUNDCURVE(CIRCULARSTRING(1 3, 3 5, 4 7, 7 3, 1 3))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 1); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + expected = "COMPOUNDCURVE(CIRCULARSTRING(1 3, 3 2, 5 6, 6 3, 1 3))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 2); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + } + + [TestMethod] + public void ExtractCompoundCurveTest() + { + var geom = "COMPOUNDCURVE(CIRCULARSTRING(1 0, 0 1, -1 0), (-1 0, 2 0))".GetGeom(); + var expected = "COMPOUNDCURVE(CIRCULARSTRING(1 0, 0 1, -1 0), (-1 0, 2 0))".GetGeom(); + var obtainedGeom = Geometry.ExtractGeometry(geom, 1); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 0); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 1); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + try + { + Geometry.ExtractGeometry(geom, 0, 1); + Assert.Fail("Should through exception : invalid index for element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for element to be extracted.", ex.Message); + } + + try + { + Geometry.ExtractGeometry(geom, 1, 3); + Assert.Fail("Should through exception : invalid index for sub-element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for sub-element to be extracted.", ex.Message); + } + } + + + [TestMethod] + public void ExtractGeometryCollectionTest() + { + var geom = "GEOMETRYCOLLECTION(LINESTRING(1 1, 2 2), COMPOUNDCURVE(CIRCULARSTRING(1 0, 0 1, -1 0), (-1 0, 2 0)))".GetGeom(); + var expected = "LINESTRING(1 1, 2 2)".GetGeom(); + var obtainedGeom = Geometry.ExtractGeometry(geom, 1); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 0); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 1); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + expected = "COMPOUNDCURVE(CIRCULARSTRING(1 0, 0 1, -1 0), (-1 0, 2 0))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 2); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + obtainedGeom = Geometry.ExtractGeometry(geom, 2, 0); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + obtainedGeom = Geometry.ExtractGeometry(geom, 2, 1); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + try + { + Geometry.ExtractGeometry(geom, 0, 1); + Assert.Fail("Should through exception : invalid index for element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for element to be extracted.", ex.Message); + } + + try + { + Geometry.ExtractGeometry(geom, 3, 1); + Assert.Fail("Should through exception : invalid index for element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for element to be extracted.", ex.Message); + } + + try + { + Geometry.ExtractGeometry(geom, 1, 2); + Assert.Fail("Should through exception : invalid index for sub-element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for sub-element to be extracted.", ex.Message); + } + + try + { + Geometry.ExtractGeometry(geom, 1, 3); + Assert.Fail("Should through exception : invalid index for sub-element to be extracted"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Invalid index for sub-element to be extracted.", ex.Message); + } + + + geom = "GEOMETRYCOLLECTION(MULTILINESTRING((1 1, 2 2), (4 4, 5 5, 7 7), (8 8, 9 9)), POLYGON((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 1, 1 1)))".GetGeom(); + expected = "LINESTRING(4 4, 5 5, 7 7)".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 2); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + expected = "LINESTRING(8 8, 9 9)".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 1, 3); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + expected = "POLYGON((0 0, 0 3, 3 3, 3 0, 0 0))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 2, 1); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + expected = "POLYGON((1 1, 1 2, 2 1, 1 1))".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 2, 2); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + geom = "GEOMETRYCOLLECTION(MULTILINESTRING((1 1, 2 2), (4 4, 5 5, 7 7), (8 8, 9 9)), POLYGON((0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 1, 1 1)), MULTIPOINT((1 1), (2 2), (4 4)))".GetGeom(); + + expected = "POINT(2 2)".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 3, 2); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + + expected = "POINT(4 4)".GetGeom(); + obtainedGeom = Geometry.ExtractGeometry(geom, 3, 3); + SqlAssert.IsTrue(expected.STEquals(obtainedGeom)); + } + + [TestMethod] + public void RemoveDuplicateVerticesTest() + { + var geometry = "POINT (1 2 3)".GetGeom(); + var expected = "POINT (1 2)".GetGeom(); + var tolerance = 0.5; + var resultantGeom = Geometry.RemoveDuplicateVertices(geometry, tolerance); + SqlAssert.IsTrue(expected.STEquals(resultantGeom)); + + geometry = "MULTIPOINT ((1 2 NULL 3), (5 5 NULL 6))".GetGeom(); + expected = "MULTIPOINT ((1 2), (5 5))".GetGeom(); + tolerance = 0.5; + resultantGeom = Geometry.RemoveDuplicateVertices(geometry, tolerance); + SqlAssert.IsTrue(expected.STEquals(resultantGeom)); + + geometry = "MULTILINESTRING((1 1, 3 2, 3.2 2.2, 3 8), (6 6, 10 10))".GetGeom(); + expected = "MULTILINESTRING ((1 1, 3.2 2.2, 3 8), (6 6, 10 10))".GetGeom(); + tolerance = 0.5; + resultantGeom = Geometry.RemoveDuplicateVertices(geometry, tolerance); + SqlAssert.IsTrue(expected.STEquals(resultantGeom)); + + geometry = "POLYGON ((1 1, 1 5, 8 2, 7.8 1.8, 1 1, 1 1))".GetGeom(); + expected = "POLYGON ((1 1, 1 5, 8 2, 1 1))".GetGeom(); + tolerance = 0.5; + resultantGeom = Geometry.RemoveDuplicateVertices(geometry, tolerance); + SqlAssert.IsTrue(expected.STEquals(resultantGeom)); + + geometry = "MULTIPOLYGON (((1 1, 1 -1, -1 -1, -1 1, 1 1)), ((1 1, 3 1, 3.1 3.2, 3.3 3.5 1 3, 1 1)))".GetGeom(); + expected = "MULTIPOLYGON (((1 1, 1 -1, -1 -1, -1 1, 1 1)), ((1 1, 3 1, 3.3 3.5, 1 1)))".GetGeom(); + tolerance = 0.5; + resultantGeom = Geometry.RemoveDuplicateVertices(geometry, tolerance); + SqlAssert.IsTrue(expected.STEquals(resultantGeom)); + + geometry = "CURVEPOLYGON(CIRCULARSTRING(1 3, 3 5, 4 7, 4.2 7.3, 4.5 7.5, 7 3, 1 3))".GetGeom(); + expected = "CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (1 3, 3 5, 4 7), (4 7, 4.5 7.5), CIRCULARSTRING (4.5 7.5, 7 3, 1 3)))".GetGeom(); + tolerance = 0.5; + resultantGeom = Geometry.RemoveDuplicateVertices(geometry, tolerance); + SqlAssert.IsTrue(expected.STEquals(resultantGeom)); + + geometry = "GEOMETRYCOLLECTION(LINESTRING(1 1,3 5, 3.2 5.1), POINT (2 3 NULL 1), MULTIPOLYGON(((5 5, 5 10, 10 15, 15 15, 15.4 15.4, 15 10, 5 5))))".GetGeom(); + expected = "GEOMETRYCOLLECTION (LINESTRING (1 1, 3.2 5.1), POINT (2 3), POLYGON ((5 5, 5 10, 10 15, 15.4 15.4, 15 10, 5 5)))".GetGeom(); + tolerance = 0.5; + resultantGeom = Geometry.RemoveDuplicateVertices(geometry, tolerance); + SqlAssert.IsTrue(expected.STEquals(resultantGeom)); + + //negative cases + try + { + geometry = "LINESTRING (1 1, 6 6, 3 3, 2 2)".GetGeom(); // linestring overlaps + tolerance = 0.5; + Geometry.RemoveDuplicateVertices(geometry, tolerance); + Assert.Fail("Should throw exception for the invalid overlapping geometry"); + } + catch(Exception ex) + { + Assert.AreEqual(ErrorMessage.InvalidGeometry, ex.Message); + } + + try + { + geometry = "MULTILINESTRING((1 1, 3 3, 12 12), (5 5, 5 5))".GetGeom(); + tolerance = 0.5; + Geometry.RemoveDuplicateVertices(geometry, tolerance); + Assert.Fail("Should throw exception for invalid geometry"); + } + catch (Exception ex) + { + Assert.AreEqual(ErrorMessage.InvalidGeometry, ex.Message); + } + } + } +} diff --git a/unittest/Properties/AssemblyInfo.cs b/unittest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..b701dd3 --- /dev/null +++ b/unittest/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("SQLSpatialTools.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SQLSpatialTools.UnitTests")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("5fcdb549-1b10-4115-9a3f-aa55584fea23")] + +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.3.0")] +[assembly: AssemblyFileVersion("1.0.3.0")] diff --git a/unittest/SQLSpatialTools.UnitTests.csproj b/unittest/SQLSpatialTools.UnitTests.csproj new file mode 100644 index 0000000..9b5b30a --- /dev/null +++ b/unittest/SQLSpatialTools.UnitTests.csproj @@ -0,0 +1,104 @@ + + + + + + Debug + AnyCPU + {5FCDB549-1B10-4115-9A3F-AA55584FEA23} + Library + Properties + SQLSpatialTools.UnitTests + SQLSpatialTools.UnitTests + v4.5 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + true + full + false + ..\output\unittest\ + ..\output\intermediate\unittest + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + ..\output\unittest\ + ..\output\intermediate\unittest + TRACE + prompt + 4 + + + + + + true + + + ..\SpatialTools.pfx + + + + ..\packages\Microsoft.SqlServer.Types.14.0.1016.290\lib\net40\Microsoft.SqlServer.Types.dll + False + + + ..\packages\MSTest.TestFramework.1.4.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + ..\packages\MSTest.TestFramework.1.4.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + + + + + + + + + + + + + Designer + + + + + {09428c16-7dae-4c28-a853-e4f04dd0bedd} + SQLSpatialTools + + + {98EFE72E-E303-4BDE-B86F-5F704DB3F5BE} + SQLSpatialTools.UnitTests.Extension + + + + + + + This project references NuGet package(s) that are missing on this computer. %0AUse NuGet Package Restore to download them. %0AFor more information, see http://go.microsoft.com/fwlink/?LinkID=322105. %0AThe missing file is {0}. + + + + + + + + if exist "$(ProjectDir)obj" rd "$(ProjectDir)obj" /S /Q +exit 0 + + \ No newline at end of file diff --git a/unittest/Utility/UtilityFunctionTests.cs b/unittest/Utility/UtilityFunctionTests.cs new file mode 100644 index 0000000..840666e --- /dev/null +++ b/unittest/Utility/UtilityFunctionTests.cs @@ -0,0 +1,452 @@ +//------------------------------------------------------------------------------ +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using Microsoft.SqlServer.Types; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SQLSpatialTools.Types; +using SQLSpatialTools.UnitTests.Extension; +using SQLSpatialTools.Utility; + +namespace SQLSpatialTools.UnitTests.Utility +{ + [TestClass] + public class UtilityFunctionTests : BaseUnitTest + { + [TestMethod] + public void DimensionalInfoTest() + { + var geom = "POINT EMPTY".GetGeom(); + Assert.AreEqual(DimensionalInfo.None, geom.STGetDimension()); + + geom = "POINT (1 1)".GetGeom(); + Assert.AreEqual(DimensionalInfo.Dim2D, geom.STGetDimension()); + + geom = "POINT (1 1 null 1)".GetGeom(); + Assert.AreEqual(DimensionalInfo.Dim2DWithMeasure, geom.STGetDimension()); + + geom = "POINT (1 1 1)".GetGeom(); + Assert.AreEqual(DimensionalInfo.Dim3D, geom.STGetDimension()); + + geom = "POINT (1 1 1 1)".GetGeom(); + Assert.AreEqual(DimensionalInfo.Dim3DWithMeasure, geom.STGetDimension()); + + Assert.AreEqual(DimensionalInfo.Dim2D.GetString(), "2 Dimensional point, with x and y"); + } + + [TestMethod] + public void IsWithinRangeTest() + { + var geom = "LineString (1 1 NULL -4, 4 4 NULL 4)".GetGeom(); + double measure = -1; + Assert.IsTrue(measure.IsWithinRange(geom)); + + measure = -3; + Assert.IsTrue(measure.IsWithinRange(geom)); + + measure = -4; + Assert.IsTrue(measure.IsWithinRange(geom)); + + measure = 2; + Assert.IsTrue(measure.IsWithinRange(geom)); + + measure = 4; + Assert.IsTrue(measure.IsWithinRange(geom)); + + measure = -5; + Assert.IsFalse(measure.IsWithinRange(geom)); + + measure = -15; + Assert.IsFalse(measure.IsWithinRange(geom)); + + measure = 5; + Assert.IsFalse(measure.IsWithinRange(geom)); + + geom = "LineString (1 1 NULL 4, -4 -4 NULL -4)".GetGeom(); + measure = -1; + Assert.IsTrue(measure.IsWithinRange(geom)); + + measure = -3; + Assert.IsTrue(measure.IsWithinRange(geom)); + + measure = -4; + Assert.IsTrue(measure.IsWithinRange(geom)); + + measure = 2; + Assert.IsTrue(measure.IsWithinRange(geom)); + + measure = 4; + Assert.IsTrue(measure.IsWithinRange(geom)); + + measure = -5; + Assert.IsFalse(measure.IsWithinRange(geom)); + + measure = -15; + Assert.IsFalse(measure.IsWithinRange(geom)); + + measure = 5; + Assert.IsFalse(measure.IsWithinRange(geom)); + + double? m = 10.0; + double? mStart = 11.0; + + Assert.IsFalse(m.IsWithinRange(mStart, null)); + } + + [TestMethod] + public void CheckLRSType() + { + var geom = "GEOMETRYCOLLECTION(POINT(3 3 1), POLYGON((0 0 2, 1 10 3, 1 0 4, 0 0 2)))".GetGeom(); + Assert.IsFalse(geom.IsLRSType()); + + geom = + "GEOMETRYCOLLECTION(LINESTRING(1 1, 3 5), MULTILINESTRING((-1 -1, 1 -5, -5 5), (-5 -1, -1 -1)), POINT(5 6))" + .GetGeom(); + Assert.IsTrue(geom.IsLRSType()); + + geom = "LINESTRING(1 1, 3 5)".GetGeom(); + Assert.IsTrue(geom.IsLRSType()); + + geom = "MULTILINESTRING((-1 -1, 1 -5, -5 5), (-5 -1, -1 -1))".GetGeom(); + Assert.IsTrue(geom.IsLRSType()); + + geom = "POINT(5 6)".GetGeom(); + Assert.IsTrue(geom.IsLRSType()); + + geom = "POLYGON((-1 -1, -1 -5, -5 -5, -5 -1, -1 -1))".GetGeom(); + Assert.IsFalse(geom.IsLRSType()); + + geom = "CIRCULARSTRING(1 1, 2 0, 2 0, 2 0, 1 1)".GetGeom(); + Assert.IsFalse(geom.IsLRSType()); + + geom = "POINT(5 6)".GetGeom(); + Assert.IsTrue(geom.IsOfSupportedTypes(OpenGisGeometryType.Point)); + } + + [TestMethod] + public void LogErrorTest() + { + Logger.LogError(new Exception("TestException")); + Logger.LogError(new Exception("TestException"), "Test"); + Logger.LogError(new Exception("TestException"), "Test : {0}", 1); + Logger.LogError(new Exception("TestException"), "Test : {0}, {1}", "Value", 1); + var innerException = new MyException("InnerException", "Stack trace"); + Logger.LogError(new Exception("TestException", innerException), "Test : {0}, {1}", "Value", 1); + } + + [TestMethod] + public void TrimDecimalPointsTest() + { + const string inputWKT = ""; + var result = inputWKT.TrimDecimalPoints(); + Assert.AreEqual(inputWKT, result); + } + + [TestMethod] + public void LRSMultiLineToSqlTest() + { + // empty LRS Point check + var point1 = new LRSPoint("LINESTRING EMPTY".GetGeom()); + var hashCode = point1.GetHashCode(); + Assert.IsTrue(hashCode > 0); + Assert.AreEqual(point1.ToString(), "POINT (0 0 )"); + + // LRS Point Equality check + point1 = new LRSPoint("POINT(1 1 0 1)".GetGeom()); + var point2 = new LRSPoint("POINT(1 1 0 1)".GetGeom()); + Assert.IsTrue(point1 == point2); + + // range check + Assert.IsTrue(point1.IsXYWithinTolerance(point2, 0.5)); + + point2 = new LRSPoint("POINT(2 2 0 2)".GetGeom()); + var point3 = new LRSPoint("POINT(3 3 0 3)".GetGeom()); + var point4 = new LRSPoint("POINT(4 4 0 4)".GetGeom()); + var lrsPoints = new List {point1, point2, point3}; + + // next and previous point + Assert.AreEqual(LRSPoint.GetNextPoint(ref lrsPoints, point2), point3); + Assert.AreEqual(LRSPoint.GetNextPoint(ref lrsPoints, point4), null); + Assert.AreEqual(LRSPoint.GetPreviousPoint(ref lrsPoints, point2), point1); + Assert.AreEqual(LRSPoint.GetPreviousPoint(ref lrsPoints, point4), null); + + // Multiline + var lrs = new LRSMultiLine(4326); + SqlGeometryBuilder geomBuilder = null; + lrs.ToSqlGeometry(ref geomBuilder); + lrs.BuildSqlGeometry(ref geomBuilder); + + geomBuilder = new SqlGeometryBuilder(); + lrs.ToSqlGeometry(ref geomBuilder); + lrs.BuildSqlGeometry(ref geomBuilder); + + var lrsLine = new LRSLine(4326); + lrsLine.AddPoint(new LRSPoint(1, 1, 0, 1, 4326)); + lrs.AddLine(lrsLine); + + geomBuilder = new SqlGeometryBuilder(); + lrs.ToSqlGeometry(ref geomBuilder); + + var lrsLine1 = new LRSLine(4326); + var wkt = lrs.ToString(); + Assert.AreEqual("MULTILINESTRING EMPTY", wkt); + Assert.AreEqual(lrs.GetPointAtM(10), null); + + var pt = new LRSPoint(1, 1, 0, 1, 4326); + lrsLine1.AddPoint(pt); + Assert.AreEqual(lrsLine1.ToString(), "POINT (1 1 1)"); + + pt = new LRSPoint(2, 2, 0, 2, 4326); + lrsLine1.AddPoint(pt); + + lrs = new LRSMultiLine(4326); + lrs.AddLine(lrsLine1); + + wkt = lrs.ToString(); + Assert.AreEqual("LINESTRING (1 1 1, 2 2 2)", wkt); + + var lrsLine2 = new LRSLine(4326); + + pt = new LRSPoint(3, 3, 0, 3, 4326); + Assert.AreEqual(pt.ToString(), "POINT (3 3 3)"); + lrsLine2.AddPoint(pt); + pt = new LRSPoint(4, 4, 0, 4, 4326); + lrsLine2.AddPoint(pt); + + lrs = new LRSMultiLine(4326); + lrs.AddLine(lrsLine1); + lrs.AddLine(lrsLine2); + geomBuilder = null; + lrs.BuildSqlGeometry(ref geomBuilder); + + Assert.AreEqual(lrs.GetPointAtM(10), null); + Assert.AreEqual(lrs.GetPointAtM(2), new LRSPoint(2, 2, 0, 2, 4326)); + wkt = lrs.ToString(); + lrs.RemoveFirst(); + Assert.AreEqual(lrs.Count, 1); + lrs.RemoveLast(); + Assert.AreEqual(lrs.Count, 0); + lrs.RemoveFirst(); + lrs.RemoveLast(); + Assert.AreEqual("MULTILINESTRING ((1 1 1, 2 2 2), (3 3 3, 4 4 4))", wkt); + } + + private class MyException : Exception + { + public MyException(string message, string stackTrace) : base(message) + { + StackTrace = stackTrace; + } + + public override string StackTrace { get; } + } + + [TestMethod] + public void ThrowIfMeasureIsNotInRangeTest() + { + const int measure = 10; + var startPoint = "POINT(1 2 0)".GetGeom(); + var endPoint = "POINT(2 2 5)".GetGeom(); + try + { + SpatialExtensions.ThrowIfMeasureIsNotInRange(measure, startPoint, endPoint); + } + catch (ArgumentException) + { + + } + } + + [TestMethod] + public void VectorTest() + { + var vec1 = new Vector3(1, 1, 0); + var vec2 = new Vector3(3, 3, 0); + var angle = vec1.AngleInDegrees(vec2); + Assert.AreEqual(angle, 38.942441268981391); + } + + [TestMethod] + public void LRSLineStringTest() + { + var lrsLine = new LRSLine(4326); + var wkt = lrsLine.ToString(); + Assert.AreEqual(wkt, "LINESTRING EMPTY"); + lrsLine.AddPoint(new LRSPoint(1, 1, 0, 1, 4326)); + lrsLine.AddPoint(new LRSPoint(2, 2, null, null, 4326)); + lrsLine.LocatePoint(3); + lrsLine.AddPoint(new LRSPoint(3, 3, 0, 3, 4326), new LRSPoint(4, 4, 0, 4, 4326)); + + var enumerator = lrsLine.GetEnumerator(); + enumerator.MoveNext(); + enumerator.MoveNext(); + enumerator.MoveNext(); + enumerator.MoveNext(); + var currentPoint = enumerator.Current; + Assert.AreEqual(currentPoint, new LRSPoint(4, 4, 0, 4, 4326)); + try + { + enumerator.MoveNext(); + // ReSharper disable once RedundantAssignment + currentPoint = enumerator.Current; + } + catch (InvalidOperationException) + { + enumerator.Reset(); + } + } + + [TestMethod] + public void STLinearMeasureProgressTest() + { + var geom = "LINESTRING EMPTY".GetGeom(); + Assert.AreEqual(geom.STLinearMeasureProgress(), LinearMeasureProgress.None); + + geom = "LINESTRING (1 1 0 1, 2 2 0 5, 3 3 0 3, 4 4 0 2, 5 5 null null)".GetGeom(); + Assert.AreEqual(geom.STLinearMeasureProgress(), LinearMeasureProgress.None); + + geom = "POINT (1 1 1 1)".GetGeom(); + Assert.AreEqual(geom.STLinearMeasureProgress(), LinearMeasureProgress.Increasing); + + geom = "LINESTRING (1 1 0 0, 2 2 0 5, 3 3 0 10)".GetGeom(); + Assert.AreEqual(geom.STLinearMeasureProgress(), LinearMeasureProgress.Increasing); + + geom = "LINESTRING (1 1 0 10, 2 2 0 5, 3 3 0 0)".GetGeom(); + Assert.AreEqual(geom.STLinearMeasureProgress(), LinearMeasureProgress.Decreasing); + + Assert.AreEqual(LinearMeasureProgress.Increasing.Value(), 1); + Assert.AreEqual(LinearMeasureProgress.Decreasing.Value(), 2); + } + + [TestMethod] + public void GetMergeTypeTest() + { + var geom1 = "LINESTRING (1 1 1 1, 2 2 2 2)".GetGeom(); + var geom2 = "POINT (1 1 1 1)".GetGeom(); + + var mergeType = geom1.GetMergeType(geom2); + Assert.AreEqual(MergeInputType.None, mergeType); + } + + [TestMethod] + public void IsNullOrEmptyTest() + { + SqlGeometry geom = null; + // ReSharper disable once ExpressionIsAlwaysNull + Assert.AreEqual(geom.IsNullOrEmpty(), true); + + geom = "LINESTRING EMPTY".GetGeom(); + Assert.AreEqual(geom.IsNullOrEmpty(), true); + } + + [TestMethod] + public void CompareGeomStringTest() + { + var geom = "POINT EMPTY".GetGeom(); + Assert.IsFalse(geom.STGeometryType().Compare(string.Empty)); + } + + [TestMethod] + public void STHasLinearMeasureTest() + { + var geom = "POINT EMPTY".GetGeom(); + Assert.IsFalse(geom.STHasLinearMeasure()); + + geom = "LINESTRING (1 1 0 1, 2 2 0 1)".GetGeom(); + Assert.IsTrue(geom.STHasLinearMeasure()); + + geom = "LINESTRING (1 1, 2 2)".GetGeom(); + Assert.IsFalse(geom.STHasLinearMeasure()); + } + + [TestMethod] + public void ThrowIfNotOfTypeTest() + { + var geom = "POINT EMPTY".GetGeom(); + try + { + SpatialExtensions.ThrowIfNotLineOrMultiLine(geom); + } + catch (ArgumentException) + { + + } + + try + { + SpatialExtensions.ThrowIfNotLine(geom); + } + catch (ArgumentException) + { + + } + + geom = "LINESTRING EMPTY".GetGeom(); + try + { + SpatialExtensions.ThrowIfNotPoint(geom); + } + catch (ArgumentException) + { + + } + + try + { + SpatialExtensions.ThrowException("Test : {0}", "Test Message"); + } + catch (ArgumentException) + { + + } + } + + [TestMethod] + public void IsTolerableTest() + { + const double distance = 0.7; + Assert.IsFalse(distance.IsTolerable(0.05)); + Assert.IsTrue(distance.IsTolerable(0.9)); + } + + [TestMethod] + public void EqualsToTest() + { + const double distance = 0.7; + Assert.IsFalse(distance.EqualsTo(0.05)); + + double? d1 = 0.5; + Assert.IsTrue(d1.EqualsTo(0.5)); + + double? d2 = 0.5; + Assert.IsFalse(distance.EqualsTo(d1)); + + Assert.IsTrue(d1.EqualsTo(d2)); + } + + + [TestMethod] + public void GISTypesTest() + { + var geom = "MULTIPOLYGON(((1 1, 1 -1, -1 -1, -1 1, 1 1)),((1 1, 3 1, 3 3, 1 3, 1 1)))".GetGeom(); + Assert.IsTrue(geom.IsMultiPolygon()); + + geom = "MULTIPOINT((2 3), (7 8 9.5))".GetGeom(); + Assert.IsTrue(geom.IsMultiPoint()); + + geom = "CURVEPOLYGON(CIRCULARSTRING(1 3, 3 5, 4 7, 7 3, 1 3))".GetGeom(); + Assert.IsTrue(geom.IsCurvePolygon()); + + geom = "COMPOUNDCURVE(CIRCULARSTRING(1 1, 1 1, 1 1), (1 1, 3 5, 5 4))".GetGeom(); + Assert.IsTrue(geom.IsCompoundCurve()); + + geom = "CIRCULARSTRING(1 1, 2 0, -1 1)".GetGeom(); + Assert.IsTrue(geom.IsCircularString()); + + geom = "POLYGON((1 1, 3 3, 3 1, 1 1))".GetGeom(); + Assert.IsTrue(geom.IsPolygon()); + } + } +} diff --git a/unittest/app.config b/unittest/app.config new file mode 100644 index 0000000..2fa6e95 --- /dev/null +++ b/unittest/app.config @@ -0,0 +1,3 @@ + + + diff --git a/unittest/packages.config b/unittest/packages.config new file mode 100644 index 0000000..a6f0a50 --- /dev/null +++ b/unittest/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file