This summer Mobility Lab was recruited by BikeArlington to devise an online tool that would aid in providing feedback on proposed Capital Bikeshare station locations from Rosslyn to Ballston Common. What we devised was new and can be harnessed almost anywhere. The Crowdsourcing tool as we called it ties Google Maps with the database of your choice allowing you to use Maps as a survey tool. In the instance of this project, users were given the opportunity to click on a proposed station pin and provide comment. Furthermore, users could pick their own locations and comment on each others’ suggestions. This article attempts to support your efforts in launching your own geographically based survey. From here things get a bit more technical. You can start by downloading the Opensource Crowdsourcing Tool here.
What you need to know for this project
You will need to be familiar with Javascript, XML, a tiny bit of jQuery, and PHP. The Google Maps API works off of Javascript so if you have a basic understanding of Javascript you’re most of the way there already.
Make sure to download the archive so you can follow along in the script as we cover its functionality. We will be using index.php.
Here are some of the things you’ll need to have to plug in to various locations as you go:
- Coordinates for the center of the map (don’t worry, if you don’t know how to get that we’ll be showing you how).
- KML file you’ll be using for click limitations.
- XML files for marker point locations and information.
A slight disclaimer: For this project we are using XML files to store the information for both the markers and their related information as well as the user registration information. It is suggested that you use your database of choice though for reasons of security and data writing speed.
The Basics of Google Map Creation – Specifying the center of your map
(You’ll need the coordinates here)
Creating a basic Google is relatively simple, all you need is to specify a location by its ID.
The Latitude and Longitude of the center of your map, which can be retrieved by right clicking the desired location on Google Maps and selecting “What’s here?” The map search bar will then be populated with the map coordinates you need.
You then need to take the comma separated coordinates and place them within the parenthesis for new.google.maps.LatLng() (line 6 in the script example below).
Script
var map;
var markers = [];
var geocoder = new google.maps.Geocoder();
function initialize() {
var myLatlng = new google.maps.LatLng(38.885965, -77.099097); //here we are setting the variable for the center of the google map
var myOptions = {
zoom: 14, //setting the initial zoom of the google map
center: myLatlng, //setting the center of the google map to the center variable
zoomControlOptions: {style: google.maps.ZoomControlStyle.SMALL}, //formatting the zoom slider
mapTypeId: google.maps.MapTypeId.ROADMAP //selecting the map render type (default available map types are ROADMAP, SATELLITE, HYBRID, TERRAIN)
}
map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
HTML
<div id="map_canvas"></div>
Populating the map
(You’ll need the XML file with the marker point locations here)
We will be referencing an XML file to populate the map with the our markers. Each marker element in the XML will represent a particular marker and its constituent information.
Example of XML:
<marker lat="LATITUDE" lng="LONGITUDE" name="STREET ADDRESS" type="DETERMINES THE TYPE OF MARKER USED">
<comment user="USER THAT MADE THE COMMENT" vote="LIKE OR DISLIKE" time="TIMESTAMP">THE COMMENT THE USER MADE, IF ANY</comment>
</marker>
Once we’ve accessed the XML file we will be running a loop to read over each of the marker elements and their corresponding values to generate the markers on the page as well as a nested loop that works its way through each of the comment elements that belong to that particular marker element.
The loop references a function external to the map generation function (Script 2). Below you will see downloadURL(filename, function(data){}) this specifies the file where our information is stored (in this case our XML file) and then what we want to do with the information that we’ve accessed.
Script 1:
downloadUrl("mapPointInfo.xml", function(data) {
//we begin adding markers here based on the data provided by mapPointInfo.xml you will want that for reference
var markers = data.documentElement.getElementsByTagName("marker");
//acquire all elements with the marker tag name e.g.: <marker></marker>
for (var i = 0; i < markers.length; i++) {
//the loop we are using to work through all the elements named marker and perform the desired task for each one
var latlng = new google.maps.LatLng(parseFloat(markers.getAttribute("lat")),
//get the attributes lat and lng from the current element and
parseFloat(markers.getAttribute("lng")));
//use them to set a new google map point variable value
var markerName=markers.getAttribute("name");
//get the name attribute from the current element this will be the locations name e.g.: Massachusetts Ave & Dupont Circle NW
var markerType=markers.getAttribute("type");
//get the type attribute from the current element
var comments = markers.getElementsByTagName("comment");
//acquire all subelements with the comment tag name that belong to this marker element e.g.: <marker><comment></comment></marker>
var contentString = '<div id="bodyContent"><div>Comments for this location</div><div>';
//starting the comment block for this marker location
var yesNo = "no";
var comUserBlurb = ' commented on this site :';
for (var j = 0; j < comments.length; j++) {
//the loop we are using to work through all the subelements named comment and perform the desired task for each one
var comUser = comments.getAttribute("user");
//get user attribute from the current subelement, this is the username for the person that made the comment
var comVote = comments.getAttribute("vote");
//get vote attribute from the current subelement, this is their simple like/dislike vote/opinion for ths location
var comTime = comments.getAttribute("time");
//get time attribute from the current subelement, this is the timestamp that the post was made
var locComment = '';
//initializing the variable for content of the subelement, the actual comment made by the user, there is the possibility of it being blank
if(comments.firstChild != null)
if(comments.firstChild.data != null)
locComment = comments.firstChild.data;
//if the subelement is not lacking a comment then set locComment to the value of the content
if(comVote!="" || comVote!=" " || comVote!=null){
//if the comVote attribute is not empty then
comUserBlurb = ' '+comVote+' this site: ';
}
if (comUser == "<? echo $_COOKIE ?>" && comVote!=""){
yesNo = 'yes';
//checking to see if the currently logged on user has like or disliked this location at any point
}
if (locComment != "" || locComment != null || locComment != " "){
//if locComment is not empty, null, or only consisting of a space then construct HTML comment line
contentString = contentString + '<div><div id="locComLeft"><div>'+ comUser + comUserBlurb+'</div><div>'+comTime+'</div></div><div>' + locComment + '</div></div>';
}
}
contentString = contentString + '</div></div>';
//add comment line to current HTML list of comments for this marker element
var marker = createMarker(contentString, latlng, markerType, markerName, yesNo);
//send the finished variable values to the createMarker function to add the marker to the map
}
Script 2:
function createMarker(contentString, latlng, markerType, markerName, yesNo) {
//this function is responsible for adding the markers to the map with all of their content
if(markerType=="pre-sug"){
//this loop determines the appropriatte marker icon to be used depend on the markerType value
markerIcon = 'images/mm_20_yellow.png';
} else if(markerType=="user-sug"){
markerIcon = 'images/mm_20_green.png';
} else {
markerIcon = 'images/cb-map-icon.png';
}
var marker = new google.maps.Marker({position: latlng, map: map, icon: markerIcon});
google.maps.event.addListener(marker, "click", function(){
//this adds the "click" listenter to the marker being created, the following actions will take place when the marker is clicked
if(markerType!="existing"){
if(yesNo=="yes"){
//if the cookie "user" is present we check to see if the user has voted
document.getElementById('madeChoice').className = "";
//if they have not then show the commenting field and allow them to/vote comment on the location
document.getElementById('likeDislike').className = "hide";
} else if(yesNo=="no"){
document.getElementById('madeChoice').className = "hide";
document.getElementById('likeDislike').className = "";
}
showCommentForm();
//run the showCommentForm function
clearMarkers();
//run the clearMarkers function
} else if (markerType=="existing"){
document.getElementById('commentForm').className = "hide";
//do not show the comment div; users cannot comment or vote on existing locations
}
document.getElementById('markerComments').innerHTML = "";
//clear the markerComments DIV
document.getElementById('comment_site_title').innerHTML = "";
//clear the comment_site_title DIV
document.getElementById('markerComments').innerHTML = contentString;
//Populate the markerComments DIV with the markers contentString value
var geoNameComment = document.getElementById('geoNameComment');
if(geoNameComment != null){
geoNameComment.value = '';
//clear the hidden Input geoNameComment value
geoNameComment.value = markerName;
//Populate the hidden Input geoNameComment value with the markers markerName value
}
document.getElementById('comment_site_title').innerHTML = markerName;
//Populate the comment_site_title DIV with the markers markerName value
});
return marker;
}
KML files
(You’ll need the KML file here)
KML files are a derivative of XML files used to display geographic data (more info found here: http://code.google.com/apis/kml/documentation/kml_tut.html). We will be using a KML file to create a regional boundary to constrain where the users can drop new markers. These can be easily created using Google Earth (http://www.google.com/earth).
Script:
var corridorLayer = new google.maps.KmlLayer('http://mobilitylab.org/bikearlington/corridor.kml', {map:map, suppressInfoWindows: true, preserveViewport:true});
//create click confinement layer from KML file, "map" applies the layer to variable map, "suppressInfoWindows" prevents default google map infowindow from appearing on marker/map click, "preserveViewport" prevents a change in the maps zoom value to fit the new layer
corridorLayer.setMap(map);
Event Handlers
The Google Maps API requires that you specify click events for either the map or the markers placed on the map. To do this we add an Event Listener using addListener. This is done during the creation process for the particular item that is clickable, in the case below for logged in users clicking the KML layer that we just added will place a new marker on the map.
Script:
google.maps.event.addListener(corridorLayer, "click", function(event) {
//if the "user" cookie is present render the map click listener event and refer to the addMarker Function
latlng = event.latLng;
//this allows logged on users to add new markers on the map
addMarker(latlng);
Reverse Geocoding
The Google Maps API allows you to Geocode a physical address (ie: taking a street address and converting that to the Longitude/Latitude coordinates of a location).
Reverse Geocoding will allow you to do the opposite, for situations like this one where you have the coordinates but would also like to have the street address (ie: receiving a street address from the coordinates presented).
Script:
function geocodePosition(latlng){
geocoder.geocode({
latLng: latlng
}, function(responses) {
if (responses && responses.length > 0) {
document.getElementById('comment_site_title').innerHTML = responses.formatted_address;
//Populate the comment_site_title DIV with the new markers street address
document.getElementById('geoNameMarker').value = responses.formatted_address;
//Populate the hidden Input geoNameMarker value with the new markers street address
} else {
document.getElementById('geoNameMarker').value = "Cannot determine address at this location.";
//if the street address cannot be determined then populate the hidden Input geoNameMarker value with this message
}
});
}