From 8df351cb834b73ee1589d6913713d99dc48ff304 Mon Sep 17 00:00:00 2001 From: vperez Date: Sun, 19 Aug 2018 20:16:54 -0300 Subject: [PATCH 1/4] Add synoptic view - Add length and directionId fields to shape - Add pathLength to stop - Add synoptic jsp,js and css --- .../org/transitclock/ipc/data/IpcRoute.java | 21 +- .../org/transitclock/ipc/data/IpcShape.java | 20 + .../org/transitclock/ipc/data/IpcStop.java | 12 +- .../org/transitclock/api/data/ApiShape.java | 7 +- .../org/transitclock/api/data/ApiStop.java | 5 +- .../api/data/ApiVehicleDetails.java | 9 +- .../main/webapp/synoptic/bus_verde_negro.png | Bin 0 -> 890 bytes .../src/main/webapp/synoptic/css/synoptic.css | 174 ++++ .../src/main/webapp/synoptic/index.jsp | 351 ++++++++ .../webapp/synoptic/javascript/synoptic.js | 781 ++++++++++++++++++ .../src/main/webapp/welcome/index.jsp | 1 + 11 files changed, 1374 insertions(+), 7 deletions(-) create mode 100644 transitclockWebapp/src/main/webapp/synoptic/bus_verde_negro.png create mode 100644 transitclockWebapp/src/main/webapp/synoptic/css/synoptic.css create mode 100644 transitclockWebapp/src/main/webapp/synoptic/index.jsp create mode 100644 transitclockWebapp/src/main/webapp/synoptic/javascript/synoptic.js diff --git a/transitclock/src/main/java/org/transitclock/ipc/data/IpcRoute.java b/transitclock/src/main/java/org/transitclock/ipc/data/IpcRoute.java index 626bea535..d3d43bbee 100644 --- a/transitclock/src/main/java/org/transitclock/ipc/data/IpcRoute.java +++ b/transitclock/src/main/java/org/transitclock/ipc/data/IpcRoute.java @@ -217,23 +217,40 @@ private static IpcDirectionsForRoute createStops(Route dbRoute, // Determine if UI stop. It is a UI stop if the stopId parameter // specified and the current stop is after the stopId for a UI // trip pattern. + TripPattern currentTripPattern=null; boolean isUiStop = true; if (stopId != null) { isUiStop = false; for (TripPattern tripPattern : uiTripPatterns) { + if(tripPattern.getDirectionId().compareTo(currentDirectionId)==0) + currentTripPattern=tripPattern; if (tripPattern.isStopAtOrAfterStop(stopId, currentStopId)) { isUiStop = true; break; } } } - + else + { + for (TripPattern tripPattern : uiTripPatterns) { + if(tripPattern.getDirectionId().compareTo(currentDirectionId)==0) + currentTripPattern=tripPattern; + } + } + Double stopPathLength=null; + if(currentTripPattern!=null) + { + + StopPath stopPath = currentTripPattern.getStopPath(currentStopId); + if(stopPath!=null) + stopPathLength=stopPath.getLength(); + } // Create the IpcStop and add it to the list of stops for the // current direction Stop stop = Core.getInstance().getDbConfig().getStop(currentStopId); IpcStop ipcStop = - new IpcStop(stop, isUiStop, currentDirectionId); + new IpcStop(stop, isUiStop, currentDirectionId,stopPathLength); ipcStopsForDirection.add(ipcStop); } ipcDirections.add(new IpcDirection(dbRoute, currentDirectionId, diff --git a/transitclock/src/main/java/org/transitclock/ipc/data/IpcShape.java b/transitclock/src/main/java/org/transitclock/ipc/data/IpcShape.java index dc0f6c600..627ccabba 100644 --- a/transitclock/src/main/java/org/transitclock/ipc/data/IpcShape.java +++ b/transitclock/src/main/java/org/transitclock/ipc/data/IpcShape.java @@ -37,7 +37,25 @@ public class IpcShape implements Serializable { private String headsign; private List locations; private boolean isUiShape; + private double length; + private String directionId; + public String getDirectionId() { + return directionId; + } + + public void setDirectionId(String directionId) { + this.directionId = directionId; + } + + public double getLength() { + return length; + } + + public void setLength(double length) { + this.length = length; + } + // For determining if segment from previous stop path can be // combined with segment from new stop path. private static final double MAX_VERTEX_DISTANCE = 3.0; @@ -48,6 +66,8 @@ public class IpcShape implements Serializable { IpcShape(TripPattern tripPattern, boolean isUiShape) { this.tripPatternId = tripPattern.getId(); + this.length=tripPattern.getLength(); + this.directionId=tripPattern.getDirectionId(); this.headsign = tripPattern.getHeadsign(); this.locations = new ArrayList(); this.isUiShape = isUiShape; diff --git a/transitclock/src/main/java/org/transitclock/ipc/data/IpcStop.java b/transitclock/src/main/java/org/transitclock/ipc/data/IpcStop.java index dff2d3e5b..47a3b8356 100644 --- a/transitclock/src/main/java/org/transitclock/ipc/data/IpcStop.java +++ b/transitclock/src/main/java/org/transitclock/ipc/data/IpcStop.java @@ -38,18 +38,28 @@ public class IpcStop implements Serializable { private final Location loc; private final boolean isUiStop; private final String directionId; + private Double stopPathLength; + public Double getStopPathLength() { + return stopPathLength; + } + + public void setStopPathLength(Double stopPathLength) { + this.stopPathLength = stopPathLength; + } + private static final long serialVersionUID = 8964112532327897125L; /********************** Member Functions **************************/ - public IpcStop(Stop dbStop, boolean aUiStop, String directionId) { + public IpcStop(Stop dbStop, boolean aUiStop, String directionId, Double stopPathLength) { this.id = dbStop.getId(); this.name = dbStop.getName(); this.code = dbStop.getCode(); this.loc = dbStop.getLoc(); this.isUiStop = aUiStop; this.directionId = directionId; + this.stopPathLength=stopPathLength; } /** diff --git a/transitclockApi/src/main/java/org/transitclock/api/data/ApiShape.java b/transitclockApi/src/main/java/org/transitclock/api/data/ApiShape.java index 7eedef0fe..be56ed114 100644 --- a/transitclockApi/src/main/java/org/transitclock/api/data/ApiShape.java +++ b/transitclockApi/src/main/java/org/transitclock/api/data/ApiShape.java @@ -48,6 +48,10 @@ public class ApiShape { @XmlElement(name = "loc") private List locations; + @XmlAttribute + private double length; + @XmlAttribute + private String directionId; /********************** Member Functions **************************/ @@ -61,7 +65,8 @@ protected ApiShape() { public ApiShape(IpcShape shape) { this.tripPatternId = shape.getTripPatternId(); this.headsign = shape.getHeadsign(); - + this.length=shape.getLength(); + this.directionId=shape.getDirectionId(); // If true then set to null so that this attribute won't then be // output as XML/JSON, therefore making output a bit more compact. this.minor = shape.isUiShape() ? null : true; diff --git a/transitclockApi/src/main/java/org/transitclock/api/data/ApiStop.java b/transitclockApi/src/main/java/org/transitclock/api/data/ApiStop.java index 1dbc80e68..eac431c62 100644 --- a/transitclockApi/src/main/java/org/transitclock/api/data/ApiStop.java +++ b/transitclockApi/src/main/java/org/transitclock/api/data/ApiStop.java @@ -32,7 +32,7 @@ * @author SkiBu Smith * */ -@XmlType(propOrder = { "id", "lat", "lon", "name", "code", "minor" }) +@XmlType(propOrder = { "id", "lat", "lon", "name", "code", "minor", "pathLength"}) public class ApiStop extends ApiTransientLocation { @XmlAttribute @@ -48,6 +48,8 @@ public class ApiStop extends ApiTransientLocation { // is not on a main trip pattern. @XmlAttribute(name = "minor") private Boolean minor; + @XmlAttribute + private Double pathLength; /********************** Member Functions **************************/ @@ -63,6 +65,7 @@ public ApiStop(IpcStop stop) { this.id = stop.getId(); this.name = stop.getName(); this.code = stop.getCode(); + this.pathLength=stop.getStopPathLength()==null?0.0:stop.getStopPathLength(); // If true then set to null so that this attribute won't then be // output as XML/JSON, therefore making output a bit more compact. this.minor = stop.isUiStop() ? null : true; diff --git a/transitclockApi/src/main/java/org/transitclock/api/data/ApiVehicleDetails.java b/transitclockApi/src/main/java/org/transitclock/api/data/ApiVehicleDetails.java index 3dae57933..f565b3640 100644 --- a/transitclockApi/src/main/java/org/transitclock/api/data/ApiVehicleDetails.java +++ b/transitclockApi/src/main/java/org/transitclock/api/data/ApiVehicleDetails.java @@ -28,6 +28,7 @@ import org.transitclock.api.rootResources.TransitimeApi.UiMode; import org.transitclock.core.BlockAssignmentMethod; import org.transitclock.ipc.data.IpcVehicle; +import org.transitclock.ipc.data.IpcVehicleComplete; import org.transitclock.utils.Time; /** @@ -102,6 +103,9 @@ public class ApiVehicleDetails extends ApiVehicleAbstract { @XmlElement private ApiHoldingTime holdingTime; + @XmlAttribute + private double distanceAlongTrip; + /** * Need a no-arg constructor for Jersey. Otherwise get really obtuse * "MessageBodyWriter not found for media type=application/json" exception. @@ -147,8 +151,9 @@ public ApiVehicleDetails(IpcVehicle vehicle, Time timeForAgency, UiMode... uiTyp nextStopName = vehicle.getNextStopName() != null ? vehicle.getNextStopName() : null; - driverId = vehicle.getAvl().getDriverId(); - + driverId = vehicle.getAvl().getDriverId(); + if(vehicle instanceof IpcVehicleComplete ) + distanceAlongTrip=((IpcVehicleComplete)vehicle).getDistanceAlongTrip(); isScheduledService = vehicle.getFreqStartTime() > 0 ? false : true; if(!isScheduledService) freqStartTime = vehicle.getFreqStartTime(); diff --git a/transitclockWebapp/src/main/webapp/synoptic/bus_verde_negro.png b/transitclockWebapp/src/main/webapp/synoptic/bus_verde_negro.png new file mode 100644 index 0000000000000000000000000000000000000000..841334ff133a8a0bedf9f64e3a4cd178d67fe9fc GIT binary patch literal 890 zcmV-=1BLvFP)F|9LGO1j>{qv2GS)61xZmfQb?lf&MCT!9z9tdqGAsoa*YrXS|a+3rw)ppI(2&I z6azJdLQ;&vgM?7Z>i(O3(_!CRXLWUFV8nj#spsxqT!EJlY>=ILyjbT-Y%ixKip zp1kAlDEi)l(O07+UMASw+GOJ41nyV2Y`!21+YLV{@b8x~7Xwv^KSp+D|b)IUXKB@?Dbg`^A9mx7v@rZuS3wX_zG6C4B+-J_UX+ zpq&|ODJZ*@i=YGZGHm;Hc#x5rpZCOlmX~&?>-d> z{aUUpqco)vi^U>9Jr)6(ychp3y?bSyg~zAnJ7{0PmBh2q5ko zpaR4>1+k#!dX(sH5b;0&hal>dN7Ph~V(RyUhzADnKM^zuOTo7@9e>~-xw+&-n QT>t<807*qoM6N<$f_%T5EC2ui literal 0 HcmV?d00001 diff --git a/transitclockWebapp/src/main/webapp/synoptic/css/synoptic.css b/transitclockWebapp/src/main/webapp/synoptic/css/synoptic.css new file mode 100644 index 000000000..3f4d895de --- /dev/null +++ b/transitclockWebapp/src/main/webapp/synoptic/css/synoptic.css @@ -0,0 +1,174 @@ + +.tooltip-left{ + visibility: hidden; + width: 120px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px 0; + position: absolute; + opacity: 0; + transition: opacity 0.5s; + +} +.tooltip-left::after{ + content: ""; + position: absolute; + top: 10px; + left: 100%; /* To the left of the tooltip */ + border-width: 5px; + border-style: solid; + border-color: transparent transparent transparent #555; +} + + +.tooltip-bottom{ + visibility: hidden; + width: 120px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px 0; + position: relative; + opacity: 0; + transition: opacity 0.5s; + +} +.tooltip-bottom::after{ + content: ""; + position: absolute; + left: 50%; + bottom: 100%; /* To the left of the tooltip */ + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent #555 transparent; +} + +.tooltip-right{ + visibility: visible; + width: 120px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px 0; + position: absolute; + opacity: 0; + transition: opacity 0.5s; + + +} + +.tooltip-right::after{ + content: " "; + position: absolute; + top: 10px; + right: 100%; /* To the left of the tooltip */ + border-width: 5px; + border-style: solid; + border-color: transparent #555 transparent transparent; +} + + + +.tooltip-right .table{ + width: 120px; + background-color: #555; + color: #fff; + text-align: center; + font-size:8px; + margin: 0 auto; +} + +.tooltipTable { + width: 120px; + background-color: #555; + color: #fff; + text-align: center; + font-size:8px; + margin: 0 auto; +} + +.tooltip-left .table{ + width: 120px; + background-color: #555; + color: #fff; + text-align: center; + font-size:8px; + margin: 0 auto; +} + +.tooltip-bottom .table{ + width: 120px; + background-color: #555; + color: #fff; + text-align: center; + font-size:8px; + margin: 0 auto; +} +.synopticTitle +{ + background-color: #555; + color: #fff; + font-size:10px; + position: absolute; + top:0; + left:0; + visibility: visible; + width:100% +} + +.menu { + visibility: visible; + width: 120px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px 0; + position: absolute; +} + +.menu-content { + visibility: visible; + width: 120px; + + position: absolute; + background-color: #555; + /* box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);*/ + +} + +.menu-content a { + color: black; + padding: 3px 3px; + text-decoration: none; + font-size: 8px; + color: #fff; + display: block; +} + +.menu-content a:hover {background-color: #777} + +.menu .menu-content { + +} +/* +.menu-options { + list-style: none; + padding: 10px 0; +} + +.menu-item { + font-weight: 500; + font-size: 14px; + padding: 10px 40px 10px 20px; + cursor: pointer; +} + +.menu-item:hover { + background: rgba(0, 0, 0, 0.2); +}*/ \ No newline at end of file diff --git a/transitclockWebapp/src/main/webapp/synoptic/index.jsp b/transitclockWebapp/src/main/webapp/synoptic/index.jsp new file mode 100644 index 000000000..f37d46f1b --- /dev/null +++ b/transitclockWebapp/src/main/webapp/synoptic/index.jsp @@ -0,0 +1,351 @@ +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1"%> + + + + + + <%@include file="/template/includes.jsp" %> + + + + + + + + + + + + + + + + + + + + + + + TheTransitClock Synoptic + + + + +
+
+ + +
+
+
+ + diff --git a/transitclockWebapp/src/main/webapp/synoptic/javascript/synoptic.js b/transitclockWebapp/src/main/webapp/synoptic/javascript/synoptic.js new file mode 100644 index 000000000..0fba76845 --- /dev/null +++ b/transitclockWebapp/src/main/webapp/synoptic/javascript/synoptic.js @@ -0,0 +1,781 @@ +var doStuff = function (indice,top, left ) { + this.__showContexMenuFromMuli({ top, left },indice) + } + + + +/** + * It defines a synoptic + * The contructor receivs as paramters: + * container:div|window + * onclick:callback + * border:int + * routeName:string + * showReturn:true|false + * drawReturnUpside:true|false + */ +class Sinoptico +{ + //{container:div|window,onclick:calback,border:int,vehicleIcon:image} + // + constructor(params) + //constructor(canvas,onclick,menu) + { + this.name=params.routeName; + this.container=params.container; + this.infoStop=(data) =>`
${data.identifier}
${data.projection}
`; + this.infoVehicle=(data)=>`
Móvil ${data.identifier}
${data.projection}
`; + if(params.infoStop!=undefined) + { + + console.log(this.infoStop({identifier:"test"})); + console.log(this.infoStop); + this.infoStop=params.infoStop; + + this.infoStop=this.infoStop.bind(this); + console.log(this.infoStop({identifier:"test"})); + } + if(params.infoVehicle!=undefined) + { + this.infoVehicle=params.infoVehicle; + } + this.__showReturn=true; + if(params.showReturn!=undefined) + this.__showReturn=params.showReturn; + this.toolTipDiv = document.createElement('div'); + this.menuDiv = document.createElement('div'); + this.menuDiv.className='menu-content'; + this.canvas = document.createElement('canvas'); + this.canvas.style.position = "absolute"; + this.canvas.border=0; + if(params.border!=undefined) + { + this.canvas.border= params.border; + } + this.canvas.style.border =this.canvas.border+"px solid"; + this.canvas.style.left="0"; + this.canvas.style.top="0"; + this.drawReturnUpside=true; + this.zoomFactor=1.0; +// if(params.drawReturnUpside!=undefined) +// this.drawReturnUpside=params.drawReturnUpside; + + + + //this.canvas=canvas; + if(params.routeName!=undefined) + { + + this.title = document.createElement('div'); + this.title.className='synopticTitle'; + this.title.innerHTML=''+params.routeName+''; + this.container.appendChild(this.title); + + } + + this.container.appendChild(this.canvas); + this.container.appendChild(this.toolTipDiv); + this.container.appendChild(this.menuDiv); + + this.ctx= this.canvas.getContext('2d'); + // this.ctx.fillRect(0, 0,80, 98); + //this.busPath=Path2d(); + + this.vehicleIcon//= document.getElementById("vehicleIcon"); + = new Image(); + this.vehicleIcon.addEventListener('load', function() { + console.log("Image loaded"); + + }, false); +// this.vehicleIcon.addEventListener('load', function() { +// // execute drawImage statements here +// }, false); + this.vehicleIcon.src="bus_verde_negro.png"; + + this.vehicleIcon.width=25; + this.vehicleIcon.height=25; + + this.linewidth=0.9;//Porcentaje que ocupa del canvas + // var lineWidth=(canvas.width*linewidth); + //this.margen=(canvas.width-lineWidth)/2.0;//MOVE TO POSITION OF THE LINE + this.forwarLinePosition=80; + this.returnLinePosition=150; + //this.separation=40; + this.linecolour="#CD5F48"; + this.resize = this.resize.bind(this); + this.init = this.init.bind(this); + this.__handleMouseEvent = this.__handleMouseEvent.bind(this); + + this.__animateBus = this.__animateBus.bind(this); + this.__showContexMenuFromMuli=this.__showContexMenuFromMuli.bind(this); + this.__zoomIn=this.__zoomIn.bind(this); + this.__zoomOut=this.__zoomOut.bind(this); + this.mapBuses=new Map(); + this.buses=null; + this.stops=null; + this.resized=false; + this.onVehiClick=params.onVehiClick; + if(params.predictionFunction!=undefined) + this.predictionFunction=params.predictionFunction; + // this.menu=menu; + this.busDistanceToLine=3; + this.textSeparation=6; + this.__mouseWheelHandler = this.__mouseWheelHandler.bind(this); + } + init() + { + window.addEventListener('resize',this.resize, false); + this.canvas.addEventListener('click', this.__handleMouseEvent); + this.canvas.addEventListener('mousedown', this.__handleMouseEvent); + this.canvas.addEventListener('contextmenu', this.__handleMouseEvent); + this.canvas.addEventListener('mousemove',this.__handleMouseEvent); + this.canvas.addEventListener('wheel',this.__mouseWheelHandler); + this.resize(); + } + + __mouseWheelHandler(e) { + + // cross-browser wheel delta + if(e.deltaY>0) + { + console.log(e); + this.__zoomOut(); + } + else + { + this.__zoomIn(); + } + } + ////////////////////////////////// + getElements(x,y) + { + var elements=[]; + for(var m=0;m=x && this.buses[m].posY+this.vehicleIcon.height>=y ) + elements.push(this.buses[m]); + } + for(var m=0;m=x && this.stops[m].posY+5>=y ) + elements.push(this.stops[m]); + } + return elements; + } + __hideContexMenu( ){ + + const menu = this.menuDiv; + menu.style.visibility = "hidden"; + + } + __hideTooltip( ){ + const tooltip=this.toolTipDiv; + // menu = window.getComputedStyle ? getComputedStyle(this.tootTipDiv, null) : this.tootTipDiv.currentStyle; + +// var menu = document.querySelector(".tooltip-right"); +// menu.style.visibility = "hidden"; +// menu.style.opacity = 0; +// menu = document.querySelector(".tooltip-left"); + tooltip.style.visibility = "hidden"; + tooltip.style.opacity = 0; + } + __showTooltipBus(elements,{ top, left }) + { + + if(this.toolTipDiv.clientWidth!=undefined) + { + if(left+this.toolTipDiv.clientWidth + Móvil ${data.identifier} + ${data.projection} + `; + tooltip.innerHTML+=this.infoVehicle(data); + } +// const toggleMenu = command => { +// menu.style.display = command === "show" ? "block" : "none"; +// menuVisible = !menuVisible; +// }; + + + } + setPredictionsContent(content) + { + const tooltip=this.toolTipDiv; + tooltip.innerHTML=content; + } + + __showTooltipStop(elements,{ top, left }) + { + top+=15; + if(this.toolTipDiv.clientWidth!=undefined) + { + left-=this.toolTipDiv.clientWidth/2; + if(left<0) + { + left=0; + + } + else if(left>window.innerWidth) + { + + left=window.innerWidth-this.toolTipDiv.clientWidth; + } + } + +// if(this.toolTipDiv.clientWidth!=undefined) +// { +// console.log("a"); +// if(left+this.toolTipDiv.clientWidth { +// menu.style.display = command === "show" ? "block" : "none"; +// menuVisible = !menuVisible; +// }; + + + } + + __showTooltip(elements,{ top, left }) + { + + if(elements[0].type=='bus') + this.__showTooltipBus(elements,{ top, left }) + else if (elements[0].type=='stop') + this.__showTooltipStop(elements,{ top, left }) + } + __showContexMenuFromMuli(event) + { + console.log("__showContexMenuFromMuli"); + console.log(event); +// { top, left },indice +// console.log(this.__menuElements); +// console.log(indice); + var top=event.target.top; + var left= event.target.left; + this.__showContexMenu([event.target.element],{ top, left}); + + } + __showContexMenu(elements,{ top, left }) + { + + const menu = this.menuDiv; + if(elements == null) + { + const menu = this.menuDiv; + menu.innerHTML=""; + var a=document.createElement("a"); + a.innerHTML="Zoom in"; + a.addEventListener('click',this.__zoomIn) + // a.onclick=function (){this.__zoomIn}; + console.log("===>"+this.zoomFactor) + menu.appendChild(a); + a=document.createElement("a"); + a.innerHTML="Zoom out"; + a.addEventListener('click',this.__zoomOut) + // a.onclick=function (){this.__zoomIn}; + console.log("===>"+this.zoomFactor) + menu.appendChild(a); + menu.style.left = `${left}px`; + menu.style.top = `${top}px`; + menu.style.visibility = "visible"; + menu.style.border='1px solid'; + return; + } + if(elements[0].type=='bus') + { + top+=this.vehicleIcon.height+3; + } + else + top+=3; + + if(left+menu.clientWidth>window.innerWidth) + { + left=window.innerWidth-menu.clientWidth; + + } + menu.style.left = `${left}px`; + menu.style.top = `${top}px`; + menu.style.visibility = "visible"; + menu.style.border='1px solid'; + if(elements[0].type=='bus') + { + + if(elements.length>1) + { + menu.innerHTML=""; + for(var pos=0;pos${data.identifier}`; + ; + } + } + else + { + menu.innerHTML="menu1menu2"; +// var a=document.createElement("a"); +// a.innerHTML="Zoom in"; +// a.addEventListener('click',this.__zoomIn) +// // a.onclick=function (){this.__zoomIn}; +// console.log("===>"+this.zoomFactor) +// menu.appendChild(a); +// a=document.createElement("a"); +// a.innerHTML="Zoom out"; +// a.addEventListener('click',this.__zoomOut) +// // a.onclick=function (){this.__zoomIn}; +// console.log("===>"+this.zoomFactor) +// menu.appendChild(a); + } + + } + + if(elements[0].type=='stop') + menu.innerHTML="stop"; + + + + + } + __zoomIn() + { + if(this.zoomFactor<4) + this.zoomFactor+=0.1; + this.resize(); + this.__hideContexMenu(); + } + __zoomOut() + { + if(this.zoomFactor>1) + this.zoomFactor-=0.1; + this.resize(); + this.__hideContexMenu(); + } + __handleMouseEvent(event) + { + var rect = this.canvas.getBoundingClientRect(); + var x = event.pageX-rect.left, + y = event.pageY-rect.top; + if(event.type=="click") + { + this.__hideTooltip(); + this.__hideContexMenu(); + var elements=this.getElements(x,y); + if(elements!=undefined && elements.length>0) + this.onVehiClick(elements) + } + if(event.type=="contextmenu") + { + this.__hideTooltip(); + event.preventDefault(); + var elements=this.getElements(x,y); + + if(elements!=undefined && elements.length>0) + { + + const origin = { + left: elements[0].posX, + top: elements[0].posY + }; + this.__showContexMenu(elements,origin); + } + else + { + const origin = { + left: x, + top: y + }; + this.__showContexMenu(null,origin); + //this.__hideContexMenu(); + } + //alert(event.type); + } + if(event.type=="mousemove") + { + var elements=this.getElements(x,y); + + if(elements!=undefined && elements.length>0) + { + const origin = { + left: elements[0].posX, + top: elements[0].posY + }; + //console.log(origin); + this.__showTooltip(elements,origin); + } + else + this.__hideTooltip(); + + } + //console.log("CLICK "+x+ " "+y+ " canvas.width " +canvas.width+ " line "+lineWidth); + + + + + } + getLineWidth() + { + //return (this.canvas.width*this.linewidth); + return (this.canvas.width-80); + } + + getLineMargin() + { + return (this.canvas.width-this.getLineWidth())/2.0; + } + drawLine(direction) + { + this.ctx.save(); + this.ctx.beginPath(); + this.ctx.lineCap="round"; + this.ctx.moveTo(this.getLineMargin(), direction==0?this.forwarLinePosition:this.returnLinePosition); + this.ctx.lineWidth = 5; + this.ctx.strokeStyle = this.linecolour; + this.ctx.lineTo(this.getLineWidth()+this.getLineMargin(), direction==0?this.forwarLinePosition:this.returnLinePosition); + this.ctx.stroke(); + this.ctx.closePath(); + this.ctx.restore(); + + + } + resize() { + console.log("RESIZE "+this.zoomFactor); + var width=this.container.innerWidth; + var height=this.container.innerHeight + if(width == undefined) + { + width=this.container.clientWidth; + height=this.container.clientHeight + } + width=this.zoomFactor*width; + this.container.style.overflow="auto"; + //console.log(this.canvas.style); + if(this.canvas.border!= undefined) + { + + width-=2*this.canvas.border; + height-=2*this.canvas.border; + } + //console.log("resizing"); +// console.log("resizing"+ this.container.innerWidth); +// console.log("resizing"+ this.container.clientWidth); +// console.log("resizing"+ this.container.clientHeight);//offsetHeight==>incluye padding, scrollbar y bordes + this.canvas.width = width; + this.canvas.height = height; + this.paint(); + this.resized=true; + } + getLastPostition(id) + { + + var lastPosition={}; + if(this.buses == undefined) + return lastPosition; + + for (var i=0; i < this.buses.length; i++) { + if (this.buses[i].id == id) { + //console.log("FOUND ID "+id); + lastPosition.posX=this.buses[i].posX; + lastPosition.posY=this.buses[i].posY; + return lastPosition; + } + + } + return lastPosition; + } + setBuses(buses) + { + // console.log("SETTING BUSES"); + for(var pos=0;posbus.posX) + xToDraw=bus.lastPosition.posX-(bus.lastPosition.posX-bus.posX)/this.steps*this.counter; + else + xToDraw=bus.lastPosition.posX+((bus.posX-bus.lastPosition.posX)/this.steps)*this.counter; + + } + this.ctx.fillStyle = bus.fill; + // this.ctx.fillRect(xToDraw, yToDraw, this.vehicleIcon.width, this.vehicleIcon.height); + this.ctx.drawImage(this.vehicleIcon,xToDraw, yToDraw,this.vehicleIcon.width, this.vehicleIcon.height); + + + //console.log(metrics); + var fontPostY=(bus.direction==0)?bus.posY-this.textSeparation:(this.drawReturnUpside==true)?bus.posY-this.textSeparation:bus.posY+metrics.height+this.vehicleIcon.height+this.textSeparation; + this.ctx.fillText(bus.identifier,xToDraw,fontPostY ); + +// ctx.stroke(); + } + this.ctx.restore(); + this.counter++; + if(this.counter<=this.steps) + { + //console.log("TIME "+ this.counter); + window.requestAnimationFrame(this.__animateBus); + } + } + setStops(stops) + { + console.log('setting stops'+stops.length); + for(var pos=0;posbus.posX) + xToDraw=bus.lastPosition.posX-(bus.lastPosition.posX-bus.posX)/this.steps*this.counter; + else + xToDraw=bus.lastPosition.posX+((bus.posX-bus.lastPosition.posX)/this.steps)*this.counter; + + }*/ + this.ctx.fillStyle = bus.fill; + // this.ctx.fillRect(xToDraw, yToDraw, this.vehicleIcon.width, this.vehicleIcon.height); + this.ctx.drawImage(this.vehicleIcon,xToDraw, yToDraw,this.vehicleIcon.width, this.vehicleIcon.height); + + + //console.log(metrics); + var fontPostY=(bus.direction==0)?bus.posY-this.textSeparation:(this.drawReturnUpside==true)?bus.posY-this.textSeparation:bus.posY+metrics.height+this.vehicleIcon.height+this.textSeparation; + this.ctx.fillText(bus.identifier,xToDraw,fontPostY ); + +// ctx.stroke(); + } + this.ctx.restore(); + + } + paint() + { + if(this.resize) + { + this.drawLine(0); + if(this.__showReturn) + this.drawLine(1); + this.paintStop(); + } + if(this.buses== undefined || this.buses==null) + { + console.warn("no bus to paint"); + return; + } + + this.paintBus(); + + this.resized=false; + // + + //this.ctx.translate(25,20); + + } + draw() + { + this.ctx.save(); + this.ctx.fillStyle = 'rgb(255,255,0)'; + + this.ctx.translate(40,40); + this.ctx.fillRect(0,0,25,25); + this.ctx.restore(); + this.ctx.save(); + this.ctx.fillStyle = 'rgb(255,0,0)'; + + //this.ctx.restore(); + // + this.ctx.translate(40,40); + this.ctx.drawImage(this.vehicleIcon,0,0); + this.ctx.fillRect(5,5,5,5); + this.ctx.restore(); + + + } + +} + diff --git a/transitclockWebapp/src/main/webapp/welcome/index.jsp b/transitclockWebapp/src/main/webapp/welcome/index.jsp index a42b59ce2..721eff099 100644 --- a/transitclockWebapp/src/main/webapp/welcome/index.jsp +++ b/transitclockWebapp/src/main/webapp/welcome/index.jsp @@ -63,6 +63,7 @@ for (WebAgency webAgency : webAgencies) { Reports API Status + Synoptic Extensions <% From 7bc89f34a1babd89ad647d669e2e2f3bca04cb80 Mon Sep 17 00:00:00 2001 From: vperez Date: Sun, 19 Aug 2018 20:20:51 -0300 Subject: [PATCH 2/4] Files to check if needed --- .../src/main/webapp/synoptic/css/avlMapUi.css | 73 ++++ .../webapp/synoptic/javascript/animation.js | 179 +++++++++ .../main/webapp/synoptic/javascript/avlMap.js | 358 ++++++++++++++++++ .../javascript/leafletRotatedMarker.js | 26 ++ .../synoptic/javascript/mapUiOptions.js | 145 +++++++ 5 files changed, 781 insertions(+) create mode 100644 transitclockWebapp/src/main/webapp/synoptic/css/avlMapUi.css create mode 100644 transitclockWebapp/src/main/webapp/synoptic/javascript/animation.js create mode 100644 transitclockWebapp/src/main/webapp/synoptic/javascript/avlMap.js create mode 100644 transitclockWebapp/src/main/webapp/synoptic/javascript/leafletRotatedMarker.js create mode 100644 transitclockWebapp/src/main/webapp/synoptic/javascript/mapUiOptions.js diff --git a/transitclockWebapp/src/main/webapp/synoptic/css/avlMapUi.css b/transitclockWebapp/src/main/webapp/synoptic/css/avlMapUi.css new file mode 100644 index 000000000..75ec0984b --- /dev/null +++ b/transitclockWebapp/src/main/webapp/synoptic/css/avlMapUi.css @@ -0,0 +1,73 @@ +body { + margin: 0px; +} + +/* For the AVL points */ +div.avlMarker { + background-color: #ff7800; + border-color: black; + border-radius: 4px; + border-style: solid; + border-width: 1px; + width:7px; + height:7px; +} + +div.avlTriangle { + width: 0px; + height: 0px; + border-bottom: 10px solid #ff7800; + border-left: 5px solid transparent; + border-right: 5px solid transparent + } + +.popupTable { + border-spacing: 0px; +} + +.popupTableLabel { + font-weight: bold; + text-align: right; +} + +/* For the params menu */ +#params { + background: lightgrey; + position:absolute; + top:10px; + right:10px; + border-radius: 25px; + padding: 2%; + border: 2px solid black; +} + + /* Playback menu */ +#playbackContainer { + position: absolute; + left: 50%; + bottom: 5%; +} +#playback { + background: lightgrey; + position: relative; + left: -50%; + text-align: center; + border-radius: 25px; + padding: 2%; + font-family: monospace; + border: 2px solid black; + width: 300px; +} + +#playback input { + background: lightgrey; +} + +#playback input:hover { + border: 1px solid #CCC; + box-shadow: 1px 1px 5px #999 +} + +#playback input:active { + background: lightblue; +} \ No newline at end of file diff --git a/transitclockWebapp/src/main/webapp/synoptic/javascript/animation.js b/transitclockWebapp/src/main/webapp/synoptic/javascript/animation.js new file mode 100644 index 000000000..c7692d36e --- /dev/null +++ b/transitclockWebapp/src/main/webapp/synoptic/javascript/animation.js @@ -0,0 +1,179 @@ +// D3-style object to create and control an animation of the AVL +// data for a particular vehicle. +// map : map or group where animation will be added. +// clock: DOM object where current time should be updated. +// icon: Leaflet icon which will be animated. +function avlAnimation(map, icon, clock) { + + var startTime, endTime, rate, currentIndex, elapsedTime, + lastTime, lineDone, paused, durations, sprite, positions, end; + + var ready = false; + + // create icon for animation and initialize values + // positions is an array of position values: { lat, lon, timestamp } + function animation(data) { + + // remove old sprite. + if (sprite) + map.removeLayer(sprite); + + positions = data + + ready = true; + startTime = positions[0].timestamp; + endTime = positions[positions.length-1].timestamp; + rate = 1; + + currentIndex = 0; // this means we're going to 1 + + elapsedTime = positions[0].timestamp, + lastTime = 0, + lineDone = 0; + + paused = true; + + durations = [] + for (var i = 0; i < positions.length - 1; i++) + durations.push(positions[i+1].timestamp - positions[i].timestamp); + + sprite = L.marker(positions[0], {icon: icon}).addTo(map); + clock.textContent = parseTime(elapsedTime); + } + + function tick() { + var now = Date.now(), + delta = now - lastTime; + + lastTime = now; + + elapsedTime += delta * rate; + + lineDone += delta * rate; + + if (lineDone > durations[currentIndex]) { + // advance index and icon + currentIndex += 1 + lineDone = 0; + + if (currentIndex == positions.length - 1) { + if (end) + end() + currentIndex = 0; + paused = true; + return; + } + + sprite.setLatLng(positions[currentIndex]) + sprite.update() + elapsedTime = positions[currentIndex].timestamp + } + else { + var pos = interpolatePosition(positions[currentIndex], positions[currentIndex+1], durations[currentIndex], lineDone) + sprite.setLatLng(pos) + sprite.update() + + } + clock.textContent = parseTime(elapsedTime); + + if (!paused) + requestAnimationFrame(tick) + } + + animation.ready = function() { + return ready; + } + + animation.start = function() { + lastTime = Date.now(); + paused = false; + tick(); + } + + animation.pause = function() { + paused = true; + } + + animation.paused = function() { + return paused; + } + + animation.onEnd = function (_) { + end = _; + } + + animation.rate = function(_) { + if(_) + rate = _; + else + return rate; + } + + // skip to next AVL + animation.next = function() { + updateToIndex(currentIndex+1); + } + + // previous AVL + animation.prev = function() { + // In most cases, we don't actually want to go *back* an index, just + // restart this one. Exception: if we are less than 500ms (in realtime) + // into this avl. + + var delta = elapsedTime - positions[currentIndex].timestamp; + if (delta/rate < 500) + updateToIndex(currentIndex-1); + else + updateToIndex(currentIndex); + } + + // find next AVL that has a different lat/lng + animation.advance = function() { + var pos = positions[currentIndex] + var nextIndex = currentIndex + 1; + while(nextIndex < positions.length) { + var next = positions[currentIndex]; + if (pos.lat != next.lat || pos.lon != next.lon) + break; + nextIndex++; + } + updateToIndex(nextIndex); + console.log(nextIndex) + } + + function updateToIndex(i) { + if (i > positions.length - 1) + i = positions.length - 1; + if (i < 0) + i = 0; + + currentIndex = i; //+= 1; + lineDone = 0; + var avl = positions[currentIndex]; + elapsedTime = avl.timestamp; + + // update GUI if tick won't. + if (paused) { + sprite.setLatLng(avl); + sprite.update(); + clock.textContent = parseTime(elapsedTime); + } + } + + function parseTime(x) { + return new Date(x).toTimeString().slice(0, 8); + } + + // taken from leafletMovingMarker.js + var interpolatePosition = function(p1, p2, duration, t) { + var k = t/duration; + k = (k>0) ? k : 0; + k = (k>1) ? 1 : k; + return L.latLng(p1.lat + k*(p2.lat-p1.lat), p1.lon + k*(p2.lon-p1.lon)); + }; + + + return animation; +} + + \ No newline at end of file diff --git a/transitclockWebapp/src/main/webapp/synoptic/javascript/avlMap.js b/transitclockWebapp/src/main/webapp/synoptic/javascript/avlMap.js new file mode 100644 index 000000000..8e68439fd --- /dev/null +++ b/transitclockWebapp/src/main/webapp/synoptic/javascript/avlMap.js @@ -0,0 +1,358 @@ +//Edit route input width. +$("#route").attr("style", "width: 200px"); + +/* For drawing the route and stops */ +var routeOptions = { + color: '#00ee00', + weight: 4, + opacity: 0.4, + lineJoin: 'round', + clickable: false +}; + + var stopOptions = { + color: '#006600', + opacity: 0.4, + radius: 4, + weight: 2, + fillColor: '#006600', + fillOpacity: 0.3, +}; + +var routePolylineOptions = {clickable: false, color: "#00f", opacity: 0.5, weight: 4}; + +var stopPopupOptions = {closeButton: false}; + +function drawAvlMarker(avl) { + var latLng = L.latLng(avl.lat, avl.lon); + + // Create the marker. Use a divIcon so that can have tooltips + var tooltip = avl.time.substring(avl.time.indexOf(' ') + 1); + var avlMarker = L.rotatedMarker(avl, { + icon: L.divIcon({ + className: 'avlMarker_', + html: "
", + iconSize: [7,7] + }), + angle: avl.heading, + title: tooltip + }).addTo(vehicleGroup); + + // Create popup with detailed info + + var labels = ["Vehicle", "GPS Time", "Time Proc", "Lat/Lon", "Speed", "Heading", "Assignment ID"], + keys = ["vehicleId", "time", "timeProcessed", "latlon", "niceSpeed", "heading", "assignmentId"]; + + // populate missing keys + avl.latlon = avl.lat + ", " + avl.lon + avl.niceSpeed = Math.round(parseFloat(avl.speed) * 10)/10 + " kph"; + + var content = $("").attr("class", "popupTable"); + + for (var i = 0; i < labels.length; i++) { + var label = $("").append(label, value) ) + } + + avlMarker.bindPopup(content[0]); + + return avlMarker; +} + +/* Called when receiving the AVL data via AJAX call */ +function processAvlCallback(jsonData) { + + /* Save avl data */ + + // List of all the latLngs + var latLngs = []; + + // So can draw separate polyline per vehicle + var previousVehicleId = ""; + var latLngsForVehicle = []; + + // reset list of vehicles + var vehicles = []; + + // For each AVL report... + var vehicle; + for (var i=0; i= 2) + L.polyline(latLngsForVehicle, routePolylineOptions).addTo(map); //.bringToBack(); + latLngsForVehicle = []; + vehicle = {id: avl.vehicleId, data: []} + vehicles.push(vehicle); + } + + // parse date string -> number + avl.timestamp = Date.parse(avl.time.replace(/-/g, '/').slice(0,-2)) + + vehicle.data.push(avl) + previousVehicleId = avl.vehicleId; + + var latLng = drawAvlMarker(avl).getLatLng(); + + latLngsForVehicle.push(latLng); + latLngs.push(latLng); + } + + // Draw polyline for the last vehicle in the AVL data + if (latLngsForVehicle.length >= 2) + L.polyline(latLngsForVehicle, routePolylineOptions).addTo(vehicleGroup); //.bringToBack(); + + // If actually read AVL data... + if (latLngs.length > 0) { + // To make all AVL reports fit in bounds of map. + map.fitBounds(latLngs); + } else { + alert("No AVL data for the criteria specified.") + } + return vehicles; +} + + +/** + * Reads in route data obtained via AJAX and draws route and stops on map. + */ +function routeConfigCallback(data, status) { + // Draw the paths for the route + + var route = data.routes[0]; + + for (var i=0; i").attr("class", "popupTable"); + var labels = ["Stop ID", "Name"], keys = ["id", "name"]; + for (var i = 0; i < labels.length; i++) { + var label = $("").append(label, value) ); + } + + stopMarker.bindPopup(content[0]); + } + } +} + +// Data in vehicles will be available as CSV when you click the `export' link. +// CSV should be the AVL CSV format used elsewhere in Transitime. +// org.transitclock.avl.AvlCsvWriter writes the following header: +// vehicleId,time,justTime,latitude,longitude,speed,heading,assignmentId,assignmentType +// org.transitclock.avl.AvlCsvRecord has required keys vehicleId, time, latitude, longitude +// all others optional +function createExport(vehicles) { + + var data = vehicles[0].data + + // set keys + var keys = ["vehicleId", "time", "latitude", "longitude", "speed", "heading", "assignmentId"] + // CSV key => JS object key + function mapKey(k) { + var o = {"latitude": "lat", "longitude": "lon"} + return o[k] || k; + } + + // write header + var text = keys[0]; + for (var i = 1; i < keys.length; i++) + text += "," + keys[i] + text += '\n' + + // write rows + for (var i = 0; i < data.length; i++) { + text += data[i][keys[0]] + for (var j = 1; j < keys.length; j++) { + var k = mapKey(keys[j]) + text += "," + data[i][k] + } + text += "\n"; + } + + var blob = new Blob([text], { type: 'text/plain' }); // change to text/csv for download prompt + $("#exportData")[0].href = window.URL.createObjectURL(blob); + } + +//Add a new layer for only route/bus markers, so that it can be refreshed +//when selections change without having to redraw tiles. +var map = L.map('map'); +L.control.scale({metric: false}).addTo(map); +L.tileLayer('http://api.tiles.mapbox.com/v4/transitime.j1g5bb0j/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoidHJhbnNpdGltZSIsImEiOiJiYnNWMnBvIn0.5qdbXMUT1-d90cv1PAIWOQ', { + attribution: '© OpenStreetMap & CC-BY-SA, Imagery © Mapbox', + maxZoom: 19 +}).addTo(map); + +//fit map to agency boundaries. +$.getJSON(apiUrlPrefix + "/command/agencyGroup", function(agencies) { + var e = agencies.agency[0].extent; + map.fitBounds([[e.minLat, e.minLon], [e.maxLat, e.maxLon]]); +}) +.fail(function(request, status, error) { + alert(error + '. ' + request.responseText); +}); + + +var vehicleGroup = L.layerGroup().addTo(map); +var routeGroup = L.layerGroup().addTo(map); +var animationGroup = L.layerGroup().addTo(map); + +//Set the CLIP_PADDING to a higher value so that when user pans on map +//the route path doesn't need to be redrawn. Note: leaflet documentation +//says that this could decrease drawing performance. But hey, it looks +//better. +L.Path.CLIP_PADDING = 0.8;0 + +if (request.v || request.r) { + // Request exists; set all the controls to match the values in the request. + $("#vehicle").val(request.v).trigger("change"); + $("#beginDate").val(request.beginDate).trigger("change"); + $("#numDays").val(parseInt(request.numDays)).trigger("change"); + $("#beginTime").val(request.beginTime).trigger("change"); + $("#endTime").val(request.endTime).trigger("change"); + $("#route").val(request.r).trigger("change"); + + //draw vehicles if there is already request information + if (request.v) { + drawAvlData(); + } + if (request.r && request.r != "") + drawRoute(request.r); + +} +else { + // no request + // set beginDate and endDate to defaults + request.beginDate = $("#beginDate").val() + request.numDays = $("#numDays").val() +} + +// draw route data when dropdown is selected +$("#route").change(function(evt) { drawRoute(evt.target.value) }); + +// draw AVL data when submit button is clicked +$("#submit").on("click", function() { + /* Set request object to match new values */ + request.v = $("#vehicle").val(); + request.beginDate = $("#beginDate").val(); + request.numDays = $("#numDays").val(); + request.beginTime = $("#beginTime").val(); + request.endTime = $("#endTime").val(); + request.r = $("#route").val(); + + // Clear existing layer and draw new objects on map. + drawAvlData(); +}); + + +//Get the AVL data via AJAX and call processAvlCallback to draw it +function drawAvlData() { + $.ajax({ + // The page being requested + url: contextPath + "/reports/avlJsonData.jsp", + // Pass in query string parameters to page being requested + data: request, + // Needed so that parameters passed properly to page being requested + traditional: true, + dataType:"json", + async: true, + // When successful process JSON data + success: function(resp) { + vehicleGroup.clearLayers(); + var vehicles = processAvlCallback(resp); + // connect export to link to csv creation. + createExport(vehicles); + if (vehicles.length) + prepareAnimation(vehicles[0].data); // only animate first vehicle returned. + }, + // When there is an AJAX problem alert the user + error: function(request, status, error) { + alert(error + '. ' + request.responseText); + }, + }); +} + +function drawRoute(route) { + routeGroup.clearLayers(); + if (route != "") { + var url = apiUrlPrefix + "/command/routesDetails?r=" + route; + $.getJSON(url, routeConfigCallback); + } +} + +/* Animation controls */ + +var busIcon = L.icon({ + iconUrl: contextPath + "/reports/images/bus.png", + iconSize: [25,25] +}); +var animate = avlAnimation(animationGroup, busIcon, $("#playbackTime")[0]); + +var playButton = contextPath + "/reports/images/playback/media-playback-start.svg", + pauseButton = contextPath + "/reports/images/playback/media-playback-pause.svg"; + +animate.onEnd(function() { + $("#playbackPlay").attr("src", playButton); +}) + +// Given a list of AVL positions, initialize the animation object. +function prepareAnimation(avlData) { + + // Make sure animation controls are in their initial state. + $("#playbackPlay").attr("src", playButton); + $("#playbackRate").text("1X"); + + animate(avlData); + +} + +$("#playbackNext").on("click", animate.next); + +$("#playbackPrev").on("click", animate.prev); + +$("#playbackPlay").on("click", function() { + + if (!animate.paused()) { + animate.pause(); + $("#playbackPlay").attr("src", playButton); + } + else { // need to start it + animate.start(); + $("#playbackPlay").attr("src", pauseButton); + } + +}); + +$("#playbackFF").on("click", function() { + var rate = animate.rate()*2; + animate.rate(rate); + $("#playbackRate").text(rate + "X"); +}); + +$("#playbackRew").on("click", function() { + var rate = animate.rate()/2; + animate.rate(rate); + $("#playbackRate").text(rate + "X"); +}); + + diff --git a/transitclockWebapp/src/main/webapp/synoptic/javascript/leafletRotatedMarker.js b/transitclockWebapp/src/main/webapp/synoptic/javascript/leafletRotatedMarker.js new file mode 100644 index 000000000..2ce968de2 --- /dev/null +++ b/transitclockWebapp/src/main/webapp/synoptic/javascript/leafletRotatedMarker.js @@ -0,0 +1,26 @@ +// Class for creating marker with an orientation. Found at +// https://www.mapbox.com/mapbox.js/example/v1.0.0/rotating-controlling-marker/ +// Orients the icon to marker.options.angle when setLatLng() is called. +// MIT-licensed code by Benjamin Becquet +// https://github.com/bbecquet/Leaflet.PolylineDecorator +L.RotatedMarker = L.Marker.extend({ +options: { angle: 0 }, +_setPos: function(pos) { + L.Marker.prototype._setPos.call(this, pos); + if (L.DomUtil.TRANSFORM) { + // use the CSS transform rule if available + this._icon.style[L.DomUtil.TRANSFORM] += ' rotate(' + this.options.angle + 'deg)'; + } else if (L.Browser.ie) { + // fallback for IE6, IE7, IE8 + var rad = this.options.angle * L.LatLng.DEG_TO_RAD, + costheta = Math.cos(rad), + sintheta = Math.sin(rad); + this._icon.style.filter += ' progid:DXImageTransform.Microsoft.Matrix(sizingMethod=\'auto expand\', M11=' + + costheta + ', M12=' + (-sintheta) + ', M21=' + sintheta + ', M22=' + costheta + ')'; + } +} +}); + +L.rotatedMarker = function(pos, options) { + return new L.RotatedMarker(pos, options); +}; diff --git a/transitclockWebapp/src/main/webapp/synoptic/javascript/mapUiOptions.js b/transitclockWebapp/src/main/webapp/synoptic/javascript/mapUiOptions.js new file mode 100644 index 000000000..41fa0faf3 --- /dev/null +++ b/transitclockWebapp/src/main/webapp/synoptic/javascript/mapUiOptions.js @@ -0,0 +1,145 @@ +/** + * Options that effect how routes, stops, and vehicles are drawn + */ + +var shapeOptions = { + color: '#00ee00', + weight: 8, + opacity: 0.8, + lineJoin: 'round' +}; + +var minorShapeOptions = { + color: '#00ee00', + weight: 2, + opacity: 0.4, +}; + +var stopOptions = { + color: '#006600', + opacity: 1.0, + radius: 4, + weight: 2, + fillColor: '#006600', + fillOpacity: 0.6, +}; + +var firstStopOptions = { + color: '#006600', + opacity: 1.0, + radius: 7, + weight: 2, + fillColor: '#ccffcc', + fillOpacity: 0.9, +} + +var minorStopOptions = { + color: '#006600', + opacity: 0.2, + radius: 3, + weight: 2, + fillColor: '#006600', + fillOpacity: 0.2, +}; + +var busIcon = L.icon({ + iconUrl: 'images/bus-24.png', + iconRetinaUrl: 'images/bus-24@2x.png', + iconSize: [24, 24], + iconAnchor: [13, 12], + popupAnchor: [0, -12], +}); + +var streetcarIcon = L.icon({ + iconUrl: 'images/rail-light-24.png', + iconRetinaUrl: 'images/rail-light-24@2x.png', + iconSize: [24, 24], + iconAnchor: [12, 12], + popupAnchor: [0, -12], +}); + +var railIcon = L.icon({ + iconUrl: 'images/rail-24.png', + iconRetinaUrl: 'images/rail-24@2x.png', + iconSize: [24, 24], + iconAnchor: [13, 12], + popupAnchor: [0, -12], +}); + +var ferryIcon = L.icon({ + iconUrl: 'images/ferry-24.png', + iconRetinaUrl: 'images/ferry-24@2x.png', + iconSize: [24, 24], + iconAnchor: [12, 12], + popupAnchor: [0, -12], +}); + +var layoverIcon = L.icon({ + iconUrl: 'images/cafe-24.png', + iconRetinaUrl: 'images/cafe-24@2x.png', + iconSize: [24, 24], + iconAnchor: [12, 12], + popupAnchor: [0, -12], + +}); + +var arrowIcon = L.icon({ + iconUrl: 'images/arrow.png', + iconSize: [30, 30], + iconAnchor: [15,15], +}); + +var vehicleMarkerOptions = { + opacity: 1.0, +}; + +var secondaryVehicleMarkerOptions = { + opacity: 0.8, +}; + +var minorVehicleMarkerOptions = { + opacity: 0.3, +}; + +var vehicleMarkerBackgroundOptions = { + radius: 12, + weight: 0, + fillColor: '#ffffff', + fillOpacity: 1.0, +}; + +var secondaryVehicleMarkerBackgroundOptions = { + radius: 12, + weight: 0, + fillColor: '#ffffff', + fillOpacity: 0.80, +}; + +var minorVehicleMarkerBackgroundOptions = { + radius: 12, + weight: 0, + fillColor: '#ffffff', + fillOpacity: 0.3, +}; + +var unassignedVehicleMarkerBackgroundOptions = { + radius: 10, + weight: 0, + fillColor: '#F0FA39', + fillOpacity: 0.6, +}; + +var vehiclePopupOptions = { + offset: L.point(0,-2), + closeButton: false +}; + +var stopPopupOptions = { + offset: L.point(0, -0), + closeButton: false +} + +var tripPatternPopupOptions = { + closeButton: false +} + From 565da79c60f4d9e4aed25dec9e391c08484b3611 Mon Sep 17 00:00:00 2001 From: vperez Date: Sun, 26 Aug 2018 23:01:12 -0300 Subject: [PATCH 3/4] Improvements in visualization - Change of icons. - Corrections in ccs and popup arrows placement --- .../api/data/ApiVehicleDetails.java | 4 + .../src/main/webapp/synoptic/css/synoptic.css | 93 +++++++++++++++--- .../src/main/webapp/synoptic/index.jsp | 30 +++--- .../webapp/synoptic/javascript/synoptic.js | 76 ++++++++++---- .../src/main/webapp/synoptic/mybus.png | Bin 0 -> 1078 bytes 5 files changed, 153 insertions(+), 50 deletions(-) create mode 100644 transitclockWebapp/src/main/webapp/synoptic/mybus.png diff --git a/transitclockApi/src/main/java/org/transitclock/api/data/ApiVehicleDetails.java b/transitclockApi/src/main/java/org/transitclock/api/data/ApiVehicleDetails.java index f565b3640..0b21b7061 100644 --- a/transitclockApi/src/main/java/org/transitclock/api/data/ApiVehicleDetails.java +++ b/transitclockApi/src/main/java/org/transitclock/api/data/ApiVehicleDetails.java @@ -106,6 +106,9 @@ public class ApiVehicleDetails extends ApiVehicleAbstract { @XmlAttribute private double distanceAlongTrip; + @XmlAttribute + private String licensePlate; + /** * Need a no-arg constructor for Jersey. Otherwise get really obtuse * "MessageBodyWriter not found for media type=application/json" exception. @@ -152,6 +155,7 @@ public ApiVehicleDetails(IpcVehicle vehicle, Time timeForAgency, UiMode... uiTyp vehicle.getNextStopName() != null ? vehicle.getNextStopName() : null; driverId = vehicle.getAvl().getDriverId(); + licensePlate=vehicle.getLicensePlate(); if(vehicle instanceof IpcVehicleComplete ) distanceAlongTrip=((IpcVehicleComplete)vehicle).getDistanceAlongTrip(); isScheduledService = vehicle.getFreqStartTime() > 0 ? false : true; diff --git a/transitclockWebapp/src/main/webapp/synoptic/css/synoptic.css b/transitclockWebapp/src/main/webapp/synoptic/css/synoptic.css index 3f4d895de..50b060c5b 100644 --- a/transitclockWebapp/src/main/webapp/synoptic/css/synoptic.css +++ b/transitclockWebapp/src/main/webapp/synoptic/css/synoptic.css @@ -1,7 +1,8 @@ .tooltip-left{ visibility: hidden; - width: 120px; + min-width: 180px; + max-width: 300px; background-color: #555; color: #fff; text-align: center; @@ -25,7 +26,37 @@ .tooltip-bottom{ visibility: hidden; - width: 120px; + min-width: 180px; + max-width: 300px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px 0; + position: relative; + opacity: 0; + transition: opacity 0.5s; + +} + +.tooltip-bottom-overflow-left{ + visibility: hidden; + min-width: 180px; + max-width: 300px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px 0; + position: relative; + opacity: 0; + transition: opacity 0.5s; + +} +.tooltip-bottom-overflow-right{ + visibility: hidden; + min-width: 180px; + max-width: 300px; background-color: #555; color: #fff; text-align: center; @@ -47,9 +78,32 @@ border-color: transparent transparent #555 transparent; } +.tooltip-bottom-overflow-right::after{ + content: ""; + position: absolute; + left: 90%; + bottom: 100%; /* To the left of the tooltip */ + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent #555 transparent; +} + + +.tooltip-bottom-overflow-left::after{ + content: ""; + position: absolute; + left: 10%; + bottom: 100%; /* To the left of the tooltip */ + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent #555 transparent; +} .tooltip-right{ visibility: visible; - width: 120px; + min-width: 180px; + max-width: 300px; background-color: #555; color: #fff; text-align: center; @@ -75,38 +129,45 @@ .tooltip-right .table{ - width: 120px; + text-align: left; + width: 100%; background-color: #555; color: #fff; - text-align: center; - font-size:8px; + font-size:10px; + padding-left:3px; + padding-right:3px; margin: 0 auto; } .tooltipTable { - width: 120px; + text-align: left; + width: 100%; background-color: #555; color: #fff; - text-align: center; - font-size:8px; + font-size:10px; + padding-left:3px; + padding-right:3px; margin: 0 auto; } .tooltip-left .table{ - width: 120px; + text-align: left; + width: 100%; background-color: #555; color: #fff; - text-align: center; - font-size:8px; + font-size:10px; + padding-left:3px; + padding-right:3px; margin: 0 auto; } .tooltip-bottom .table{ - width: 120px; + text-align: left; + width: 100%; background-color: #555; color: #fff; - text-align: center; font-size:8px; + margin: 0 auto; } .synopticTitle @@ -123,7 +184,7 @@ .menu { visibility: visible; - width: 120px; + width: 180px; background-color: #555; color: #fff; text-align: center; @@ -134,7 +195,7 @@ .menu-content { visibility: visible; - width: 120px; + width: 180px; position: absolute; background-color: #555; diff --git a/transitclockWebapp/src/main/webapp/synoptic/index.jsp b/transitclockWebapp/src/main/webapp/synoptic/index.jsp index f37d46f1b..ab7a059f2 100644 --- a/transitclockWebapp/src/main/webapp/synoptic/index.jsp +++ b/transitclockWebapp/src/main/webapp/synoptic/index.jsp @@ -132,7 +132,7 @@ function predictionCallback(preds, status) { var stopName = routeStopPreds.stopName; if (routeStopPreds.stopCode) stopName += " (" + routeStopPreds.stopCode + ")"; - var content = '
").attr("class", "popupTableLabel").text(labels[i] + ": "); + var value = $("").text(avl[keys[i]]); + content.append( $("
").attr("class", "popupTableLabel").text(labels[i] + ": "); + var value = $("").text(stop[keys[i]]); + content.append( $("
' + var content = '
Route: ' + routeStopPreds.routeName + '
' + ''; //if (verbose) // content += 'Stop Id: ' + routeStopPreds.stopId + '
'; @@ -149,21 +149,27 @@ function predictionCallback(preds, status) { content += ''; content +='
Stop: ' + stopName + '
Destination: ' + routeStopPreds.dest[i].headsign + '
'; // Add each prediction for the current destination + if (routeStopPreds.dest[i].pred.length > 0) { - content += ''; - + content += ''; + content += ''; + content += ''; + content += ''; for (var j in routeStopPreds.dest[i].pred) { // Separators between the predictions - if (j == 1) - content += ', '; - else if (j ==2) - content += ' & ' + // Add the actual prediction var pred = routeStopPreds.dest[i].pred[j]; - content += pred.min; + content+= '"; + var ident=test.getVehicleIdentifier(pred.vehicle ); + if(ident==null) + ident=pred.vehicle; + content+= '"; + content+= '"; + - // Added any special indicators for if schedule based, + // Added any special indipredcators for if schedule based, // delayed, or not yet departed from terminal /* if (pred.scheduleBased) @@ -178,11 +184,9 @@ function predictionCallback(preds, status) { */ // If in verbose mode add vehicle info //if (verbose) - content += ' (vehicle ' + pred.vehicle + ')'; } - content += ' minutes'; - content += ''; + content += '
tripVehicleMinutes
'+ pred.trip + "'+ ident + "'+ pred.min + "
'; } else { // There are no predictions so let user know @@ -213,7 +217,7 @@ function vehicleUpdate(vehicleDetail, status) console.log(vehicle); var directionVehicle=(vehicle.direction=="0")?0:1; var gpsTimeStr = dateFormat(vehicle.loc.time); - buses.push({id:vehicle.id, projection:vehicle.distanceAlongTrip/getShapeLength(vehicle.tripPattern),identifier:vehicle.id,direction:directionVehicle,gpsTimeStr:gpsTimeStr,nextStopName:vehicle.nextStopName,schAdh:vehicle.schAdhStr,trip:vehicle.trip}); + buses.push({id:vehicle.id, projection:vehicle.distanceAlongTrip/getShapeLength(vehicle.tripPattern),identifier:vehicle.licensePlate,direction:directionVehicle,gpsTimeStr:gpsTimeStr,nextStopName:vehicle.nextStopName,schAdh:vehicle.schAdhStr,trip:vehicle.trip}); } test.setBuses(buses); test.steps=100; diff --git a/transitclockWebapp/src/main/webapp/synoptic/javascript/synoptic.js b/transitclockWebapp/src/main/webapp/synoptic/javascript/synoptic.js index 0fba76845..c02f3c589 100644 --- a/transitclockWebapp/src/main/webapp/synoptic/javascript/synoptic.js +++ b/transitclockWebapp/src/main/webapp/synoptic/javascript/synoptic.js @@ -59,7 +59,10 @@ class Sinoptico this.zoomFactor=1.0; // if(params.drawReturnUpside!=undefined) // this.drawReturnUpside=params.drawReturnUpside; - + this.stopImg = new Image(); + this.stopImg.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEwAACxMBAJqcGAAAAPVJREFUOI2Vkl1qwlAQhT/z1GQpNYRQ29XUJYkiqOiDS7GldBENFUSx\ne2jjy/XhnluHGPJzYEgyOTNz5gfu8QRsgD1wkX0DayCv4f8jAbaAa7ENENcFf4rwC0yk5EE2AqbA\nnzgf1SSh8gl4bFCZAmdx17bnULkp2CYpFZOjnpxkd8VMMSvw03ZSYlEdnsWzfAX4NTn8sLomSOQr\nox6yaxEBR70Pe8QF7iEC3vTxWiENKmYx1nMHfhUOfyRph+oZtzVmwRlWeW5JkgE/4i7tjxh/nkHJ\nDL+qRPYCzE3ld+63RmyUNNmyLtgix19YoYol8AUsbM8BV0fAV591YB1RAAAAAElFTkSuQmCC\n'; + this.stopImg.width=10; + this.stopImg.height=10; //this.canvas=canvas; @@ -90,10 +93,10 @@ class Sinoptico // this.vehicleIcon.addEventListener('load', function() { // // execute drawImage statements here // }, false); - this.vehicleIcon.src="bus_verde_negro.png"; + this.vehicleIcon.src="mybus.png"; - this.vehicleIcon.width=25; - this.vehicleIcon.height=25; + this.vehicleIcon.width=30; + this.vehicleIcon.height=30; this.linewidth=0.9;//Porcentaje que ocupa del canvas // var lineWidth=(canvas.width*linewidth); @@ -118,8 +121,9 @@ class Sinoptico if(params.predictionFunction!=undefined) this.predictionFunction=params.predictionFunction; // this.menu=menu; - this.busDistanceToLine=3; + this.busDistanceToLine=7; this.textSeparation=6; + this.__mouseWheelHandler = this.__mouseWheelHandler.bind(this); } init() @@ -132,7 +136,19 @@ class Sinoptico this.canvas.addEventListener('wheel',this.__mouseWheelHandler); this.resize(); } - + getVehicleIdentifier(id) + { + if(this.buses!=undefined) + for(var m=0;mwindow.innerWidth) + else if(left+this.toolTipDiv.clientWidth>window.innerWidth+leftScroll) { - - left=window.innerWidth-this.toolTipDiv.clientWidth; + //alert(this.container.scrollLeft); + this.toolTipDiv.className="tooltip-bottom-overflow-right"; + //left+=this.toolTipDiv.clientWidth*0.45; + left-=this.toolTipDiv.clientWidth*0.40; } } @@ -280,7 +309,7 @@ class Sinoptico // } // } - this.toolTipDiv.className="tooltip-bottom"; + //menu = window.getComputedStyle ? getComputedStyle(this.tootTipDiv, null) : this.tootTipDiv.currentStyle; const tooltip=this.toolTipDiv; //const tooltip=document.getElementById("myToolTip"); @@ -604,11 +633,11 @@ class Sinoptico this.ctx.font="10px Georgia"; var metrics=this.ctx.measureText("anyText"); metrics.height=parseInt(this.ctx.font.match(/\d+/), 10); - this.ctx.clearRect(0, this.forwarLinePosition-this.vehicleIcon.height-metrics.height-this.textSeparation-1, this.canvas.width, this.vehicleIcon.height+metrics.height+this.textSeparation-2);//Tenemos que tener el cuadrado de los buses. + this.ctx.clearRect(0, this.forwarLinePosition-(this.vehicleIcon.height+metrics.height+this.textSeparation+this.busDistanceToLine+1), this.canvas.width, (this.vehicleIcon.height+metrics.height+this.textSeparation));//Tenemos que tener el cuadrado de los buses. if(this.drawReturnUpside==true) - this.ctx.clearRect( 0,this.returnLinePosition-this.vehicleIcon.height-metrics.height-this.textSeparation-1, this.canvas.width, this.vehicleIcon.height+metrics.height+this.textSeparation-2);//Tenemos que tener el cuadrado de los buses. + this.ctx.clearRect( 0,this.returnLinePosition-(this.vehicleIcon.height+metrics.height+this.textSeparation+this.busDistanceToLine+1), this.canvas.width,(this.vehicleIcon.height+metrics.height+this.textSeparation));//Tenemos que tener el cuadrado de los buses. else - this.ctx.clearRect( 0,this.returnLinePosition+this.busDistanceToLine, this.canvas.width, this.vehicleIcon.height+metrics.height+this.textSeparation+3);//Tenemos que tener el cuadrado de los buses. + this.ctx.clearRect( 0,this.returnLinePosition+this.busDistanceToLine, this.canvas.width, (this.vehicleIcon.height+metrics.height+this.textSeparation));//Tenemos que tener el cuadrado de los buses. for(var pos=0;posWFU8GbZ8()Nlj2>E@cM*00WvyL_t(Y$IX^qXp?6Y z$N$gsex!ZVSUZy#kx?d*O@^!&mN4iXE9hiH8Acj#f<>>imJW&#P}xvd21>2yU8po- zhi$BEltQ-&gHaXhz>Bcz$c2F-DO4(&kG@~c*+pt-mN#jd+RX#+#ryE*{5a1!=Q)RA z(TJ$;_HVbl!as!}iXz^mnWl-uUmeE&UHdOH#=ZdnaS?j;_}Swf!{ft{LWMLm;9RK^bGXF?Iuw04Gq8gHv9U4C&CQhs zJDtuv*vdym;H5@Hh(@ERRLdA!R%Z!L&!?g4xocfmSis=mAZlxCD*_vafj}StK@e63 zR&^B*hackBueSgI)6>%k27|FgBC)m7!fI`8g~#Io03`lMVAIY`#qDO5+w^=Iny%#p zoI?-U2q z9A+|^f`6;RD%no5B6G!4DIy#zoV7#L7i1)iLo6zq08TMh~UHk%ExSS({0hP3L4o12^EmE_Lr z_1agJW6n9Wd2MwS#Hs)*H44sLIg_6_m24qPbtiu~i8`gO>{W9s0p}dy(J(IbUjY4` zb0&W%L$X$i?kjw-XT^9J)00qrcOShWdu0D?j$_G1z50xKz6B z1xY~HcU{m)FWR#J;Kj=q7`ZZnrVpA9GRBB4$cU%;2OePpBS wKVK&zy1M2A{B=aCuHKuSo1G`3-PQQ{2NK(8u2~O>IRF3v07*qoM6N<$f|Ud8h5!Hn literal 0 HcmV?d00001 From a9935d576eafecb385bda8025354ead806a14763 Mon Sep 17 00:00:00 2001 From: vperez Date: Tue, 28 Aug 2018 20:58:18 -0300 Subject: [PATCH 4/4] Start to add headway information to API --- .../org/transitclock/ipc/data/IpcVehicleComplete.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/transitclock/src/main/java/org/transitclock/ipc/data/IpcVehicleComplete.java b/transitclock/src/main/java/org/transitclock/ipc/data/IpcVehicleComplete.java index cdd33990d..2477d8ca3 100644 --- a/transitclock/src/main/java/org/transitclock/ipc/data/IpcVehicleComplete.java +++ b/transitclock/src/main/java/org/transitclock/ipc/data/IpcVehicleComplete.java @@ -51,6 +51,8 @@ public class IpcVehicleComplete extends IpcVehicleGtfsRealtime { private final double distanceToNextStop; private final double distanceOfNextStopFromTripStart; private final double distanceAlongTrip; + private double headway; + private String headwayVehicleId; private static final long serialVersionUID = 8154105842499551461L; @@ -87,7 +89,12 @@ public IpcVehicleComplete(VehicleState vs) { } this.distanceOfNextStopFromTripStart = sumOfStopPathLengths; this.distanceAlongTrip = - sumOfStopPathLengths - this.distanceToNextStop; + sumOfStopPathLengths - this.distanceToNextStop; + if(vs.getHeadway()!=null) + { + this.headway=vs.getHeadway().getHeadway(); + this.headwayVehicleId=vs.getHeadway().getOtherVehicleId(); + } } else { // Vehicle not assigned to trip so null out parameters this.originStopId = null;