# -----------------------------------------------------------------------------
# 
# Well-known binary generator for RGeo
# 
# -----------------------------------------------------------------------------
# Copyright 2010 Daniel Azuma
# 
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
# * Redistributions of source code must retain the above copyright notice,
#   this list of conditions and the following disclaimer.
# * 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.
# * Neither the name of the copyright holder, nor the names of any other
#   contributors to this software, 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 OWNER 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.
# -----------------------------------------------------------------------------
;


module RGeo
  
  module WKRep
    
    
    # This class provides the functionality of serializing a geometry as
    # WKB (well-known binary) format. You may also customize the
    # serializer to generate PostGIS EWKB extensions to the output, or to
    # follow the Simple Features Specification 1.2 extensions for Z and M
    # coordinates.
    # 
    # To use this class, create an instance with the desired settings and
    # customizations, and call the generate method.
    # 
    # === Configuration options
    # 
    # The following options are recognized. These can be passed to the
    # constructor, or set on the object afterwards.
    # 
    # <tt>:type_format</tt>::
    #   The format for type codes. Possible values are <tt>:wkb11</tt>,
    #   indicating SFS 1.1 WKB (i.e. no Z or M values); <tt>:ewkb</tt>,
    #   indicating the PostGIS EWKB extensions (i.e. Z and M presence
    #   flagged by the two high bits of the type code, and support for
    #   embedded SRID); or <tt>:wkb12</tt> (indicating SFS 1.2 WKB
    #   (i.e. Z and M presence flagged by adding 1000 and/or 2000 to
    #   the type code.) Default is <tt>:wkb11</tt>.
    # <tt>:emit_ewkb_srid</tt>::
    #   If true, embed the SRID in the toplevel geometry. Available only
    #   if <tt>:type_format</tt> is <tt>:ewkb</tt>. Default is false.
    # <tt>:hex_format</tt>::
    #   If true, output a hex string instead of a byte string.
    #   Default is false.
    # <tt>:little_endian</tt>::
    #   If true, output little endian (NDR) byte order. If false, output
    #   big endian (XDR), or network byte order. Default is false.
    
    class WKBGenerator
      
      
      # :stopdoc:
      TYPE_CODES = {
        Features::Point => 1,
        Features::LineString => 2,
        Features::LinearRing => 2,
        Features::Line => 2,
        Features::Polygon => 3,
        Features::MultiPoint => 4,
        Features::MultiLineString => 5,
        Features::MultiPolygon => 6,
        Features::GeometryCollection => 7,
      }
      # :startdoc:
      
      
      # Create and configure a WKB generator. See the WKBGenerator
      # documentation for the options that can be passed.
      
      def initialize(opts_={})
        @type_format = opts_[:type_format] || :wkb11
        @emit_ewkb_srid = opts_[:emit_ewkb_srid] ? true : false if @type_format == :ewkb
        @hex_format = opts_[:hex_format] ? true : false
        @little_endian = opts_[:little_endian] ? true : false
      end
      
      
      # Returns the format for type codes. See WKBGenerator for details.
      def type_format
        @type_format
      end
      
      # Sets the format for type codes. See WKBGenerator for details.
      def type_format=(value_)
        @type_format = value_
      end
      
      # Returns whether SRID is embedded. See WKBGenerator for details.
      def emit_ewkb_srid?
        @emit_ewkb_srid
      end
      
      # Sets whether SRID is embedded. Available only when the type_format
      # is <tt>:ewkb</tt>. See WKBGenerator for details.
      def emit_ewkb_srid=(value_)
        @emit_ewkb_srid = @type_format == :ewkb && value_
      end
      
      # Returns whether output is converted to hex.
      # See WKBGenerator for details.
      def hex_format?
        @hex_format
      end
      
      # Sets whether output is converted to hex.
      # See WKBGenerator for details.
      def hex_format=(value_)
        @hex_format = value_ ? true : false
      end
      
      # Returns whether output is little-endian (NDR).
      # See WKBGenerator for details.
      def little_endian?
        @little_endian
      end
      
      # Sets whether output is little-endian (NDR).
      # See WKBGenerator for details.
      def little_endian=(value_)
        @little_endian = value_ ? true : false
      end
      
      
      # Generate and return the WKB format for the given geometry object,
      # according to the current settings.
      
      def generate(obj_)
        factory_ = obj_.factory
        if @type_format == :ewkb || @type_format == :wkb12
          @cur_has_z = factory_.has_capability?(:z_coordinate)
          @cur_has_m = factory_.has_capability?(:m_coordinate)
        else
          @cur_has_z = nil
          @cur_has_m = nil
        end
        @cur_dims = 2 + (@cur_has_z ? 1 : 0) + (@cur_has_m ? 1 : 0)
        _start_emitter
        _generate_feature(obj_, true)
        _finish_emitter
      end
      
      
      def _generate_feature(obj_, toplevel_=false)  # :nodoc:
        _emit_byte(@little_endian ? 1 : 0)
        type_ = obj_.geometry_type
        type_code_ = TYPE_CODES[type_]
        unless type_code_
          raise Errors::ParseError, "Unrecognized Geometry Type: #{type_}"
        end
        emit_srid_ = false
        if @type_format == :ewkb
          type_code_ |= 0x80000000 if @cur_has_z
          type_code_ |= 0x40000000 if @cur_has_m
          if @emit_ewkb_srid && toplevel_
            type_code_ |= 0x20000000
            emit_srid_ = true
          end
        elsif @type_format == :wkb12
          type_code_ += 1000 if @cur_has_z
          type_code_ += 2000 if @cur_has_m
        end
        _emit_integer(type_code_)
        _emit_integer(obj_.srid) if emit_srid_
        if type_ == Features::Point
          _emit_doubles(_point_coords(obj_))
        elsif type_.subtype_of?(Features::LineString)
          _emit_line_string_coords(obj_)
        elsif type_ == Features::Polygon
          exterior_ring_ = obj_.exterior_ring
          if exterior_ring_.is_empty?
            _emit_integer(0)
          else
            _emit_integer(1 + obj_.num_interior_rings)
            _emit_line_string_coords(exterior_ring_)
            obj_.interior_rings.each{ |r_| _emit_line_string_coords(r_) }
          end
        elsif type_ == Features::GeometryCollection
          _emit_integer(obj_.num_geometries)
          obj_.each{ |g_| _generate_feature(g_) }
        elsif type_ == Features::MultiPoint
          _emit_integer(obj_.num_geometries)
          obj_.each{ |g_| _generate_feature(g_) }
        elsif type_ == Features::MultiLineString
          _emit_integer(obj_.num_geometries)
          obj_.each{ |g_| _generate_feature(g_) }
        elsif type_ == Features::MultiPolygon
          _emit_integer(obj_.num_geometries)
          obj_.each{ |g_| _generate_feature(g_) }
        end
      end
      
      
      def _point_coords(obj_, array_=[])  # :nodoc:
        array_ << obj_.x
        array_ << obj_.y
        array_ << obj_.z if @cur_has_z
        array_ << obj_.m if @cur_has_m
        array_
      end
      
      
      def _emit_line_string_coords(obj_)  # :nodoc:
        array_ = []
        obj_.points.each{ |p_| _point_coords(p_, array_) }
        _emit_integer(obj_.num_points)
        _emit_doubles(array_)
      end
      
      
      def _start_emitter  # :nodoc:
        @cur_array = []
      end
      
      
      def _emit_byte(value_)  # :nodoc:
        @cur_array << [value_].pack("C")
      end
      
      
      def _emit_integer(value_)  # :nodoc:
        @cur_array << [value_].pack(@little_endian ? 'V' : 'N')
      end
      
      
      def _emit_doubles(array_)  # :nodoc:
        @cur_array << array_.pack(@little_endian ? 'E*' : 'G*')
      end
      
      
      def _finish_emitter  # :nodoc:
        str_ = @cur_array.join
        @cur_array = nil
        @hex_format ? str_.unpack("H*")[0] : str_
      end
      
      
    end
    
    
  end
  
end