# frozen_string_literal: true
# -----------------------------------------------------------------------------
#
# Cartesian bounding box
#
# -----------------------------------------------------------------------------
module RGeo
module Cartesian
# This is a bounding box for Cartesian data.
# The simple cartesian implementation uses this internally to compute
# envelopes. You may also use it directly to compute and represent
# bounding boxes.
#
# A bounding box is a set of ranges in each dimension: X, Y, as well
# as Z and M if supported. You can compute a bounding box for one or
# more geometry objects by creating a new bounding box object, and
# adding the geometries to it. You may then query it for the bounds,
# or use it to determine whether it encloses other geometries or
# bounding boxes.
class BoundingBox
# Create a bounding box given two corner points.
# The bounding box will be given the factory of the first point.
# You may also provide the same options available to
# BoundingBox.new.
def self.create_from_points(point1, point2, opts = {})
factory = point1.factory
new(factory, opts).add_geometry(point1).add(point2)
end
# Create a bounding box given a geometry to surround.
# The bounding box will be given the factory of the geometry.
# You may also provide the same options available to
# BoundingBox.new.
def self.create_from_geometry(geom, opts = {})
factory = geom.factory
new(factory, opts).add_geometry(geom)
end
# Create a new empty bounding box with the given factory.
#
# The factory defines the coordinate system for the bounding box,
# and also defines whether it should track Z and M coordinates.
# All geometries will be cast to this factory when added to this
# bounding box, and any generated envelope geometry will have this
# as its factory.
#
# Options include:
#
# [:ignore_z]
# If true, ignore z coordinates even if the factory supports them.
# Default is false.
# [:ignore_m]
# If true, ignore m coordinates even if the factory supports them.
# Default is false.
def initialize(factory, opts = {})
@factory = factory
if (values = opts[:raw])
@has_z, @has_m, @min_x, @max_x, @min_y, @max_y, @min_z, @max_z, @min_m, @max_m = values
else
@has_z = !opts[:ignore_z] && factory.property(:has_z_coordinate) ? true : false
@has_m = !opts[:ignore_m] && factory.property(:has_m_coordinate) ? true : false
@min_x = @max_x = @min_y = @max_y = @min_z = @max_z = @min_m = @max_m = nil
end
end
def eql?(rhs) # :nodoc:
rhs.is_a?(BoundingBox) && @factory == rhs.factory &&
@min_x == rhs.min_x && @max_x == rhs.max_x &&
@min_y == rhs.min_y && @max_y == rhs.max_y &&
@min_z == rhs.min_z && @max_z == rhs.max_z &&
@min_m == rhs.min_m && @max_m == rhs.max_m
end
alias == eql?
# Returns the bounding box's factory.
attr_reader :factory
# Returns true if this bounding box is still empty.
def empty?
@min_x.nil?
end
# Returns true if this bounding box is degenerate. That is,
# it is nonempty but contains only a single point because both
# the X and Y spans are 0. Infinitesimal boxes are also
# always degenerate.
def infinitesimal?
@min_x && @min_x == @max_x && @min_y == @max_y
end
# Returns true if this bounding box is degenerate. That is,
# it is nonempty but has zero area because either or both
# of the X or Y spans are 0.
def degenerate?
@min_x && (@min_x == @max_x || @min_y == @max_y)
end
# Returns true if this bounding box tracks Z coordinates.
attr_reader :has_z
# Returns true if this bounding box tracks M coordinates.
attr_reader :has_m
# Returns the minimum X, or nil if this bounding box is empty.
attr_reader :min_x
# Returns the maximum X, or nil if this bounding box is empty.
attr_reader :max_x
# Returns the midpoint X, or nil if this bounding box is empty.
def center_x
@max_x ? (@max_x + @min_x) * 0.5 : nil
end
# Returns the X span, or 0 if this bounding box is empty.
def x_span
@max_x ? @max_x - @min_x : 0
end
# Returns the minimum Y, or nil if this bounding box is empty.
attr_reader :min_y
# Returns the maximum Y, or nil if this bounding box is empty.
attr_reader :max_y
# Returns the midpoint Y, or nil if this bounding box is empty.
def center_y
@max_y ? (@max_y + @min_y) * 0.5 : nil
end
# Returns the Y span, or 0 if this bounding box is empty.
def y_span
@max_y ? @max_y - @min_y : 0
end
# Returns the minimum Z, or nil if this bounding box is empty.
attr_reader :min_z
# Returns the maximum Z, or nil if this bounding box is empty.
attr_reader :max_z
# Returns the midpoint Z, or nil if this bounding box is empty or has no Z.
def center_z
@max_z ? (@max_z + @min_z) * 0.5 : nil
end
# Returns the Z span, 0 if this bounding box is empty, or nil if it has no Z.
def z_span
@has_z ? (@max_z ? @max_z - @min_z : 0) : nil
end
# Returns the minimum M, or nil if this bounding box is empty.
attr_reader :min_m
# Returns the maximum M, or nil if this bounding box is empty.
attr_reader :max_m
# Returns the midpoint M, or nil if this bounding box is empty or has no M.
def center_m
@max_m ? (@max_m + @min_m) * 0.5 : nil
end
# Returns the M span, 0 if this bounding box is empty, or nil if it has no M.
def m_span
@has_m ? (@max_m ? @max_m - @min_m : 0) : nil
end
# Returns a point representing the minimum extent in all dimensions,
# or nil if this bounding box is empty.
def min_point
if @min_x
extras = []
extras << @min_z if @has_z
extras << @min_m if @has_m
@factory.point(@min_x, @min_y, *extras)
end
end
# Returns a point representing the maximum extent in all dimensions,
# or nil if this bounding box is empty.
def max_point
if @min_x
extras = []
extras << @max_z if @has_z
extras << @max_m if @has_m
@factory.point(@max_x, @max_y, *extras)
end
end
# Adjusts the extents of this bounding box to encompass the given
# object, which may be a geometry or another bounding box.
# Returns self.
def add(geometry)
case geometry
when BoundingBox
add(geometry.min_point)
add(geometry.max_point)
when Feature::Geometry
if geometry.factory == @factory
add_geometry(geometry)
else
add_geometry(Feature.cast(geometry, @factory))
end
end
self
end
# Converts this bounding box to an envelope, which will be the
# empty collection (if the bounding box is empty), a point (if the
# bounding box is not empty but both spans are 0), a line (if only
# one of the two spans is 0) or a polygon (if neither span is 0).
def to_geometry
if @min_x
extras = []
extras << @min_z if @has_z
extras << @min_m if @has_m
point_min = @factory.point(@min_x, @min_y, *extras)
if infinitesimal?
point_min
else
extras = []
extras << @max_z if @has_z
extras << @max_m if @has_m
point_max = @factory.point(@max_x, @max_y, *extras)
if degenerate?
@factory.line(point_min, point_max)
else
@factory.polygon(@factory.linear_ring([point_min,
@factory.point(@max_x, @min_y, *extras), point_max,
@factory.point(@min_x, @max_y, *extras), point_min]))
end
end
else
@factory.collection([])
end
end
# Returns true if this bounding box contains the given object,
# which may be a geometry or another bounding box.
#
# Supports these options:
#
# [:ignore_z]
# Ignore the Z coordinate when testing, even if both objects
# have Z. Default is false.
# [:ignore_m]
# Ignore the M coordinate when testing, even if both objects
# have M. Default is false.
def contains?(rhs, opts = {})
if Feature::Geometry === rhs
contains?(BoundingBox.new(@factory).add(rhs))
elsif rhs.empty?
true
elsif empty?
false
elsif @min_x > rhs.min_x || @max_x < rhs.max_x || @min_y > rhs.min_y || @max_y < rhs.max_y
false
elsif @has_m && rhs.has_m && !opts[:ignore_m] && (@min_m > rhs.min_m || @max_m < rhs.max_m)
false
elsif @has_z && rhs.has_z && !opts[:ignore_z] && (@min_z > rhs.min_z || @max_z < rhs.max_z)
false
else
true
end
end
# Returns this bounding box subdivided, as an array of bounding boxes.
# If this bounding box is empty, returns the empty array.
# If this bounding box is a point, returns a one-element array
# containing the current point.
# If the x or y span is 0, bisects the line.
# Otherwise, generally returns a 4-1 subdivision in the X-Y plane.
# Does not subdivide on Z or M.
#
# [:bisect_factor]
# An optional floating point value that should be greater than 1.0.
# If the ratio between the larger span and the smaller span is
# greater than this factor, the bounding box is divided only in
# half instead of fourths.
def subdivide(opts = {})
return [] if empty?
if infinitesimal?
return [
BoundingBox.new(@factory, raw: [@has_z, @has_m,
@min_x, @max_x, @min_y, @max_y, @min_z, @max_z, @min_m, @max_m])
]
end
factor = opts[:bisect_factor]
factor ||= 1 if degenerate?
if factor
if x_span > y_span * factor
return [
BoundingBox.new(@factory, raw: [@has_z, @has_m,
@min_x, center_x, @min_y, @max_y, @min_z, @max_z, @min_m, @max_m]),
BoundingBox.new(@factory, raw: [@has_z, @has_m,
center_x, @max_x, @min_y, @max_y, @min_z, @max_z, @min_m, @max_m])
]
elsif y_span > x_span * factor
return [
BoundingBox.new(@factory, raw: [@has_z, @has_m,
@min_x, @max_x, @min_y, center_y, @min_z, @max_z, @min_m, @max_m]),
BoundingBox.new(@factory, raw: [@has_z, @has_m,
@min_x, @max_x, center_y, @max_y, @min_z, @max_z, @min_m, @max_m])
]
end
end
[
BoundingBox.new(@factory, raw: [@has_z, @has_m,
@min_x, center_x, @min_y, center_y, @min_z, @max_z, @min_m, @max_m]),
BoundingBox.new(@factory, raw: [@has_z, @has_m,
center_x, @max_x, @min_y, center_y, @min_z, @max_z, @min_m, @max_m]),
BoundingBox.new(@factory, raw: [@has_z, @has_m,
@min_x, center_x, center_y, @max_y, @min_z, @max_z, @min_m, @max_m]),
BoundingBox.new(@factory, raw: [@has_z, @has_m,
center_x, @max_x, center_y, @max_y, @min_z, @max_z, @min_m, @max_m])
]
end
def add_geometry(geometry)
case geometry
when Feature::Point
add_point(geometry)
when Feature::LineString
geometry.points.each { |p| add_point(p) }
when Feature::Polygon
geometry.exterior_ring.points.each { |p| add_point(p) }
when Feature::MultiPoint
geometry.each { |p| add_point(p) }
when Feature::MultiLineString
geometry.each { |line| line.points.each { |p| add_point(p) } }
when Feature::MultiPolygon
geometry.each { |poly| poly.exterior_ring.points.each { |p| add_point(p) } }
when Feature::GeometryCollection
geometry.each { |g| add_geometry(g) }
end
self
end
private
def add_point(point)
if @min_x
x = point.x
@min_x = x if x < @min_x
@max_x = x if x > @max_x
y_ = point.y
@min_y = y_ if y_ < @min_y
@max_y = y_ if y_ > @max_y
if @has_z
z_ = point.z
@min_z = z_ if z_ < @min_z
@max_z = z_ if z_ > @max_z
end
if @has_m
m_ = point.m
@min_m = m_ if m_ < @min_m
@max_m = m_ if m_ > @max_m
end
else
@min_x = @max_x = point.x
@min_y = @max_y = point.y
@min_z = @max_z = point.z if @has_z
@min_m = @max_m = point.m if @has_m
end
end
end
end
end