defmodule Geo.WKB do alias Geo.Point alias Geo.LineString alias Geo.Polygon alias Geo.MultiPoint alias Geo.MultiLineString alias Geo.MultiPolygon alias Geo.GeometryCollection alias Geo.WKB.Reader alias Geo.WKB.Writer alias Geo.Utils use Bitwise @moduledoc """ Converts to and from WKB and EWKB point = Geo.WKB.decode("0101000000000000000000F03F000000000000F03F") Geo.Point[coordinates: {1, 1}, srid: nil] Geo.WKT.encode(point) "POINT(1 1)" point = Geo.WKB.decode("0101000020E61000009EFB613A637B4240CF2C0950D3735EC0") Geo.Point[coordinates: {36.9639657, -121.8097725}, srid: 4326] """ @doc """ Takes a Geometry and returns a WKB string. The endian decides what the byte order will be """ @spec encode(binary, Geo.endian) :: binary def encode(geom, endian \\ :xdr) do writer = Writer.start(endian) do_encode(geom, writer) end defp do_encode(%GeometryCollection{} = geom, writer) do type = Utils.type_to_hex(geom, geom.srid != nil) |> Integer.to_string(16) |> Utils.pad_left(8) srid = if geom.srid, do: Integer.to_string(geom.srid, 16) |>Utils.pad_left(8), else: "" count = Integer.to_string(Enum.count(geom.geometries), 16) |>Utils.pad_left(8) writer = Writer.write(writer, type) writer = Writer.write(writer, srid) writer = Writer.write(writer, count) coordinates = Enum.map(geom.geometries, fn(x) -> x = %{x | srid: nil} encode(x, Writer.get_endian(writer)) end) coordinates = Enum.join(coordinates) Writer.get_wkb(writer) <> coordinates end defp do_encode(geom, writer) do type = Utils.type_to_hex(geom, geom.srid != nil) |> Integer.to_string(16) |> Utils.pad_left(8) srid = if geom.srid, do: Integer.to_string(geom.srid, 16) |>Utils.pad_left(8), else: "" writer = Writer.write(writer, type) writer = Writer.write(writer, srid) writer = encode_coordinates(writer, geom) Writer.get_wkb(writer) end defp encode_coordinates(writer, %Point{coordinates: {0, 0}}) do Writer.write(writer,Utils.repeat("0", 32)) end defp encode_coordinates(writer, %Point{coordinates: {x, y}}) do x = x |> Utils.float_to_hex(64) |> Integer.to_string(16) y = y |> Utils.float_to_hex(64) |> Integer.to_string(16) writer = Writer.write(writer, x) Writer.write(writer, y) end defp encode_coordinates(writer, %LineString{coordinates: coordinates}) do number_of_points = Integer.to_string(length(coordinates), 16) |> Utils.pad_left(8) writer = Writer.write(writer, number_of_points) {_nils, writer} = Enum.map_reduce(coordinates, writer, fn(pair, acc) -> acc = encode_coordinates(acc, %Point{ coordinates: pair }) {nil, acc} end) writer end defp encode_coordinates(writer, %Polygon{coordinates: coordinates}) do number_of_lines = Integer.to_string(length(coordinates), 16) |> Utils.pad_left(8) writer = Writer.write(writer, number_of_lines) {_nils, writer} = Enum.map_reduce(coordinates, writer, fn(line, acc) -> acc = encode_coordinates(acc, %LineString{ coordinates: line }) {nil, acc} end) writer end defp encode_coordinates(writer, %MultiPoint{coordinates: coordinates}) do writer = Writer.write(writer, Integer.to_string(length(coordinates), 16) |> Utils.pad_left(8)) geoms = Enum.map(coordinates, fn(geom) -> encode(%Point{ coordinates: geom }, Writer.get_endian(writer)) end) |> Enum.join Writer.write_no_endian(writer, geoms) end defp encode_coordinates(writer, %MultiLineString{coordinates: coordinates}) do writer = Writer.write(writer, Integer.to_string(length(coordinates), 16) |> Utils.pad_left(8)) geoms = Enum.map(coordinates, fn(geom) -> encode(%LineString{ coordinates: geom }, Writer.get_endian(writer)) end) |> Enum.join Writer.write_no_endian(writer, geoms) end defp encode_coordinates(writer, %MultiPolygon{coordinates: coordinates}) do writer = Writer.write(writer, Integer.to_string(length(coordinates), 16) |> Utils.pad_left(8)) geoms = Enum.map(coordinates, fn(geom) -> encode(%Polygon{ coordinates: geom }, Writer.get_endian(writer)) end) |> Enum.join Writer.write_no_endian(writer, geoms) end @doc """ Takes a WKB string and returns a Geometry """ @spec decode(binary, [Geo.geometry]) :: Geo.geometry def decode(wkb, geometries \\ []) do wkb_reader = Reader.start(wkb) { type, wkb_reader } = Reader.read(wkb_reader, 8) type = String.to_integer(type, 16) {srid, wkb_reader} = if (type &&& 0x20000000) != 0 do { srid, wkb_reader } = Reader.read(wkb_reader, 8) {String.to_integer(srid, 16), wkb_reader} else {nil, wkb_reader} end type = Utils.hex_to_type(type &&& 0xff) {coordinates, wkb_reader} = decode_coordinates(type, wkb_reader) geometries = case type do %Geo.GeometryCollection{} -> coordinates = Enum.map(coordinates, fn(x) -> %{ x | srid: srid } end) %{ type | geometries: coordinates, srid: srid } _ -> geometries ++ [ %{ type | coordinates: coordinates, srid: srid }] end if(Reader.eof?(wkb_reader)) do return_geom(geometries) else Reader.get_wkb(wkb_reader) |> decode(geometries) end end defp return_geom(%GeometryCollection{} = geom) do geom end defp return_geom(geom) when is_list(geom) do if length(geom) == 1 do hd(geom) else geom end end defp decode_coordinates(%Point{}, wkb_reader) do {x, wkb_reader} = Reader.read(wkb_reader, 16) x = Utils.hex_to_float(x) {y, wkb_reader} = Reader.read(wkb_reader, 16) y = Utils.hex_to_float(y) { {x,y}, wkb_reader } end defp decode_coordinates(%LineString{}, wkb_reader) do {number_of_points, wkb_reader} = Reader.read(wkb_reader, 8) number_of_points = number_of_points |> String.to_integer(16) Enum.map_reduce(Enum.to_list(0..(number_of_points-1)), wkb_reader, fn(_x, acc) -> decode_coordinates(%Point{}, acc) end) end defp decode_coordinates(%Polygon{}, wkb_reader) do {number_of_lines, wkb_reader} = Reader.read(wkb_reader, 8) number_of_lines = number_of_lines |> String.to_integer(16) Enum.map_reduce(Enum.to_list(0..(number_of_lines-1)), wkb_reader, fn(_x, acc) -> decode_coordinates(%LineString{}, acc) end) end defp decode_coordinates(%GeometryCollection{}, wkb_reader) do {_number_of_items, wkb_reader} = Reader.read(wkb_reader, 8) { decode(Reader.get_wkb(wkb_reader)), Reader.start("00") } end defp decode_coordinates(_geom, wkb_reader) do {_number_of_items, wkb_reader} = Reader.read(wkb_reader, 8) decoded_geom = Reader.get_wkb(wkb_reader) |> decode coordinates = if is_list(decoded_geom) do Enum.map(decoded_geom, fn(x) -> x.coordinates end) else [decoded_geom.coordinates] end { coordinates, Reader.start("00") } end end