-- 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)