ioquatix/geospatial

View on GitHub
lib/geospatial/map.rb

Summary

Maintainability
A
35 mins
Test Coverage
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

require_relative 'location'
require_relative 'box'
require_relative 'filter'

require_relative 'hilbert/curve'

module Geospatial
    # A point is a location on a map with a specific hash representation based on the map. A point might store multi-dimentional data (e.g. longitude, latitude, time) which is hashed to a single column.
    class Point
        def initialize(map, coordinates, object = nil)
            @map = map
            @coordinates = coordinates
            @object = object
        end
        
        attr :object
        
        def [] index
            @coordinates[index]
        end
        
        def []= index, value
            @coordinates[index] = value
        end
        
        attr :coordinates
        
        alias to_a coordinates
        
        def eql?(other)
            self.class.eql?(other.class) and @coordinates.eql?(other.coordinates)
        end
        
        def hash
            @hash ||= @map.hash_for_coordinates(@coordinates).freeze
        end
    end
    
    class Map
        def self.for_earth(order = 20)
            self.new(Hilbert::Curve.new(Dimensions.for_earth, order))
        end
        
        def initialize(curve)
            @curve = curve
            @points = []
            @bounds = nil
        end
        
        attr :curve
        
        def order
            @curve.order
        end
        
        def bounds
            unless @bounds
                origin = @curve.origin
                size = @curve.size
                
                @bounds = Box.new(origin, size)
            end
            
            return @bounds
        end
        
        attr :points
        
        def hash_for_coordinates(coordinates)
            @curve.map(coordinates)
        end
        
        def point_for_hash(hash)
            Point.new(self, @curve.unmap(hash))
        end
        
        def point_for_coordinates(coordinates, object = nil)
            Point.new(self, coordinates, object)
        end
        
        def point_for_object(object)
            Point.new(self, object.to_a, object)
        end
        
        def << object
            @points << point_for_coordinates(object.to_a, object)
            
            return self
        end
        
        def count
            @points.count
        end
        
        def sort!
            @points.sort_by!(&:hash)
        end
        
        def query(region, **options)
            filter = filter_for(region, **options)
            
            return filter.apply(@points).map(&:object)
        end
        
        def traverse(region, depth: 0)
            @curve.traverse do |child_origin, child_size, prefix, order|
                child = Box.new(Vector.elements(child_origin), Vector.elements(child_size))
                
                # puts "Considering (order=#{order}) #{child.inspect}..."
                
                if region.intersect?(child)
                    if order == depth # at bottom
                        # puts "at bottom -> found prefix #{prefix.to_s(2)} (#{child.inspect})"
                        yield(child, prefix, order); :skip
                    elsif region.include?(child)
                        # puts "include child -> found prefix #{prefix.to_s(2)} (#{child.inspect})"
                        yield(child, prefix, order); :skip
                    else
                        # puts "going deeper..."
                    end
                else
                    # puts "out of bounds."
                    :skip
                end
            end
        end
        
        def filter_for(*regions, **options)
            filter = Filter.new(@curve)
            
            regions.each do |region|
                # The filter will coalesce sequential segments of the curve into a single range.
                traverse(region, **options) do |child, prefix, order|
                    filter.add(prefix, order)
                end
            end
            
            return filter
        end
    end
end