var canvasWidth;
var canvasHeight;
var mapScale = 43;
var c;
var ct;
var offset = 0;
var animationId;
var map = new SubwayMap();
var timer;
/**
* Subway Map Object constructor
* @class SubwayMap
*/
function SubwayMap(){
/**
* @property that
*/
var that = this;
/**
* Array of subway stations, represents a route
* @property route
*/
this.route = [];
/**
* Array of subway lines
* @property subwayLines
*/
this.subwayLines = [];
/**
* Calculates subway line relative width
* @method getLineWidth
* @private
* @return {number} width of the subway line
*/
getLineWidth = function() {
return canvasWidth * 0.005;
}
/**
* Calculates subway station relative radiues
* @method getStationRadius
* @private
* @return {number} radius of the subway station
*/
getStationRadius = function() {
return canvasWidth * 0.01;
}
/**
* Calculates X coordinate on the canvas plane
* @method getCanvasX
* @private
* @param {number} X relative X coordinate
* @return {number} X coordinate on the canvas plane
*/
getCanvasX = function(X) {
return (canvasWidth * X)/mapScale;
}
/**
* Calculates Y coordinate on the canvas plane
* @method getCanvasY
* @private
* @param {number} Y relative Y coordinate
* @return {number} Y coordinate on the canvas plane
*/
getCanvasY = function(Y) {
return (canvasHeight * Y)/mapScale;
};
/**
* Calculates an array of station between station of origin to closest intersection with passed subway line
* @method getStationsToLine
* @private
* @param {Object} stationfrom station of origin
* @param {number} lineTo subway line id
* @return {Object[]} an array of stations
*/
getStationsToLine = function (stationfrom, lineTo) {
var lineStations = getStationsByLineId(stationfrom.line_id);
var indexOfOrigin = lineStations.indexOf(stationfrom);
var candidateOne = [], candidateTwo = [];
var candidateOneFound = false;
var candidateTwoFound = false;
var i = indexOfOrigin;
var j = indexOfOrigin;
parentLoop:
for(var i = indexOfOrigin; i < lineStations.length; i++) {
candidateOne.push(lineStations[i]);
if(lineStations[i].intersection_id != -1) {
var sts1 = getStationsByIntersectionId(lineStations[i].intersection_id);
childLoop:
for(var k = 0; k < sts1.length; k++) {
if(sts1[k].line_id == lineTo) {
candidateOneFound = true;
break parentLoop;
}
}
}
}
parentLoop:
for(var i = indexOfOrigin; i >= 0; i--) {
candidateTwo.push(lineStations[i]);
if(lineStations[i].intersection_id != -1) {
var sts1 = getStationsByIntersectionId(lineStations[i].intersection_id);
childLoop:
for(var k = 0; k < sts1.length; k++) {
if(sts1[k].line_id == lineTo) {
candidateTwoFound = true;
break parentLoop;
}
}
}
}
var candidateOneTrueStations = [];
var candidateTwoTrueStations = [];
for(var i = 0; i < candidateOne.length; i++) {
if(candidateOne[i].name != "not-a-station") {
candidateOneTrueStations.push(candidateOne[i]);
}
}
for(var i = 0; i < candidateTwo.length; i++) {
if(candidateTwo[i].name != "not-a-station") {
candidateTwoTrueStations.push(candidateTwo[i]);
}
}
if(!candidateOneFound) return candidateTwo;
if(!candidateTwoFound) return candidateOne;
if(candidateOneTrueStations.length >= candidateTwoTrueStations.length) return candidateTwo;
if(candidateOneTrueStations.length <= candidateTwoTrueStations.length) return candidateOne;
}
/**
* Calculates an array of subway stations with a passed intersection ID
* @method getStationsByIntersectionId
* @private
* @param {number} intersection_id intersection ID
* @return {Object[]} an array of subway stations
*/
getStationsByIntersectionId = function(intersection_id) {
var intersections = [];
for(var i = 0; i < that.subwayLines.length; i++) {
var curLine = that.subwayLines[i];
for(var j = 0; j < curLine.stations.length; j++) {
if(curLine.stations[j].intersection_id == intersection_id) {
intersections.push(curLine.stations[j]);
}
}
}
return intersections;
}
/**
* Calculates an array of subway lines which intersects two given lines
* @method getSharedLines
* @private
* @param {number} lineOne first subway line ID
* @param {number} lineTwo second subway line ID
* @return {Object[]} an aray of subway lines
*/
getSharedLines = function(lineOne, lineTwo) {
var lineOneIntersections = getIntersectionsByLineId(lineOne);
var lineTwoIntersections = getIntersectionsByLineId(lineTwo);
var lineOneIntersectionLines = []
for(var i = 0; i < lineOneIntersections.length; i++) {
var stns = getStationsByIntersectionId(lineOneIntersections[i].intersection_id)
for(var j = 0; j < stns.length; j++) {
if(stns[j].line_id != lineOne) lineOneIntersectionLines.push(stns[j].line_id)
}
}
var lineTwoIntersectionLines = []
for(var i = 0; i < lineTwoIntersections.length; i++) {
var stns = getStationsByIntersectionId(lineTwoIntersections[i].intersection_id)
for(var j = 0; j < stns.length; j++) {
if(stns[j].line_id != lineTwo) lineTwoIntersectionLines.push(stns[j].line_id)
}
}
var sharedLines = [];
for(var i = 0; i < lineOneIntersectionLines.length; i++) {
for(var j = 0; j < lineTwoIntersectionLines.length; j++) {
if(lineOneIntersectionLines[i] == lineTwoIntersectionLines[j]) {
sharedLines.push(lineOneIntersectionLines[i]);
}
}
}
var uniqueSharedLines = sharedLines.filter(function(elem, pos) {
return sharedLines.indexOf(elem) == pos;
});
return uniqueSharedLines;
};
/**
* Calculates an array of subway stations on a given subway line which intersects with other subway lines
* @method getIntersectionsByLineId
* @private
* @param {number} line_id subway line ID
* @return {Object[]} an array of subway stations
*/
getIntersectionsByLineId = function(line_id) {
var line = getStationsByLineId(line_id).slice();
var i = line.length;
while(i--) {
if(line[i].intersection_id == -1)
line.splice(i, 1);
};
return line;
}
/**
* Calculates an array of colors for a given station
* @method getStationColors
* @private
* @param {Object} station a subway station
* @return {String[]} an array of string which represents subway station color to be paint
*/
getStationColors = function(station) {
var colors = [];
intersectionId = station.intersection_id;
if(intersectionId == -1) {
for(var i = 0; i < that.subwayLines.length; i++) {
if(station.line_id == that.subwayLines[i].id) {
colors.push(that.subwayLines[i].color);
break;
}
}
} else {
for(var i = 0; i < that.subwayLines.length; i++) {
var curLine = that.subwayLines[i];
for(var j = 0; j < curLine.stations.length; j++) {
var curStation = that.subwayLines[i].stations[j];
if(curStation.intersection_id == intersectionId) {
colors.push(curLine.color);
}
}
}
}
return colors;
}
/**
* Gets a station object on a given line which has shared intersection with a given station
* @method getStationOnIntersectedLine
* @private
* @param {number} lineId subway line ID
* @param {Object} station subway station
* @return {Object} a subway station
*/
getStationOnIntersectedLine = function (lineId, station) {
var intersections = getIntersectionsByLineId(lineId);
for(var i = 0; i < intersections.length; i++) {
if(station.intersection_id == intersections[i].intersection_id) {
return intersections[i];
}
}
}
/**
* Gets a subway line object by subway line name
* @method getLineByName
* @private
* @param {string} name subway line name
* @return {Object} a subway line
*/
getLineByName = function(name){
for(var i = 0; i < that.subwayLines.length; i++) {
if(that.subwayLines[i].name == name) {
return that.subwayLines[i];
}
}
};
/**
* Gets an array of subway stations by subway line ID
* @method getStationsByLineId
* @private
* @param {number} id subway line ID
* @return {Object[]} an array of subway stations
*/
getStationsByLineId = function(id){
for(var i = 0; i < that.subwayLines.length; i++) {
if(that.subwayLines[i].id == id) {
return that.subwayLines[i].stations;
}
}
};
/**
* Gets a subway station with by subway station ID
* @method getStationById
* @private
* @param {number} id subway station ID
* @return {Object} a subway station
*/
getStationById = function(id){
for(var i = 0; i < that.subwayLines.length; i++) {
var line = that.subwayLines[i];
for(j = 0; j < line.stations.length; j++) {
var station = line.stations[j];
if(station.station_id == id) {
return station;
}
}
}
};
/**
* Draws subway lines on canvas
* @method drawSubwayLines
* @private
*/
drawSubwayLines = function(){
for(var i = 0; i < that.subwayLines.length; i++) {
for(var j = 0; j < that.subwayLines[i].stations.length; j++) {
if(j == 0) continue;
var curStation = that.subwayLines[i].stations[j];
var prevStation = that.subwayLines[i].stations[j - 1];
if(curStation.name == "Biblioteka Imeni Lenina" && that.subwayLines[i].name == "Sokolnicheskaya"){
ct.beginPath();
ct.moveTo(getCanvasX(prevStation.X),getCanvasY(prevStation.Y) + 1.25 * getLineWidth());
ct.lineTo(getCanvasX(curStation.X),getCanvasX(curStation.Y) + 1.25 * getLineWidth());
ct.strokeStyle = that.subwayLines[i].color;
ct.lineWidth = getLineWidth();
ct.stroke();
continue;
}
if(curStation.name == "Ploshad' revolutcii" && that.subwayLines[i].name == "Arbatskaya"){
ct.beginPath();
ct.moveTo(getCanvasX(prevStation.X),getCanvasY(prevStation.Y) - 1.25 * getLineWidth());
ct.lineTo(getCanvasX(curStation.X),getCanvasX(curStation.Y) - 1.25 * getLineWidth());
ct.strokeStyle = that.subwayLines[i].color;
ct.lineWidth = getLineWidth();
ct.stroke();
continue;
}
ct.beginPath();
ct.moveTo(getCanvasX(prevStation.X),getCanvasY(prevStation.Y));
ct.lineTo(getCanvasX(curStation.X),getCanvasX(curStation.Y));
ct.strokeStyle = that.subwayLines[i].color;
ct.lineWidth = getLineWidth();
ct.stroke();
}
}
};
/**
* Draws subway stations on canvas
* @method drawSubwayStations
* @private
*/
drawSubwayStations = function(){
for(var i = 0; i < that.subwayLines.length; i++) {
for(var j = 0; j < that.subwayLines[i].stations.length; j++) {
var curStation = that.subwayLines[i].stations[j];
if(curStation.name == "not-a-station") continue;
ct.beginPath();
ct.arc(getCanvasX(curStation.X),getCanvasY(curStation.Y),getStationRadius() + 1,0,2*Math.PI);
ct.fillStyle = 'black';
ct.fill();
var colors = getStationColors(curStation);
var tau = Math.PI * 2;
var frac = tau/colors.length;
for(var k = 0; k < colors.length; k++) {
ct.beginPath();
ct.moveTo(getCanvasX(curStation.X), getCanvasY(curStation.Y));
ct.arc(getCanvasX(curStation.X),getCanvasY(curStation.Y),getStationRadius(), k * frac, (k + 1) * frac);
ct.fillStyle = colors[k];
ct.fill();
}
}
}
};
/**
* Draws subway station labels on canvas
* @method drawLabels
* @private
*/
drawLabels = function() {
var names = [];
for(var i = 0; i < that.subwayLines.length; i++) {
for(var j = 0; j < that.subwayLines[i].stations.length; j++) {
var curStation = that.subwayLines[i].stations[j];
if(curStation.name == "not-a-station") continue;
if(curStation.name != "Arbatskaya" && curStation.name != "Smolenskaya") {
if(names.indexOf(curStation.name) > -1) {
continue;
}
}
names.push(curStation.name);
var xOffset, yOffset;
if(curStation.labelposition == "top-right") {
xOffset = 5;
yOffset = getStationRadius() * -3.2;
}
if(curStation.labelposition == "right") {
xOffset = getStationRadius() * 2;
yOffset = -7.5;
}
if(curStation.labelposition == "bottom-right") {
xOffset = 5;
yOffset = getStationRadius() * 1.5;
}
if(curStation.labelposition == "left") {
xOffset = -1 * ct.measureText(curStation.name).width - getStationRadius() * 2;
yOffset = -7.5;
}
if(curStation.labelposition == "bottom-left") {
xOffset = -1 * ct.measureText(curStation.name).width - getStationRadius() * 1.5;
yOffset = getStationRadius() * 1.5;
}
ct.lineWidth = 1;
ct.strokeStyle = "rgba(0, 0, 0, 0.3)";
ct.fillStyle = "rgba(200, 200, 200, 0.3)";
roundRect(
ct,
getCanvasX(curStation.X) + xOffset - 5,
getCanvasY(curStation.Y) + yOffset,
ct.measureText(curStation.name).width + 10,
15,
3,
true
);
ct.textBaseline = 'top';
ct.fillStyle = "black";
ct.font = "10px sans-serif";
ct.fillText(curStation.name, getCanvasX(curStation.X) + xOffset, getCanvasY(curStation.Y) + yOffset);
}
}
}
/**
* Gets an array of subway stations between station of origin and station of destination on the same subway line
* @method getStationsBetween
* @private
* @param {Object} from station of origin
* @param {Object} to station of destination
* @return {Object[]} an array of subway stations
*/
getStationsBetween = function(from, to) {
if(from.station_id == to.station_id) {
return [];
}
var lineStations = getStationsByLineId(from.line_id);
var candidateOne = [], candidateTwo = [], stationsBetween = [];
var indexOfOrigin = lineStations.indexOf(from);
var indexOfDestination = lineStations.indexOf(to);
var i = indexOfOrigin;
var j = indexOfOrigin;
candidateOne.push(lineStations[i]);
candidateTwo.push(lineStations[j]);
do {
i++;
j--;
if(from.line_id == 1) {
if(i == lineStations.length) i = 0;
if(j == -1) j = lineStations.length-1;
if(lineStations[i].name != 'not-a-station') candidateOne.push(lineStations[i]);
if(lineStations[j].name != 'not-a-station') candidateTwo.push(lineStations[j]);
} else {
if(i == lineStations.length) i = lineStations.length - 1;
if(j == -1) j = 0;
candidateOne.push(lineStations[i]);
candidateTwo.push(lineStations[j]);
}
} while(i!=indexOfDestination && j!=indexOfDestination);
if(candidateOne[candidateOne.length - 1] == to) return candidateOne;
else return candidateTwo;
}
/**
* A method to test the subway map
* @method testMap
*/
this.testMap = function() {
var origins = map.subwayLines;
var destinations = map.subwayLines;
for(var i = 0; i < origins.length; i++) {
var originLine = origins[i];
for(var j = 0; j < originLine.stations.length; j++) {
var originStation = origins[i].stations[j];
for(var k = 0; k < destinations.length; k++) {
var destinationLine = destinations[k];
for(var m = 0; m < destinations[k].stations.length; m++) {
var destinationStation = destinations[k].stations[m];
if(originStation.station_id == destinationStation.station_id) continue;
if(originStation.name == "not-a-station") continue;
if(destinationStation.name == "not-a-station") continue;
var sts = [];
that.setRoute(originStation.station_id, destinationStation.station_id);
console.log("From: " + originStation.name + "(" + originLine.name + ") To: " + destinationStation.name + "(" +destinationLine.name+ ") Total stations between: " + that.route.length);
}
}
}
}
}
/**
* Calculates most optimal route between two given stations
* @method setRoute
* @param {number} StationIdfrom ID of the station of origin
* @param {number} StationIdto ID of the station of destination
* @return {Object[]} an array of stations
*/
this.setRoute = function(StationIdfrom, StationIdto){
that.route = [];
var tempRoute = [];
var dirtyCandidates = [];
from = getStationById(StationIdfrom);
to = getStationById(StationIdto);
if(from.line_id == to.line_id) {
that.route = getStationsBetween(from, to);
}
else {
var fromLineIntersections = getIntersectionsByLineId(from.line_id);
var toLineIntersections = getIntersectionsByLineId(to.line_id);
var froms = [];
if(from.intersection_id != -1) {
froms = getStationsByIntersectionId(from.intersection_id);
} else {
froms.push(from);
}
for(var j = 0; j < froms.length; j++) {
var sharedLines = getSharedLines(froms[j].line_id, to.line_id);
for(var i = 0; i < sharedLines.length; i++) {
var originLineStations = getStationsToLine(froms[j], sharedLines[i]);
var destinationLineStations = getStationsToLine(to, sharedLines[i]);
var stn1 = getStationOnIntersectedLine(sharedLines[i],originLineStations[originLineStations.length - 1]);
var stn2 = getStationOnIntersectedLine(sharedLines[i],destinationLineStations[destinationLineStations.length - 1]);
var stationBetween = getStationsBetween(stn1, stn2);
destinationLineStations.reverse();
var dirtyCandidate = originLineStations.concat(stationBetween, destinationLineStations);
dirtyCandidates.push(dirtyCandidate);
}
}
var sharedIntersections = [];
for(var i = 0; i < fromLineIntersections.length; i++) {
for(var j = 0; j < toLineIntersections.length; j++) {
if(fromLineIntersections[i].intersection_id == toLineIntersections[j].intersection_id) {
sharedIntersections.push({
"from" : fromLineIntersections[i],
"to" : toLineIntersections[j]
});
}
}
}
for(var i = 0; i < sharedIntersections.length; i++) {
var arr1 = getStationsBetween(from, sharedIntersections[i].from);
var arr2 = getStationsBetween(sharedIntersections[i].to, to);
dirtyCandidate = arr1.concat(arr2);
dirtyCandidates.push(dirtyCandidate);
}
var cleanCandidates = [];
for(var i = 0; i < dirtyCandidates.length; i++) {
var dirtyCandidate = dirtyCandidates[i];
var cleanCandidate = [];
parentLoop:
for(var j = 0; j < dirtyCandidate.length; j++) {
if(dirtyCandidate[j].name != "not-a-station") {
for(var k = 0; k < cleanCandidate.length; k++){
if(dirtyCandidate[j].X == cleanCandidate[k].X && dirtyCandidate[j].Y == cleanCandidate[k].Y) {
continue parentLoop;
}
}
cleanCandidate.push(dirtyCandidate[j]);
}
}
cleanCandidates.push(cleanCandidate);
}
var candidateCount = 100;
for(var i = 0; i < cleanCandidates.length; i++) {
if(cleanCandidates[i].length < candidateCount) {
candidateCount = cleanCandidates[i].length;
tempRoute = dirtyCandidates[i];
}
}
that.route = tempRoute;
}
};
/**
* Draws the route
* @method drawRoute
* @private
*/
drawRoute = function() {
offset++;
if (offset > 1000) {
offset = 0;
}
for(var i = 0; i < that.route.length; i++) {
if(i == 0) continue;
var curStation = that.route[i];
var prevStation = that.route[i - 1];
ct.beginPath();
ct.moveTo(getCanvasX(prevStation.X),getCanvasY(prevStation.Y));
ct.lineTo(getCanvasX(curStation.X),getCanvasX(curStation.Y));
ct.strokeStyle = "black";
ct.setLineDash([10]);
ct.lineDashOffset = -offset;
ct.lineWidth = 10;
ct.stroke();
}
}
/**
* Description
* @method drawSubwayMap
* @return
*/
this.drawSubwayMap = function(){
c.attr('width', $(c).parent().width());
c.attr('height', $(c).parent().width());
canvasWidth = c.width();
canvasHeight = c.height();
drawSubwayLines();
drawLabels();
if(map.route.length > 0) {
drawRoute(route);
}
drawSubwayStations();
}
}
/**
* Draws rounded rectangle on a canvas
* @method roundRect
* @param {Object} ctx a canvas context
* @param {number} x coordinate
* @param {number} y coordinate
* @param {number} width
* @param {number} height
* @param {number} radius
* @param {boolean} fill
* @param {boolean} stroke
*/
function roundRect(ctx, x, y, width, height, radius, fill, stroke) {
if (typeof stroke == 'undefined') {
stroke = true;
}
if (typeof radius === 'undefined') {
radius = 5;
}
if (typeof radius === 'number') {
radius = {tl: radius, tr: radius, br: radius, bl: radius};
} else {
var defaultRadius = {tl: 0, tr: 0, br: 0, bl: 0};
for (var side in defaultRadius) {
radius[side] = radius[side] || defaultRadius[side];
}
}
ctx.beginPath();
ctx.moveTo(x + radius.tl, y);
ctx.lineTo(x + width - radius.tr, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
ctx.lineTo(x + width, y + height - radius.br);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
ctx.lineTo(x + radius.bl, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
ctx.lineTo(x, y + radius.tl);
ctx.quadraticCurveTo(x, y, x + radius.tl, y);
ctx.closePath();
if (fill) {
ctx.fill();
}
if (stroke) {
ctx.stroke();
}
}
/**
* Fills dropdown menus with stations data
* @method fillDropDown
* @param {Object[]} data an array of stations
* @param {string} id id of the select element
*/
var fillDropDown = function(data, id) {
var select = $(id);
var searchItems = [];
var icon;
for(var i = 0; i < data.length; i++) {
var curLine = data[i];
for(var j = 0; j < curLine.stations.length; j++) {
var curStation = curLine.stations[j];
if(curStation.name == "not-a-station") continue;
var icon;
switch(curLine.id) {
case "1":
icon = "images/lines/brown.png";
break;
case "2":
icon = "images/lines/green.png";
break;
case "3":
icon = "images/lines/orange.png";
break;
case "4":
icon = "images/lines/red.png";
break;
case "5":
icon = "images/lines/darkblue.png";
break;
case "6":
icon = "images/lines/lightblue.png";
break;
case "7":
icon = "images/lines/pink.png";
break;
case "8":
icon = "images/lines/yellow.png";
break;
}
searchItems.push({
value: curStation.station_id,
label: curStation.name,
icon: icon
});
}
}
$(id).autocomplete({
source: searchItems
});
}
$("#clear").click(function() {
map.drawSubwayMap();
});
/**
* Renders canvas
* @method render
*/
function render() {
if(map.route.length > 0) {
timer = setTimeout(function() {
animationId = requestAnimationFrame(render);
map.drawSubwayMap();
}, 1000 / 100);
} else {
map.drawSubwayMap();
}
}
$(document).ready( function(){
$("#route").click(function() {
cancelAnimationFrame(animationId);
clearTimeout(timer);
map.setRoute($("#originHidden").val(), $("#destinationHidden").val());
render();
});
$("#test").click(function() {
var origins = map.testMap();
});
var req = $.get( "get-data.php")
.fail(function() {
})
.done(function(data) {
map.subwayLines = data;
fillDropDown(data, "#origin");
fillDropDown(data, "#destination");
map.drawSubwayMap();
});
c = $('#canvas');
ct = c.get(0).getContext('2d');
$(window).resize(render);
render();
});