geojson 0.0.2 → 1.0.0
raw patch · 21 files changed
+1370/−574 lines, 21 filesdep +aesondep +bytestringdep +lensdep −json
Dependencies added: aeson, bytestring, lens, text
Dependencies removed: json
Files
- README.md +55/−0
- doctests/Main.hs +6/−1
- etc/CHANGELOG.md +9/−0
- etc/LICENCE +0/−0
- etc/LICENCE.md +20/−0
- geojson.cabal +32/−18
- src/Data/Geospatial.hs +75/−145
- src/Data/Geospatial/BasicTypes.hs +60/−0
- src/Data/Geospatial/CRS.hs +137/−0
- src/Data/Geospatial/GeoFeature.hs +203/−0
- src/Data/Geospatial/GeoFeatureCollection.hs +200/−0
- src/Data/Geospatial/GeoPosition.hs +38/−0
- src/Data/Geospatial/Geometry.hs +242/−0
- src/Data/Geospatial/Geometry/Aeson.hs +67/−0
- src/Data/Geospatial/Geometry/GeoLine.hs +37/−0
- src/Data/Geospatial/Geometry/GeoMultiLine.hs +38/−0
- src/Data/Geospatial/Geometry/GeoMultiPoint.hs +38/−0
- src/Data/Geospatial/Geometry/GeoMultiPolygon.hs +38/−0
- src/Data/Geospatial/Geometry/GeoPoint.hs +37/−0
- src/Data/Geospatial/Geometry/GeoPolygon.hs +38/−0
- src/Text/GeoJSON.hs +0/−410
+ README.md view
@@ -0,0 +1,55 @@+# geojson [](https://travis-ci.org/domdere/hs-geojson) [](https://hackage.haskell.org/package/geojson)++A thin GeoJSON Layer above the `aeson` library++## Building the project++Install the dependencies with:++ cabal install --dependencies-only++Optionally add `--enable-tests` if you intend to run the unit tests++The project must be "configured" at least once everytime `geojson.cabal` changes, this can be done with:++ cabal configure++If you wish to run the unit tests you will have to run:++ cabal configure --enable-tests++Then finally build it with:++ cabal build++See `cabal build --help` for more build options.++## Running Unit Tests++**After** running `cabal build`, you can run the unit tests with the command:++ cabal test++## Adding Unit tests++Unit tests are written with [**doctest**] [doctest-github], for instructions on how to add unit tests+see the **doctest** [**User Guide**] [doctest-userguide].++Currently only files in the `src/` directory are searched for tests, it is assumed that the code in `main/`+is a thin layer of code that uses modules from `src/`.++## Development: Cabal Dependency Hell?++Cabal's great, but when you are developing a few different projects with their own dependency chains, sometimes installing all your libraries to the same place causes problems,++Consider trying [`cabal-dev`] [cabal-dev]. In terms of using it, all thats required is replacing `cabal` with `cabal-dev` in all the above command lines.++It will download and install all the dependencies for your project and install them in a `cabal-dev/` directory in your project directory, and they will only be used for this project.++Those with newer versions of `cabal` (`>= 1.18` I think) can skip `cabal-dev` and instead run `cabal sandbox init`, and just start runnign the above instructions+as is from `cabal install --only-dependencies`++[doctest-github]: https://github.com/sol/doctest-haskell "sol/doctest-haskell on GitHub.com"+[doctest-userguide]: https://github.com/sol/doctest-haskell/blob/master/README.markdown#usage "doctest Usage Guide"+[cabal-dev]: https://github.com/creswick/cabal-dev "creswick/cabal-dev on GitHub.com"+
doctests/Main.hs view
@@ -8,6 +8,11 @@ import System.FilePath import Test.DocTest +extensions :: [String]+extensions =+ [ "OverloadedStrings"+ ]+ docopts :: [FilePath] docopts = [ "-isrc"@@ -15,7 +20,7 @@ , "-optP-include" , "-optPdist/build/autogen/cabal_macros.h" , "-hide-all-packages"- ] ++ map ("-package=" ++) deps ++ opts+ ] ++ map ("-package=" ++) deps ++ opts ++ map ("-X" ++) extensions -- the list of all file paths to search for source files sourceDirs :: [FilePath]
+ etc/CHANGELOG.md view
@@ -0,0 +1,9 @@+# 1.0.x++## 0.0.2 -> 2.0.0++- Migrated from `json` to `aeson`, all `JSON` instances were removed and replaced with `ToJSON` and `FromJSON` instances+- `GeoFeature` and `GeoFeatureCollection` are now of kind `* -> *`, parameterised on the Property type, `GeoFeature a` and `GeoFeatureCollection a` are in `ToJSON` and/or `FromJSON` if `a` is in `ToJSON` and/or `FromJSON` respectively.+- Lenses and prisms have been generated for each of the types.+- Orphan Instances were removed. Orphan Instances are bad. Breaking type class coherency had some rather annoying consequences, you only need to import `Data.Geospatial` now and the `ToJSON` and `FromJSON` instances will come with it. I wanted to split up the files and thought seperating the instances from the declarations was a good idea but it wasnt. Instead I split the data types into seperate modules and kept the instances with their respective data type declarations.+
− etc/LICENCE
+ etc/LICENCE.md view
@@ -0,0 +1,20 @@+# BSD 3-Clause License++Copyright (c) 2014, Dom De Re+All rights reserved.++Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.++3. Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,+OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+POSSIBILITY OF SUCH DAMAGE.
geojson.cabal view
@@ -1,12 +1,12 @@ name: geojson-version: 0.0.2-license: MIT-license-file: etc/LICENCE+version: 1.0.0+license: BSD3+license-file: etc/LICENCE.md author: Dom De Re maintainer: Dom De Re-copyright: Copyright (C) 2010-2013+copyright: Copyright (C) 2013-2014 synopsis: A thin GeoJSON Layer above the json library-category: Text+category: Data description: A thin GeoJSON Layer above the json library. . It currently conforms to version 1.0 of the GeoJSON spec which can be found here:@@ -20,10 +20,9 @@ bug-reports: https://github.com/domdere/hs-geojson/issues cabal-version: >= 1.10 build-type: Custom-tested-with: GHC == 7.4.1- , GHC == 7.6.3---extra-source-files: etc/CONTRIBUTORS,--- etc/CREDITS+tested-with: GHC == 7.6.3+extra-source-files: README.md,+ etc/CHANGELOG.md source-repository head type: git@@ -41,7 +40,9 @@ default-language: Haskell2010 build-depends: base < 5 && >= 4- , json+ , aeson == 0.7.*+ , lens == 4.1.*+ , text == 1.1.* ghc-options: -Wall -fno-warn-unused-imports@@ -51,9 +52,22 @@ hs-source-dirs: src - exposed-modules: Text.GeoJSON- , Data.Geospatial+ exposed-modules: Data.Geospatial + other-modules: Data.Geospatial.BasicTypes+ , Data.Geospatial.CRS+ , Data.Geospatial.GeoFeature+ , Data.Geospatial.GeoFeatureCollection+ , Data.Geospatial.Geometry+ , Data.Geospatial.Geometry.GeoMultiLine+ , Data.Geospatial.Geometry.GeoMultiPoint+ , Data.Geospatial.Geometry.GeoMultiPolygon+ , Data.Geospatial.Geometry.GeoLine+ , Data.Geospatial.Geometry.GeoPoint+ , Data.Geospatial.Geometry.GeoPolygon+ , Data.Geospatial.Geometry.Aeson+ , Data.Geospatial.GeoPosition+ test-suite doctests type: exitcode-stdio-1.0@@ -64,12 +78,12 @@ default-language: Haskell2010 - build-depends:- base < 5 && >= 3,- doctest >= 0.9.7,- filepath >= 1.3,- directory >= 1.1,- QuickCheck >= 2.0+ build-depends: base < 5 && >= 3+ , bytestring == 0.10.*+ , doctest >= 0.9.7+ , filepath >= 1.3+ , directory >= 1.1+ , QuickCheck >= 2.0 ghc-options: -Wall
src/Data/Geospatial.hs view
@@ -1,147 +1,77 @@+-------------------------------------------------------------------+-- |+-- Module : Data.Geospatial+-- Copyright : (C) 2014 Dom De Re+-- License : BSD-style (see the file etc/LICENSE.md)+-- Maintainer : Dom De Re+-- -- Refer to the GeoJSON Spec http://www.geojson.org/geojson-spec.html--module Data.Geospatial (- -- Types- Latitude- , Longitude- , Easting- , Northing- , Altitude- , GeoPositionWithoutCRS- , GeoPosition(..)- , GeoPoint(..)- , GeoMultiPoint(..)- , GeoPolygon(..)- , GeoMultiPolygon(..)- , GeoLine(..)- , GeoMultiLine(..)- , GeoPolyLine(..)- , GeospatialGeometry(..)- , Name- , Code- , Href- , FormatString- , ProjectionType- , CRSObject(..)- , FeatureID- , GeoProperty(..)- , GeoPropertyObject- , BoundingBoxWithoutCRS- , GeoFeature(..)- , GeoFeatureCollection(..)- -- Functions- , stripCRSFromPosition- , defaultCRS- ) where--import Text.JSON--type Latitude = Float-type Longitude = Float-type Easting = Float-type Northing = Float-type Altitude = Float---- | ("WithoutCRS" is a catch all for indeterminate CRSs and for expression of positions--- before a CRS has been determined--type GeoPositionWithoutCRS = [Float]---- | see Section 2.1.1 "Position" in the GeoJSON Spec,--- I make the assumption here that the only position types we will use will--- involve easting/northing (+/- Altitude) or lon/lat (+/- Altitude)-data GeoPosition =- LonLat Longitude Latitude- | LonLatAlt Longitude Latitude Altitude- | EastingNorthing Easting Northing- | EastingNorthingAlt Easting Northing Altitude---- | the GeoPosition is a bit special in that when you convert it to GeoJSON,--- it will lose the CRS info attached to it and cannot be read back in--- from the GeoJSON. Hence it is ineligible for the JSON type class,--- so this function will strip it down to a GeoPositionWithoutCRS, which is eligible-stripCRSFromPosition :: GeoPosition -> GeoPositionWithoutCRS-stripCRSFromPosition (LonLat lon lat) = [lon, lat]-stripCRSFromPosition (LonLatAlt lon lat alt) = [lon, lat, alt]-stripCRSFromPosition (EastingNorthing easting northing) = [easting, northing]-stripCRSFromPosition (EastingNorthingAlt easting northing alt) = [easting, northing, alt]---- These are all using newtype so that I can override their JSON instances..--newtype GeoPoint = GeoPoint GeoPositionWithoutCRS deriving (Show, Eq)-newtype GeoMultiPoint = GeoMultiPoint [GeoPoint] deriving (Show, Eq)-newtype GeoPolygon = GeoPolygon [GeoPositionWithoutCRS] deriving (Show, Eq)-newtype GeoMultiPolygon = GeoMultiPolygon [GeoPolygon] deriving (Show, Eq)-newtype GeoLine = GeoLine [GeoPositionWithoutCRS] deriving (Show, Eq)-newtype GeoMultiLine = GeoMultiLine [GeoLine] deriving (Show, Eq)--data GeoPolyLine = Poly GeoPolygon | LineString GeoLine---- | See section 2.1 "Geometry Objects" in the GeoJSON Spec.-data GeospatialGeometry =- NoGeometry- | Point GeoPoint- | MultiPoint GeoMultiPoint- | Polygon GeoPolygon- | MultiPolygon GeoMultiPolygon- | Line GeoLine- | MultiLine GeoMultiLine- | Collection [GeospatialGeometry] deriving (Show, Eq)--type Name = String-type Code = Int-type Href = String-type FormatString = String-type ProjectionType = String---- | See Section 3 "Coordinate Reference System Objects" in the GeoJSON Spec--- "NoCRS" is required because no 'crs' attribute in a GeoJSON feature is NOT the same thing as--- a null 'crs' attribute. no 'crs' value implies the default CRS, while a null CRS means--- you cannot assume a CRS, null will mapped to NoCRS while a non-existent attribute will--- be mapped to a Nothing Maybe value-data CRSObject =- NoCRS- | NamedCRS Name- | EPSG Code- | LinkedCRS Href FormatString deriving (Show, Eq)---- | The default CRS according to Section 3 "Coordinate Reference System Objects" is WGS84 which I believe,--- from http://spatialreference.org/ref/epsg/4326/ -> [JSON](http://spatialreference.org/ref/epsg/4326/json/ "WGS84 in JSON")--- is represented thus:-defaultCRS :: CRSObject-defaultCRS = EPSG 4326--type FeatureID = String--data GeoProperty =- StringProperty String- | FloatProperty Float- | DoubleProperty Double- | IntProperty Int- | PropertyObject GeoPropertyObject deriving (Show, Eq)---- | According to section 2.2 "Feature Objects" of the GeoJSON spec, the "properties" object--- can be any JSON object, or a null value, we may as well just make it a Type Alias--- for JSValue, even though it has a wider scope (Integer, Rational, String etc...)--- to take advantage of the solid JSON code out there to handle generic objects and the null value-type GeoPropertyObject = JSValue---- | See Section 4 "Bounding Boxes" of the GeoJSON spec,--- The length of the list/array must be 2*n where n is the dimensionality of the position type for the CRS--- with min values first followed by the max values, wich both the min/max sets following the same axis order as the CRS,--- e.g for WGS84: minLongitude, minLatitude, maxLongitude, maxLatitude--- The spec mentions that it can be part of a geometry object too but doesnt give an example,--- This implementation will ignore bboxes on Geometry objects, they can be added if required.-type BoundingBoxWithoutCRS = [Float]---- | See Section 2.2 "Feature Objects" of the GeoJSON spec.-data GeoFeature = GeoFeature {- bbox :: Maybe BoundingBoxWithoutCRS,- geometry :: GeospatialGeometry,- properties :: GeoPropertyObject,- featureId :: Maybe FeatureID } deriving (Show, Eq)---- | See Section 2.3 "Feature Collection Objects" of the GeoJSON spec-data GeoFeatureCollection = GeoFeatureCollection (Maybe BoundingBoxWithoutCRS) [GeoFeature] deriving (Show, Eq)-+--+-------------------------------------------------------------------+module Data.Geospatial (+ -- * Types+ Latitude+ , Longitude+ , Easting+ , Northing+ , Altitude+ , GeoPositionWithoutCRS+ , GeoPosition(..)+ , GeoPoint(..)+ , GeoMultiPoint(..)+ , GeoPolygon(..)+ , GeoMultiPolygon(..)+ , GeoLine(..)+ , GeoMultiLine(..)+ , GeospatialGeometry(..)+ , Name+ , Code+ , Href+ , FormatString+ , ProjectionType+ , CRSObject(..)+ , FeatureID+ , BoundingBoxWithoutCRS+ , GeoFeature(..)+ , GeoFeatureCollection(..)+ -- * Functions+ , stripCRSFromPosition+ , defaultCRS+ -- * Lenses+ -- ** Geometry Lenses+ , unGeoPoint+ , unGeoMultiPoint+ , unGeoPolygon+ , unGeoLine+ , unGeoMultiLine+ , unGeoMultiPolygon+ -- ** Feature Lenses+ , bbox+ , geometry+ , properties+ , featureId+ , boundingbox+ , geofeatures+ -- * Prisms+ -- ** Geometry+ , _NoGeometry+ , _Point+ , _MultiPoint+ , _Polygon+ , _MultiPolygon+ , _Line+ , _MultiLine+ , _Collection+ -- ** CRS+ , _NoCRS+ , _NamedCRS+ , _EPSG+ , _LinkedCRS+ ) where +import Data.Geospatial.BasicTypes+import Data.Geospatial.CRS+import Data.Geospatial.GeoFeature+import Data.Geospatial.GeoFeatureCollection+import Data.Geospatial.Geometry+import Data.Geospatial.GeoPosition
+ src/Data/Geospatial/BasicTypes.hs view
@@ -0,0 +1,60 @@+-------------------------------------------------------------------+-- |+-- Module : Data.Geospatial.BasicTypes+-- Copyright : (C) 2014 Dom De Re+-- License : BSD-style (see the file etc/LICENSE.md)+-- Maintainer : Dom De Re+--+-- Basic types for GeoJSON representations.+-------------------------------------------------------------------+module Data.Geospatial.BasicTypes (+ -- * Coordinate types+ Latitude+ , Longitude+ , Easting+ , Northing+ , Altitude+ , GeoPositionWithoutCRS+ -- * CRS Reference types+ , Name+ , Code+ , Href+ , FormatString+ , ProjectionType+ -- * Feature Types+ , BoundingBoxWithoutCRS+ , FeatureID+ ) where++import Data.Text ( Text )++type Latitude = Float+type Longitude = Float+type Easting = Float+type Northing = Float+type Altitude = Float++-- | (`GeoPositionWithoutCRS` is a catch all for indeterminate CRSs and for expression of positions+-- before a CRS has been determined+--+type GeoPositionWithoutCRS = [Float]++type Name = Text+type Code = Int+type Href = Text+type FormatString = Text+type ProjectionType = Text++-- Feature Types++type FeatureID = Text++-- | See Section 4 /Bounding Boxes/ of the GeoJSON spec,+-- The length of the list/array must be 2*n where n is the dimensionality of the position type for the CRS+-- with min values first followed by the max values, wich both the min/max sets following the same axis order as the CRS,+-- e.g for WGS84: minLongitude, minLatitude, maxLongitude, maxLatitude+-- The spec mentions that it can be part of a geometry object too but doesnt give an example,+-- This implementation will ignore bboxes on Geometry objects, they can be added if required.+type BoundingBoxWithoutCRS = [Float]++
+ src/Data/Geospatial/CRS.hs view
@@ -0,0 +1,137 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-------------------------------------------------------------------+-- |+-- Module : Data.Geospatial.CRS+-- Copyright : (C) 2014 Dom De Re+-- License : BSD-style (see the file etc/LICENSE.md)+-- Maintainer : Dom De Re+--+-- See Section 3 /Coordinate Reference System Objects/+-- in the GeoJSON Spec+--+-------------------------------------------------------------------+module Data.Geospatial.CRS (+ -- * Types+ CRSObject(..)+ -- * Functions+ , defaultCRS+ -- * Prisms+ , _NoCRS+ , _NamedCRS+ , _EPSG+ , _LinkedCRS+ ) where++import Data.Geospatial.BasicTypes+import Data.Geospatial.Geometry+import Data.Geospatial.GeoPosition++import Control.Applicative ( (<$>), (<*>) )+import Control.Lens ( makePrisms )+import Control.Monad ( mzero )+import Data.Aeson+ ( FromJSON(..)+ , ToJSON(..)+ , Value(..)+ , Object+ , (.:)+ , (.=)+ , object+ )+import Data.Aeson.Types ( Parser )+import Data.Text ( Text )++-- $setup+--+-- >>> import qualified Data.Aeson as A+-- >>> import qualified Data.ByteString.Lazy.Char8 as BS+--+-- Test CRS Data+-- >>> let testLinkCRSJSON = "{\"type\":\"link\",\"properties\":{\"href\":\"www.google.com.au\",\"type\":\"proj4\"}}"+-- >>> let testLinkCRS = LinkedCRS "www.google.com.au" "proj4"+-- >>> let testEPSGJSON = "{\"type\":\"epsg\",\"properties\":{\"code\":4326}}"+-- >>> let testEPSG = EPSG 4326+-- >>> let testNamedCRSJSON = "{\"type\":\"name\",\"properties\":{\"name\":\"urn:ogc:def:crs:OGC:1.3:CRS84\"}}"+-- >>> let testNamedCRS = NamedCRS "urn:ogc:def:crs:OGC:1.3:CRS84"+--++-- | See Section 3 /Coordinate Reference System Objects/ in the GeoJSON Spec+-- `NoCRS` is required because no 'crs' attribute in a GeoJSON feature is NOT the same thing as+-- a null 'crs' attribute. no 'crs' value implies the default CRS, while a null CRS means+-- you cannot assume a CRS, null will mapped to `NoCRS` while a non-existent attribute will+-- be mapped to a `Nothing` `Maybe` value+data CRSObject =+ NoCRS+ | NamedCRS !Name+ | EPSG Code+ | LinkedCRS !Href !FormatString deriving (Show, Eq)++makePrisms ''CRSObject++-- | The default CRS according to Section 3 /Coordinate Reference System Objects/ is WGS84 which I believe,+-- from <http://spatialreference.org/ref/epsg/4326/> which translates to this in JSON: <http://spatialreference.org/ref/epsg/4326/json/>)+-- is represented thus:+defaultCRS :: CRSObject+defaultCRS = EPSG 4326++-- instances++-- |+-- encode and decodes CRS Objects to and from GeoJSON+--+-- >>> (A.decode . BS.pack) testLinkCRSJSON == Just testLinkCRS+-- True+--+-- >>> (A.decode . BS.pack) testNamedCRSJSON == Just testNamedCRS+-- True+--+-- >>> (A.decode . BS.pack) testEPSGJSON == Just testEPSG+-- True+--+-- Aeson doesnt decode "null" to `Null` unfortunately+--+-- (A.decode . BS.pack) "null" == Just NoCRS+-- True+--+instance FromJSON CRSObject where+ parseJSON Null = return NoCRS+ parseJSON (Object obj) = do+ crsType <- obj .: "type"+ crsObjectFromAeson crsType obj+ parseJSON _ = mzero++-- |+-- encode CRS Objects to GeoJSON+--+-- >>> (A.decode . A.encode) testLinkCRS == Just testLinkCRS+-- True+--+-- >>> (A.decode . A.encode) testNamedCRS == Just testNamedCRS+-- True+--+-- >>> (A.decode . A.encode) testEPSG == Just testEPSG+-- True+--+-- >>> A.encode NoCRS+-- "null"+--+instance ToJSON CRSObject where+ toJSON (NamedCRS name) = object ["type" .= ("name" :: Text), "properties" .= object ["name" .= name]]+ toJSON (EPSG code) = object ["type" .= ("epsg" :: Text), "properties" .= object ["code" .= code]]+ toJSON (LinkedCRS href format) = object ["type" .= ("link" :: Text), "properties" .= object ["href" .= href, "type" .= format]]+ toJSON NoCRS = Null++-- helpers++crsPropertyFromAesonObj :: (FromJSON a) => Text -> Object -> Parser a+crsPropertyFromAesonObj name obj = do+ props <- obj .: "properties"+ props .: name++crsObjectFromAeson :: Text -> Object -> Parser CRSObject+crsObjectFromAeson "name" obj = NamedCRS <$> crsPropertyFromAesonObj "name" obj+crsObjectFromAeson "epsg" obj = EPSG <$> crsPropertyFromAesonObj "code" obj+crsObjectFromAeson "link" obj = LinkedCRS <$> crsPropertyFromAesonObj "href" obj <*> crsPropertyFromAesonObj "type" obj+crsObjectFromAeson _ _ = mzero++
+ src/Data/Geospatial/GeoFeature.hs view
@@ -0,0 +1,203 @@+{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, TemplateHaskell #-}+-------------------------------------------------------------------+-- |+-- Module : Data.Geospatial.GeoFeature+-- Copyright : (C) 2014 Dom De Re+-- License : BSD-style (see the file etc/LICENSE.md)+-- Maintainer : Dom De Re+--+-- See Section 2.2 /Feature Objects/ of the GeoJSON spec.+-- Parameterised on the property type+--+-------------------------------------------------------------------+module Data.Geospatial.GeoFeature (+ -- * Types+ GeoFeature(..)+ -- * Lenses+ , bbox+ , geometry+ , properties+ , featureId+ ) where++import Data.Geospatial.BasicTypes+import Data.Geospatial.Geometry+import Data.Geospatial.Geometry.Aeson+import Data.Geospatial.GeoPosition++import Prelude ( Show, Eq(..), ($) )+import Control.Applicative ( (<$>), (<*>) )+import Control.Lens ( makeLenses )+import Control.Monad ( mzero )+import Data.Aeson ( FromJSON(..), ToJSON(..), Value(..), Object, (.:), (.:?), (.=), object )+import Data.List ( (++) )+import Data.Maybe ( Maybe )+import Data.Text ( Text )++-- $setup+--+-- >>> import qualified Data.Aeson as A+-- >>> import qualified Data.ByteString.Lazy.Char8 as BS+-- >>> import Data.Function ( (.) )+-- >>> import Data.Int ( Int )+-- >>> import Data.Maybe ( Maybe(..) )+-- >>> import Data.String+-- >>> import qualified Data.Text as T+--+-- >>> let decode' = A.decode . BS.pack; decode' :: (FromJSON a) => String -> Maybe a+--+-- Test Bounding Box Data+-- >>> let lshapedPolyVertices = [[120.0, -15.0], [127.0, -15.0], [127.0, -25.0], [124.0, -25.0], [124.0, -18.0], [120.0, -18.0]] :: [GeoPositionWithoutCRS]+-- >>> let emptyVertices = [] :: [GeoPositionWithoutCRS]+-- >>> let testLatLonBBox = [-32, 147.5, -29.5, 151.0] :: BoundingBoxWithoutCRS+-- >>> let testLatLonBBoxJSON = "[-32,147.5,-29.5,151]"+-- >>> let testEmptyBBox = [] :: BoundingBoxWithoutCRS+-- >>> let testEmptyBBoxJSON = "[]"+--+-- End Test Bounding Box Data+-- Test Geometry Data+-- Polys+-- >>> let lShapedPolyJSON = "{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}"+--+-- Upside down L Shaped Poly+--+-- (120, -15) (127, -15)+-- *---------------------------*+-- | |+-- | |+-- | (124, -18) |+-- *---------------* |+-- (120, -18) | |+-- | |+-- | |+-- | |+-- | |+-- | |+-- | |+-- *-----------*+-- (124, -25) (127, -25)+--+-- >>> let lShapedGeoPoly = GeoPolygon lshapedPolyVertices+-- >>> let lShapedPoly = Polygon lShapedGeoPoly+-- >>> let emptyPolyJSON = "{\"type\":\"Polygon\",\"coordinates\":[]}"+-- >>> let emptyGeoPoly = GeoPolygon emptyVertices+-- >>> let emptyPoly = Polygon emptyGeoPoly+--+-- Multi Polys+-- >>> let emptyMultiPolyJSON = "{\"type\":\"MultiPolygon\",\"coordinates\":[]}"+-- >>> let emptyMultiGeoPoly = GeoMultiPolygon []+-- >>> let emptyMultiPoly = MultiPolygon emptyMultiGeoPoly+-- >>> let singlePolyMultiPolyJSON = "{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]}"+-- >>> let singlePolyGeoMultiPoly = GeoMultiPolygon [lShapedGeoPoly]+-- >>> let singlePolyMultiPoly = MultiPolygon singlePolyGeoMultiPoly+-- >>> let multiPolyJSON = "{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]}"+-- >>> let geoMultiPoly = GeoMultiPolygon [lShapedGeoPoly, emptyGeoPoly]+-- >>> let multiPoly = MultiPolygon geoMultiPoly+--+-- Line Data+-- >>> let lShapedLineJSON = "{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}"+-- >>> let lShapedGeoLine = GeoLine lshapedPolyVertices+-- >>> let lShapedLine = Line lShapedGeoLine+-- >>> let emptyLineJSON = "{\"type\":\"Line\",\"coordinates\":[]}"+-- >>> let emptyGeoLine = GeoLine emptyVertices+-- >>> let emptyLine = Line emptyGeoLine+--+-- Multi Lines+-- >>> let emptyMultiLineJSON = "{\"type\":\"MultiLine\",\"coordinates\":[]}"+-- >>> let emptyMultiGeoLine = GeoMultiLine []+-- >>> let emptyMultiLine = MultiLine emptyMultiGeoLine+-- >>> let singleLineMultiLineJSON = "{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]}"+-- >>> let singleLineGeoMultiLine = GeoMultiLine [lShapedGeoLine]+-- >>> let singleLineMultiLine = MultiLine singleLineGeoMultiLine+-- >>> let multiLineJSON = "{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]}"+-- >>> let geoMultiLine = GeoMultiLine [lShapedGeoLine, emptyGeoLine]+-- >>> let multiLine = MultiLine geoMultiLine+-- >>> let emptyCollectionJSON = "{\"type\":\"GeometryCollection\",\"geometries\":[]}"+-- >>> let emptyCollection = Collection []+-- >>> let bigassCollectionJSON = "{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]}"+-- >>> let bigassCollection = Collection [singleLineMultiLine, emptyMultiLine, emptyLine, multiLine, lShapedLine, multiPoly, singlePolyMultiPoly, lShapedPoly, emptyMultiPoly, lShapedPoly]+--+-- End Test Geometry Data+--+-- Test Properties+-- >>> let testProperties = object [(T.pack "depth") .= (5 :: Int), (T.pack "comment") .= (T.pack "Bore run over by dump truck")]+--+-- Test Features+-- >>> let bigFeatureJSON = "{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"}"+-- >>> let bigFeature = GeoFeature (Just testLatLonBBox) bigassCollection testProperties (Just "GW001")+-- >>> let featureWithNoPropertiesJSON = "{\"type\":\"Feature\",\"properties\":null,\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"}"+-- >>> let featureWithNoProperties = let GeoFeature bbox geometry _ featureId = bigFeature in GeoFeature bbox geometry Null featureId+-- >>> let featureWithNoGeometryJSON = "{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":null,\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"}"+-- >>> let featureWithNoGeometry = let GeoFeature bbox _ props featureId = bigFeature in GeoFeature bbox NoGeometry props featureId+-- >>> let featureWithNoIdJSON = "{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151]}"+-- >>> let featureWithNoId = let GeoFeature bbox geometry props _ = bigFeature in GeoFeature bbox geometry props Nothing+-- >>> let featureWithNoBBoxJSON = "{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"id\":\"GW001\"}"+-- >>> let featureWithNoBBox = let GeoFeature _ geometry props featureId = bigFeature in GeoFeature Nothing geometry props featureId+--++-- | See Section 2.2 /Feature Objects/ of the GeoJSON spec.+-- Parameterised on the property type+data GeoFeature a = GeoFeature {+ _bbox :: Maybe BoundingBoxWithoutCRS,+ _geometry :: GeospatialGeometry,+ _properties :: a,+ _featureId :: Maybe FeatureID } deriving (Show, Eq)++makeLenses ''GeoFeature++-- instances++-- | decodes Feature objects to and from GeoJSON+--+-- >>> decode' bigFeatureJSON == Just bigFeature+-- True+--+-- >>> decode' featureWithNoPropertiesJSON == Just featureWithNoProperties+-- True+--+-- >>> decode' featureWithNoIdJSON == Just featureWithNoId+-- True+--+-- >>> decode' featureWithNoBBoxJSON == Just featureWithNoBBox+-- True+--+-- >>> decode' featureWithNoGeometryJSON == Just featureWithNoGeometry+-- True+--+instance (FromJSON a) => FromJSON (GeoFeature a) where+-- parseJSON :: Value -> Parse a+ parseJSON (Object obj) = do+ objType <- obj .: ("type" :: Text)+ if objType /= ("Feature" :: Text)+ then+ mzero+ else+ GeoFeature+ <$> obj .:? ("bbox" :: Text)+ <*> obj .: ("geometry" :: Text)+ <*> obj .: ("properties" :: Text)+ <*> obj .:? ("id" :: Text)+ parseJSON _ = mzero++-- | encodes Feature objects to and from GeoJSON+--+-- >>> (A.decode . A.encode) bigFeature == Just bigFeature+-- True+--+-- >>> (A.decode . A.encode) featureWithNoProperties == Just featureWithNoProperties+-- True+--+-- >>> (A.decode . A.encode) featureWithNoId == Just featureWithNoId+-- True+--+-- >>> (A.decode . A.encode) featureWithNoBBox == Just featureWithNoBBox+-- True+--+-- >>> (A.decode . A.encode) featureWithNoGeometry == Just featureWithNoGeometry+-- True+--+instance (ToJSON a) => ToJSON (GeoFeature a) where+-- toJSON :: a -> Value+ toJSON (GeoFeature bbox' geom props featureId') = object $ baseAttributes ++ optAttributes "bbox" bbox' ++ optAttributes "id" featureId'+ where+ baseAttributes = ["type" .= ("Feature" :: Text), "properties" .= props, "geometry" .= geom]
+ src/Data/Geospatial/GeoFeatureCollection.hs view
@@ -0,0 +1,200 @@+{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, TemplateHaskell #-}+-------------------------------------------------------------------+-- |+-- Module : Data.Geospatial.GeoFeature+-- Copyright : (C) 2014 Dom De Re+-- License : BSD-style (see the file etc/LICENSE.md)+-- Maintainer : Dom De Re+--+-- See Section 2.3 /Feature Collection Objects/ of the GeoJSON spec+--+-------------------------------------------------------------------+module Data.Geospatial.GeoFeatureCollection (+ -- * Types+ GeoFeatureCollection(..)+ -- * Lenses+ , boundingbox+ , geofeatures+ ) where++import Data.Geospatial.BasicTypes+import Data.Geospatial.GeoFeature+import Data.Geospatial.Geometry+import Data.Geospatial.Geometry.Aeson+import Data.Geospatial.GeoPosition++import Prelude ( Show, Eq(..), ($) )+import Control.Applicative ( (<$>), (<*>) )+import Control.Lens ( makeLenses )+import Control.Monad ( mzero, return )+import Data.Aeson ( FromJSON(..), ToJSON(..), Value(..), Object, (.:), (.:?), (.=), object )+import Data.List ( (++) )+import Data.Maybe ( Maybe(..) )+import Data.Text ( Text )++-- $setup+--+-- >>> import qualified Data.Aeson as A+-- >>> import qualified Data.ByteString.Lazy.Char8 as BS+-- >>> import Data.Function ( (.) )+-- >>> import Data.Int ( Int )+-- >>> import Data.Maybe ( Maybe(..) )+-- >>> import Data.String+-- >>> import qualified Data.Text as T+--+-- >>> let decode' = A.decode . BS.pack; decode' :: (FromJSON a) => String -> Maybe a+--+-- Test Bounding Box Data+-- >>> let lshapedPolyVertices = [[120.0, -15.0], [127.0, -15.0], [127.0, -25.0], [124.0, -25.0], [124.0, -18.0], [120.0, -18.0]] :: [GeoPositionWithoutCRS]+-- >>> let emptyVertices = [] :: [GeoPositionWithoutCRS]+-- >>> let testLatLonBBox = [-32, 147.5, -29.5, 151.0] :: BoundingBoxWithoutCRS+-- >>> let testLatLonBBoxJSON = "[-32,147.5,-29.5,151]"+-- >>> let testEmptyBBox = [] :: BoundingBoxWithoutCRS+-- >>> let testEmptyBBoxJSON = "[]"+--+-- End Test Bounding Box Data+-- Test Geometry Data+-- Polys+-- >>> let lShapedPolyJSON = "{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}"+--+-- Upside down L Shaped Poly+--+-- (120, -15) (127, -15)+-- *---------------------------*+-- | |+-- | |+-- | (124, -18) |+-- *---------------* |+-- (120, -18) | |+-- | |+-- | |+-- | |+-- | |+-- | |+-- | |+-- *-----------*+-- (124, -25) (127, -25)+--+-- >>> let lShapedGeoPoly = GeoPolygon lshapedPolyVertices+-- >>> let lShapedPoly = Polygon lShapedGeoPoly+-- >>> let emptyPolyJSON = "{\"type\":\"Polygon\",\"coordinates\":[]}"+-- >>> let emptyGeoPoly = GeoPolygon emptyVertices+-- >>> let emptyPoly = Polygon emptyGeoPoly+--+-- Multi Polys+-- >>> let emptyMultiPolyJSON = "{\"type\":\"MultiPolygon\",\"coordinates\":[]}"+-- >>> let emptyMultiGeoPoly = GeoMultiPolygon []+-- >>> let emptyMultiPoly = MultiPolygon emptyMultiGeoPoly+-- >>> let singlePolyMultiPolyJSON = "{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]}"+-- >>> let singlePolyGeoMultiPoly = GeoMultiPolygon [lShapedGeoPoly]+-- >>> let singlePolyMultiPoly = MultiPolygon singlePolyGeoMultiPoly+-- >>> let multiPolyJSON = "{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]}"+-- >>> let geoMultiPoly = GeoMultiPolygon [lShapedGeoPoly, emptyGeoPoly]+-- >>> let multiPoly = MultiPolygon geoMultiPoly+--+-- Line Data+-- >>> let lShapedLineJSON = "{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}"+-- >>> let lShapedGeoLine = GeoLine lshapedPolyVertices+-- >>> let lShapedLine = Line lShapedGeoLine+-- >>> let emptyLineJSON = "{\"type\":\"Line\",\"coordinates\":[]}"+-- >>> let emptyGeoLine = GeoLine emptyVertices+-- >>> let emptyLine = Line emptyGeoLine+--+-- Multi Lines+-- >>> let emptyMultiLineJSON = "{\"type\":\"MultiLine\",\"coordinates\":[]}"+-- >>> let emptyMultiGeoLine = GeoMultiLine []+-- >>> let emptyMultiLine = MultiLine emptyMultiGeoLine+-- >>> let singleLineMultiLineJSON = "{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]}"+-- >>> let singleLineGeoMultiLine = GeoMultiLine [lShapedGeoLine]+-- >>> let singleLineMultiLine = MultiLine singleLineGeoMultiLine+-- >>> let multiLineJSON = "{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]}"+-- >>> let geoMultiLine = GeoMultiLine [lShapedGeoLine, emptyGeoLine]+-- >>> let multiLine = MultiLine geoMultiLine+-- >>> let emptyCollectionJSON = "{\"type\":\"GeometryCollection\",\"geometries\":[]}"+-- >>> let emptyCollection = Collection []+-- >>> let bigassCollectionJSON = "{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]}"+-- >>> let bigassCollection = Collection [singleLineMultiLine, emptyMultiLine, emptyLine, multiLine, lShapedLine, multiPoly, singlePolyMultiPoly, lShapedPoly, emptyMultiPoly, lShapedPoly]+--+-- End Test Geometry Data+--+-- Test Properties+-- >>> let testProperties = object [(T.pack "depth") .= (5 :: Int), (T.pack "comment") .= (T.pack "Bore run over by dump truck")]+--+-- Test Features+-- >>> let bigFeatureJSON = "{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"}"+-- >>> let bigFeature = GeoFeature (Just testLatLonBBox) bigassCollection testProperties (Just "GW001")+-- >>> let featureWithNoPropertiesJSON = "{\"type\":\"Feature\",\"properties\":null,\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"}"+-- >>> let featureWithNoProperties = let GeoFeature bbox geometry _ featureId = bigFeature in GeoFeature bbox geometry Null featureId+-- >>> let featureWithNoGeometryJSON = "{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":null,\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"}"+-- >>> let featureWithNoGeometry = let GeoFeature bbox _ props featureId = bigFeature in GeoFeature bbox NoGeometry props featureId+-- >>> let featureWithNoIdJSON = "{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151]}"+-- >>> let featureWithNoId = let GeoFeature bbox geometry props _ = bigFeature in GeoFeature bbox geometry props Nothing+-- >>> let featureWithNoBBoxJSON = "{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"id\":\"GW001\"}"+-- >>> let featureWithNoBBox = let GeoFeature _ geometry props featureId = bigFeature in GeoFeature Nothing geometry props featureId+-- >>> let bigAssFeatureCollectionJSON = "{\"bbox\":[-32,147.5,-29.5,151],\"features\":[{\"geometry\":{\"geometries\":[{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"}],\"type\":\"MultiLine\"},{\"coordinates\":[],\"type\":\"MultiLine\"},{\"coordinates\":[],\"type\":\"Line\"},{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"},{\"coordinates\":[],\"type\":\"Line\"}],\"type\":\"MultiLine\"},{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"},{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"},{\"coordinates\":[],\"type\":\"Polygon\"}],\"type\":\"MultiPolygon\"},{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"}],\"type\":\"MultiPolygon\"},{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"},{\"coordinates\":[],\"type\":\"MultiPolygon\"},{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"}],\"type\":\"GeometryCollection\"},\"id\":\"GW001\",\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"}},{\"bbox\":[-32,147.5,-29.5,151],\"geometry\":null,\"id\":\"GW001\",\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"}},{\"geometry\":{\"geometries\":[{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"}],\"type\":\"MultiLine\"},{\"coordinates\":[],\"type\":\"MultiLine\"},{\"coordinates\":[],\"type\":\"Line\"},{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"},{\"coordinates\":[],\"type\":\"Line\"}],\"type\":\"MultiLine\"},{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"},{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"},{\"coordinates\":[],\"type\":\"Polygon\"}],\"type\":\"MultiPolygon\"},{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"}],\"type\":\"MultiPolygon\"},{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"},{\"coordinates\":[],\"type\":\"MultiPolygon\"},{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"}],\"type\":\"GeometryCollection\"},\"id\":\"GW001\",\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"}},{\"bbox\":[-32,147.5,-29.5,151],\"geometry\":{\"geometries\":[{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"}],\"type\":\"MultiLine\"},{\"coordinates\":[],\"type\":\"MultiLine\"},{\"coordinates\":[],\"type\":\"Line\"},{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"},{\"coordinates\":[],\"type\":\"Line\"}],\"type\":\"MultiLine\"},{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"},{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"},{\"coordinates\":[],\"type\":\"Polygon\"}],\"type\":\"MultiPolygon\"},{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"}],\"type\":\"MultiPolygon\"},{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"},{\"coordinates\":[],\"type\":\"MultiPolygon\"},{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"}],\"type\":\"GeometryCollection\"},\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"}},{\"bbox\":[-32,147.5,-29.5,151],\"geometry\":{\"geometries\":[{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"}],\"type\":\"MultiLine\"},{\"coordinates\":[],\"type\":\"MultiLine\"},{\"coordinates\":[],\"type\":\"Line\"},{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"},{\"coordinates\":[],\"type\":\"Line\"}],\"type\":\"MultiLine\"},{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"},{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"},{\"coordinates\":[],\"type\":\"Polygon\"}],\"type\":\"MultiPolygon\"},{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"}],\"type\":\"MultiPolygon\"},{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"},{\"coordinates\":[],\"type\":\"MultiPolygon\"},{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"}],\"type\":\"GeometryCollection\"},\"id\":\"GW001\",\"type\":\"Feature\",\"properties\":null},{\"bbox\":[-32,147.5,-29.5,151],\"geometry\":{\"geometries\":[{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"}],\"type\":\"MultiLine\"},{\"coordinates\":[],\"type\":\"MultiLine\"},{\"coordinates\":[],\"type\":\"Line\"},{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"},{\"coordinates\":[],\"type\":\"Line\"}],\"type\":\"MultiLine\"},{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"},{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"},{\"coordinates\":[],\"type\":\"Polygon\"}],\"type\":\"MultiPolygon\"},{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"}],\"type\":\"MultiPolygon\"},{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"},{\"coordinates\":[],\"type\":\"MultiPolygon\"},{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"}],\"type\":\"GeometryCollection\"},\"id\":\"GW001\",\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"}}],\"type\":\"FeatureCollection\"}"+-- >>> let bigAssFeatureCollection = GeoFeatureCollection (Just testLatLonBBox) [featureWithNoBBox, featureWithNoGeometry, featureWithNoBBox, featureWithNoId, featureWithNoProperties, bigFeature]+-- >>> let bigAssFeatureCollectionWithNoBBoxJSON = "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"id\":\"GW001\"},{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":null,\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"},{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"id\":\"GW001\"},{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151]},{\"type\":\"Feature\",\"properties\":null,\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"},{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"}]}"+-- >>> let bigAssFeatureCollectionWithNoBBox = let GeoFeatureCollection _ features = bigAssFeatureCollection in GeoFeatureCollection Nothing features+-- >>> let emptyFeatureCollectionJSON = "{\"type\":\"FeatureCollection\",\"features\":[]}"+-- >>> let emptyFeatureCollection = GeoFeatureCollection Nothing [] :: GeoFeatureCollection Value+-- >>> let emptyFeatureCollectionWithBBoxJSON = "{\"type\":\"FeatureCollection\",\"features\":[],\"bbox\":[-32,147.5,-29.5,151]}"+-- >>> let emptyFeatureCollectionWithBBox = GeoFeatureCollection (Just testLatLonBBox) []; emptyFeatureCollectionWithBBox :: GeoFeatureCollection Value+--++-- | See Section 2.3 /Feature Collection Objects/ of the GeoJSON spec+--+data GeoFeatureCollection a = GeoFeatureCollection+ { _boundingbox :: Maybe BoundingBoxWithoutCRS+ , _geofeatures :: [GeoFeature a]+ } deriving (Show, Eq)++makeLenses ''GeoFeatureCollection++-- instances++-- | Decodes FeatureCollection objects to and from GeoJSON+--+-- >>> decode' bigAssFeatureCollectionJSON == Just bigAssFeatureCollection+-- True+--+-- >>> decode' bigAssFeatureCollectionWithNoBBoxJSON == Just bigAssFeatureCollectionWithNoBBox+-- True+--+-- >>> decode' emptyFeatureCollectionWithBBoxJSON == Just emptyFeatureCollectionWithBBox+-- True+--+-- >>> decode' emptyFeatureCollectionJSON == Just emptyFeatureCollection+-- True+--+instance (FromJSON a) => FromJSON (GeoFeatureCollection a) where+-- parseJSON :: Value -> Parse a+ parseJSON (Object obj) = do+ objType <- obj .: ("type" :: Text)+ if objType /= ("FeatureCollection" :: Text)+ then+ mzero+ else+ GeoFeatureCollection+ <$> obj .:? ("bbox" :: Text)+ <*> obj .: ("features" :: Text)+ parseJSON _ = mzero++-- | Encodes FeatureCollection objects to and from GeoJSON+--+-- >>> (A.decode . A.encode) bigAssFeatureCollection == Just bigAssFeatureCollection+-- True+--+-- >>> (A.decode . A.encode) bigAssFeatureCollectionWithNoBBox == Just bigAssFeatureCollectionWithNoBBox+-- True+--+-- >>> (A.decode . A.encode) emptyFeatureCollectionWithBBox == Just emptyFeatureCollectionWithBBox+-- True+--+-- >>> (A.decode . A.encode) emptyFeatureCollection == Just emptyFeatureCollection+-- True+--+instance (ToJSON a) => ToJSON (GeoFeatureCollection a) where+-- toJSON :: a -> Value+ toJSON (GeoFeatureCollection bbox' features) = object $ baseAttributes ++ optAttributes "bbox" bbox'+ where+ baseAttributes = ["type" .= ("FeatureCollection" :: Text), "features" .= features]
+ src/Data/Geospatial/GeoPosition.hs view
@@ -0,0 +1,38 @@+-------------------------------------------------------------------+-- |+-- Module : Data.Geospatial.GeoPosition+-- Copyright : (C) 2014 Dom De Re+-- License : BSD-style (see the file etc/LICENSE.md)+-- Maintainer : Dom De Re+--+-- see Section 2.1.1 /Position/ in the GeoJSON Spec+-------------------------------------------------------------------+module Data.Geospatial.GeoPosition (+ -- * Type+ GeoPosition(..)+ -- * Functions+ , stripCRSFromPosition+ ) where++import Data.Geospatial.BasicTypes++-- | see Section 2.1.1 /Position/ in the GeoJSON Spec,+-- I make the assumption here that the only position types we will use will+-- involve easting or northing (+ve or -ve Altitude) or lon or lat (+ve or -ve Altitude)+data GeoPosition =+ LonLat Longitude Latitude+ | LonLatAlt Longitude Latitude Altitude+ | EastingNorthing Easting Northing+ | EastingNorthingAlt Easting Northing Altitude++-- | the `GeoPosition` is a bit special in that when you convert it to GeoJSON,+-- it will lose the CRS info attached to it and cannot be read back in+-- from the GeoJSON. Hence it is ineligible for the `FromJSON` type class,+-- so this function will strip it down to a `GeoPositionWithoutCRS`, which is eligible+stripCRSFromPosition :: GeoPosition -> GeoPositionWithoutCRS+stripCRSFromPosition (LonLat lon lat) = [lon, lat]+stripCRSFromPosition (LonLatAlt lon lat alt) = [lon, lat, alt]+stripCRSFromPosition (EastingNorthing easting northing) = [easting, northing]+stripCRSFromPosition (EastingNorthingAlt easting northing alt) = [easting, northing, alt]++
+ src/Data/Geospatial/Geometry.hs view
@@ -0,0 +1,242 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-------------------------------------------------------------------+-- |+-- Module : Data.Geospatial.Geometry+-- Copyright : (C) 2014 Dom De Re+-- License : BSD-style (see the file etc/LICENSE.md)+-- Maintainer : Dom De Re+--+-- See section 2.1 "Geometry Objects" in the GeoJSON Spec.+--+-------------------------------------------------------------------+module Data.Geospatial.Geometry (+ -- * Types+ GeoPoint(..)+ , GeoMultiPoint(..)+ , GeoPolygon(..)+ , GeoMultiPolygon(..)+ , GeoLine(..)+ , GeoMultiLine(..)+ , GeospatialGeometry(..)+ -- * Lenses+ , unGeoPoint+ , unGeoMultiPoint+ , unGeoPolygon+ , unGeoMultiPolygon+ , unGeoLine+ , unGeoMultiLine+ -- * Prisms+ , _NoGeometry+ , _Point+ , _MultiPoint+ , _Polygon+ , _MultiPolygon+ , _Line+ , _MultiLine+ , _Collection+ ) where++import Data.Geospatial.Geometry.Aeson+import Data.Geospatial.Geometry.GeoLine+import Data.Geospatial.Geometry.GeoMultiLine+import Data.Geospatial.Geometry.GeoMultiPoint+import Data.Geospatial.Geometry.GeoMultiPolygon+import Data.Geospatial.Geometry.GeoPoint+import Data.Geospatial.Geometry.GeoPolygon++import Control.Applicative ( (<$>) )+import Control.Lens ( makePrisms )+import Control.Monad ( mzero )+import Data.Aeson+ ( FromJSON(..)+ , ToJSON(..)+ , Value(..)+ , Object+ , (.:)+ , (.=)+ , object+ )+import Data.Aeson.Types ( Parser )+import Data.Text ( Text )++-- $setup+-- >>> import Data.Geospatial.BasicTypes+--+-- >>> import qualified Data.Aeson as A+-- >>> import qualified Data.ByteString.Lazy.Char8 as BS+--+-- >>> let lshapedPolyVertices = [[120.0, -15.0], [127.0, -15.0], [127.0, -25.0], [124.0, -25.0], [124.0, -18.0], [120.0, -18.0]] :: [GeoPositionWithoutCRS]+-- >>> let emptyVertices = [] :: [GeoPositionWithoutCRS]+--+-- >>> let decode' = A.decode . BS.pack; decode' :: (FromJSON a) => String -> Maybe a+--+-- Test Geometry Data+-- Polys+-- >>> let lShapedPolyJSON = "{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Polygon\"}"+--+-- Upside down L Shaped Poly+--+-- (120, -15) (127, -15)+-- *---------------------------*+-- | |+-- | |+-- | (124, -18) |+-- *---------------* |+-- (120, -18) | |+-- | |+-- | |+-- | |+-- | |+-- | |+-- | |+-- *-----------*+-- (124, -25) (127, -25)+--+-- >>> let lShapedGeoPoly = GeoPolygon lshapedPolyVertices+-- >>> let lShapedPoly = Polygon lShapedGeoPoly+-- >>> let emptyPolyJSON = "{\"coordinates\":[],\"type\":\"Polygon\"}"+-- >>> let emptyGeoPoly = GeoPolygon emptyVertices+-- >>> let emptyPoly = Polygon emptyGeoPoly+--+-- Multi Polys+-- >>> let emptyMultiPolyJSON = "{\"coordinates\":[],\"type\":\"MultiPolygon\"}"+-- >>> let emptyMultiGeoPoly = GeoMultiPolygon []+-- >>> let emptyMultiPoly = MultiPolygon emptyMultiGeoPoly+-- >>> let singlePolyMultiPolyJSON = "{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]}"+-- >>> let singlePolyGeoMultiPoly = GeoMultiPolygon [lShapedGeoPoly]+-- >>> let singlePolyMultiPoly = MultiPolygon singlePolyGeoMultiPoly+-- >>> let multiPolyJSON = "{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]}"+-- >>> let geoMultiPoly = GeoMultiPolygon [lShapedGeoPoly, emptyGeoPoly]+-- >>> let multiPoly = MultiPolygon geoMultiPoly+--+-- Line Data+-- >>> let lShapedLineJSON = "{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}"+-- >>> let lShapedGeoLine = GeoLine lshapedPolyVertices+-- >>> let lShapedLine = Line lShapedGeoLine+-- >>> let emptyLineJSON = "{\"type\":\"Line\",\"coordinates\":[]}"+-- >>> let emptyGeoLine = GeoLine emptyVertices+-- >>> let emptyLine = Line emptyGeoLine+--+-- Multi Lines+-- >>> let emptyMultiLineJSON = "{\"type\":\"MultiLine\",\"coordinates\":[]}"+-- >>> let emptyMultiGeoLine = GeoMultiLine []+-- >>> let emptyMultiLine = MultiLine emptyMultiGeoLine+-- >>> let singleLineMultiLineJSON = "{\"coordinates\":[{\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]],\"type\":\"Line\"}],\"type\":\"MultiLine\"}"+-- >>> let singleLineGeoMultiLine = GeoMultiLine [lShapedGeoLine]+-- >>> let singleLineMultiLine = MultiLine singleLineGeoMultiLine+-- >>> let multiLineJSON = "{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]}"+-- >>> let geoMultiLine = GeoMultiLine [lShapedGeoLine, emptyGeoLine]+-- >>> let multiLine = MultiLine geoMultiLine+-- >>> let emptyCollectionJSON = "{\"type\":\"GeometryCollection\",\"geometries\":[]}"+-- >>> let emptyCollection = Collection []+-- >>> let bigassCollectionJSON = "{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]}"+-- >>> let bigassCollection = Collection [singleLineMultiLine, emptyMultiLine, emptyLine, multiLine, lShapedLine, multiPoly, singlePolyMultiPoly, lShapedPoly, emptyMultiPoly, lShapedPoly]+--+-- End Test Geometry Data+--+--++-- | See section 2.1 /Geometry Objects/ in the GeoJSON Spec.+data GeospatialGeometry =+ NoGeometry+ | Point GeoPoint+ | MultiPoint GeoMultiPoint+ | Polygon GeoPolygon+ | MultiPolygon GeoMultiPolygon+ | Line GeoLine+ | MultiLine GeoMultiLine+ | Collection [GeospatialGeometry] deriving (Show, Eq)++makePrisms ''GeospatialGeometry++geometryFromAeson :: String -> Value -> Parser GeospatialGeometry+geometryFromAeson "Point" obj = Point <$> parseJSON obj+geometryFromAeson "MultiPoint" obj = MultiPoint <$> parseJSON obj+geometryFromAeson "Polygon" obj = Polygon <$> parseJSON obj+geometryFromAeson "MultiPolygon" obj = MultiPolygon <$> parseJSON obj+geometryFromAeson "Line" obj = Line <$> parseJSON obj+geometryFromAeson "MultiLine" obj = MultiLine <$> parseJSON obj+geometryFromAeson "GeometryCollection" (Object jsonObj) = Collection <$> (jsonObj .: ("geometries" :: Text))+geometryFromAeson "GeometryCollection" _ = mzero+geometryFromAeson _ _ = mzero+++-- |+-- encodes and Geometry Objects to GeoJSON+-- (refer to source to see the values for the test values)+--+-- >>> A.encode NoGeometry+-- "null"+--+-- >>> (A.decode . A.encode) lShapedPoly == Just lShapedPoly+-- True+--+-- >>> (A.decode . A.encode) emptyPoly == Just emptyPoly+-- True+--+-- >>> (A.decode . A.encode) emptyMultiPoly == Just emptyMultiPoly+-- True+--+-- >>> (A.decode . A.encode) singleLineMultiLine == Just singleLineMultiLine+-- True+--+-- >>> (A.decode . A.encode) multiLine == Just multiLine+-- True+--+-- >>> (A.decode . A.encode) emptyCollection == Just emptyCollection+-- True+--+-- >>> (A.decode . A.encode) bigassCollection == Just bigassCollection+-- True+--+instance ToJSON GeospatialGeometry where+-- toJSON :: a -> Value+ toJSON NoGeometry = Null+ toJSON (Point point) = toJSON point+ toJSON (MultiPoint points) = toJSON points+ toJSON (Polygon vertices) = toJSON vertices+ toJSON (MultiPolygon vertices) = toJSON vertices+ toJSON (Line vertices) = toJSON vertices+ toJSON (MultiLine vertices) = toJSON vertices+ toJSON (Collection geometries) = object+ [ "type" .= ("GeometryCollection" :: Text)+ , "geometries" .= geometries+ ]++-- |+-- encodes and decodes Geometry Objects to and from GeoJSON+-- (refer to source to see the values for the test values)+--+-- >>> decode' lShapedPolyJSON == Just lShapedPoly+-- True+--+-- >>> decode' emptyPolyJSON == Just emptyPoly+-- True+--+-- >>> decode' emptyMultiPolyJSON == Just emptyMultiPoly+-- True+--+-- >>> decode' singleLineMultiLineJSON == Just singleLineMultiLine+-- True+--+-- >>> decode' multiLineJSON == Just multiLine+-- True+--+-- >>> decode' emptyCollectionJSON == Just emptyCollection+-- True+--+-- >>> decode' bigassCollectionJSON == Just bigassCollection+-- True+--+-- Aeson doesnt decode "null" into `Null` unfortunately+--+-- decode' "null" :: Maybe GeospatialGeometry+-- Just NoGeometry+--+instance FromJSON GeospatialGeometry where+-- parseJSON :: Value -> Parser a+ parseJSON Null = return NoGeometry+ parseJSON (Object obj) = do+ geometryType <- obj .: ("type" :: Text)+ geometryFromAeson geometryType (Object obj)+ parseJSON _ = mzero
+ src/Data/Geospatial/Geometry/Aeson.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE OverloadedStrings #-}+-------------------------------------------------------------------+-- |+-- Module : Data.Geosptial.Geometry.Aeson+-- Copyright : (C) 2014 Dom De Re+-- License : BSD-style (see the file etc/LICENSE.md)+-- Maintainer : Dom De Re+--+-- Some helpers for some of the common Aeson ops+-------------------------------------------------------------------+module Data.Geospatial.Geometry.Aeson (+ -- * Geometry+ readGeometryGeoAeson+ , makeGeometryGeoAeson+ -- * Optional fields+ , optValFromObj+ , optAttributes+ ) where++import Control.Applicative ( (<$>) )+import Control.Monad ( mzero )+import Data.Aeson+ ( FromJSON(..)+ , ToJSON(..)+ , Object+ , Value+ , (.:)+ , (.:?)+ , (.=)+ , object+ )+import Data.Aeson.Types ( Pair, Parser )+import Data.Maybe ( Maybe(..) )+import Data.Text ( Text )++-- | A generic function that can be used to read in the GeoJSON for:+-- `GeoPoint`, `GeoMultiPoint`, `GeoLine`, `GeoMultiLine`, `GeoPolygon` and `GeoMultiPolygon`+-- Takes in a String for the GeoJSON geometry type, the type constructor+-- for the datatype and the JSON object containing both the 'type' val and the 'coordinates' val+--+readGeometryGeoAeson :: (FromJSON a, FromJSON b) => String -> (a -> b) -> Object -> Parser b+readGeometryGeoAeson geomTypeString geomType geopointObj = do+ geometryType <- geopointObj .: "type"+ if geometryType == geomTypeString+ then+ geomType <$> geopointObj .: "coordinates"+ else+ mzero++-- | The inverse to the above, you just give it the type string and the value for the coordinates+-- and it will create the JSON object+--+makeGeometryGeoAeson :: (ToJSON a) => String -> a -> Value+makeGeometryGeoAeson typeString coordinates =+ object ["type" .= typeString, "coordinates" .= coordinates]++-- | get an optional value out of a JSON object:+--+optValFromObj :: (FromJSON a) => Text -> Object -> Parser (Maybe a)+optValFromObj = flip (.:?)++-- | The other way around, given an optional value, will return the attributes that+-- should be added to the makeObj input+--+optAttributes :: (ToJSON a) => Text -> Maybe a -> [Pair]+optAttributes _ Nothing = []+optAttributes name (Just x) = [name .= x]
+ src/Data/Geospatial/Geometry/GeoLine.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE TemplateHaskell #-}+-------------------------------------------------------------------+-- |+-- Module : Data.Geospatial.Geometry.GeoLine+-- Copyright : (C) 2014 Dom De Re+-- License : BSD-style (see the file etc/LICENSE.md)+-- Maintainer : Dom De Re+--+-------------------------------------------------------------------+module Data.Geospatial.Geometry.GeoLine (+ -- * Type+ GeoLine(..)+ -- * Lenses+ , unGeoLine+ ) where++import Data.Geospatial.BasicTypes+import Data.Geospatial.Geometry.Aeson++import Control.Lens ( makeLenses )+import Control.Monad ( mzero )+import Data.Aeson ( FromJSON(..), ToJSON(..), Value(..), Object )++newtype GeoLine = GeoLine { _unGeoLine :: [GeoPositionWithoutCRS] } deriving (Show, Eq)++makeLenses ''GeoLine++-- instances++instance ToJSON GeoLine where+-- toJSON :: a -> Value+ toJSON = makeGeometryGeoAeson "Line" . _unGeoLine++instance FromJSON GeoLine where+-- parseJSON :: Value -> Parser a+ parseJSON (Object o) = readGeometryGeoAeson "Line" GeoLine o+ parseJSON _ = mzero
+ src/Data/Geospatial/Geometry/GeoMultiLine.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE TemplateHaskell #-}+-------------------------------------------------------------------+-- |+-- Module : Data.Geospatial.Geometry.GeoMultiLine+-- Copyright : (C) 2014 Dom De Re+-- License : BSD-style (see the file etc/LICENSE.md)+-- Maintainer : Dom De Re+--+-------------------------------------------------------------------+module Data.Geospatial.Geometry.GeoMultiLine (+ -- * Type+ GeoMultiLine(..)+ -- * Lenses+ , unGeoMultiLine+ ) where++import Data.Geospatial.BasicTypes+import Data.Geospatial.Geometry.GeoLine+import Data.Geospatial.Geometry.Aeson++import Control.Lens ( makeLenses )+import Control.Monad ( mzero )+import Data.Aeson ( FromJSON(..), ToJSON(..), Value(..), Object )++newtype GeoMultiLine = GeoMultiLine { _unGeoMultiLine :: [GeoLine] } deriving (Show, Eq)++makeLenses ''GeoMultiLine++-- instances++instance ToJSON GeoMultiLine where+-- toJSON :: a -> Value+ toJSON = makeGeometryGeoAeson "MultiLine" . _unGeoMultiLine++instance FromJSON GeoMultiLine where+-- parseJSON :: Value -> Parser a+ parseJSON (Object o) = readGeometryGeoAeson "MultiLine" GeoMultiLine o+ parseJSON _ = mzero
+ src/Data/Geospatial/Geometry/GeoMultiPoint.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE TemplateHaskell #-}+-------------------------------------------------------------------+-- |+-- Module : Data.Geospatial.Geometry.GeoMultiPoint+-- Copyright : (C) 2014 Dom De Re+-- License : BSD-style (see the file etc/LICENSE.md)+-- Maintainer : Dom De Re+--+-------------------------------------------------------------------+module Data.Geospatial.Geometry.GeoMultiPoint (+ -- * Type+ GeoMultiPoint(..)+ -- * Lenses+ , unGeoMultiPoint+ ) where++import Data.Geospatial.BasicTypes+import Data.Geospatial.Geometry.GeoPoint+import Data.Geospatial.Geometry.Aeson++import Control.Lens ( makeLenses )+import Control.Monad ( mzero )+import Data.Aeson ( FromJSON(..), ToJSON(..), Value(..), Object )++newtype GeoMultiPoint = GeoMultiPoint { _unGeoMultiPoint :: [GeoPoint] } deriving (Show, Eq)++makeLenses ''GeoMultiPoint++-- instances++instance ToJSON GeoMultiPoint where+-- toJSON :: a -> Value+ toJSON = makeGeometryGeoAeson "MultiPoint" . _unGeoMultiPoint++instance FromJSON GeoMultiPoint where+-- parseJSON :: Value -> Parser a+ parseJSON (Object o) = readGeometryGeoAeson "MultiPoint" GeoMultiPoint o+ parseJSON _ = mzero
+ src/Data/Geospatial/Geometry/GeoMultiPolygon.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE TemplateHaskell #-}+-------------------------------------------------------------------+-- |+-- Module : Data.Geospatial.Geometry.GeoMultiPolygon+-- Copyright : (C) 2014 Dom De Re+-- License : BSD-style (see the file etc/LICENSE.md)+-- Maintainer : Dom De Re+--+-------------------------------------------------------------------+module Data.Geospatial.Geometry.GeoMultiPolygon (+ -- * Type+ GeoMultiPolygon(..)+ -- * Lenses+ , unGeoMultiPolygon+ ) where++import Data.Geospatial.BasicTypes+import Data.Geospatial.Geometry.GeoPolygon+import Data.Geospatial.Geometry.Aeson++import Control.Lens ( makeLenses )+import Control.Monad ( mzero )+import Data.Aeson ( FromJSON(..), ToJSON(..), Value(..), Object )++newtype GeoMultiPolygon = GeoMultiPolygon { _unGeoMultiPolygon :: [GeoPolygon] } deriving (Show, Eq)++makeLenses ''GeoMultiPolygon++-- instances++instance ToJSON GeoMultiPolygon where+-- toJSON :: a -> Value+ toJSON = makeGeometryGeoAeson "MultiPolygon" . _unGeoMultiPolygon++instance FromJSON GeoMultiPolygon where+-- parseJSON :: Value -> Parser a+ parseJSON (Object o) = readGeometryGeoAeson "MultiPolygon" GeoMultiPolygon o+ parseJSON _ = mzero
+ src/Data/Geospatial/Geometry/GeoPoint.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE TemplateHaskell #-}+-------------------------------------------------------------------+-- |+-- Module : Data.Geospatial.Geometry.GeoPoint+-- Copyright : (C) 2014 Dom De Re+-- License : BSD-style (see the file etc/LICENSE.md)+-- Maintainer : Dom De Re+--+-------------------------------------------------------------------+module Data.Geospatial.Geometry.GeoPoint (+ -- * Type+ GeoPoint(..)+ -- * Lenses+ , unGeoPoint+ ) where++import Data.Geospatial.BasicTypes+import Data.Geospatial.Geometry.Aeson++import Control.Lens ( makeLenses )+import Control.Monad ( mzero )+import Data.Aeson ( FromJSON(..), ToJSON(..), Value(..), Object )++newtype GeoPoint = GeoPoint { _unGeoPoint :: GeoPositionWithoutCRS } deriving (Show, Eq)++makeLenses ''GeoPoint++-- instances++instance ToJSON GeoPoint where+-- toJSON :: a -> Value+ toJSON = makeGeometryGeoAeson "Point" . _unGeoPoint++instance FromJSON GeoPoint where+-- parseJSON :: Value -> Parser a+ parseJSON (Object o) = readGeometryGeoAeson "Point" GeoPoint o+ parseJSON _ = mzero
+ src/Data/Geospatial/Geometry/GeoPolygon.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE TemplateHaskell #-}+-------------------------------------------------------------------+-- |+-- Module : Data.Geospatial.Geometry.GeoPolygon+-- Copyright : (C) 2014 Dom De Re+-- License : BSD-style (see the file etc/LICENSE.md)+-- Maintainer : Dom De Re+--+-------------------------------------------------------------------+module Data.Geospatial.Geometry.GeoPolygon (+ -- * Type+ GeoPolygon(..)+ -- * Lenses+ , unGeoPolygon+ ) where++import Data.Geospatial.BasicTypes+import Data.Geospatial.Geometry.Aeson+import Data.Geospatial.GeoPosition++import Control.Lens ( makeLenses )+import Control.Monad ( mzero )+import Data.Aeson ( FromJSON(..), ToJSON(..), Value(..), Object )++newtype GeoPolygon = GeoPolygon { _unGeoPolygon :: [GeoPositionWithoutCRS] } deriving (Show, Eq)++makeLenses ''GeoPolygon++-- instances++instance ToJSON GeoPolygon where+-- toJSON :: a -> Value+ toJSON = makeGeometryGeoAeson "Polygon" . _unGeoPolygon++instance FromJSON GeoPolygon where+-- parseJSON :: Value -> Parser a+ parseJSON (Object o) = readGeometryGeoAeson "Polygon" GeoPolygon o+ parseJSON _ = mzero
− src/Text/GeoJSON.hs
@@ -1,410 +0,0 @@-{-# OPTIONS_GHC -fno-warn-orphans #-}--- Only export the JSON instances-module Text.GeoJSON () where--import Control.Applicative-import Text.JSON--import Data.Geospatial---- | This module only exports the JSON Instances for the types from Data.Geospatial.--- .--- Hence you will have to refer to the `Data.Geospatial` documentation to find the documentation--- related to the Instances contained in this module.---- $setup--- Test Bounding Box Data--- >>> let lshapedPolyVertices = [[120.0, -15.0], [127.0, -15.0], [127.0, -25.0], [124.0, -25.0], [124.0, -18.0], [120.0, -18.0]] :: [GeoPositionWithoutCRS]--- >>> let emptyVertices = [] :: [GeoPositionWithoutCRS]--- >>> let testLatLonBBox = [-32, 147.5, -29.5, 151.0] :: BoundingBoxWithoutCRS--- >>> let testLatLonBBoxJSON = "[-32,147.5,-29.5,151]"--- >>> let testEmptyBBox = [] :: BoundingBoxWithoutCRS--- >>> let testEmptyBBoxJSON = "[]"------ End Test Bounding Box Data--- Test Geometry Data--- Polys--- >>> let lShapedPolyJSON = "{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}"------ Upside down L Shaped Poly------ (120, -15) (127, -15)--- *---------------------------*--- | |--- | |--- | (124, -18) |--- *---------------* |--- (120, -18) | |--- | |--- | |--- | |--- | |--- | |--- | |--- *-----------*--- (124, -25) (127, -25)------ >>> let lShapedGeoPoly = GeoPolygon lshapedPolyVertices--- >>> let lShapedPoly = Polygon lShapedGeoPoly--- >>> let emptyPolyJSON = "{\"type\":\"Polygon\",\"coordinates\":[]}"--- >>> let emptyGeoPoly = GeoPolygon emptyVertices--- >>> let emptyPoly = Polygon emptyGeoPoly------ Multi Polys--- >>> let emptyMultiPolyJSON = "{\"type\":\"MultiPolygon\",\"coordinates\":[]}"--- >>> let emptyMultiGeoPoly = GeoMultiPolygon []--- >>> let emptyMultiPoly = MultiPolygon emptyMultiGeoPoly--- >>> let singlePolyMultiPolyJSON = "{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]}"--- >>> let singlePolyGeoMultiPoly = GeoMultiPolygon [lShapedGeoPoly]--- >>> let singlePolyMultiPoly = MultiPolygon singlePolyGeoMultiPoly--- >>> let multiPolyJSON = "{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]}"--- >>> let geoMultiPoly = GeoMultiPolygon [lShapedGeoPoly, emptyGeoPoly]--- >>> let multiPoly = MultiPolygon geoMultiPoly------ Line Data--- >>> let lShapedLineJSON = "{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}"--- >>> let lShapedGeoLine = GeoLine lshapedPolyVertices--- >>> let lShapedLine = Line lShapedGeoLine--- >>> let emptyLineJSON = "{\"type\":\"Line\",\"coordinates\":[]}"--- >>> let emptyGeoLine = GeoLine emptyVertices--- >>> let emptyLine = Line emptyGeoLine------ Multi Lines--- >>> let emptyMultiLineJSON = "{\"type\":\"MultiLine\",\"coordinates\":[]}"--- >>> let emptyMultiGeoLine = GeoMultiLine []--- >>> let emptyMultiLine = MultiLine emptyMultiGeoLine--- >>> let singleLineMultiLineJSON = "{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]}"--- >>> let singleLineGeoMultiLine = GeoMultiLine [lShapedGeoLine]--- >>> let singleLineMultiLine = MultiLine singleLineGeoMultiLine--- >>> let multiLineJSON = "{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]}"--- >>> let geoMultiLine = GeoMultiLine [lShapedGeoLine, emptyGeoLine]--- >>> let multiLine = MultiLine geoMultiLine--- >>> let emptyCollectionJSON = "{\"type\":\"GeometryCollection\",\"geometries\":[]}"--- >>> let emptyCollection = Collection []--- >>> let bigassCollectionJSON = "{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]}"--- >>> let bigassCollection = Collection [singleLineMultiLine, emptyMultiLine, emptyLine, multiLine, lShapedLine, multiPoly, singlePolyMultiPoly, lShapedPoly, emptyMultiPoly, lShapedPoly]------ End Test Geometry Data------ Test CRS Data--- >>> let testLinkCRSJSON = "{\"type\":\"link\",\"properties\":{\"href\":\"www.google.com.au\",\"type\":\"proj4\"}}"--- >>> let testLinkCRS = LinkedCRS "www.google.com.au" "proj4"--- >>> let testEPSGJSON = "{\"type\":\"epsg\",\"properties\":{\"code\":4326}}"--- >>> let testEPSG = EPSG 4326--- >>> let testNamedCRSJSON = "{\"type\":\"name\",\"properties\":{\"name\":\"urn:ogc:def:crs:OGC:1.3:CRS84\"}}"--- >>> let testNamedCRS = NamedCRS "urn:ogc:def:crs:OGC:1.3:CRS84"------ Test Properties--- >>> let testProperties = makeObj [("depth", showJSON (5 :: Int)), ("comment", showJSON "Bore run over by dump truck")]------ Test Features--- >>> let bigFeatureJSON = "{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"}"--- >>> let bigFeature = GeoFeature (Just testLatLonBBox) bigassCollection testProperties (Just "GW001")--- >>> let featureWithNoPropertiesJSON = "{\"type\":\"Feature\",\"properties\":null,\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"}"--- >>> let featureWithNoProperties = let GeoFeature bbox geometry _ featureId = bigFeature in GeoFeature bbox geometry JSNull featureId--- >>> let featureWithNoGeometryJSON = "{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":null,\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"}"--- >>> let featureWithNoGeometry = let GeoFeature bbox _ props featureId = bigFeature in GeoFeature bbox NoGeometry props featureId--- >>> let featureWithNoIdJSON = "{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151]}"--- >>> let featureWithNoId = let GeoFeature bbox geometry props _ = bigFeature in GeoFeature bbox geometry props Nothing--- >>> let featureWithNoBBoxJSON = "{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"id\":\"GW001\"}"--- >>> let featureWithNoBBox = let GeoFeature _ geometry props featureId = bigFeature in GeoFeature Nothing geometry props featureId--- >>> let bigAssFeatureCollectionJSON = "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"id\":\"GW001\"},{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":null,\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"},{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"id\":\"GW001\"},{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151]},{\"type\":\"Feature\",\"properties\":null,\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"},{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"}],\"bbox\":[-32,147.5,-29.5,151]}"--- >>> let bigAssFeatureCollection = GeoFeatureCollection (Just testLatLonBBox) [featureWithNoBBox, featureWithNoGeometry, featureWithNoBBox, featureWithNoId, featureWithNoProperties, bigFeature]--- >>> let bigAssFeatureCollectionWithNoBBoxJSON = "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"id\":\"GW001\"},{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":null,\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"},{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"id\":\"GW001\"},{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151]},{\"type\":\"Feature\",\"properties\":null,\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"},{\"type\":\"Feature\",\"properties\":{\"depth\":5,\"comment\":\"Bore run over by dump truck\"},\"geometry\":{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"MultiLine\",\"coordinates\":[]},{\"type\":\"Line\",\"coordinates\":[]},{\"type\":\"MultiLine\",\"coordinates\":[{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Line\",\"coordinates\":[]}]},{\"type\":\"Line\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"Polygon\",\"coordinates\":[]}]},{\"type\":\"MultiPolygon\",\"coordinates\":[{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]},{\"type\":\"MultiPolygon\",\"coordinates\":[]},{\"type\":\"Polygon\",\"coordinates\":[[120,-15],[127,-15],[127,-25],[124,-25],[124,-18],[120,-18]]}]},\"bbox\":[-32,147.5,-29.5,151],\"id\":\"GW001\"}]}"--- >>> let bigAssFeatureCollectionWithNoBBox = let GeoFeatureCollection _ features = bigAssFeatureCollection in GeoFeatureCollection Nothing features--- >>> let emptyFeatureCollectionJSON = "{\"type\":\"FeatureCollection\",\"features\":[]}"--- >>> let emptyFeatureCollection = GeoFeatureCollection Nothing []--- >>> let emptyFeatureCollectionWithBBoxJSON = "{\"type\":\"FeatureCollection\",\"features\":[],\"bbox\":[-32,147.5,-29.5,151]}"--- >>> let emptyFeatureCollectionWithBBox = GeoFeatureCollection (Just testLatLonBBox) []--------- helper functions:---- | A generic function that can be used to read in the GeoJSON for:--- GeoPoint, GeoMultiPoint, GeoLine, GeoMultiLine, GeoPolygon and GeoMultiPolygon--- Takes in a String for the GeoJSON geometry type, the type constructor--- for the datatype and the JSON object containing both the 'type' val and the 'coordinates' val-readGeometryGeoJSON :: (JSON a, JSON b) => String -> (a -> b) -> JSValue -> Result b -readGeometryGeoJSON geomTypeString geomType json = do- geopointObj <- readJSON json- geometryType <- valFromObj "type" geopointObj- if geometryType == geomTypeString- then- geomType <$> valFromObj "coordinates" geopointObj- else- fail $ "Invalid Geometry Type: " ++ geometryType---- | The inverse to the above, you just give it the type string and the value for the coordinates--- and it will create the JSON object-makeGeometryGeoJSON :: (JSON a) => String -> a -> JSValue-makeGeometryGeoJSON typeString coordinates = - makeObj [("type", showJSON typeString), ("coordinates", showJSON coordinates)]--geometryFromJSON :: String -> JSValue -> Result GeospatialGeometry-geometryFromJSON "Point" obj = Point <$> readJSON obj-geometryFromJSON "MultiPoint" obj = MultiPoint <$> readJSON obj-geometryFromJSON "Polygon" obj = Polygon <$> readJSON obj-geometryFromJSON "MultiPolygon" obj = MultiPolygon <$> readJSON obj-geometryFromJSON "Line" obj = Line <$> readJSON obj-geometryFromJSON "MultiLine" obj = MultiLine <$> readJSON obj-geometryFromJSON "GeometryCollection" (JSObject jsonObj) = Collection <$> (valFromObj "geometries" jsonObj >>= readJSON)-geometryFromJSON "GeometryCollection" _ = Error "Invalid value type for 'geometries' attribute.."-geometryFromJSON typeString _ = Error $ "Invalid Geometry Type: " ++ typeString--crsPropertyFromObj :: (JSON a) => String -> JSObject JSValue -> Result a-crsPropertyFromObj name obj = do- props <- valFromObj "properties" obj- valFromObj name props--crsObjectFromJSON :: String -> JSObject JSValue -> Result CRSObject-crsObjectFromJSON "name" obj = NamedCRS <$> crsPropertyFromObj "name" obj-crsObjectFromJSON "epsg" obj = EPSG <$> crsPropertyFromObj "code" obj-crsObjectFromJSON "link" obj = LinkedCRS <$> crsPropertyFromObj "href" obj <*> crsPropertyFromObj "type" obj-crsObjectFromJSON typeString _ = Error $ "Invalid or unimplemented CRS Object Type: " ++ typeString---- | get an optional value out of a JSON object:-optValFromObj :: (JSON a) => String -> JSObject JSValue -> Result (Maybe a)-optValFromObj attribute object = resultToMaybe $ valFromObj attribute object- where- resultToMaybe (Ok x) = Ok $ Just x- resultToMaybe (Error _) = Ok Nothing---- | The other way around, given an optional value, will return the attributes that--- should be added to the makeObj input-optAttributes :: (JSON a) => String -> Maybe a -> [(String, JSValue)]-optAttributes _ Nothing = []-optAttributes name (Just x) = [(name, showJSON x)]--- end helper functions---- Conversion of Geospatial data types into GeoJSON,--- We do this by inducting all the types from Data.Geospatial.Types into--- the JSON typeclass--instance JSON GeoPoint where- readJSON = readGeometryGeoJSON "Point" GeoPoint-- showJSON (GeoPoint point) = makeGeometryGeoJSON "Point" point--instance JSON GeoMultiPoint where- readJSON = readGeometryGeoJSON "MultiPoint" GeoMultiPoint-- showJSON (GeoMultiPoint points) = makeGeometryGeoJSON "MultiPoint" points--instance JSON GeoPolygon where- readJSON = readGeometryGeoJSON "Polygon" GeoPolygon-- showJSON (GeoPolygon vertices) = makeGeometryGeoJSON "Polygon" vertices--instance JSON GeoMultiPolygon where- readJSON = readGeometryGeoJSON "MultiPolygon" GeoMultiPolygon-- showJSON (GeoMultiPolygon polys) = makeGeometryGeoJSON "MultiPolygon" polys--instance JSON GeoLine where- readJSON = readGeometryGeoJSON "Line" GeoLine-- showJSON (GeoLine line) = makeGeometryGeoJSON "Line" line--instance JSON GeoMultiLine where- readJSON = readGeometryGeoJSON "MultiLine" GeoMultiLine-- showJSON (GeoMultiLine lines') = makeGeometryGeoJSON "MultiLine" lines'---- |--- encodes and decodes Geometry Objects to and from GeoJSON--- (refer to source to see the values for the test values)------ >>> encode NoGeometry--- "null"------ >>> decode "null" :: Result GeospatialGeometry--- Ok NoGeometry------ >>> encode lShapedPoly == lShapedPolyJSON--- True------ >>> decode lShapedPolyJSON == Ok lShapedPoly--- True------ >>> encode emptyPoly == emptyPolyJSON--- True------ >>> decode emptyPolyJSON == Ok emptyPoly--- True------ >>> encode emptyMultiPoly == emptyMultiPolyJSON--- True------ >>> decode emptyMultiPolyJSON == Ok emptyMultiPoly--- True------ >>> encode singleLineMultiLine == singleLineMultiLineJSON--- True------ >>> decode singleLineMultiLineJSON == Ok singleLineMultiLine--- True------ >>> encode multiLine == multiLineJSON--- True------ >>> decode multiLineJSON == Ok multiLine--- True------ >>> encode emptyCollection == emptyCollectionJSON--- True------ >>> decode emptyCollectionJSON == Ok emptyCollection--- True------ >>> encode bigassCollection == bigassCollectionJSON--- True------ >>> decode bigassCollectionJSON == Ok bigassCollection--- True----instance JSON GeospatialGeometry where- readJSON JSNull = Ok NoGeometry- readJSON json = do- geometryObj <- readJSON json- geometryType <- valFromObj "type" geometryObj- geometryFromJSON geometryType (JSObject geometryObj)-- showJSON (NoGeometry) = JSNull- showJSON (Point point) = showJSON point- showJSON (MultiPoint points) = showJSON points- showJSON (Polygon vertices) = showJSON vertices- showJSON (MultiPolygon vertices) = showJSON vertices- showJSON (Line vertices) = showJSON vertices- showJSON (MultiLine vertices) = showJSON vertices- showJSON (Collection geometries) = makeObj [("type", showJSON "GeometryCollection"), ("geometries", showJSON geometries)]---- |--- encode and decodes CRS Objects to and from GeoJSON------ >>> encode testLinkCRS == testLinkCRSJSON--- True------ >>> decode testLinkCRSJSON == Ok testLinkCRS--- True------ >>> encode testNamedCRS == testNamedCRSJSON--- True------ >>> decode testNamedCRSJSON == Ok testNamedCRS--- True------ >>> encode testEPSG == testEPSGJSON--- True------ >>> decode testEPSGJSON == Ok testEPSG--- True------ >>> encode NoCRS--- "null"------ >>> decode "null" == Ok NoCRS--- True----instance JSON CRSObject where- readJSON JSNull = Ok NoCRS- readJSON json = do- crsObject <- readJSON json- crsType <- valFromObj "type" crsObject- crsObjectFromJSON crsType crsObject-- showJSON (NamedCRS name) = makeObj [("type", showJSON "name"), ("properties", showJSON (makeObj [("name", showJSON name)]))]- showJSON (EPSG code) = makeObj [("type", showJSON "epsg"), ("properties", showJSON (makeObj [("code", showJSON code)]))]- showJSON (LinkedCRS href format ) = makeObj [("type", showJSON "link"), ("properties", showJSON (makeObj [("href", showJSON href), ("type", showJSON format)]))]- showJSON NoCRS = JSNull---- | encodes and decodes Feature objects to and from GeoJSON------ >>> encode bigFeature == bigFeatureJSON--- True------ >>> decode bigFeatureJSON == Ok bigFeature--- True------ >>> encode featureWithNoProperties == featureWithNoPropertiesJSON--- True------ >>> decode featureWithNoPropertiesJSON == Ok featureWithNoProperties--- True------ >>> encode featureWithNoId == featureWithNoIdJSON--- True------ >>> decode featureWithNoIdJSON == Ok featureWithNoId--- True------ >>> encode featureWithNoBBox == featureWithNoBBoxJSON--- True------ >>> decode featureWithNoBBoxJSON == Ok featureWithNoBBox--- True------ >>> encode featureWithNoGeometry == featureWithNoGeometryJSON--- True------ >>> decode featureWithNoGeometryJSON == Ok featureWithNoGeometry--- True----instance JSON GeoFeature where- readJSON json = do- obj <- readJSON json- objType <- valFromObj "type" obj- if objType /= "Feature"- then- fail $ "Invalid GeoJSON type for a Feature: " ++ objType- else- GeoFeature <$> optValFromObj "bbox" obj- <*> valFromObj "geometry" obj- <*> valFromObj "properties" obj- <*> optValFromObj "id" obj-- showJSON (GeoFeature bbox' geom props featureId') = makeObj $ baseAttributes ++ optAttributes "bbox" bbox' ++ optAttributes "id" featureId'- where- baseAttributes = [("type", showJSON "Feature"), ("properties", showJSON props), ("geometry", showJSON geom)]---- | Encodes and Decodes FeatureCollection objects to and from GeoJSON------ >>> encode bigAssFeatureCollection == bigAssFeatureCollectionJSON--- True------ >>> decode bigAssFeatureCollectionJSON == Ok bigAssFeatureCollection--- True------ >>> encode bigAssFeatureCollectionWithNoBBox == bigAssFeatureCollectionWithNoBBoxJSON--- True------ >>> decode bigAssFeatureCollectionWithNoBBoxJSON == Ok bigAssFeatureCollectionWithNoBBox--- True------ >>> encode emptyFeatureCollectionWithBBox == emptyFeatureCollectionWithBBoxJSON--- True------ >>> decode emptyFeatureCollectionWithBBoxJSON == Ok emptyFeatureCollectionWithBBox--- True------ >>> encode emptyFeatureCollection == emptyFeatureCollectionJSON--- True------ >>> decode emptyFeatureCollectionJSON == Ok emptyFeatureCollection--- True----instance JSON GeoFeatureCollection where- readJSON json = do- obj <- readJSON json- objType <- valFromObj "type" obj- if objType /= "FeatureCollection"- then- fail $ "Invalid GeoJSON type for a Feature Collection: " ++ objType- else- GeoFeatureCollection <$> optValFromObj "bbox" obj <*> valFromObj "features" obj-- showJSON (GeoFeatureCollection bbox' features) = makeObj $ baseAttributes ++ optAttributes "bbox" bbox'- where- baseAttributes = [("type", showJSON "FeatureCollection"), ("features", showJSON features)]-