Divvy Voronoi Diagram
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"> | |
<html> | |
<head> | |
<meta name="generator" content= | |
"HTML Tidy for Windows (vers 14 February 2006), see www.w3.org"> | |
<meta name="viewport" content="initial-scale=1.0, user-scalable=no"> | |
<style type="text/css"> | |
html { height: 100% } | |
body { height: 100%; margin: 0; padding: 0 } | |
#map-canvas { height: 100% } | |
#bikesUI, #docksUI { | |
background-color: #fff; | |
border: 1px solid #ddd; | |
cursor: pointer; | |
float: left; | |
text-align: center; | |
} | |
#bikesText, #docksText { | |
color: rgb(25,25,25); | |
font-family: Roboto,Arial,sans-serif; | |
font-size: 16px; | |
line-height: 25px; | |
padding: 10px; | |
} | |
.control { | |
} | |
.control.Selected { | |
font-weight: bold; | |
} | |
</style> | |
<script src="https://d3js.org/d3.v3.min.js" type="text/javascript"></script> | |
<script type="text/javascript" src= | |
"https://maps.googleapis.com/maps/api/js?key=AIzaSyAbM8EJ5BxK80eOtDUTBtBflW77ISJrfic"></script> | |
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> | |
<script type="text/javascript"> | |
// borrowed some of this from https://www.divvybikes.com/assets/js/chicago/stations.js | |
var map; | |
var infowindow; | |
var bounds; | |
var stationList; | |
var fillOpacity = 0.3; | |
var iconScale = 0.5; | |
var divvyColor = '#38b5e6'; | |
var im = 'https://www.robotwoods.com/dev/misc/bluecircle.png'; | |
var userPos; | |
function stationControl(controlDiv, map, bikes) { | |
// We set up a variable for this since we're adding event listeners | |
// later. | |
var control = this; | |
// Set the center property upon construction | |
controlDiv.style.clear = 'both'; | |
// Set CSS for the control border | |
var bikesUI = document.createElement('div'); | |
bikesUI.id = 'bikesUI'; | |
bikesUI.title = 'Click for available Bikes'; | |
bikesUI.className = 'control'; | |
if(bikes) bikesUI.className += ' Selected'; | |
controlDiv.appendChild(bikesUI); | |
// Set CSS for the control interior | |
var bikesText = document.createElement('div'); | |
bikesText.id = 'bikesText'; | |
bikesText.innerHTML = 'Bikes'; | |
bikesUI.appendChild(bikesText); | |
// Set CSS for the setCenter control border | |
var docksUI = document.createElement('div'); | |
docksUI.id = 'docksUI'; | |
docksUI.title = 'Click for available Docks'; | |
docksUI.className = 'control'; | |
if(!bikes) docksUI.className += ' Selected'; | |
controlDiv.appendChild(docksUI); | |
// Set CSS for the control interior | |
var docksText = document.createElement('div'); | |
docksText.id = 'docksText'; | |
docksText.innerHTML = 'Docks'; | |
docksUI.appendChild(docksText); | |
// Set up the click event listener for 'Center Map': Set the center of | |
// the map | |
// to the current center of the control. | |
bikesUI.addEventListener('click', function() { | |
bikesUI.className = 'control Selected'; | |
docksUI.className = 'control'; | |
clearMap(); | |
drawMap(true); | |
}); | |
// Set up the click event listener for 'Set Center': Set the center of | |
// the control to the current center of the map. | |
docksUI.addEventListener('click', function() { | |
docksUI.className = 'control Selected'; | |
bikesUI.className = 'control'; | |
clearMap(); | |
drawMap(false); | |
}); | |
} | |
// http://code.tutsplus.com/tutorials/quick-tip-cross-domain-ajax-request-with-yql-and-jquery--net-10225 | |
// Accepts a url and a callback function to run. | |
function requestCrossDomain(site, callback) { | |
// If no url was passed, exit. | |
if (!site) { | |
alert('No site was passed.'); | |
return false; | |
} | |
// Take the provided url, and add it to a YQL query. Make sure you encode it! | |
var yql = 'https://query.yahooapis.com/v1/public/yql?q=' + encodeURIComponent('select * from html where url="' + site + '"') + '&format=xml'; | |
// Request that YSQL string, and run a callback function. | |
// Pass a defined function to prevent cache-busting. | |
$.get(yql, cbFunc); | |
function cbFunc(data) { | |
// parse xml response | |
var xml = data.documentElement.outerHTML, | |
xmlDoc = $.parseXML(xml), | |
$xml = $(xmlDoc), | |
$body = $xml.find('body'); | |
// If we have something to work with... | |
if($body.text()) { | |
// If the user passed a callback, and it | |
// is a function, call it, and send through the data var. | |
if(typeof callback === 'function') { | |
//callback($body.text()); | |
var json = $.parseJSON($body.text()); | |
callback(json); | |
} | |
} | |
// Else, Maybe we requested a site that doesn't exist, and nothing returned. | |
else throw new Error('Nothing returned from getJSON.'); | |
} | |
} | |
// clear markers and polygons | |
function clearMap() { | |
for(var j = 0; j < stationList.active.length; j++) { | |
if(stationList.active[j].marker) stationList.active[j].marker.setMap(null); | |
if(stationList.active[j].polygon) stationList.active[j].polygon.setMap(null); | |
} | |
for(var j = 0; j < stationList.inactive.length; j++) { | |
if(stationList.inactive[j].marker) stationList.inactive[j].marker.setMap(null); | |
} | |
} | |
// draw map | |
function drawMap(bikes) { | |
// initialize station list | |
stationList = {active: [], inactive: []}; | |
//$.getJSON("stations.json", function(stations) { | |
requestCrossDomain('feeds.divvybikes.com/stations/stations.json', function(stations) { | |
// iterate through each station | |
$.each(stations.stationBeanList, function(i, station) { | |
// get marker location | |
var point = new google.maps.LatLng(station.latitude, station.longitude); | |
bounds.extend(point); | |
var image = 'https://member.divvybikes.com/assets/images/chicago/icons/stations/map-icon-inservice-legend.png' | |
var active = true; | |
if((!bikes && (station.availableDocks == 0)) || (bikes && (station.availableBikes == 0))) { | |
image = 'https://member.divvybikes.com/assets/images/chicago/icons/stations/map-icon-inservice-legend.png'; | |
active = false; | |
} | |
// create marker | |
var marker = new google.maps.Marker({ | |
icon: { | |
url: image, | |
scaledSize: new google.maps.Size(41*iconScale, 53*iconScale) | |
}, | |
position : point, | |
map : map, | |
title : station.stationName | |
}); | |
var contentString = station.stationName + '<br> ' + 'Available Bikes: ' + station.availableBikes + '<br> ' + 'Available Docks: ' + station.availableDocks; | |
// add listener to marker click | |
google.maps.event.addListener(marker, 'click', function() { | |
infowindow.setContent(contentString); | |
infowindow.open(map, this); | |
// iterate through station list | |
for(var j = 0; j < stationList.length; j++) { | |
var opacity = 0.0; | |
// check if this marker was clicked | |
if(this == stationList[j].marker) { | |
opacity = fillOpacity; | |
} | |
// set opacity | |
stationList[j].polygon.setOptions({ | |
fillOpacity: opacity | |
}); | |
} | |
}); | |
// add marker to station list | |
s = {marker: marker, name: station.stationName, position: [station.latitude, station.longitude], content: contentString} | |
if((!bikes && (station.availableDocks == 0)) || (bikes && (station.availableBikes == 0))) stationList.inactive.push(s); | |
else stationList.active.push(s); | |
}) | |
// generate voronoi polygons | |
voronoiDiagram(userPos); | |
}); | |
} | |
// initialize call for window load | |
function initialize() { | |
// specify location | |
var chicago = new google.maps.LatLng(41.898188, -87.631073); | |
userPos = chicago; | |
// show bikes first by default | |
var type = 'bikes'; | |
// create map and info window | |
var mapOptions = { | |
center: chicago, | |
zoom: 14 | |
}; | |
map = new google.maps.Map(document.getElementById("map-canvas"), mapOptions); | |
infowindow = new google.maps.InfoWindow(); | |
// keep track of bounds for recentering the map | |
bounds = new google.maps.LatLngBounds(); | |
// create controls | |
var stationControlDiv = document.createElement('div'); | |
var control = new stationControl(stationControlDiv, map, type); | |
// show controls | |
stationControlDiv.index = 1; | |
//stationControlDiv.style['padding-top'] = '10px'; | |
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(stationControlDiv); | |
// get location | |
// Try HTML5 geolocation. | |
if (navigator.geolocation) { | |
navigator.geolocation.getCurrentPosition(function(position) { | |
var pos = { | |
lat: position.coords.latitude, | |
lng: position.coords.longitude | |
}; | |
map.setCenter(pos); | |
var userMarker = new google.maps.Marker({ | |
position: pos, | |
map: map, | |
icon: im | |
}); | |
// store user position | |
userPos = pos; | |
// draw map | |
drawMap(type); | |
}, function() { | |
// couldn't get position, draw map anyway | |
drawMap(type); | |
}); | |
} else { | |
// Browser doesn't support Geolocation draw map anyway | |
drawMap(type); | |
} | |
} | |
// from https://github.com/substack/point-in-polygon | |
function pointInPolygon(point, vs) { | |
// ray-casting algorithm based on | |
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html | |
var xi, xj, i, intersect, | |
x = point[0], | |
y = point[1], | |
inside = false; | |
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) { | |
xi = vs[i][0], | |
yi = vs[i][1], | |
xj = vs[j][0], | |
yj = vs[j][1], | |
intersect = ((yi > y) != (yj > y)) | |
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi); | |
if (intersect) inside = !inside; | |
} | |
return inside; | |
} | |
function voronoiDiagram(pos) { | |
// generate voronio polygons | |
var widthAdj = (bounds.getNorthEast().lng() - bounds.getSouthWest().lng())/2.0; | |
var heightAdj = (bounds.getNorthEast().lat() - bounds.getSouthWest().lat())/2.0; | |
var voronoi = d3.geom.voronoi().clipExtent([[bounds.getSouthWest().lat() - heightAdj, bounds.getSouthWest().lng() - widthAdj], [bounds.getNorthEast().lat() + heightAdj, bounds.getNorthEast().lng() + widthAdj]]); | |
var positions = []; | |
for(i = 0; i < stationList.active.length; i++) { | |
positions.push(stationList.active[i].position); | |
} | |
var polygons = voronoi(positions); | |
// iterate through polygons, draw on map | |
if(!polygons) return false; | |
for(var i = 0; i < polygons.length; i++) { | |
var voronoiCoords = []; | |
// store coordinates for each polygon | |
for(var j = 0; j < polygons[i].length; j++) { | |
voronoiCoords.push(new google.maps.LatLng(polygons[i][j][0], polygons[i][j][1])); | |
} | |
// complete polygon | |
voronoiCoords.push(new google.maps.LatLng(polygons[i][0][0], polygons[i][0][1])); | |
// create google maps polygon and draw | |
var voronoiPolygon = new google.maps.Polygon({ | |
paths: voronoiCoords, | |
strokeColor: divvyColor, | |
strokeOpacity: fillOpacity, | |
strokeWeight: 3, | |
fillColor: divvyColor, | |
fillOpacity: 0.0 | |
}); | |
voronoiPolygon.setMap(map); | |
// add listener to polygon click | |
google.maps.event.addListener(voronoiPolygon, 'click', function(event) { | |
// iterate through station list | |
for(var j = 0; j < stationList.active.length; j++) { | |
var opacity = 0.0; | |
// check if this marker was clicked | |
if(this == stationList.active[j].polygon) { | |
opacity = fillOpacity; | |
// open marker's info window | |
infowindow.setContent(stationList.active[j].content); | |
infowindow.open(map, stationList.active[j].marker); | |
} | |
// set opacity | |
stationList.active[j].polygon.setOptions({ | |
fillOpacity: opacity | |
}); | |
} | |
}); | |
stationList.active[i].polygon = voronoiPolygon; | |
// check for closest station to geolocation | |
if(pos) { | |
if(pointInPolygon([pos.lat, pos.lng], polygons[i])) { | |
infowindow.setContent(stationList.active[i].content); | |
infowindow.open(map, stationList.active[i].marker); | |
stationList.active[i].polygon.setOptions({ | |
fillOpacity: fillOpacity | |
}); | |
} | |
} | |
} | |
} | |
// call initialize on window load | |
google.maps.event.addDomListener(window, 'load', initialize); | |
</script> | |
<title></title> | |
</head> | |
<body> | |
<div id="map-canvas"> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment