How does one return and accept a geolocation type in GraphQL? Something that has a latitude
and longitude
?
Naive Implementation
The naive approach is to create a location type with a latitude and longitude.
Types::LocationType = GraphQL::ObjectType.define do
name 'Location'
description 'A geo location.'
field :latitude, !types.Float, 'Lat.'
field :longitude, !types.Float, 'Lon.'
end
This can be returned as field :location, Types::LocationType
in any GraphQL ObjectType
.
To accept a location in a mutation we create an input type with the same properties.
Types::InputLocationType = GraphQL::InputObjectType.define do
name 'InputLocation'
description 'A geo location.'
argument :latitude, !types.Float, 'Lat.'
argument :longitude, !types.Float, 'Lon.'
end
A query can ask for a location.
location {
latitude
longitude
}
This needs to be wired to our MongoDB storage. I’m using mongoid-geospatial and saving locations as a Point. The latter can accept latitude and logitude but then stores these as x
and y
. The location type specifies property: :x
and property :y
where appropriate, which is used when converting a Point
into a LocationType
, while the mutation converts input to a hash that is fed into Point.new
automatically.
You can see this code in 33-minutes-server@c1fb00, in which I confused :x
and :y
coordinates of Point
. This was fixed in 33-minutes-server@9fc79d.
Custom GraphQL Scalar Type
The naive approach requires separate input and output types and the knowledge of a location internals (latitude, longitude). Lets elevate a location to a first class scalar type, similar to how dates and times are implemented and call this geo coordinates. I’m using geo_coord that can parse and format geo coordinates according to multiple existing conventions, such as pairs of latitude and longitude or a combination of degrees, minutes and seconds.
Types::GeoCoordinates = GraphQL::ScalarType.define do
name 'GeoCoordinates'
description 'Geo coordinate, latitude followed by longitude.'
coerce_input ->(value, _ctx) { Geo::Coord.parse(value) }
coerce_result ->(value, _ctx) { value.to_s }
end
We need to implement Point#to_s
used above and convert a Geo::Coord
to a Hash
with latitude and longitude in our mutations to wire this up with mongoid-geospatial.
module Mongoid
module Geospatial
class Point
def to_s
Geo::Coord.new(y, x).to_s
end
end
end
end
Note that Point
seems to implement support for latitude and longitude backwards. This is mongoid-geospatial#61.
This is implemented in 33-minutes-server@28f309.
Location Formats
The values passed back-and-forth are much more readable now and our API accepts standard notations (eg. 50.004444, 36.231389
or 50° 0′ 16″ N, 36° 13′ 53″ E
) and returns a location
as a string (eg. 50° 0′ 16″ N, 36° 13′ 53″ E
). This allows for seamless future improvements in the location formats, but burdens the client to parse a complex response.
The first attempt at easing this is to return a location in the simpler lat,lon
format.
Types::GeoCoordinates = GraphQL::ScalarType.define do
name 'GeoCoordinates'
coerce_input ->(value, _ctx) { Geo::Coord.parse(value) }
coerce_result ->(value, _ctx) { [value.y, value.x].map(&:to_s).join(',') }
end
We can do better and just return an array, as well as allow multiple kinds of location inputs.
Types::GeoCoordinates = GraphQL::ScalarType.define do
name 'GeoCoordinates'
coerce_input ->(value, _ctx) {
case value
when Array
Geo::Coord.new(value[0], value[1])
else
Geo::Coord.parse(value)
end
}
coerce_result ->(value, _ctx) { [value.y, value.x] }
end
This is implemented in 33-minutes-server@098ff1 and 33-minutes-server@cc159b.