diff --git a/src/js/app.js b/src/js/app.js index e399a16..0480916 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -43,6 +43,15 @@ const getSelectedDatasetURL = () => { return `${publicationInUse.url}${datasetInUse.filename}`; }; +const getSelectedDatasetMarkersURL = () => { + // Get the publication currently selected (date) + const publicationInUse = indexData.publications + .find(pub => pub.date === document.getElementById('day').value); + // Get the dataset currently selected (group of lines) within the publication selected + // Compute URL of dataset selected + return `${publicationInUse.url}markers.json`; +}; + // Load the available line-directions within this group of lines const loadAvailableLineDirections = () => { fetch(getSelectedDatasetURL()) @@ -110,7 +119,7 @@ const processIndex = () => { const defaultDatasetURL = `${publications[0].url}${publications[0].datasets[0].filename}`; Object.assign(options, { selectedDate: publications[0].date }); fetch(defaultDatasetURL).then(r => r.json()) - .then((defaultData) => { new PTDS(defaultData, options); }); + .then((defaultData) => { new PTDS(defaultData, options, null); }); }; // Form submission handler @@ -140,7 +149,12 @@ const formSubmit = (event) => { options.mode = 'spiralSimulation'; } Object.assign(options, { selectedDate: document.getElementById('day').value }); - new PTDS(data, options); + + const urlMarkersSelected = getSelectedDatasetMarkersURL(); + fetch(urlMarkersSelected).then(r => r.json()) + .then((markerdata) => { + new PTDS(data, options, markerdata); + }); }); }; diff --git a/src/js/models/vehiclejourney.js b/src/js/models/vehiclejourney.js index 866acd9..bb30a67 100644 --- a/src/js/models/vehiclejourney.js +++ b/src/js/models/vehiclejourney.js @@ -142,8 +142,9 @@ export default class VehicleJourney { // Extract array of static schedule distances at each stop const staticDistances = this.journeyPattern.distances; - return Object.values(this.rt).map(({ vehicleNumber, times, distances }) => ({ + return Object.values(this.rt).map(({ vehicleNumber, times, distances, markers }) => ({ vehicleNumber, + markers, // Enrich the vehicles position data with the distance since the last stop // and the index of that stop, as well as the status compared to the schedule positions: times.map((time, index) => { diff --git a/src/js/ptdataset.js b/src/js/ptdataset.js index 1d21a61..fa7f5ac 100644 --- a/src/js/ptdataset.js +++ b/src/js/ptdataset.js @@ -14,12 +14,17 @@ import TimeUtils from './timeutils'; * Class representing a public transport dataset */ export default class PTDataset { - constructor(inputData, referenceDate) { + constructor(inputData, referenceDate, markerData) { this.referenceDate = referenceDate; Object.assign(this, PTDataset.computeStopsAndStopAreas(inputData.scheduledStopPoints)); Object.assign(this, this.computeLinesJourneyPatterns(inputData.journeyPatterns)); this.vehicleJourneys = this.computeVehicleJourneys(inputData.vehicleJourneys); this.stopsLinks = this.computeLinks(); + this.markers = null; + if (markerData != null && markerData.markers != null) { + this.markers = this.computeMarkers(markerData.markers); + this.addMarkersToDataset(this.markers); + } // Compute times of the first and last stop of any journey in the dataset this.earliestTime = Math.min(...Object.values(this.journeyPatterns) @@ -28,6 +33,33 @@ export default class PTDataset { .map(jp => jp.firstAndLastTimes.last)); } + addMarkersToDataset(markers) { + for (const marker of markers) { + const { vehicleJourneyCode, vehicleNumber } = marker.reference; + if (Object.prototype.hasOwnProperty.call(this.vehicleJourneys, vehicleJourneyCode)) { + const vehicleJourneyData = this.vehicleJourneys[vehicleJourneyCode]; + const { rt } = vehicleJourneyData; + if (rt != null && vehicleNumber != null + && Object.prototype.hasOwnProperty.call(rt, vehicleNumber)) { + const vehicleData = rt[vehicleNumber]; + if (Object.prototype.hasOwnProperty.call(vehicleData, 'markers')) { + const markersData = vehicleData.markers; + markersData.push(marker); + } else { + vehicleData.markers = [marker]; + } + } else if (vehicleNumber == null) { + if (Object.prototype.hasOwnProperty.call(vehicleJourneyData, 'markers')) { + const markersData = vehicleJourneyData.markers; + markersData.push(marker); + } else { + vehicleJourneyData.markers = [marker]; + } + } + } + } + } + /** * Convert raw stops data into rich Stop and StopArea objects, * storing them in an object indexed by their code for fast lookup @@ -190,4 +222,14 @@ export default class PTDataset { vehicleJourney => vehicleJourney.code, ); } + + computeMarkers(markers) { + return markers.map(({ id, reference, time, message, url }) => ({ + id, + reference, + time: TimeUtils.secondsToDateObject(time, this.referenceDate), + message, + url, + })); + } } diff --git a/src/js/ptds.js b/src/js/ptds.js index 8672bed..9b5d361 100644 --- a/src/js/ptds.js +++ b/src/js/ptds.js @@ -17,8 +17,8 @@ const d3 = Object.assign({}, { * Main class */ export default class PTDS { - constructor(inputData, options) { - this.data = new PTDataset(inputData, options.selectedDate); + constructor(inputData, options, markerData) { + this.data = new PTDataset(inputData, options.selectedDate, markerData); this.options = options; if (['dual', 'marey'].includes(options.mode)) { diff --git a/src/js/viz_components/mareydiagram.js b/src/js/viz_components/mareydiagram.js index 513b308..9b5be19 100644 --- a/src/js/viz_components/mareydiagram.js +++ b/src/js/viz_components/mareydiagram.js @@ -657,8 +657,9 @@ export default class MareyDiagram { * @return {Array.} - Trip drawing information */ computeTrips() { + /* // Compute drawing information for the trips of the reference journey pattern - const trips = this.journeyPatternMix.referenceJP.vehicleJourneys + const tripsOld = this.journeyPatternMix.referenceJP.vehicleJourneys .map(({ code, staticSchedule, firstAndLastTimes, realTimeData }) => ({ code, // For the reference journey pattern there is only one sequence @@ -674,13 +675,65 @@ export default class MareyDiagram { }))], })), firstAndLastTimes, + markers: null, })); + */ + + const trips = []; + // Compute drawing information for the trips of the reference journey pattern + for (const { code, staticSchedule, firstAndLastTimes, realTimeData, markers } of + this.journeyPatternMix.referenceJP.vehicleJourneys) { + const tripMarkers = []; + // If there are markers to add, add them + if (markers) { + // Deep clone markers + const leftOverMarkers = markers.slice(); + for (const [indexP, { time, distance }] of staticSchedule.entries()) { + for (const [indexM, marker] of leftOverMarkers.entries()) { + if (indexP !== staticSchedule.length - 1) { + const nextPosition = staticSchedule[indexP + 1]; + if (time <= marker.time && marker.time < nextPosition.time) { + const x1 = distance; + const y1 = time; + const x2 = nextPosition.distance; + const y2 = nextPosition.time; + const yM = marker.time; + const xM = (((x1 - x2) / (y1 - y2)) * yM) + - (((x1 * y2) - (x2 * y1)) / (y1 - y2)); + marker.distance = xM; + tripMarkers.push(marker); + leftOverMarkers.splice(indexM, 1); + break; + } + } + } + } + } + trips.push({ + code, + // For the reference journey pattern there is only one sequence + staticSequences: [staticSchedule.map(({ time, distance }) => ({ time, distance }))], + realtimeSequences: realTimeData.map(({ vehicleNumber, positions }) => ({ + vehicleNumber, + // Again, only one sequence per vehicle for the reference journey pattern + sequences: [positions.map(({ time, distanceFromStart, status, prognosed }) => ({ + time, + distance: distanceFromStart, + status, + prognosed, + }))], + })), + markers: tripMarkers, + firstAndLastTimes, + }); + } // Then compute the trip drawing information for the other journey patterns that share // at least one link with the reference JP for (const otherJP of this.journeyPatternMix.otherJPs) { // Iterate over the trips of the journey pattern for (const vehicleJourney of otherJP.journeyPattern.vehicleJourneys) { + const tripMarkers = []; // Min and max time of every static/realtime position of the current journey, // only for the shared segments let minTime = null; @@ -711,9 +764,37 @@ export default class MareyDiagram { })); } + // If there are markers to add, add them + if (vehicleJourney.markers) { + // Deep clone markers + const leftOverMarkers = vehicleJourney.markers.slice(); + for (const staticSequence of staticSequences) { + for (const [indexP, { time, distance }] of staticSequence.entries()) { + for (const [indexM, marker] of leftOverMarkers.entries()) { + if (indexP !== staticSequence.length - 1) { + const nextPosition = staticSequence[indexP + 1]; + if (time <= marker.time && marker.time < nextPosition.time) { + const x1 = distance; + const y1 = time; + const x2 = nextPosition.distance; + const y2 = nextPosition.time; + const yM = marker.time; + const xM = (((x1 - x2) / (y1 - y2)) * yM) + - (((x1 * y2) - (x2 * y1)) / (y1 - y2)); + marker.distance = xM; + tripMarkers.push(marker); + leftOverMarkers.splice(indexM, 1); + break; + } + } + } + } + } + } + const realtimeSequences = []; // Iterate over each of the real time vehicles - for (const { vehicleNumber, positions } of vehicleJourney.realTimeData) { + for (const { vehicleNumber, positions, markers } of vehicleJourney.realTimeData) { const vehicleSequences = []; // Iterate over the shared sequence @@ -747,6 +828,32 @@ export default class MareyDiagram { }; }); + // If there are markers to add, add them + if (markers) { + // Deep clone markers + const leftOverMarkers = markers.slice(); + for (const [indexP, { time, distance }] of vehicleSequence.entries()) { + for (const [indexM, marker] of leftOverMarkers.entries()) { + if (indexP !== vehicleSequence.length - 1) { + const nextPosition = vehicleSequence[indexP + 1]; + if (time <= marker.time && marker.time < nextPosition.time) { + const x1 = distance; + const y1 = time; + const x2 = nextPosition.distance; + const y2 = nextPosition.time; + const yM = marker.time; + const xM = (((x1 - x2) / (y1 - y2)) * yM) + - (((x1 * y2) - (x2 * y1)) / (y1 - y2)); + marker.distance = xM; + tripMarkers.push(marker); + leftOverMarkers.splice(indexM, 1); + break; + } + } + } + } + } + // Filter out sequences with zero length (can happen that a vehicle belonging to a // journey pattern that shares >1 link(s) with the reference one does not have any // positions to be drawn because the positions are not part of the shared links) @@ -767,6 +874,7 @@ export default class MareyDiagram { code: vehicleJourney.code, staticSequences, realtimeSequences, + markers: tripMarkers, firstAndLastTimes: { first: minTime, last: maxTime }, }); } @@ -976,5 +1084,33 @@ export default class MareyDiagram { .attr('cx', ({ distance }) => this.xScale(distance)) // Trip enter + update > realtime vehicle sequences > realtime position enter .attr('cy', ({ time }) => this.yScale(time)); + + // Draw the markers at the end so that they are on top of everything else + // Trip enter + update > marker selection + const tripMarkersSel = tripsEnterUpdateSel + .selectAll('g.marker') + .data(({ markers }) => (typeof markers !== 'undefined' ? markers : [])); + + // Trip enter + update > marker exit + tripMarkersSel.exit().remove(); + + // Trip enter + update > marker enter + const tripMarkersGroup = tripMarkersSel.enter().append('g').attr('class', 'marker'); + tripMarkersGroup + .on('mouseover', function f() { + d3event.stopPropagation(); + d3.select(this).append('text').attr('class', 'message').text(({ message, url }) => `${message} | ${url}`); + }) + .on('mouseout', function f() { + d3event.stopPropagation(); + d3.select(this).select('text.message').remove(); + }) + .merge(tripMarkersSel) + .attr('transform', ({ distance, time }) => `translate(${this.xScale(distance)},${this.yScale(time)})`); + + tripMarkersGroup.append('image') + .attr('width', 15) + .attr('height', 15) + .attr('xlink:href', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAADC0lEQVR4nO2aPWgUQRiGn1XjT4yQwiBGCAhaxDQSBEEJYqNC1NhEbCQWYikEC1vBaBkQCSKWYqFY2Kig8a8QIdGAiI0/KIiCQUW8i4IJWYvZJXfnTG5mb3ZnQuaBD3Kb2W/f99293dlJIBAIBAKBQCAQCATmiIBjwBgwBcQLtMrAE2C/aQDDHoi3XWd0ze/2QGxepXUl3PRAaF71uNZsJAngC7BeJ6kFSBlYU7lhiWTQ2mK0OOG/Ey4L4HsBQlzxoXaDLIDRAoS4QstbNzCL+xuW7ZoBOnWTGvFAsO0a0TUPsBJ46oFoW/UCaDYJAKANeOeB+EbrE9Buaj6lHfE+4NpE1noJdGQ1n9IMXPXAjGldB1ZnMXxYsf0I8MMDY/XqJ3DU0FsVM8CA4ncbgHsemFTVQ9SX/AAwrRNA2uwS4mlQSwScBH57YDitP8Ap5BO75cDFirHaAcTAa8TESEYnMO6B+VfAVoXGLYhHYOV4owBi4C9iMWGpZOwy4HQypmjjs8AFYIVEVwScQL6aZRxAWs+ATYp9tgNvCjT/Edil0NKBuBeo9s0cQAz8QiQrYxXijOT9HnEDaFVo6Kf+k6qhANK6g3rRZB9iUcW28UngkOKYrcA1zT5WAoiBr0CfokcbcMui+buop7N7gc8GvawFkNYVoEXR6zhQasB4KekhowW4jPlXznoAMfAe2Knot5lsj8vxZF8ZO4C3GbXmEkCMmEGeR0w8amkChhAzsXp9ppOxTYo+55JjZdWZWwBpTQBdit7dzP92OYZ64tWV9G5UX+4BxIip6SDyZfcI6AVuI+7sk8nPvfOMH0x62tBWSABpjSJeoLKyDhGOTU11sXmwGPiGmKCY0p/sa1tP4QFUXg09GsfvAR7kqKMK2XdOK6UGeI5YrbnP3B8qNgJ7EAsW23I+fqT8kJB3AK6p8ixbQFhUhABcC3BNCMC1ANeEACTbpgpXURyl2g2yACYKEOIKLW995DcNdV0HdJMa8kCs7Tqraz7lIOL/6soeiM9aZeARBmc+EAgEAoHA4uEfmPh3WpWTDh8AAAAASUVORK5CYII='); } } diff --git a/src/scss/main.scss b/src/scss/main.scss index 75a5662..2800cbc 100644 --- a/src/scss/main.scss +++ b/src/scss/main.scss @@ -48,6 +48,10 @@ svg { } #marey { + g.marker text{ + font-family: FontAwesome; + } + .marey-scroll { rect.selection { stroke: black; @@ -71,7 +75,7 @@ svg { display: none; } text { - transform: translate(4px, 0px); + transform: translate(8px, 0px); } } rect.selection {