| 1 |
<?php |
<?php |
| 2 |
// $Id: gmap_polyutil.inc,v 1.2 2008/07/15 16:30:29 bdragon Exp $ |
// $Id: gmap_polyutil.inc,v 1.3 2008/07/15 18:00:25 bdragon Exp $ |
| 3 |
|
|
| 4 |
/** |
/** |
| 5 |
* @file |
* @file |
| 13 |
* [3] http://mathworld.wolfram.com/ |
* [3] http://mathworld.wolfram.com/ |
| 14 |
*/ |
*/ |
| 15 |
|
|
| 16 |
|
DEFINE('GMAP_DP_EPSILON', 0.00001); |
| 17 |
|
DEFINE('GMAP_ZOOM_LEVELS', 18); |
| 18 |
|
DEFINE('GMAP_ZOOM_FACTOR', 2); |
| 19 |
|
|
| 20 |
|
|
| 21 |
/** |
/** |
| 22 |
* Encode a numeric value (steps 3-11 of "Encoding Latitudes and Longitudes" @ [1]) |
* The following three functions will encode numbers so that they may be used |
| 23 |
|
* in "Encoded Polylines" on Google Maps. The encoding is described here: |
| 24 |
|
* http://code.google.com/apis/maps/documentation/polylinealgorithm.html |
| 25 |
|
* |
| 26 |
|
* Numbers for latitudes/longitudes and levels are encoded slightly |
| 27 |
|
* differently--when generating Encoded Polylines, latitudes and longitudes are |
| 28 |
|
* encoded with gmap_polyutil_encode_signed(), and "levels" are encoded using |
| 29 |
|
* gmap_polyutil_encode_unsigned(). |
| 30 |
*/ |
*/ |
| 31 |
function gmap_polyutil_e5_to_encoded($e5) { |
function gmap_polyutil_encode_latlon($x) { |
| 32 |
// As described in http://code.google.com/apis/maps/documentation/polylinealgorithm.html |
$x = round($x * 1e5) << 1; |
| 33 |
$work = $e5; // Make a copy. |
if ($x < 0) { $x = ~ $x; } |
| 34 |
$work <<= 1; // 4) Shift left (Note: I have no clue what happens if PHP_INT_SIZE != 4.) |
return _gmap_polyutil_encode($x); |
| 35 |
if ($e5 < 0) { |
} |
| 36 |
$work = ~$work; // 5) Invert if negative. |
|
| 37 |
} |
function gmap_polyutil_encode_levels($x) { |
| 38 |
$out = ''; |
return _gmap_polyutil_encode(abs($x)); |
|
// While we are NOT on the last chunk... |
|
|
while ($work >= 32) { |
|
|
// This combines the rest of the steps together. |
|
|
$out .= chr((32 | ($work & 31)) + 63); |
|
|
$work >>= 5; |
|
|
} |
|
|
// Last chunk doesn't get ORed with 32. |
|
|
$out .= chr(($work & 31) + 63); |
|
|
return $out; |
|
| 39 |
} |
} |
| 40 |
|
|
| 41 |
|
function _gmap_polyutil_encode($x) { |
| 42 |
|
$result = ''; |
| 43 |
|
while ($x >= 32) { |
| 44 |
|
$result .= chr((32 | ($x & 31)) + 63); |
| 45 |
|
$x >>= 5; |
| 46 |
|
} |
| 47 |
|
$result .= chr(($x & 31) + 63); |
| 48 |
|
return $result; |
| 49 |
|
} |
| 50 |
|
|
| 51 |
|
/** |
| 52 |
|
* Distance in two dimensions. |
| 53 |
|
* √((x1-x0)^2 + (y1-y0)^2) |
| 54 |
|
*/ |
| 55 |
function gmap_polyutil_dist($p1, $p2) { |
function gmap_polyutil_dist($p1, $p2) { |
|
// Distance in two dimensions. |
|
|
// √((x1-x0)^2 + (y1-y0)^2) |
|
| 56 |
return sqrt(pow($p2[0] - $p1[0], 2) + pow($p2[1] - $p1[1], 2)); |
return sqrt(pow($p2[0] - $p1[0], 2) + pow($p2[1] - $p1[1], 2)); |
| 57 |
} |
} |
| 58 |
|
|
| 59 |
/** |
/** |
| 60 |
* Distance calculation between a point and a line segment. |
* Distance between a point and a line segment. |
| 61 |
* @param $p |
* @param $q |
| 62 |
* Point to measure. |
* Point to measure. |
| 63 |
* @param $lp1 |
* @param $p1 |
| 64 |
* Point 1 on line. |
* Start point of line segment. |
| 65 |
* @param $lp2 |
* @param $p2 |
| 66 |
* Point 2 on line. |
* End point of line segment. |
| 67 |
*/ |
*/ |
| 68 |
function gmap_polyutil_distance($p, $lp1, $lp2) { |
function gmap_polyutil_point_line_dist($q, $p1, $p2) { |
| 69 |
if ($lp1[0] == $lp2[0] && $lp1[1] == $lp2[1]) { |
if ($p1[0] == $p2[0] && $p1[1] == $p2[1]) { |
| 70 |
// The "line" is really being a point. Just use simple distance. |
// lp1 and lp2 are the same point--they don't define a line--so we return |
| 71 |
return gmap_polyutil_dist($p, $lp1); |
// the distance between two points. |
| 72 |
} |
return gmap_polyutil_dist($q, $p1); |
| 73 |
// mathematica code: (q-p1).(p2-p1)/(p2-p1).(p2-p1); |
} |
| 74 |
// I asked maxima, and it said ((p2y-p1y)*(qy-p1y)+(p2x-p1x)*(qx-p1x))/((p2y-p1y)^2+(p2x-p1x)^2). |
|
| 75 |
$tmp = (($lp2[1]-$lp1[1])*($p[1]-$lp1[1])+($lp2[0]-$lp1[0])*($p[0]-$lp1[0]))/(pow($lp2[1]-$lp1[1], 2) + pow($lp2[0]-$lp1[0], 2)); |
// Use the dot product to find where q lies with respect to the line segment |
| 76 |
// Point is not alongside segment, is further off in $lp1's direction. |
// p1p2. For more information, see: |
| 77 |
if ($tmp <= 0) { |
// http://local.wasp.uwa.edu.au/~pbourke/geometry/pointline/ |
| 78 |
return gmap_polyutil_dist($lp1, $p); |
// http://www.codeguru.com/forum/printthread.php?t=194400 |
| 79 |
} |
$u = (($p2[1]-$p1[1])*($q[1]-$p1[1]) + ($p2[0]-$p1[0])*($q[0]-$p1[0])) / (pow($p2[1]-$p1[1], 2) + pow($p2[0]-$p1[0], 2)); |
| 80 |
// Point is not alongside segment, is further off in $lp2's direction. |
|
| 81 |
else if ($tmp >= 1) { |
if ($u <= 0) { // point is not alongside segment, it is further off in $p1's direction |
| 82 |
return gmap_polyutil_dist($lp2, $p); |
return gmap_polyutil_dist($q, $p1); |
| 83 |
} |
} |
| 84 |
else { |
elseif ($u >= 1) { // point is not alongside segment, it is further off in $p2's direction |
| 85 |
// mathematica code: Norm[q-(p1+u (p2-p1))] |
return gmap_polyutil_dist($q, $p2); |
| 86 |
// which means q and (p1+u (p2-p1)) are the things we are calculating distance on. |
} |
| 87 |
// maxima sez: (p1+u*(p2-p1)) => [(p2x-p1x)*u+p1x,(p2y-p1y)*u+p1y] |
else { // point is alongside segment |
| 88 |
return gmap_polyutil_dist($p, array(($lp2[0]-$lp1[0])*$tmp + $lp1[0], ($lp2[1]-$lp1[1])*$tmp + $lp1[1])); |
// calculate distance between q and the nearest point on the line segment |
| 89 |
|
// use $u to calculate the nearest point on the line segment: |
| 90 |
|
// p1 + u*(p2 - p1) => [p1x + u*(p2x - p1x), p1y + u*(p2y - p1y)] |
| 91 |
|
return gmap_polyutil_dist($q, array( $p1[0] + $u*($p2[0] - $p1[0]), $p1[1] + $u*($p2[1] - $p1[1]))); |
| 92 |
|
} |
| 93 |
|
} |
| 94 |
|
|
| 95 |
|
/** |
| 96 |
|
* Implementation of the Douglas-Peucker polyline simplification algorithm. See: |
| 97 |
|
* http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/algorithm.html |
| 98 |
|
* |
| 99 |
|
* @param $points |
| 100 |
|
* An array of coordinate pairs. |
| 101 |
|
* @return |
| 102 |
|
* An array of keys => weights; the keys correspond with indices of points in |
| 103 |
|
* the $points array. Some points may be insignificant according to the |
| 104 |
|
* algorithm--they will not have entries in the return array. The "weights" |
| 105 |
|
* are actually the point's distance from the line segment that it subdivides. |
| 106 |
|
*/ |
| 107 |
|
function gmap_polyutil_dp_encode($points) { |
| 108 |
|
$weights = array(); |
| 109 |
|
|
| 110 |
|
if (count($points) > 2) { |
| 111 |
|
// the 'stack' holds line segments to be simplified |
| 112 |
|
$stack[] = array(0, count($points) - 1); |
| 113 |
|
|
| 114 |
|
while (count($stack) > 0) { |
| 115 |
|
// take a line segment to look at |
| 116 |
|
$segment = array_pop($stack); |
| 117 |
|
|
| 118 |
|
// figure out which subdividing point is the furthest off the line segment |
| 119 |
|
$max_dist = 0; |
| 120 |
|
for ($i = $segment[0] + 1; $i < $segment[1]; $i++) { |
| 121 |
|
$dist = gmap_polyutil_point_line_dist($points[$i], $points[$segment[0]], $points[$segment[1]]); |
| 122 |
|
if ($dist > $max_dist) { |
| 123 |
|
$max_dist = $dist; |
| 124 |
|
$max_i = $i; |
| 125 |
|
} |
| 126 |
|
} |
| 127 |
|
|
| 128 |
|
// if the subdividing point found above is significantly off the line |
| 129 |
|
// segment then we want to simplify further. Add sub-segments to the stack. |
| 130 |
|
if ($max_dist > GMAP_DP_EPSILON) { |
| 131 |
|
$weights[$max_i] = $max_dist; |
| 132 |
|
array_push($stack, array($segment[0], $max_i)); |
| 133 |
|
array_push($stack, array($max_i, $segment[1])); |
| 134 |
|
} |
| 135 |
|
} |
| 136 |
|
} |
| 137 |
|
|
| 138 |
|
// The first and last points of the line should always be visible. |
| 139 |
|
$levels = _gmap_polyutil_zoom_levels(); |
| 140 |
|
$weights[0] = $levels[0]; |
| 141 |
|
$weights[count($points) -1] = $levels[0]; |
| 142 |
|
|
| 143 |
|
return $weights; |
| 144 |
|
} |
| 145 |
|
|
| 146 |
|
/** |
| 147 |
|
* Simplify a set of points and generate an "Encoded Polyline" for Google Maps. |
| 148 |
|
* @param $points |
| 149 |
|
* An array of coordinate pairs as latitude, longitude. |
| 150 |
|
* @return |
| 151 |
|
* An array containing the point and zoom information necessary to display |
| 152 |
|
* encoded polylines on Google Maps: 'points', 'levels', 'numLevels', and 'zoomFactor'. |
| 153 |
|
*/ |
| 154 |
|
function gmap_polyutil_polyline($points) { |
| 155 |
|
$points_encoded = ''; |
| 156 |
|
$levels_encoded = ''; |
| 157 |
|
|
| 158 |
|
// simplify the line |
| 159 |
|
$weights = gmap_polyutil_dp_encode($points); |
| 160 |
|
|
| 161 |
|
$previous = array(0, 0); |
| 162 |
|
foreach ($points as $i => $p) { |
| 163 |
|
if (isset($weights[$i])) { |
| 164 |
|
// encode each simplified point |
| 165 |
|
// the deltas between points are encoded, rather than the point values themselves |
| 166 |
|
$points_encoded .= gmap_polyutil_encode_latlon($p[0] - $previous[0]) . gmap_polyutil_encode_latlon($p[1] - $previous[1]); |
| 167 |
|
$levels_encoded .= gmap_polyutil_encode_levels(_gmap_polyutil_get_zoom_level($weights[$i])); |
| 168 |
|
$previous = $p; |
| 169 |
|
} |
| 170 |
|
} |
| 171 |
|
|
| 172 |
|
return array( |
| 173 |
|
'points' => $points_encoded, |
| 174 |
|
'levels' => $levels_encoded, |
| 175 |
|
'numLevels' => GMAP_ZOOM_LEVELS, |
| 176 |
|
'zoomFactor' => GMAP_ZOOM_FACTOR, |
| 177 |
|
); |
| 178 |
|
} |
| 179 |
|
|
| 180 |
|
/** |
| 181 |
|
* Build a logarithmic scale of zoom levels. |
| 182 |
|
*/ |
| 183 |
|
function _gmap_polyutil_zoom_levels() { |
| 184 |
|
static $levels; |
| 185 |
|
if (!isset($levels)) { |
| 186 |
|
for ($i = 0; $i < GMAP_ZOOM_LEVELS; $i++) { |
| 187 |
|
$levels[$i] = GMAP_DP_EPSILON * pow(GMAP_ZOOM_FACTOR, GMAP_ZOOM_LEVELS - $i - 1); |
| 188 |
|
} |
| 189 |
|
} |
| 190 |
|
return $levels; |
| 191 |
|
} |
| 192 |
|
|
| 193 |
|
/** |
| 194 |
|
* Place points in levels based on their "weight" -- a value derived from |
| 195 |
|
* distance calculations in the simplification algorithm, gmap_polyutil_dp_encode(). |
| 196 |
|
*/ |
| 197 |
|
function _gmap_polyutil_get_zoom_level($weight) { |
| 198 |
|
$levels = _gmap_polyutil_zoom_levels(); |
| 199 |
|
$i = 0; |
| 200 |
|
while ($levels[$i] > $weight) { |
| 201 |
|
$i++; |
| 202 |
} |
} |
| 203 |
|
return GMAP_ZOOM_LEVELS - $i - 1; |
| 204 |
} |
} |