geojson (empty) → 0.0.1
raw patch · 6 files changed
+746/−0 lines, 6 filesdep +QuickCheckdep +basedep +directorybuild-type:Customsetup-changed
Dependencies added: QuickCheck, base, directory, doctest, filepath, json
Files
- Setup.hs +75/−0
- doctests/Main.hs +40/−0
- etc/LICENCE +0/−0
- geojson.cabal +79/−0
- src/Data/Geospatial.hs +147/−0
- src/Text/GeoJSON.hs +405/−0
+ Setup.hs view
@@ -0,0 +1,75 @@+#!/usr/bin/env runhaskell+{-# OPTIONS_GHC -Wall #-}+module Main (main) where++import Data.List ( nub )+import Data.Version ( showVersion )+import Control.Applicative+import Distribution.Package ( PackageName(PackageName), PackageId, InstalledPackageId, packageVersion, packageName )+import Distribution.PackageDescription ( PackageDescription(), TestSuite(..) )+import Distribution.Simple ( defaultMainWithHooks, UserHooks(..), simpleUserHooks )+import Distribution.Simple.Utils ( rewriteFile, createDirectoryIfMissingVerbose, findProgramVersion, currentDir )+import Distribution.Simple.BuildPaths ( autogenModulesDir )+import Distribution.Simple.Setup ( BuildFlags(buildVerbosity), fromFlag )+import Distribution.Simple.LocalBuildInfo ( withLibLBI, withTestLBI, LocalBuildInfo(), ComponentLocalBuildInfo(componentPackageDeps) )+import Distribution.Verbosity ( Verbosity )+import Distribution.Version ( Version )+import System.Directory ( getDirectoryContents )+import System.FilePath ( (</>) )++main :: IO ()+main = defaultMainWithHooks simpleUserHooks+ { buildHook = \pkg lbi hooks flags -> do+ generateBuildModule (fromFlag (buildVerbosity flags)) pkg lbi+ buildHook simpleUserHooks pkg lbi hooks flags+ }++generateBuildModule :: Verbosity -> PackageDescription -> LocalBuildInfo -> IO ()+generateBuildModule verbosity pkg lbi = do+ let dir = autogenModulesDir lbi+ createDirectoryIfMissingVerbose verbosity True dir+ withLibLBI pkg lbi $ \_ libcfg ->+ withTestLBI pkg lbi $ \suite suitecfg -> do+ ghcOpts <-+ generateCabalDevOpts+ <$> isCabalDevPresent+ <*> getGhcVersion verbosity+ rewriteFile (dir </> "Build_" ++ testName suite ++ ".hs") $ unlines+ [ "module Build_" ++ testName suite ++ " where"+ , "deps :: [String]"+ , "deps = " ++ show (formatdeps (testDeps libcfg suitecfg))+ , "opts :: [String]"+ , "opts = " ++ show ghcOpts+ ]+ where+ formatdeps = map (formatone . snd)+ formatone p = case packageName p of+ PackageName n -> n ++ "-" ++ showVersion (packageVersion p)++isCabalDevPresent :: IO Bool+--isCabalDevPresent = (not . null) <$> matchFileGlob "cabal-dev/"+isCabalDevPresent = do+ contents <- getDirectoryContents currentDir+ return $ "cabal-dev" `elem` contents++getGhcVersion :: Verbosity -> IO (Maybe Version)+getGhcVersion verb = findProgramVersion+ "--version"+ (last . words)+ verb+ "ghc"++generateCabalDevOpts :: Bool -> Maybe Version -> [String]+generateCabalDevOpts isCabalDev version =+ case version of+ Nothing -> []+ Just version' ->+ let+ baseOpts = [ "-Lcabal-dev/lib"+ , "-package-conf=cabal-dev/packages-" ++ showVersion version' ++ ".conf"+ ]+ in+ if isCabalDev then baseOpts else []++testDeps :: ComponentLocalBuildInfo -> ComponentLocalBuildInfo -> [(InstalledPackageId, PackageId)]+testDeps xs ys = nub $ componentPackageDeps xs ++ componentPackageDeps ys
+ doctests/Main.hs view
@@ -0,0 +1,40 @@+module Main where++import Build_doctests (deps, opts)+import Control.Applicative+import Control.Monad+import Data.List+import System.Directory+import System.FilePath+import Test.DocTest++docopts :: [FilePath]+docopts =+ [ "-isrc"+ , "-idist/build/autogen"+ , "-optP-include"+ , "-optPdist/build/autogen/cabal_macros.h"+ , "-hide-all-packages"+ ] ++ map ("-package=" ++) deps ++ opts++-- the list of all file paths to search for source files+sourceDirs :: [FilePath]+sourceDirs = ["src"]++main :: IO ()+main = getSources >>= \sources -> doctest $ docopts ++ sources++getFilesAndDirectories :: FilePath -> IO ([FilePath], [FilePath])+getFilesAndDirectories dir = do+ c <- map (dir </>) . filter (`notElem` ["..", "."]) <$> getDirectoryContents dir+ (,) <$> filterM doesDirectoryExist c <*> filterM doesFileExist c++isSourceFile :: FilePath -> Bool+isSourceFile p = (takeFileName p /= "Setup.hs") && (".hs" `isSuffixOf` p)++getSources :: IO [FilePath]+getSources = liftM (filter isSourceFile . concat) (mapM go sourceDirs)+ where+ go dir = do+ (dirs, files) <- getFilesAndDirectories dir+ (files ++) . concat <$> mapM go dirs
+ etc/LICENCE view
+ geojson.cabal view
@@ -0,0 +1,79 @@+name: geojson+version: 0.0.1+license: MIT+license-file: etc/LICENCE+author: Dom De Re+maintainer: Dom De Re+copyright: Copyright (C) 2010-2013+synopsis: A thin GeoJSON Layer above the json library+category: Text+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:+ .+ <http://geojson.org/geojson-spec.html>+ .+ Its built on top of the `json` library and doesn't currently take+ advantage of laziness as much as it could, so those are areas for improvement++homepage: https://github.com/domdere/hs-geojson/issues+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++source-repository head+ type: git+ location: https://github.com/domdere/hs-geojson.git++source-repository this+ type: git+ location: https://github.com/domdere/hs-geojson.git+ tag: 0.0.1++flag small_base+ description: Choose the new, split-up base package.++library+ default-language: Haskell2010++ build-depends: base < 5 && >= 4+ , json++ ghc-options: -Wall+ -fno-warn-unused-imports+ -fno-warn-unused-binds+ -fno-warn-unused-do-bind+ -fno-warn-type-defaults++ hs-source-dirs: src++ exposed-modules: Text.GeoJSON+ , Data.Geospatial++test-suite doctests+ type:+ exitcode-stdio-1.0++ main-is:+ Main.hs++ default-language:+ Haskell2010++ build-depends:+ base < 5 && >= 3,+ doctest >= 0.9.7,+ filepath >= 1.3,+ directory >= 1.1,+ QuickCheck >= 2.0++ ghc-options:+ -Wall+ -threaded++ hs-source-dirs:+ doctests
+ src/Data/Geospatial.hs view
@@ -0,0 +1,147 @@+-- 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)++
+ src/Text/GeoJSON.hs view
@@ -0,0 +1,405 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+-- Only export the JSON instances+module Text.GeoJSON () where++import Control.Applicative+import Text.JSON++import Data.Geospatial++-- $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)]+