Skip to content

Commit

Permalink
Added encoded polyline support by including Joel Rosenberg's GMapPoly…
Browse files Browse the repository at this point in the history
…lineEncoder class. Also added helper methods to create encoded polylines and encoded polylines from GeoRuby objects.

(cherry picked from commit f94c3d1)
  • Loading branch information
kueda authored and ewildgoose committed Jun 18, 2009
1 parent 5d5e2b7 commit 3db1316
Show file tree
Hide file tree
Showing 4 changed files with 461 additions and 15 deletions.
395 changes: 395 additions & 0 deletions lib/gm_plugin/encoder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,395 @@
#--
#
# Utility for creating Google Maps Encoded GPolylines
#
# License: You may distribute this code under the same terms as Ruby itself
#
# Author: Joel Rosenberg
# Modified for use in YM4R by Ken-ichi Ueda
#
# ( Drawing from the official example pages as well as Mark McClure's work )
#
# == Example
#
# data = [
# [ 37.4419, -122.1419],
# [ 37.4519, -122.1519],
# [ 37.4619, -122.1819],
# ]
#
# encoder = GMapPolylineEncoder.new()
# result = encoder.encode( data )
#
# javascript << " var myLine = new GPolyline.fromEncoded({\n"
# javascript << " color: \"#FF0000\",\n"
# javascript << " weight: 10,\n"
# javascript << " opacity: 0.5,\n"
# javascript << " zoomFactor: #{result[:zoomFactor]},\n"
# javascript << " numLevels: #{result[:numLevels]},\n"
# javascript << " points: \"#{result[:points]}\",\n"
# javascript << " levels: \"#{result[:levels]}\"\n"
# javascript << " });"
#
# == Methods
#
# Constructor args (all optional):
# :numLevels (default 18)
# :zoomFactor (default 2)
# :reduce: Reduce points (default true)
# :escape: Escape backslashes (default true)
#
# encode( points ) method
# points (required): array of longitude, latitude pairs
#
# returns hash with keys :points, :levels, :zoomFactor, :numLevels
#
# == Background
#
# Description: http://www.google.com/apis/maps/documentation/#Encoded_Polylines
# API: http://www.google.com/apis/maps/documentation/reference.html#GPolyline
# Hints: http://www.google.com/apis/maps/documentation/polylinealgorithm.html
#
# Example Javascript for instantiating an encoded polyline:
# var encodedPolyline = new GPolyline.fromEncoded({
# color: "#FF0000",
# weight: 10,
# points: "yzocFzynhVq}@n}@o}@nzD",
# levels: "BBB",
# zoomFactor: 32,
# numLevels: 4
# });
#
# == Changes
#
# 06.29.2007 - Release 0.1
# Profiling showed that distance() accounted for 50% of the time when
# processing McClure's British coast data. By moving the distance
# calculation into encode(), we can cache a few of the calculations
# (magnitude) and eliminate the overhead of the function call. This
# reduced the time to encode by ~ 30%
#
# 06.21.2007 Implementing the Doublas-Peucker algorithm for removing superflous
# points as per Mark McClure's design:
# http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/
#
# 10.14.2006 Cleaned up (and finally grasped) zoom levels
#
# 09.2006 First port of the official example's javascript. Ignoring zoom
# levels for now, showing points at all zoom levels
#
#++

module Ym4r
module GmPlugin
class GMapPolylineEncoder
attr_accessor :reduce, :escape #zoomFactor and numLevels need side effects
attr_reader :zoomFactor, :numLevels

# The minimum distance from the line that a point must exceed to avoid
# elimination under the DP Algorithm.
@@dp_threshold = 0.00001

def initialize(options = {})
# There are no required parameters

# Nice defaults
@numLevels = options.has_key?(:numLevels) ? options[:numLevels] : 18
@zoomFactor = options.has_key?(:zoomFactor) ? options[:zoomFactor] : 2

# Calculate the distance thresholds for each zoom level
calculate_zoom_breaks()

# By default we'll simplify the polyline unless told otherwise
@reduce = ! options.has_key?(:reduce) ? true : options[:reduce]

# Escape by default; most people are using this in a web context
@escape = ! options.has_key?(:escape) ? true : options[:escape]

end

def numLevels=( new_num_levels )
@numLevels = new_num_levels
# We need to recalculate our zoom breaks
calculate_zoom_breaks()
end

def zoomFactor=( new_zoom_factor )
@zoomFactor = new_zoom_factor
# We need to recalculate our zoom breaks
calculate_zoom_breaks()
end

def encode( points )

#
# This is an implementation of the Douglas-Peucker algorithm for simplifying
# a line. You can thing of it as an elimination of points that do not
# deviate enough from a vector. That threshold for point elimination is in
# @@dp_threshold. See
#
# http://everything2.com/index.pl?node_id=859282
#
# for an explanation of the algorithm
#

max_dist = 0 # Greatest distance we measured during the run
stack = []
distances = Array.new(points.size)

if(points.length > 2)
stack << [0, points.size-1]

while(stack.length > 0)
current_line = stack.pop()
p1_idx = current_line[0]
pn_idx = current_line[1]
pb_dist = 0
pb_idx = nil

x1 = points[p1_idx][0]
y1 = points[p1_idx][1]
x2 = points[pn_idx][0]
y2 = points[pn_idx][1]

# Caching the line's magnitude for performance
magnitude = Math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
magnitude_squared = magnitude ** 2

# Find the farthest point and its distance from the line between our pair
for i in (p1_idx+1)..(pn_idx-1)

# Refactoring distance computation inline for performance
#current_distance = compute_distance(points[i], points[p1_idx], points[pn_idx])

#
# This uses Euclidian geometry. It shouldn't be that big of a deal since
# we're using it as a rough comparison for line elimination and zoom
# calculation.
#
# TODO: Implement Haversine functions which would probably bring this to
# a snail's pace (ehhhh)
#

px = points[i][0]
py = points[i][1]

current_distance = nil

if( magnitude == 0 )
# The line is really just a point
current_distance = Math.sqrt((x2-px)**2 + (y2-py)**2)
else

u = (((px - x1) * (x2 - x1)) + ((py - y1) * (y2 - y1))) / magnitude_squared

if( u <= 0 || u > 1 )
# The point is closest to an endpoint. Find out which one
ix = Math.sqrt((x1 - px)**2 + (y1 - py)**2)
iy = Math.sqrt((x2 - px)**2 + (y2 - py)**2)
if( ix > iy )
current_distance = iy
else
current_distance = ix
end
else
# The perpendicular point intersects the line
ix = x1 + u * (x2 - x1)
iy = y1 + u * (y2 - y1)
current_distance = Math.sqrt((ix - px)**2 + (iy - py)**2)
end
end

# See if this distance is the greatest for this segment so far
if(current_distance > pb_dist)
pb_dist = current_distance
pb_idx = i
end
end

# See if this is the greatest distance for all points
if(pb_dist > max_dist)
max_dist = pb_dist
end

if(pb_dist > @@dp_threshold)
# Our point, Pb, that had the greatest distance from the line, is also
# greater than our threshold. Process again using Pb as a new
# start/end point. Record this distance - we'll use it later when
# creating zoom values
distances[pb_idx] = pb_dist
stack << [p1_idx, pb_idx]
stack << [pb_idx, pn_idx]
end

end
end

# Force line endpoints to be included (sloppy, but faster than checking for
# endpoints in encode_points())
distances[0] = max_dist
distances[distances.length-1] = max_dist

# Create Base64 encoded strings for our points and zoom levels
points_enc = encode_points( points, distances)
levels_enc = encode_levels( points, distances, max_dist)

# Make points_enc an escaped string if desired.
# We should escape the levels too, in case google pulls a switcheroo
@escape && points_enc && points_enc.gsub!( /\\/, '\\\\\\\\' )


# Returning a hash. Yes, I am a Perl programmer
return {
:points => points_enc,
:levels => levels_enc,
:zoomFactor => @zoomFactor,
:numLevels => @numLevels,
}

end

private

def calculate_zoom_breaks()
# Calculate the distance thresholds for each zoom level
@zoom_level_breaks = Array.new(@numLevels);

for i in 0..(@numLevels-1)
@zoom_level_breaks[i] = @@dp_threshold * (@zoomFactor ** ( @numLevels-i-1));
end

return
end

def encode_points( points, distances )
encoded = ""

plat = 0
plon = 0

#points.each do |point| # Gah, need the distances.
for i in 0..(points.size() - 1)
if(! @reduce || distances[i] != nil )
point = points[i]
late5 = (point[0] * 1e5).floor();
lone5 = (point[1] * 1e5).floor();

dlat = late5 - plat
dlon = lone5 - plon

plat = late5;
plon = lone5;

# I used to need this for some reason
#encoded << encodeSignedNumber(Fixnum.induced_from(dlat)).to_s
#encoded << encodeSignedNumber(Fixnum.induced_from(dlon)).to_s
encoded << encodeSignedNumber(dlat).to_s
encoded << encodeSignedNumber(dlon).to_s
end
end

return encoded

end

def encode_levels( points, distances, max_dist )

encoded = "";

# Force startpoint
encoded << encodeNumber(@numLevels - 1)

if( points.size() > 2 )
for i in 1..(points.size() - 2)
distance = distances[i]
if( ! @reduce || distance != nil)
computed_level = 0

while (distance < @zoom_level_breaks[computed_level]) do
computed_level += 1
end

encoded << encodeNumber( @numLevels - computed_level - 1 )
end
end
end

# Force endpoint
encoded << encodeNumber(@numLevels - 1)

return encoded;

end

def compute_distance( point, lineStart, lineEnd )

#
# Note: This has been refactored to encode() inline for performance and
# computation caching
#

px = point[0]
py = point[1]
x1 = lineStart[0]
y1 = lineStart[1]
x2 = lineEnd[0]
y2 = lineEnd[1]

distance = nil

magnitude = Math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

if( magnitude == 0 )
return Math.sqrt((x2-px)**2 + (y2-py)**2)
end

u = (((px - x1) * (x2 - x1)) + ((py - y1) * (y2 - y1))) / (magnitude**2)

if( u <= 0 || u > 1 )
# The point is closest to an endpoint. Find out which
ix = Math.sqrt((x1 - px)**2 + (y1 - py)**2)
iy = Math.sqrt((x2 - px)**2 + (y2 - py)**2)
if( ix > iy )
distance = iy
else
distance = ix
end
else
# The perpendicular point intersects the line
ix = x1 + u * (x2 - x1)
iy = y1 + u * (y2 - y1)
distance = Math.sqrt((ix - px)**2 + (iy - py)**2)
end

return distance
end

def encodeSignedNumber(num)
# Based on the official google example

sgn_num = num << 1

if( num < 0 )
sgn_num = ~(sgn_num)
end

return encodeNumber(sgn_num)
end

def encodeNumber(num)
# Based on the official google example

encoded = "";

while (num >= 0x20) do
encoded << ((0x20 | (num & 0x1f)) + 63).chr;
num = num >> 5;
end

encoded << (num + 63).chr;
return encoded;
end

end
end
end
Loading

0 comments on commit 3db1316

Please sign in to comment.