PHP, JavaScript and Google Maps API in MOPSI Karol Waga 15.02.2011 Table of contents PHP JavaScript Google Maps API PHP – server side designed for web development to produce dynamic web pages interpreted by a web server available as a processor for most modern web servers and as a standalone interpreter on most operating systems Use of PHP in MOPSI Retrieving routes and photos Handling data sent by mobile devices Recommendation, clustering, and search algorithms Running scripts (for example, route segmentation and route reduction) Getting routes session_start(); include("../inc/general.php"); include("getRoutes.php"); $queryStr = $_GET['q']; $text = explode(";", $queryStr); $q = $text[0]; // user $dateArr = $text[1]; // date $maxDistance = $text[2]; $timeThreshold = $text[3]; $date_array = explode("-",$dateArr); $startday = $date_array[0]; $startmonth = $date_array[1]; $startyear = $date_array[2]; $endday = $date_array[3]; $endmonth = $date_array[4]; $endyear = $date_array[5]; $start_day = '\''.$startyear."-".$startmonth."-".$startday.'\''; $end_day = '\''.$endyear."-".$endmonth."-".$endday.'\''; $inputprefix1 = "reduction/input_" . session_id(); $inputprefix2 = "measured_routes/"; getRoutes($q, $maxDistance, $timeThreshold, $start_day, $end_day, $inputprefix1,$inputprefix2); Retrieve route from database $path = str_replace("inc/module_routeCollections","",dirname(__FILE__)); $database = new database(); $lnk = $database->connectToDb(); mysql_set_charset("utf8", $lnk); $minDistance = 10; $message = ""; $routeInfo = ""; $common = new common(); $isResults = false; $cacheAddresses = false; if ($timeThreshold >= DEFAULT_TIME_SEGMENTATION_THRESHOLD && $maxDistance >= DEFAULT_DISTANCE_SEGMENTATION_THRESHOLD) { $cacheAddresses = true; } $maxDistance = $maxDistance/1000; // convert meters to kilometers // Select all points for user in selected time period $sql = "SELECT `Latitude`, `Longitude`, `Timestamp`, `Address`, `ID` FROM `Tracking` " . "WHERE `Staff_ID` = $userId AND `Date` BETWEEN $start_day AND $end_day ORDER BY Timestamp Desc "; $rs = mysql_query ( $sql, $lnk); if ( mysql_num_rows( $rs ) > 0 ) { $isResults = true; $rs_arr = preprocessQueryResult($rs); } else { $rs_arr = array(); } Preparing input for scripts session_start(); $routeNumber = $_POST["routeNumber"]; $segmentsNo = $_POST["segmentsNo"]; $isFiltered = ($_POST["filtering"]=="true"); $inputprefix = "reduction/input_" . session_id(); $routeFile = "./" . $inputprefix . ".txt"; $fp= @fopen($routeFile, "r"); $id = 0; $routeString = ""; if($fp) { while( $str = fgets( $fp ) ) { if(strpos($str, "*") !== false) // find the route segment flag { $id ++; }else if($id == $routeNumber) // write the selected route { $routeString .= $str; }else if($id > $routeNumber) // ignore rest of the route { break; } } fclose($fp); $prefix = "routeSegmentation"; $originalRoute = $prefix."/route_filt/originalRoute.txt"; $filteredRoute = $prefix."/filteredRoute.txt"; $file= @fopen($originalRoute, "w"); if ($file) { flock($file, 2); fwrite($file, $routeString); flock($file, 3); } fclose($file); } Calling external scripts if ($isFiltered) { $rs = shell_exec($prefix."/route_filt/route_filt ".$originalRoute." ".$filteredRoute); $rs = shell_exec($prefix."/routeseg ".$filteredRoute." ".$prefix."/segmentedRoute.txt ".$segmentsNo); } else $rs = shell_exec($prefix."/routeseg ".$originalRoute." ".$prefix."/segmentedRoute.txt ".$segmentsNo); Keywords clustering function clusterServices($lnk) { $services = mysql_query("SELECT * FROM `search_cache` WHERE `Municipality`='iitti'", $lnk); if ( (!$services) || (($num_rows = mysql_num_rows($services)) == 0) ) { die('Invalid query: ' . mysql_error()); } $i=0; while($result = mysql_fetch_assoc($services)) { $serv_id = $result['SERV_ID']; $keyword_ids = mysql_query("SELECT * FROM `search_link` WHERE `SERV_ID`='$serv_id'", $lnk); while ($rs = mysql_fetch_assoc($keyword_ids)) { $kw_id = $rs['KW_ID']; $actual_keyword = mysql_query("SELECT * FROM `search_kw` WHERE `KW_ID`='$kw_id'", $lnk); $keyword = mysql_fetch_assoc($actual_keyword); $keyword = $keyword['Keyword']; if (!isOnTheList($kw_id,$list)) {$list[$i] = $kw_id; $list2[$i++] = $keyword;}} } $k = 0; $table = array(array()); for ($i = 0; $i < count($list); $i++) { if (isInTable($table,$list[$i])) { continue; } $idx = 0; $table[$k][$idx++] = $list[$i]; $neighbors = searchNeighborKeywords($list[$i],$lnk); for ($j = 0; $j < count($neighbors); $j++) { if (isInVector($list,$neighbors[$j])) $table[$k][$idx++] = $neighbors[$j]; } $k++; } for($i=0; $i < count($table); $i++) { for($j=0; $j < count($table[$i]); $j++) { $table = mergeClusters($table,$i,$j); $table[$i][$j] = ucfirst($list2[array_search($table[$i][$j],$list)]);//convert number (keyword_id) to name (keyword) } sort($table[$i]); } $table2 = sortCells($table); return $table2; Changes to clustering algorithm Bus search function findNearestStop($lat, $lng, $city) { global $lnk, $stop_services_selection, $stop_id; $stop_id = array(); $city_id_selection = mysql_query("SELECT * FROM `bus_places_codes` WHERE `place_name` = '$city'", $lnk); $city_id = mysql_fetch_assoc($city_id_selection); $city_id = $city_id['place_id']; $bus_stops_selection = mysql_query("SELECT * FROM `bus_stations` WHERE `city_id` = '$city_id'", $lnk);$i = 0; while($result = mysql_fetch_assoc($bus_stops_selection)) { if ($result['lat']!=0) { } $stop_id[$i] = $result['id']; $stop_name[$i] = $result['name']; $stop_lat[$i] = $result['lat']; $stop_lng[$i] = $result['lon']; $stop_distance[$i] = getDistance($result['lat'],$result['lon'], $lat, $lng); $i++; } $shortest_distance = 9999999; $closest_stop_index = -1; for ($i=0; $i<count($stop_distance); $i++) { if ($stop_distance[$i]<$shortest_distance) {$shortest_distance = $stop_distance[$i];$closest_stop_index = $i;} } $stop_services_selection = mysql_query("SELECT * FROM `bus_stops` WHERE `station_id` = '$stop_id[$closest_stop_index]' ORDER BY `departure_time`", $lnk); $stop_name = $stop_name[$closest_stop_index]; $stop_id = $stop_id[$closest_stop_index]; print $stop_name."|".$shortest_distance."|".$stop_id."|".$stop_lat[$closest_stop_index]."|".$stop_lng[$closest_stop_index]."\n".mysql_num_rows($stop_services_sel ection)."\n"; return $stop_services_selection; } Nearest bus stop search User can find timetable from the nearest bus stop JavaScript – client side designed for web development to produce dynamic web pages interpreted by a web browser available as a processor for most modern web servers and as a standalone interpreter on most operating systems Use of JavaScript in MOPSI presenting results retrieved from server clustering of routes, photos and users jQuery library for better visual outlook with Google Maps API Displaying photo results function xmlPhotoParse( xml ) { var point = new GLatLng(parseFloat(markers[i].getAttribute("lat")), parseFloat(markers[i].getAttribute("lon"))); pointList.push(point); titleStr= markers[i].getAttribute("title"); urlStr = markers[i].getAttribute("url"); addressStr = markers[i].getAttribute("address"); dateStr = markers[i].getAttribute("date"); photoid = markers[i].getAttribute("id"); author = markers[i].getAttribute("author"); distStr = formatMeters( dist ); // the nearest place info = "<table cellspacing='0' rowspacing='0'><tr><td align='center' colspan='2'><b>"+titleStr+"</b></td></tr><tr><td>Address:</td><td>"+addressStr+"</td></tr>" +"<tr><td>Date:</td><td>"+dateStr+"</td></tr><tr><td>Distance:</td><td>"+distStr +'</td></tr><tr><td colspan="2" align="center"><a href="'+urlStr+'" TARGET="_blank"><img border="0" src="http://cs.joensuu.fi/paikka/mobile_photo/thumb-'+ photoid +'" /></a></td></tr>' +"<tr><td colspan='2' align='center'>"+author+"</td></tr></table>"; var picNum = m+1; // get the icon num if(picNum > 20) picNum = 'empty' // use the yellow bubbles for photos search results picNum = "yellow" + picNum; var marker = createResultMarker(point, info, picNum, 5, ""); marker.top = 3; // set the local result bubble on the top map.addOverlay(marker); markerList.push(marker); var markerNum = markerList.length-1; resultStr += 'onmouseover="showEdit(this,' + markerNum + ')" onmouseout="closeEdit(this,' + markerNum + ')">' + '<td class="icon" width="30" border="0" align="center">' + '<a href="javascript:myclick(' + markerNum + ')">' + '<img border="0" src="' + './Icon/'+ picNum +'.png" >' + '<\/a>' + '</td>' + '<td width="140" border="0" >' + '<div class="jinfo">' + info + '</div>' + '<a href="javascript:myclickRoute(' + markerNum + ')">' + 'Check route' + '<\/a>' + '</td>' + '<td width="100" border="0" align="center"><div class="jaction">' + '<div id="editpart" class="edit' + markerNum + '" style="display:none"> <a href="javascript:void(0);" onclick="photoAdd(' + markerNum +',\''+photoid+'\');"> <span class="jaction" style="font-weight:bold;">Connect</span> </a> </div>' + '<a href="'+urlStr+'" TARGET="_blank"> <img class="imgstyle" src="http://cs.joensuu.fi/paikka/mobile_photo/thumb-'+ photoid +'" /></a><br>'+ author + '</div></td>' + '</tr>'; Displaying photo results Clustering of photos function gridClustering() { var maxLat = mapBounding.maxLat; var maxLng = mapBounding.maxLng; var minLat = mapBounding.minLat; var minLng = mapBounding.minLng; var deltaLat = mapBounding.deltaLat; var deltaLng = mapBounding.deltaLng; var len = markers.length; var clusters = new Array(); var photoClusters = new Array(); var lat, lng; var k, total; var numArray, rowNum, columnNum; numArray = calcClusterParam(); rowNum = numArray[0]; columnNum = numArray[1]; total = rowNum * columnNum; clusters = new Array(); //grid-based clustering, assign points to the given cluster for(var i=0; i < len; i++) { lat = markers[i].realCoords.lat(); lng = markers[i].realCoords.lng(); k = decideClusterNum(lat, lng, maxLat, maxLng, deltaLat, deltaLng, rowNum, columnNum); if(k == -1) { continue; } if(clusters['"'+k+'"'] == null) { clusters['"'+k+'"'] = new photoCluster(k); clusters['"'+k+'"'].group.push(i); clusters.push(clusters['"'+k+'"']); }else {clusters['"'+k+'"'].group.push(i); } } photoClusters = mergeClusters(clusters, columnNum); return photoClusters;} Clustering of photos jQuery $("#dialogPhoto").dialog({ autoOpen: false, height: 500, width: 350, modal: true, resizable: true, buttons: { 'Cancel': function() { $(this).dialog('close'); },}, close: function() { } }); jQuery Google Maps API launched in 2005 by Google allow developers to integrate Google Maps into their websites initially contained only JavaScript API extended with web services for performing geocoding, and generating driving directions free for commercial use providing that the site on which it is being used is publicly accessible and does not charge for access (other sites can purchase Google Maps API Premier) used by over 350000, including MOPSI website Use of Google Maps API in MOPSI displaying routes, photos and users displaying search results segmented route visualization bus routes visualization geocoding, reverse geocoding Displaying search results info = "<table cellspacing='0' rowspacing='0'><tr><td align='center' colspan='2'><b>"+titleStr +"</b></td></tr><tr><td>Address:</td><td>"+addressStr+"</td></tr>" +"</td></tr><tr><td>Phone:</td><td>"+telStr+"</td></tr>" +"</td></tr><tr><td>Link:</td><td>"+urlStr+"</td></tr>" +"<tr><td>Distance:</td><td>"+distStr +"</td></tr></table>"; var picNum = m+1; // get the icon num if(picNum > 20) picNum = 'empty'; // use the green bubbles for local search results picNum = "green" + picNum; var marker = createResultMarker(point, info, picNum, 9, service_photo); marker.top = 2; // set the local result bubble on the top map.addOverlay(marker); Displaying search results Segmented route visualization function animateRoute(route, icon, timeout, isPolyline) { if (isAnimationOn) return 0; if (!isPolyline) { route = ajaxPost("../route/getSpecificRoute.php","routeNumber="+route); if (document.getElementById("custom").checked) { timeout = parseFloat(document.getElementById("animationSpeed").value); if (isNaN(timeout)) timeout=0.5; } else { var startTime = coordinates[2]; var timeInterval = point_data[step+1].split(" "); timeout = timeInterval[2]-startTime; }; timeout*=1000; } //marker creation var point; var infoMarker; var infoTimeMarker; if (isPolyline) point = route.getVertex(0); else//isString { var point_data = route.split("\n"); var coordinates = point_data[0].split(" "); point = new GLatLng(coordinates[0],coordinates[1]); if (!isPolyline) if (isAnalysisMode) { infoMarker = new createAnimationLabeledTextMarker(point,formatSpeed(point_stats_array[0].split("")[6]),"speed"); infoTimeMarker = new createAnimationLabeledTextMarker(point,formatSeconds(point_stats_array[0].split(" ")[1]),"time"); map.addOverlay(infoMarker); map.addOverlay(infoTimeMarker); } } var marker = createAnimationMarker(point,icon); map.addOverlay(marker); if (infoMarker==undefined) infoMarker = ""; moveToStep(marker, infoMarker, infoTimeMarker, route, icon, isPolyline, timeout, 0); isAnimationOn = true; } Segmented route visualization Bus routes visualization function setDirections(aPoints, strRoute, locale, numText, timeLabels){ document.getElementById("statistics„ ).innerHTML += numText; map.clearOverlays(); gdir = new GDirections(map); gdir.loadFromWaypoints(aPoints[0], {"locale": locale}); GEvent.addListener(gdir, "load", onGDirectionsLoad); document.getElementById("busDirections").innerHTML = strRoute; for (var i=0;i<timeLabels.length;i++) map.addOverlay(timeLabels[i]); window.setTimeout(function(){ getBusRouteBounds(); },1000); } Bus routes visualization Geocoding process of finding associated geographic coordinates (often expressed as latitude and longitude) from other geographic data, such as street addresses, or postal codes Google Geocoding is free up to 2500 queries per day, but with numerous restrictions for example geocoding results without displaying them on a Google Map is prohibited Geocoding geocoder = new GClientGeocoder(); var point = new GLatLng(lat, lon); userpoint = point; var latlng = point; geocoder.getLocations(latlng, getCurrentAddr); lat_des = formatLat(lat); lon_des = formatLon(lon); userinfo = '<b>Current location: </b><br>' + lat_des + " " + lon_des + '<br>' + city+" "+country; Geocoding function getCurrentAddr(response) { if (!response || response.Status.code != 200) { alert("Status Code:" + response.Status.code); } else { place = response.Placemark[0]; currentAddress = place.address; } } Reverse geocoding the opposite to geocoding: finding an associated textual location such as a street address, from geographic coordinates Google map object function createMap( lat, lon, canvas_id, zoom ) { var map = new GMap2(document.getElementById(canvas_id)); map.setCenter(new GLatLng(lat, lon), zoom); map.addControl(new GMapTypeControl()); //Map Type-choosing tool. map.addControl(new GLargeMapControl()); //Zoom tool. if(loggedInUser && loggedInUser.settings.wheel_zoom) { map.enableScrollWheelZoom(); map.enableContinuousZoom(); } return map; } Listeners on Google map clicklistener = GEvent.addListener(map, "click", function(overlay, latlng){ if (overlay instanceof GMarker) { for (var j=0;j<segmentMiddleMarkerArray.length;j++) { if (segmentMiddleMarkerArray[j].getLatLng() == overlay.getLatLng()) showSegmentBubble(segmentMiddleMarkerArray[j].getLatL ng().lat(), segmentMiddleMarkerArray[j].getLatLng().lng(), speedStrArray[j], distanceStrArray[j], timeStrArray[j], iconArrayNonAnimated[j], j+1); } }}); GIS databases PostgreSQL with PostGIS Oracle with Oracle Spatial MySQL spatial extention PostgreSQL with PostGIS open source software program that adds support for geographic objects to the PostgreSQL geometry types for points, polygons, etc. spatial operators for determining geospatial measurements like area, distance, length and perimeter spatial operators for determining geospatial set operations, like union, difference, symmetric difference and buffers spatial indexes for high speed spatial querying index selectivity support, to provide high performance query plans for mixed spatial/non-spatial queries Oracle with Oracle Spatial separately-licensed option component of the Oracle database managing geographic and location-data in a native type within an Oracle database schema that prescribes the storage, syntax, and semantics of supported geometric data types spatial indexing system operators, functions, and procedures for performing area-of-interest queries, spatial join queries, and other spatial analysis operations MySQL spatial extenstion supports spatial extensions to enable the generation, storage, and analysis of geographic features before MySQL 5.0.16, these features are available for MyISAM tables only, as of MySQL 5.0.16, InnoDB, NDB, BDB, and ARCHIVE support spatial features MySQL spatial extension MySQL has data types that correspond to OpenGIS classes. Some of these types hold single geometry values: GEOMETRY POINT LINESTRING POLYGON GEOMETRY can store geometry values of any type. The other single-value types (POINT, LINESTRING, and POLYGON) restrict their values to a particular geometry type. The other data types hold collections of values: MULTIPOINT MULTILINESTRING MULTIPOLYGON GEOMETRYCOLLECTION GEOMETRYCOLLECTION can store a collection of objects of any type. The other collection types (MULTIPOINT, MULTILINESTRING, MULTIPOLYGON, and GEOMETRYCOLLECTION) restrict collection members to those having a particular geometry type.