|
|
@ -1,73 +1,64 @@ |
|
|
|
require('ol/ol.css'); |
|
|
|
require("ol/ol.css"); |
|
|
|
|
|
|
|
const axios = require('axios'); |
|
|
|
const {Map, View} = require('ol'); |
|
|
|
const { Map, View } = require("ol"); |
|
|
|
|
|
|
|
const GeoJSON = require('ol/format/GeoJSON').default; |
|
|
|
const reader = new GeoJSON({featureProjection: 'EPSG:3857'}); |
|
|
|
const GeoJSON = require("ol/format/GeoJSON").default; |
|
|
|
const reader = new GeoJSON({ featureProjection: "EPSG:3857" }); |
|
|
|
|
|
|
|
const TileLayer = require('ol/layer/Tile').default; |
|
|
|
const XYZSource = require('ol/source/XYZ').default; |
|
|
|
const TileLayer = require("ol/layer/Tile").default; |
|
|
|
const XYZSource = require("ol/source/XYZ").default; |
|
|
|
|
|
|
|
const VectorLayer = require('ol/layer/Vector').default; |
|
|
|
const VectorSource = require('ol/source/Vector').default; |
|
|
|
const {getVectorContext} = require('ol/render'); |
|
|
|
const VectorLayer = require("ol/layer/Vector").default; |
|
|
|
const VectorSource = require("ol/source/Vector").default; |
|
|
|
const { getVectorContext } = require("ol/render"); |
|
|
|
|
|
|
|
const Feature = require('ol/Feature').default; |
|
|
|
const Point = require('ol/geom/Point').default; |
|
|
|
const LineString = require('ol/geom/LineString').default; |
|
|
|
const Point = require("ol/geom/Point").default; |
|
|
|
|
|
|
|
const proj = require('ol/proj'); |
|
|
|
const proj = require("ol/proj"); |
|
|
|
|
|
|
|
const {Style, Fill, Stroke, Circle, Icon} = require('ol/style'); |
|
|
|
const color = require('color'); |
|
|
|
const { Style, Fill, Stroke, Circle, Icon } = require("ol/style"); |
|
|
|
const colorModule = require("color"); |
|
|
|
|
|
|
|
const mapboxToken = `pk.eyJ1IjoibWF0dGVvZGVsYWJyZSIsImEiOiJja2NxaTUyMmUwcmFhMn\
|
|
|
|
h0NmFsdzQ3emxqIn0.cyxF0h36emIMTk3cc4VqUw`;
|
|
|
|
const mapboxToken = "pk.eyJ1IjoibWF0dGVvZGVsYWJyZSIsImEiOiJja2NxaTUyMmUwcmFhMn\ |
|
|
|
h0NmFsdzQ3emxqIn0.cyxF0h36emIMTk3cc4VqUw"; |
|
|
|
|
|
|
|
const simulation = require('../tam/simulation'); |
|
|
|
const network = require('../tam/network.json'); |
|
|
|
const simulation = require("../tam/simulation"); |
|
|
|
const network = require("../tam/network.json"); |
|
|
|
|
|
|
|
const lineFeaturesOrder = (feature1, feature2) => |
|
|
|
{ |
|
|
|
const lines1 = feature1.get('lines'); |
|
|
|
const lineFeaturesOrder = (feature1, feature2) => { |
|
|
|
const lines1 = feature1.get("lines"); |
|
|
|
|
|
|
|
if (lines1.length === 0) |
|
|
|
{ |
|
|
|
if (lines1.length === 0) { |
|
|
|
return -1; |
|
|
|
} |
|
|
|
|
|
|
|
const lines2 = feature2.get('lines'); |
|
|
|
const lines2 = feature2.get("lines"); |
|
|
|
|
|
|
|
if (lines2.length === 0) |
|
|
|
{ |
|
|
|
if (lines2.length === 0) { |
|
|
|
return 1; |
|
|
|
} |
|
|
|
|
|
|
|
return Math.min(...lines1) - Math.min(...lines2); |
|
|
|
}; |
|
|
|
|
|
|
|
const makeDataSources = () => |
|
|
|
{ |
|
|
|
const makeDataSources = () => { |
|
|
|
const segmentsSource = new VectorSource(); |
|
|
|
const stopsSource = new VectorSource(); |
|
|
|
|
|
|
|
const readFeatures = hash => Object.values(hash).map(json => |
|
|
|
{ |
|
|
|
const readFeatures = hash => Object.values(hash).map(json => { |
|
|
|
json.properties.lines = json.properties.routes.filter( |
|
|
|
|
|
|
|
// Only consider normal routes (excluding alternate routes)
|
|
|
|
([lineRef, routeRef]) => |
|
|
|
network.lines[lineRef].routes[routeRef].state === 'normal' |
|
|
|
network.lines[lineRef].routes[routeRef].state === "normal" |
|
|
|
).map(([lineRef]) => lineRef); |
|
|
|
|
|
|
|
if (json.properties.lines.length >= 1) |
|
|
|
{ |
|
|
|
if (json.properties.lines.length >= 1) { |
|
|
|
json.properties.colors = json.properties.lines.map( |
|
|
|
lineRef => network.lines[lineRef].color); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
json.properties.colors = ['#FFFFFF']; |
|
|
|
lineRef => network.lines[lineRef].color |
|
|
|
); |
|
|
|
} else { |
|
|
|
json.properties.colors = ["#FFFFFF"]; |
|
|
|
} |
|
|
|
|
|
|
|
return reader.readFeature(json); |
|
|
@ -75,19 +66,19 @@ const makeDataSources = () => |
|
|
|
|
|
|
|
segmentsSource.addFeatures(readFeatures(network.segments)); |
|
|
|
stopsSource.addFeatures(readFeatures(network.stops)); |
|
|
|
return {segmentsSource, stopsSource}; |
|
|
|
return { segmentsSource, stopsSource }; |
|
|
|
}; |
|
|
|
|
|
|
|
const makeBorderColor = mainColor => |
|
|
|
{ |
|
|
|
const hsl = color(mainColor).hsl(); |
|
|
|
const makeBorderColor = mainColor => { |
|
|
|
const hsl = colorModule(mainColor).hsl(); |
|
|
|
|
|
|
|
hsl.color = Math.max(0, hsl.color[2] -= 20); |
|
|
|
return hsl.hex(); |
|
|
|
}; |
|
|
|
|
|
|
|
const makeCourseColor = mainColor => |
|
|
|
{ |
|
|
|
const hsl = color(mainColor).hsl(); |
|
|
|
const makeCourseColor = mainColor => { |
|
|
|
const hsl = colorModule(mainColor).hsl(); |
|
|
|
|
|
|
|
hsl.color = Math.max(0, hsl.color[2] += 10); |
|
|
|
return hsl.hex(); |
|
|
|
}; |
|
|
@ -100,43 +91,41 @@ const sizes = { |
|
|
|
courseSize: 15, |
|
|
|
courseOuterBorder: 13, |
|
|
|
courseBorder: 10, |
|
|
|
courseInnerBorder: 7, |
|
|
|
courseInnerBorder: 7 |
|
|
|
}; |
|
|
|
|
|
|
|
const segmentBorderStyle = feature => new Style({ |
|
|
|
stroke: new Stroke({ |
|
|
|
color: makeBorderColor(feature.get('colors')[0]), |
|
|
|
width: sizes.segmentOuter, |
|
|
|
}), |
|
|
|
color: makeBorderColor(feature.get("colors")[0]), |
|
|
|
width: sizes.segmentOuter |
|
|
|
}) |
|
|
|
}); |
|
|
|
|
|
|
|
const segmentInnerStyle = feature => new Style({ |
|
|
|
stroke: new Stroke({ |
|
|
|
color: feature.get('colors')[0], |
|
|
|
width: sizes.segmentInner, |
|
|
|
}), |
|
|
|
color: feature.get("colors")[0], |
|
|
|
width: sizes.segmentInner |
|
|
|
}) |
|
|
|
}); |
|
|
|
|
|
|
|
const stopStyle = feature => new Style({ |
|
|
|
image: new Circle({ |
|
|
|
fill: new Fill({ |
|
|
|
color: feature.get('colors')[0], |
|
|
|
color: feature.get("colors")[0] |
|
|
|
}), |
|
|
|
stroke: new Stroke({ |
|
|
|
color: makeBorderColor(feature.get('colors')[0]), |
|
|
|
width: sizes.stopBorder, |
|
|
|
color: makeBorderColor(feature.get("colors")[0]), |
|
|
|
width: sizes.stopBorder |
|
|
|
}), |
|
|
|
radius: sizes.stopRadius, |
|
|
|
}), |
|
|
|
radius: sizes.stopRadius |
|
|
|
}) |
|
|
|
}); |
|
|
|
|
|
|
|
const courseStyles = {}; |
|
|
|
|
|
|
|
const getCourseStyle = color => |
|
|
|
{ |
|
|
|
if (!(color in courseStyles)) |
|
|
|
{ |
|
|
|
const icon = document.createElement('canvas'); |
|
|
|
const getCourseStyle = lineColor => { |
|
|
|
if (!(lineColor in courseStyles)) { |
|
|
|
const icon = window.document.createElement("canvas"); |
|
|
|
|
|
|
|
const shapeSize = sizes.courseSize; |
|
|
|
const iconSize = sizes.courseSize + sizes.courseOuterBorder; |
|
|
@ -147,18 +136,17 @@ const getCourseStyle = color => |
|
|
|
const cx = icon.width / 2; |
|
|
|
const cy = icon.height / 2; |
|
|
|
|
|
|
|
const iconCtx = icon.getContext('2d'); |
|
|
|
const iconCtx = icon.getContext("2d"); |
|
|
|
|
|
|
|
for (let [color, size] of [ |
|
|
|
[makeBorderColor(color), sizes.courseOuterBorder], |
|
|
|
[color, sizes.courseBorder], |
|
|
|
[makeCourseColor(color), sizes.courseInnerBorder] |
|
|
|
]) |
|
|
|
{ |
|
|
|
for (const [color, size] of [ |
|
|
|
[makeBorderColor(lineColor), sizes.courseOuterBorder], |
|
|
|
[lineColor, sizes.courseBorder], |
|
|
|
[makeCourseColor(lineColor), sizes.courseInnerBorder] |
|
|
|
]) { |
|
|
|
iconCtx.fillStyle = color; |
|
|
|
iconCtx.strokeStyle = color; |
|
|
|
iconCtx.lineWidth = size; |
|
|
|
iconCtx.lineJoin = 'round'; |
|
|
|
iconCtx.lineJoin = "round"; |
|
|
|
iconCtx.miterLimit = 200000; |
|
|
|
|
|
|
|
iconCtx.beginPath(); |
|
|
@ -170,34 +158,34 @@ const getCourseStyle = color => |
|
|
|
iconCtx.fill(); |
|
|
|
} |
|
|
|
|
|
|
|
courseStyles[color] = new Style({ |
|
|
|
courseStyles[lineColor] = new Style({ |
|
|
|
image: new Icon({ |
|
|
|
img: icon, |
|
|
|
imgSize: [icon.width, icon.height], |
|
|
|
}), |
|
|
|
imgSize: [icon.width, icon.height] |
|
|
|
}) |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
return courseStyles[color]; |
|
|
|
return courseStyles[lineColor]; |
|
|
|
}; |
|
|
|
|
|
|
|
const createMap = target => |
|
|
|
{ |
|
|
|
const createMap = target => { |
|
|
|
|
|
|
|
// Map background
|
|
|
|
const backgroundSource = new XYZSource({ |
|
|
|
url: 'https://api.mapbox.com/' + [ |
|
|
|
'styles', 'v1', 'mapbox', 'streets-v11', |
|
|
|
'tiles', '512', '{z}', '{x}', '{y}', |
|
|
|
].join('/') + `?access_token=${mapboxToken}`, |
|
|
|
tileSize: [512, 512], |
|
|
|
url: `https://api.mapbox.com/${[ |
|
|
|
"styles", "v1", "mapbox", "streets-v11", |
|
|
|
"tiles", "512", "{z}", "{x}", "{y}" |
|
|
|
].join("/")}?access_token=${mapboxToken}`,
|
|
|
|
tileSize: [512, 512] |
|
|
|
}); |
|
|
|
|
|
|
|
const backgroundLayer = new TileLayer({ |
|
|
|
source: backgroundSource, |
|
|
|
source: backgroundSource |
|
|
|
}); |
|
|
|
|
|
|
|
// Static data overlay
|
|
|
|
const {segmentsSource, stopsSource} = makeDataSources(); |
|
|
|
const { segmentsSource, stopsSource } = makeDataSources(); |
|
|
|
|
|
|
|
const segmentsBorderLayer = new VectorLayer({ |
|
|
|
source: segmentsSource, |
|
|
@ -205,7 +193,7 @@ const createMap = target => |
|
|
|
style: segmentBorderStyle, |
|
|
|
|
|
|
|
updateWhileInteracting: true, |
|
|
|
updateWhileAnimating: true, |
|
|
|
updateWhileAnimating: true |
|
|
|
}); |
|
|
|
|
|
|
|
const segmentsInnerLayer = new VectorLayer({ |
|
|
@ -214,7 +202,7 @@ const createMap = target => |
|
|
|
style: segmentInnerStyle, |
|
|
|
|
|
|
|
updateWhileInteracting: true, |
|
|
|
updateWhileAnimating: true, |
|
|
|
updateWhileAnimating: true |
|
|
|
}); |
|
|
|
|
|
|
|
const stopsLayer = new VectorLayer({ |
|
|
@ -224,7 +212,7 @@ const createMap = target => |
|
|
|
|
|
|
|
minZoom: 13, |
|
|
|
updateWhileInteracting: true, |
|
|
|
updateWhileAnimating: true, |
|
|
|
updateWhileAnimating: true |
|
|
|
}); |
|
|
|
|
|
|
|
// Setup map
|
|
|
@ -232,7 +220,7 @@ const createMap = target => |
|
|
|
center: proj.fromLonLat([3.88, 43.605]), |
|
|
|
zoom: 14, |
|
|
|
maxZoom: 22, |
|
|
|
constrainResolution: true, |
|
|
|
constrainResolution: true |
|
|
|
}); |
|
|
|
|
|
|
|
const map = new Map({ |
|
|
@ -241,9 +229,9 @@ const createMap = target => |
|
|
|
backgroundLayer, |
|
|
|
segmentsBorderLayer, |
|
|
|
segmentsInnerLayer, |
|
|
|
stopsLayer, |
|
|
|
stopsLayer |
|
|
|
], |
|
|
|
view, |
|
|
|
view |
|
|
|
}); |
|
|
|
|
|
|
|
// Run courses simulation
|
|
|
@ -252,26 +240,25 @@ const createMap = target => |
|
|
|
// Course on which the view is currently focused
|
|
|
|
let focusedCourse = null; |
|
|
|
|
|
|
|
const startFocus = courseId => |
|
|
|
{ |
|
|
|
if (courseId in simulInstance.courses) |
|
|
|
{ |
|
|
|
const startFocus = courseId => { |
|
|
|
if (courseId in simulInstance.courses) { |
|
|
|
const course = simulInstance.courses[courseId]; |
|
|
|
|
|
|
|
view.animate({ |
|
|
|
center: course.position, |
|
|
|
duration: 500, |
|
|
|
}, () => focusedCourse = courseId); |
|
|
|
duration: 500 |
|
|
|
}, () => { |
|
|
|
focusedCourse = courseId; |
|
|
|
}); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const stopFocus = () => |
|
|
|
{ |
|
|
|
const stopFocus = () => { |
|
|
|
focusedCourse = null; |
|
|
|
}; |
|
|
|
|
|
|
|
// Draw courses directly on the map
|
|
|
|
map.on('postcompose', ev => |
|
|
|
{ |
|
|
|
map.on("postcompose", ev => { |
|
|
|
simulInstance.update(); |
|
|
|
|
|
|
|
// The normal way to access a layer’s vector context is through the
|
|
|
@ -281,17 +268,16 @@ const createMap = target => |
|
|
|
// if no stop is visible. This hack listens to the global `postcompose`
|
|
|
|
// event, which is always triggered at every frame, and reconstructs
|
|
|
|
// the stops layer’s vector context from internal variables
|
|
|
|
if (stopsLayer.renderer_) |
|
|
|
{ |
|
|
|
/* eslint-disable no-underscore-dangle */ |
|
|
|
if (stopsLayer.renderer_) { |
|
|
|
ev.context = stopsLayer.renderer_.context; |
|
|
|
ev.inversePixelTransform |
|
|
|
= stopsLayer.renderer_.inversePixelTransform; |
|
|
|
ev.inversePixelTransform = |
|
|
|
stopsLayer.renderer_.inversePixelTransform; |
|
|
|
/* eslint-enable no-underscore-dangle */ |
|
|
|
|
|
|
|
const ctx = getVectorContext(ev); |
|
|
|
let rotation = 0; |
|
|
|
|
|
|
|
for (let course of Object.values(simulInstance.courses)) |
|
|
|
{ |
|
|
|
for (const course of Object.values(simulInstance.courses)) { |
|
|
|
const color = network.lines[course.line].color; |
|
|
|
const style = getCourseStyle(color); |
|
|
|
|
|
|
@ -299,10 +285,10 @@ const createMap = target => |
|
|
|
ctx.setStyle(style); |
|
|
|
|
|
|
|
const point = new Point(course.position); |
|
|
|
|
|
|
|
ctx.drawGeometry(point); |
|
|
|
|
|
|
|
if (course.id === focusedCourse) |
|
|
|
{ |
|
|
|
if (course.id === focusedCourse) { |
|
|
|
view.setCenter(course.position); |
|
|
|
} |
|
|
|
} |
|
|
@ -313,20 +299,17 @@ const createMap = target => |
|
|
|
|
|
|
|
map.render(); |
|
|
|
|
|
|
|
map.on('singleclick', ev => |
|
|
|
{ |
|
|
|
map.on("singleclick", ev => { |
|
|
|
const mousePixel = map.getPixelFromCoordinate(ev.coordinate); |
|
|
|
const maxDistance = sizes.courseSize + sizes.courseOuterBorder; |
|
|
|
|
|
|
|
for (let course of Object.values(simulInstance.courses)) |
|
|
|
{ |
|
|
|
for (const course of Object.values(simulInstance.courses)) { |
|
|
|
const coursePixel = map.getPixelFromCoordinate(course.position); |
|
|
|
const dx = mousePixel[0] - coursePixel[0]; |
|
|
|
const dy = mousePixel[1] - coursePixel[1]; |
|
|
|
const distance = dx * dx + dy * dy; |
|
|
|
|
|
|
|
if (distance <= maxDistance * maxDistance) |
|
|
|
{ |
|
|
|
if (distance <= maxDistance * maxDistance) { |
|
|
|
startFocus(course.id); |
|
|
|
return; |
|
|
|
} |
|
|
|