Merge service alerts relating to stops into their page
Merge service alerts relating to stops into their page

--- a/include/common-geo.inc.php
+++ b/include/common-geo.inc.php
@@ -1,8 +1,9 @@
 <?php
 // SELECT array_to_string(array(SELECT REPLACE(name_2006, ',', '\,') as name FROM suburbs order by name), ',')
 $suburbs = explode(",", "Acton,Ainslie,Amaroo,Aranda,Banks,Barton,Belconnen,Bonner,Bonython,Braddon,Bruce,Calwell,Campbell,Chapman,Charnwood,Chifley,Chisholm,City,Conder,Cook,Curtin,Deakin,Dickson,Downer,Duffy,Dunlop,Evatt,Fadden,Farrer,Fisher,Florey,Flynn,Forrest,Franklin,Fraser,Fyshwick,Garran,Gilmore,Giralang,Gordon,Gowrie,Greenway,Griffith,Gungahlin,Hackett,Hall,Harrison,Hawker,Higgins,Holder,Holt,Hughes,Hume,Isaacs,Isabella Plains,Kaleen,Kambah,Kingston,Latham,Lawson,Lyneham,Lyons,Macarthur,Macgregor,Macquarie,Mawson,McKellar,Melba,Mitchell,Monash,Narrabundah,Ngunnawal,Nicholls,Oaks Estate,O'Connor,O'Malley,Oxley,Page,Palmerston,Parkes,Pearce,Phillip,Pialligo,Red Hill,Reid,Richardson,Rivett,Russell,Scullin,Spence,Stirling,Symonston,Tharwa,Theodore,Torrens,Turner,Wanniassa,Waramanga,Watson,Weetangera,Weston,Yarralumla");
-function staticmap($mapPoints, $zoom = 0, $markerImage = "iconb", $collapsible = true)
+function staticmap($mapPoints, $zoom = 0, $markerImage = "iconb", $collapsible = true, $twotone = false)
 {
+	global $labsPath;
 	$width = 300;
 	$height = 300;
 	$metersperpixel[9] = 305.492 * $width;
@@ -12,14 +13,11 @@
 	$metersperpixel[13] = 19.093 * $width;
 	$metersperpixel[14] = 9.547 * $width;
 	$metersperpixel[15] = 4.773 * $width;
-	$metersperpixel[16] = 2.387 * $width;
+	//$metersperpixel[16] = 2.387 * $width;
 	// $metersperpixel[17]=1.193*$width;
 	$center = "";
 	$markers = "";
-	$minlat = 999;
-	$minlon = 999;
-	$maxlat = 0;
-	$maxlon = 0;
+	$mapwidthinmeters = 50;
 	if (sizeof($mapPoints) < 1) return "map error";
 	if (sizeof($mapPoints) === 1) {
 		if ($zoom == 0) $zoom = 14;
@@ -28,27 +26,30 @@
 	}
 	else {
 		foreach ($mapPoints as $index => $mapPoint) {
-			$markers.= $mapPoint[0] . "," . $mapPoint[1] . "," . $markerImage . ($index + 1);
+			if ($twotone && $index == 0) {
+				$markers.= $mapPoint[0] . "," . $mapPoint[1] . "," . "iconr" . ($index + 1);
+				$center = "{$mapPoints[0][0]},{$mapPoints[0][1]}";
+			}
+			else {
+				$markers.= $mapPoint[0] . "," . $mapPoint[1] . "," . $markerImage . ($index + 1);
+			}
 			if ($index + 1 != sizeof($mapPoints)) $markers.= "|";
-			if ($mapPoint[0] < $minlat) $minlat = $mapPoint[0];
-			if ($mapPoint[0] > $maxlat) $maxlat = $mapPoint[0];
-			if ($mapPoint[1] < $minlon) $minlon = $mapPoint[1];
-			if ($mapPoint[1] > $maxlon) $maxlon = $mapPoint[1];
+			$dist = distance($mapPoints[0][0], $mapPoint[0][1], $mapPoint[0], $mapPoint[1]);
+			$mapwidthinmeters = ($dist > $mapwidthinmeters ? $dist : $mapwidthinmeters);
 			$totalLat+= $mapPoint[0];
 			$totalLon+= $mapPoint[1];
 		}
 		if ($zoom == 0) {
 			$mapwidthinmeters = distance($minlat, $minlon, $minlat, $maxlon);
 			foreach (array_reverse($metersperpixel, true) as $zoomLevel => $maxdistance) {
-				if ($zoom == 0 && $mapwidthinmeters < ($maxdistance + 50)) $zoom = $zoomLevel;
+				if ($zoom == 0 && $mapwidthinmeters * 1.5 < ($maxdistance)) $zoom = $zoomLevel;
 			}
 		}
 		$center = $totalLat / sizeof($mapPoints) . "," . $totalLon / sizeof($mapPoints);
 	}
 	$output = "";
 	if ($collapsible) $output.= '<div class="map" data-role="collapsible" data-collapsed="true"><h3>Open Map...</h3>';
-	$output.= '<img class="map" src="' . curPageURL() . '/lib/staticmaplite/staticmap.php?center=' . $center . '&amp;zoom=' . $zoom . '&amp;size=' . $width . 'x' . $height . '&amp;markers=' . 
-$markers . '" width=' . $width . ' height=' . $height . '>';
+	$output.= '<img class="map" src="' . curPageURL() . '/' . $labsPath . '/lib/staticmaplite/staticmap.php?center=' . $center . '&amp;zoom=' . $zoom . '&amp;size=' . $width . 'x' . $height . '&amp;markers=' . $markers . '" width=' . $width . ' height=' . $height . '>';
 	if ($collapsible) $output.= '</div>';
 	return $output;
 }
@@ -66,11 +67,11 @@
 	$c = 2 * atan2(sqrt($a) , sqrt(1 - $a));
 	$km = $r * $c;
 	if ($roundLargeValues) {
-	  if ($km < 1) return floor($km * 1000);
-	  else return round($km,2)."k";
-	} else return floor($km * 1000);
+		if ($km < 1) return floor($km * 1000);
+		else return round($km, 2) . "k";
+	}
+	else return floor($km * 1000);
 }
-
 function decodePolylineToArray($encoded)
 {
 	// source: http://latlongeeks.com/forum/viewtopic.php?f=4&t=5

--- a/include/common-request.inc.php
+++ b/include/common-request.inc.php
@@ -15,7 +15,7 @@
 	$nearby = true;
 }
 if (isset($_REQUEST['suburb'])) {
-	$suburb = filter_var($_REQUEST['suburb'], FILTER_SANITIZE_STRING);
+	$suburb = $_REQUEST['suburb'];
 }
 $pageKey = filter_var($_REQUEST['pageKey'], FILTER_SANITIZE_NUMBER_INT);
 $lat = filter_var($_REQUEST['lat'], FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);

--- a/include/common-template.inc.php
+++ b/include/common-template.inc.php
@@ -23,9 +23,6 @@
 	$url.= "&guid=ON";
 	return str_replace("&", "&amp;", $url);
 }
-
-$labsPath = "";
-if (strstr($_SERVER['PHP_SELF'],"labs")) $labsPath = "../";
 
 function include_header($pageTitle, $pageType, $opendiv = true, $geolocate = false, $datepicker = false)
 {
@@ -141,21 +138,7 @@
     -moz-border-radius: 15px;
 border-radius: 15px;
     }
- 
-/*#leftcolumn { 
-	float: none;
-}			
-.min-width-768px #leftcolumn {
-	float: left;
-	width: 30%;
-}
-#rightcolumn { 
-	float: none;
-}			
-.min-width-768px #rightcolumn {
-	float: right;
-	width: 68%;
-}*/	
+
 
 #footer {
 clear:both;
@@ -178,6 +161,134 @@
 width:auto; 
 height:auto; 
 }
+
+
+// adaptive layout from jQuery Mobile docs site
+.type-interior .content-secondary {
+	border-right: 0;
+	border-left: 0;
+	margin: 10px -15px 0;
+	background: #fff;
+	border-top: 1px solid #ccc;
+}
+.type-home .ui-content {
+	margin-top: 5px;
+}
+.type-interior .ui-content {
+	padding-bottom: 0;
+}
+.content-secondary .ui-collapsible-contain {
+	padding: 10px 15px;
+
+}
+.content-secondary .ui-collapsible-heading {
+	margin: 0 0 30px;
+}
+.content-secondary .ui-collapsible-heading-collapsed,
+.content-secondary .ui-collapsible-content {
+	padding:0;
+	margin: 0;
+}
+@media all and (min-width: 650px){
+.content-secondary {
+		text-align: left;
+		float: left;
+		width: 45%;
+		background: none;
+		border-top: 0;
+	}
+	.content-secondary,
+	.type-interior .content-secondary {
+		margin: 30px 0 20px 2%;
+		padding: 20px 4% 0 0;
+			background: none;
+	}
+	.type-index .content-secondary {
+		padding: 0;
+	}
+	.type-index .content-secondary .ui-listview {
+		margin: 0;
+	}
+	.content-primary {
+		width: 45%;
+		float: right;
+		margin-top: 30px;
+		margin-right: 1%;
+		padding-right: 1%;
+	}
+	.content-primary ul:first-child {
+		margin-top: 0;
+	}
+
+	.type-interior .content-primary {
+		padding: 1.5em 6% 3em 0;
+		margin: 0;
+	}
+	/* fix up the collapsibles - expanded on desktop */
+	.content-secondary .ui-collapsible-heading {
+		display: none;
+	}
+	.content-secondary .ui-collapsible-contain {
+		margin:0;
+	}
+	.content-secondary .ui-collapsible-content {
+		display: block;
+		margin: 0;
+		padding: 0;
+	}
+		.type-interior  .content-secondary .ui-li-divider {
+		padding-top: 1em;
+		padding-bottom: 1em;
+	}
+	.type-interior .content-secondary {
+		margin: 0;
+		padding: 0;
+	}
+	
+}
+@media all and (min-width: 750px){
+	.type-home .ui-content,
+	.type-interior .ui-content {
+		background-position: 39%;
+	}
+	.content-secondary {
+		width: 34%;
+	}
+	.content-primary {
+		width: 56%;
+		padding-right: 1%;
+	}	
+	.type-interior .ui-content {
+		background-position: 34%;
+	}
+}
+
+@media all and (min-width: 1200px){
+	.type-home .ui-content{
+		background-position: 38.5%;
+	}
+	.type-interior .ui-content {
+		background-position: 30%;
+	}
+	.content-secondary {
+		width: 30%;
+		padding-right:6%;
+		margin: 30px 0 20px 5%;
+	}
+	.type-interior .content-secondary {
+		margin: 0;
+		padding: 0;
+	}
+	.content-primary {
+		width: 50%;
+		margin-right: 5%;
+		padding-right: 3%;
+	}
+	.type-interior .content-primary {
+		width: 60%;
+	}
+}
+
 </style>';
 	if (strstr($_SERVER['HTTP_USER_AGENT'], 'iPhone') || strstr($_SERVER['HTTP_USER_AGENT'], 'iPod') || strstr($_SERVER['HTTP_USER_AGENT'], 'iPad')) {
 		echo '<meta name="apple-mobile-web-app-capable" content="yes" />
@@ -211,10 +322,6 @@
 $(document).ready(function() {
         $('#here').click(function(event) { $('#geolocate').val(geolocate()); return false;});
         $('#here').show();
-	/*if ($.mobile.media('screen and (min-width: 768px)')) {
-	  $('map a:first').click();
-	  $('#settings a:first').click();
-	}*/
 });
 ";
 		if (!isset($_SESSION['lat']) || $_SESSION['lat'] == "") echo "geolocate();";

--- a/include/common-transit.inc.php
+++ b/include/common-transit.inc.php
@@ -5,16 +5,16 @@
 	'weekday'
 );
 
-function service_period()
+function service_period($date = "")
 {
 	
 	if (isset($_SESSION['service_period'])) return $_SESSION['service_period'];
-	$override = getServiceOverride();
+	$override = getServiceOverride($date);
 	if ($override['service_id']){
 		return $override['service_id'];
 	}
 
-	switch (date('w')) {
+	switch (date('w',($date != "" ? $date : time()))) {
 	case 0:
 		return 'sunday';
 	case 6:
@@ -23,9 +23,12 @@
 		return 'weekday';
 	}
 }
-function midnight_seconds()
+function midnight_seconds($time = "")
 {
 	// from http://www.perturb.org/display/Perlfunc__Seconds_Since_Midnight.html
+	if ($time != "") {
+		return (date("G", $time) * 3600) + (date("i", $time) * 60) + date("s", $time);
+	}
 	if (isset($_SESSION['time'])) {
 		$time = strtotime($_SESSION['time']);
 		return (date("G", $time) * 3600) + (date("i", $time) * 60) + date("s", $time);

--- a/include/common.inc.php
+++ b/include/common.inc.php
@@ -6,7 +6,7 @@
 	"phperror",
 	"awsotp",
 	//"squallotp",
-	"vanilleotp",
+	//"vanilleotp",
 	"database",
 	"other"
 );
@@ -26,6 +26,13 @@
 		$otpAPIurl = 'http://10.0.1.135:8080/opentripplanner-api-webapp/';
 }
 if (isDebug("phperror")) error_reporting(E_ALL ^ E_NOTICE);
+$labsPath = "";
+if (strstr($_SERVER['PHP_SELF'],"labs")) $labsPath = "../";
+
+function isDebugServer()
+{
+	return !isset($_SERVER['SERVER_NAME']) || $_SERVER['SERVER_NAME'] == "10.0.1.154" || $_SERVER['SERVER_NAME'] == "10.1.0.4" || $_SERVER['SERVER_NAME'] == "localhost" || $_SERVER['SERVER_NAME'] == "127.0.0.1" ;
+}
 
 include_once ("common-geo.inc.php");
 include_once ("common-net.inc.php");
@@ -36,10 +43,7 @@
 include_once ("common-session.inc.php");
 include_once ("common-template.inc.php");
 
-function isDebugServer()
-{
-	return $_SERVER['SERVER_NAME'] == "10.0.1.154" || $_SERVER['SERVER_NAME'] == "localhost" || $_SERVER['SERVER_NAME'] == "127.0.0.1" || !$_SERVER['SERVER_NAME'];
-}
+
 function isAnalyticsOn()
 {
 	return !isDebugServer();

--- a/include/db/route-dao.inc.php
+++ b/include/db/route-dao.inc.php
@@ -14,6 +14,21 @@
 	return $query->fetch(PDO::FETCH_ASSOC);
 }
 
+function getRouteByFullName($routeFullName)
+{
+	global $conn;
+	$query = "Select * from routes where route_short_name||route_long_name = :routeFullName LIMIT 1";
+	debug($query, "database");
+	$query = $conn->prepare($query);
+	$query->bindParam(":routeFullName", $routeFullName);
+	$query->execute();
+	if (!$query) {
+		databaseError($conn->errorInfo());
+		return Array();
+	}
+	return $query->fetch(PDO::FETCH_ASSOC);
+}
+
 function getRoutes()
 {
 	global $conn;
@@ -32,7 +47,8 @@
 	global $conn;
 	if ($routeNumber != "") {
 		$query = "Select distinct routes.route_id,routes.route_short_name,routes.route_long_name,service_id from routes  join trips on trips.route_id =
-routes.route_id join stop_times on stop_times.trip_id = trips.trip_id where route_short_name = :routeNumber order by route_short_name;";
+routes.route_id join stop_times on stop_times.trip_id = trips.trip_id
+where route_short_name = :routeNumber OR route_short_name LIKE :routeNumber2 order by route_short_name;";
 	}
 	else {
 		$query = "SELECT DISTINCT route_short_name from routes order by route_short_name";
@@ -41,6 +57,8 @@
 	$query = $conn->prepare($query);
 	if ($routeNumber != "") {
 		$query->bindParam(":routeNumber", $routeNumber);
+                $routeNumber2 = "% ".$routeNumber;
+		$query->bindParam(":routeNumber2", $routeNumber2);
 	}
 	$query->execute();
 	if (!$query) {

--- a/include/db/servicealert-dao.inc.php
+++ b/include/db/servicealert-dao.inc.php
@@ -1,10 +1,10 @@
 <?php
-function getServiceOverride() {
+function getServiceOverride($date="") {
 	global $conn;
 	$query = "Select * from calendar_dates where date = :date and exception_type = '1' LIMIT 1";
-	 debug($query,"database");
+	// debug($query,"database");
 	$query = $conn->prepare($query); // Create a prepared statement
-	$query->bindParam(":date", date("Ymd"));
+	$query->bindParam(":date", date("Ymd",($date != "" ? $date : time())));
 	$query->execute();
 	if (!$query) {
 		databaseError($conn->errorInfo());
@@ -12,4 +12,42 @@
 	}
 	return $query->fetch(PDO::FETCH_ASSOC);
 }
+
+function getCurrentAlerts() {
+		global $conn;
+	$query = "SELECT * from servicealerts_alerts";
+	//debug($query, "database");
+	$query = $conn->prepare($query);
+	//if ($stop_sequence != "") $query->bindParam(":stop_sequence", $stop_sequence);
+	$query->execute();
+	if (!$query) {
+		databaseError($conn->errorInfo());
+		return Array();
+	}
+	return $query->fetchAll();
+}
+function getInformedAlerts($id,$filter_class,$filter_id) {
+	
+		global $conn;
+	$query = "SELECT * from servicealerts_informed where servicealert_id = :servicealert_id";
+	
+	if ($filter_class != "" && $filter_id != "") {
+		$query .= " AND (informed_class = :informed_class OR informed_class = 'network') AND informed_id = :informed_id";
+	
+	}
+	//debug($query, "database");
+	$query = $conn->prepare($query);
+	if ($filter_class != "" && $filter_id != "") {
+		$query->bindParam(":informed_class", $filter_class);
+		$query->bindParam(":informed_id", $filter_id);
+	}
+	$query->bindParam(":servicealert_id", $id);
+	$query->execute();
+	if (!$query) {
+		databaseError($conn->errorInfo());
+		return Array();
+	}
+	return $query->fetchAll();
+}
+
 ?>

--- a/include/db/stop-dao.inc.php
+++ b/include/db/stop-dao.inc.php
@@ -13,12 +13,13 @@
 	}
 	return $query->fetch(PDO::FETCH_ASSOC);
 }
-function getStops($timingPointsOnly = false, $firstLetter = "")
+function getStops($timingPointsOnly = false, $firstLetter = "", $startsWith = "")
 {
 	global $conn;
 	$conditions = Array();
 	if ($timingPointsOnly) $conditions[] = "substr(stop_code,1,2) != 'Wj'";
 	if ($firstLetter != "") $conditions[] = "substr(stop_name,1,1) = :firstLetter";
+	if ($startsWith != "") $conditions[] = "stop_name like :startsWith";
 	$query = "Select * from stops";
 	if (sizeof($conditions) > 0) {
 		if (sizeof($conditions) > 1) {
@@ -31,6 +32,11 @@
 	$query.= " order by stop_name;";
 	$query = $conn->prepare($query);
         if ($firstLetter != "") $query->bindParam(":firstLetter", $firstLetter);
+        
+        if ($startsWith != "") {
+            $startsWith = $startsWith."%";
+            $query->bindParam(":startsWith", $startsWith);
+        }
 	$query->execute();
 	if (!$query) {
 		databaseError($conn->errorInfo());
@@ -72,11 +78,34 @@
 	}
 	return $query->fetchAll();
 }
+function getStopsByStopCode($stop_code,$startsWith = "")
+{
+	global $conn;
+	$query = "Select * from stops where (stop_code = :stop_code OR stop_code LIKE :stop_code2)";
+                if ($startsWith != "") $query .= " AND stop_name like :startsWith";
+
+	debug($query, "database");
+	$query = $conn->prepare($query);
+        
+	$query->bindParam(":stop_code", $stop_code);
+        $stop_code2 = $stop_code . "%";
+	$query->bindParam(":stop_code2", $stop_code2);
+ if ($startsWith != "") {
+            $startsWith = $startsWith."%";
+            $query->bindParam(":startsWith", $startsWith);
+        }
+	$query->execute();
+	if (!$query) {
+		databaseError($conn->errorInfo());
+		return Array();
+	}
+	return $query->fetchAll();
+}
 function getStopRoutes($stopID, $service_period)
 {
 	if ($service_period == "") $service_period = service_period();
 	global $conn;
-	$query = "SELECT service_id,trips.route_id,route_short_name,route_long_name
+	$query = "SELECT distinct service_id,trips.route_id,route_short_name,route_long_name
 FROM stop_times join trips on trips.trip_id =
 stop_times.trip_id join routes on trips.route_id = routes.route_id WHERE stop_id = :stopID AND service_id=:service_period";
 	debug($query, "database");
@@ -90,9 +119,10 @@
 	}
 	return $query->fetchAll();
 }
-function getStopTrips($stopID, $service_period = "", $afterTime = "")
+function getStopTrips($stopID, $service_period = "", $afterTime = "", $limit = "")
 {
 	if ($service_period == "") $service_period = service_period();
+        	if ($limit != "") $limitSQL = " LIMIT :limit ";
 	global $conn;
 	if ($afterTime != "") {
 		$query = " SELECT stop_times.trip_id,stop_times.arrival_time,stop_times.stop_id,stop_sequence,service_id,trips.route_id,route_short_name,route_long_name, end_times.arrival_time as end_time
@@ -105,7 +135,7 @@
 AND stop_times.trip_id = end_times.trip_id
 AND service_id=:service_period
 AND end_times.arrival_time > :afterTime
-ORDER BY end_time";
+ORDER BY end_time $limitSQL";
 	}
 	else {
 		$query = "SELECT stop_times.trip_id,arrival_time,stop_times.stop_id,stop_sequence,service_id,trips.route_id,route_short_name,route_long_name
@@ -115,12 +145,13 @@
 join routes on trips.route_id = routes.route_id
 WHERE stop_times.stop_id = :stopID
 AND service_id=:service_period
-ORDER BY arrival_time";
+ORDER BY arrival_time $limitSQL";
 	}
 	debug($query, "database");
 	$query = $conn->prepare($query);
 	$query->bindParam(":service_period", $service_period);
 	$query->bindParam(":stopID", $stopID);
+        if ($limit != "") $query->bindParam(":limit", $limit);
         if ($afterTime != "") $query->bindParam(":afterTime", $afterTime);
 	$query->execute();
 	if (!$query) {

--- a/include/db/trip-dao.inc.php
+++ b/include/db/trip-dao.inc.php
@@ -75,8 +75,9 @@
 					$distance_between_timepoints+= distance($stopTimes[$k - 1]["stop_lat"], $stopTimes[$k - 1]["stop_lon"], $stopTimes[$k]["stop_lat"], $stopTimes[$k]["stop_lon"]);
 				}
 				$next_timepoint = $stopTimes[$k];
-				$rv[] = $stopTime;
+				
 			}
+			$rv[] = $stopTime;
 		}
 		else {
 			// is untimed point
@@ -94,10 +95,11 @@
 				$stopTime["arrival_time"] = $cur_timepoint["arrival_time"];
 			}
 			$rv[] = $stopTime;
-			//var_dump($rv);
+			
 			
 		}
 	}
+	//var_dump($rv);
 	return $rv;
 }
 function getTripPreviousTimePoint($tripID, $stop_sequence)
@@ -144,7 +146,10 @@
 	// limit interpolation to between nearest actual points.
 	$prevTimePoint = getTripPreviousTimePoint($tripID, $stop_sequence);
 	$nextTimePoint = getTripNextTimePoint($tripID, $stop_sequence);
-	$range = "AND stop_sequence >= '{$prevTimePoint['stop_sequence']}' AND stop_sequence <= '{$nextTimePoint['stop_sequence']}'";
+	//echo " prev {$lowestDelta['stop_sequence']} next {$nextTimePoint['stop_sequence']} ";
+	$range = "";
+	if ($prevTimePoint != "") $range .= " AND stop_sequence >= '{$prevTimePoint['stop_sequence']}'";
+	if ($nextTimePoint != "") $range .= " AND stop_sequence <= '{$nextTimePoint['stop_sequence']}'";
 	foreach (getTimeInterpolatedTrip($tripID, $range) as $tripStop) {
 		if ($tripStop['stop_sequence'] == $stop_sequence) return $tripStop;
 	}

--- /dev/null
+++ b/js/flot/excanvas.js
@@ -1,1 +1,1428 @@
-
+// Copyright 2006 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+// Known Issues:
+//
+// * Patterns only support repeat.
+// * Radial gradient are not implemented. The VML version of these look very
+//   different from the canvas one.
+// * Clipping paths are not implemented.
+// * Coordsize. The width and height attribute have higher priority than the
+//   width and height style values which isn't correct.
+// * Painting mode isn't implemented.
+// * Canvas width/height should is using content-box by default. IE in
+//   Quirks mode will draw the canvas using border-box. Either change your
+//   doctype to HTML5
+//   (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
+//   or use Box Sizing Behavior from WebFX
+//   (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
+// * Non uniform scaling does not correctly scale strokes.
+// * Filling very large shapes (above 5000 points) is buggy.
+// * Optimize. There is always room for speed improvements.
+
+// Only add this code if we do not already have a canvas implementation
+if (!document.createElement('canvas').getContext) {
+
+(function() {
+
+  // alias some functions to make (compiled) code shorter
+  var m = Math;
+  var mr = m.round;
+  var ms = m.sin;
+  var mc = m.cos;
+  var abs = m.abs;
+  var sqrt = m.sqrt;
+
+  // this is used for sub pixel precision
+  var Z = 10;
+  var Z2 = Z / 2;
+
+  /**
+   * This funtion is assigned to the <canvas> elements as element.getContext().
+   * @this {HTMLElement}
+   * @return {CanvasRenderingContext2D_}
+   */
+  function getContext() {
+    return this.context_ ||
+        (this.context_ = new CanvasRenderingContext2D_(this));
+  }
+
+  var slice = Array.prototype.slice;
+
+  /**
+   * Binds a function to an object. The returned function will always use the
+   * passed in {@code obj} as {@code this}.
+   *
+   * Example:
+   *
+   *   g = bind(f, obj, a, b)
+   *   g(c, d) // will do f.call(obj, a, b, c, d)
+   *
+   * @param {Function} f The function to bind the object to
+   * @param {Object} obj The object that should act as this when the function
+   *     is called
+   * @param {*} var_args Rest arguments that will be used as the initial
+   *     arguments when the function is called
+   * @return {Function} A new function that has bound this
+   */
+  function bind(f, obj, var_args) {
+    var a = slice.call(arguments, 2);
+    return function() {
+      return f.apply(obj, a.concat(slice.call(arguments)));
+    };
+  }
+
+  function encodeHtmlAttribute(s) {
+    return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
+  }
+
+  function addNamespacesAndStylesheet(doc) {
+    // create xmlns
+    if (!doc.namespaces['g_vml_']) {
+      doc.namespaces.add('g_vml_', 'urn:schemas-microsoft-com:vml',
+                         '#default#VML');
+
+    }
+    if (!doc.namespaces['g_o_']) {
+      doc.namespaces.add('g_o_', 'urn:schemas-microsoft-com:office:office',
+                         '#default#VML');
+    }
+
+    // Setup default CSS.  Only add one style sheet per document
+    if (!doc.styleSheets['ex_canvas_']) {
+      var ss = doc.createStyleSheet();
+      ss.owningElement.id = 'ex_canvas_';
+      ss.cssText = 'canvas{display:inline-block;overflow:hidden;' +
+          // default size is 300x150 in Gecko and Opera
+          'text-align:left;width:300px;height:150px}';
+    }
+  }
+
+  // Add namespaces and stylesheet at startup.
+  addNamespacesAndStylesheet(document);
+
+  var G_vmlCanvasManager_ = {
+    init: function(opt_doc) {
+      if (/MSIE/.test(navigator.userAgent) && !window.opera) {
+        var doc = opt_doc || document;
+        // Create a dummy element so that IE will allow canvas elements to be
+        // recognized.
+        doc.createElement('canvas');
+        doc.attachEvent('onreadystatechange', bind(this.init_, this, doc));
+      }
+    },
+
+    init_: function(doc) {
+      // find all canvas elements
+      var els = doc.getElementsByTagName('canvas');
+      for (var i = 0; i < els.length; i++) {
+        this.initElement(els[i]);
+      }
+    },
+
+    /**
+     * Public initializes a canvas element so that it can be used as canvas
+     * element from now on. This is called automatically before the page is
+     * loaded but if you are creating elements using createElement you need to
+     * make sure this is called on the element.
+     * @param {HTMLElement} el The canvas element to initialize.
+     * @return {HTMLElement} the element that was created.
+     */
+    initElement: function(el) {
+      if (!el.getContext) {
+        el.getContext = getContext;
+
+        // Add namespaces and stylesheet to document of the element.
+        addNamespacesAndStylesheet(el.ownerDocument);
+
+        // Remove fallback content. There is no way to hide text nodes so we
+        // just remove all childNodes. We could hide all elements and remove
+        // text nodes but who really cares about the fallback content.
+        el.innerHTML = '';
+
+        // do not use inline function because that will leak memory
+        el.attachEvent('onpropertychange', onPropertyChange);
+        el.attachEvent('onresize', onResize);
+
+        var attrs = el.attributes;
+        if (attrs.width && attrs.width.specified) {
+          // TODO: use runtimeStyle and coordsize
+          // el.getContext().setWidth_(attrs.width.nodeValue);
+          el.style.width = attrs.width.nodeValue + 'px';
+        } else {
+          el.width = el.clientWidth;
+        }
+        if (attrs.height && attrs.height.specified) {
+          // TODO: use runtimeStyle and coordsize
+          // el.getContext().setHeight_(attrs.height.nodeValue);
+          el.style.height = attrs.height.nodeValue + 'px';
+        } else {
+          el.height = el.clientHeight;
+        }
+        //el.getContext().setCoordsize_()
+      }
+      return el;
+    }
+  };
+
+  function onPropertyChange(e) {
+    var el = e.srcElement;
+
+    switch (e.propertyName) {
+      case 'width':
+        el.getContext().clearRect();
+        el.style.width = el.attributes.width.nodeValue + 'px';
+        // In IE8 this does not trigger onresize.
+        el.firstChild.style.width =  el.clientWidth + 'px';
+        break;
+      case 'height':
+        el.getContext().clearRect();
+        el.style.height = el.attributes.height.nodeValue + 'px';
+        el.firstChild.style.height = el.clientHeight + 'px';
+        break;
+    }
+  }
+
+  function onResize(e) {
+    var el = e.srcElement;
+    if (el.firstChild) {
+      el.firstChild.style.width =  el.clientWidth + 'px';
+      el.firstChild.style.height = el.clientHeight + 'px';
+    }
+  }
+
+  G_vmlCanvasManager_.init();
+
+  // precompute "00" to "FF"
+  var decToHex = [];
+  for (var i = 0; i < 16; i++) {
+    for (var j = 0; j < 16; j++) {
+      decToHex[i * 16 + j] = i.toString(16) + j.toString(16);
+    }
+  }
+
+  function createMatrixIdentity() {
+    return [
+      [1, 0, 0],
+      [0, 1, 0],
+      [0, 0, 1]
+    ];
+  }
+
+  function matrixMultiply(m1, m2) {
+    var result = createMatrixIdentity();
+
+    for (var x = 0; x < 3; x++) {
+      for (var y = 0; y < 3; y++) {
+        var sum = 0;
+
+        for (var z = 0; z < 3; z++) {
+          sum += m1[x][z] * m2[z][y];
+        }
+
+        result[x][y] = sum;
+      }
+    }
+    return result;
+  }
+
+  function copyState(o1, o2) {
+    o2.fillStyle     = o1.fillStyle;
+    o2.lineCap       = o1.lineCap;
+    o2.lineJoin      = o1.lineJoin;
+    o2.lineWidth     = o1.lineWidth;
+    o2.miterLimit    = o1.miterLimit;
+    o2.shadowBlur    = o1.shadowBlur;
+    o2.shadowColor   = o1.shadowColor;
+    o2.shadowOffsetX = o1.shadowOffsetX;
+    o2.shadowOffsetY = o1.shadowOffsetY;
+    o2.strokeStyle   = o1.strokeStyle;
+    o2.globalAlpha   = o1.globalAlpha;
+    o2.font          = o1.font;
+    o2.textAlign     = o1.textAlign;
+    o2.textBaseline  = o1.textBaseline;
+    o2.arcScaleX_    = o1.arcScaleX_;
+    o2.arcScaleY_    = o1.arcScaleY_;
+    o2.lineScale_    = o1.lineScale_;
+  }