diff --git a/.babelrc b/.babelrc index 7c1000df..394f0590 100644 --- a/.babelrc +++ b/.babelrc @@ -5,12 +5,5 @@ ], "plugins": [ "@babel/plugin-transform-runtime" - ], - "env": { - "development": { - "plugins": [ - "react-refresh/babel" - ] - } - } + ] } \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index e4334a2c..7d3ced80 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,5 @@ { - "parser": "babel-eslint", + "parser": "@babel/eslint-parser", "parserOptions": { "sourceType": "module" }, @@ -7,37 +7,33 @@ "rules": { "no-plusplus": [0], "no-restricted-syntax": [0], - "no-restricted-globals": "off", "no-console": 0, "no-nested-ternary": 0, - "no-continue": "off", - "max-len": 0, - "no-unused-vars": 0, - "guard-for-in": 0, - "no-underscore-dangle": 0, - "func-names": 0, - "import/named": [1], + "no-unused-vars": 1, + "class-methods-use-this": 1, "import/order": 0, "import/prefer-default-export": 0, + "import/no-extraneous-dependencies": [ + 2, + { + "devDependencies": [ + "webpack.*.js" + ] + } + ], "react/no-unused-prop-types": 0, "react/forbid-prop-types": 0, "react/jsx-filename-extension": [1, { "extensions": [".js"] }], "react/no-array-index-key": [0], "react/destructuring-assignment": 0, - "react/jsx-closing-bracket-location": 0, - "react/jsx-closing-tag-location": 0, - "react/jsx-curly-spacing": 0, - "react/jsx-equals-spacing": 0, - "react/jsx-first-prop-new-line": 0, - "react/jsx-indent": 0, - "react/jsx-indent-props": 0, - "react/jsx-max-props-per-line": 0, - "react/jsx-one-expression-per-line": 0, - "react/jsx-props-no-multi-spaces": 0, - "react/jsx-tag-spacing": 0, - "react/jsx-wrap-multilines": 0, + "react/jsx-props-no-spreading": 1, + "react/jsx-fragments": 1, + "react/function-component-definition": 0, + "react/no-unused-class-component-methods": 1, + "react/no-unstable-nested-components": 1, "jsx-a11y/img-has-alt": [0], - "jsx-a11y/alt-text": 0 + "jsx-a11y/alt-text": 0, + "prettier/prettier": 0 }, "env": { "browser": true, diff --git a/Dockerfile b/Dockerfile index 97c29a42..f0e8a7b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,8 +11,6 @@ RUN apt-get update \ && cp ./azcopy /usr/bin/ \ && rm -rf /var/lib/apt/lists/* -ENV NODE_OPTIONS=--openssl-legacy-provider - ENV WORK /opt/publisher RUN mkdir -p ${WORK} diff --git a/compose.yaml b/compose.yaml index 436b86dc..6e9cbb8f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,11 +2,15 @@ services: redis: image: redis + logging: + driver: none ports: - 6379:6379 publisher-postgres: image: postgres + logging: + driver: none ports: - 5433:5432 environment: @@ -41,6 +45,8 @@ services: publisher-render: image: hsl-map-publisher pull_policy: never # use the same build as publisher-server + logging: + driver: none ports: - 5000:5000 environment: diff --git a/package.json b/package.json index 5d568425..77b64f17 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,10 @@ "scripts": { "eslint-check": "eslint --print-config . | eslint-config-prettier-check", "prettier": "prettier \"src/**/*.{js,css}\" \"scripts/**/*.js\" \"migrations/**/*.js\" \"seeds/**/*.js\" \"*.js\" --write", - "lint": "eslint \"src/**/*.js\" \"scripts/**/*.js\" --fix", + "lint": "eslint \"src/**/*.js\" \"scripts/**/*.js\"", "build": "cross-env NODE_ENV=production webpack --config webpack.prod.js", "serve": "serve -l 5000 dist", - "start": "webpack-serve ./webpack.dev.js", + "start": "webpack serve -c ./webpack.dev.js", "start:production": "serve -l 5000 dist", "server": "node -r dotenv/config scripts/server", "server:production": "node -r dotenv/config scripts/server.js", @@ -49,82 +49,72 @@ }, "devDependencies": { "@babel/core": "^7.23.9", + "@babel/eslint-parser": "^7.24.5", "@babel/plugin-transform-runtime": "^7.23.9", "@babel/preset-env": "^7.23.9", "@babel/preset-react": "^7.23.3", - "babel-eslint": "^9.0.0", - "babel-loader": "^8.3.0", - "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-loader": "^9.1.3", "cross-env": "^5.1.1", - "css-loader": "^0.28.7", - "eslint": "^5.8.0", - "eslint-config-airbnb": "^17.1.0", - "eslint-config-prettier": "^3.1.0", + "css-loader": "~6", + "dotenv-webpack": "^8.1.0", + "eslint": "~8", + "eslint-config-airbnb": "~19.0.4", + "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-webpack": "^0.10.1", - "eslint-loader": "^2.1.1", "eslint-plugin-import": "^2.14.0", "eslint-plugin-jsx-a11y": "^6.1.2", - "eslint-plugin-prettier": "^3.0.0", + "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-react": "^7.11.1", + "eslint-plugin-react-hooks": "^4", + "eslint-webpack-plugin": "^4.1.0", + "html-webpack-plugin": "^5.6.0", "husky": "^8.0.0", "lint-staged": "^13.0.2", - "prettier": "^1.14.3", - "react-refresh": "^0.14.0", - "webpack-cli": "^3.0.8", - "webpack-serve": "^1.0.4", - "worker-loader": "^2.0.0" + "prettier": "^3.2.5", + "style-loader": "~4.0.0", + "webpack-cli": "~5.1.4", + "webpack-dev-server": "~5.0.4", + "worker-loader": "^3.0.8" }, "dependencies": { + "@apollo/client": "^3.10.4", "@azure/storage-blob": "^10.4.0", - "@koa/cors": "^2.2.1", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", + "@koa/cors": "^5.0.0", "@turf/turf": "^6.5.0", - "apollo-cache-inmemory": "^1.1.1", - "apollo-client": "^2.0.3", - "apollo-link": "^1.2.2", - "apollo-link-http": "^1.2.0", - "babel-polyfill": "^6.26.0", "bullmq": "^1.86.2", - "cheerio": "=1.0.0-rc.3", + "cheerio": "1.0.0-rc.12", "classnames": "^2.2.5", - "clean-webpack-plugin": "^0.1.19", "dotenv": "^8.1.0", - "dotenv-webpack": "^1.7.0", "fs-extra": "9.0.0", - "graphql": "^0.11.7", - "graphql-tag": "^2.5.0", + "graphql": "^16.8.1", "haversine": "^1.1.1", "hsl-map-style": "hsldevcom/hsl-map-style#master", - "html-webpack-plugin": "^4.0.0-alpha", "ioredis": "^5.0.6", "knex": "^2.0.0", "koa": "^2.5.3", "koa-body": "^6.0.1", "koa-json-body": "^5.3.0", - "koa-router": "^7.4.0", - "koa-session": "^5.10.1", + "koa-router": "^12.0.1", + "koa-session": "^6.4.0", "lodash": "^4.17.21", "moment": "^2.29.4", - "node-fetch": "^1.7.3", + "node-fetch": "~2", "p-map": "^1.2.0", "pdf-merger-js": "^3.4.0", "pg": "^8.7.3", "prop-types": "^15.6.0", - "puppeteer": "^13.7.0", + "puppeteer": "^22.9.0", "qrcode": "^1.0.0", "qs": "^6.5.2", - "raw-loader": "^0.5.1", - "react": "18.2.0", - "react-apollo": "^2.0.1", - "react-dom": "18.2.0", + "react": "~18.2", + "react-dom": "~18.2", "react-measure": "^2.5.2", "recompose": "^0.30.0", - "segseg": "^0.2.2", - "serve": "^13.0.2", - "style-loader": "^0.19.0", + "segseg": "^1.0.0", + "serve": "^14.2.3", "uuid": "^3.1.0", "viewport-mercator-project": "^4.1.1", - "webpack": "^4.12.1", + "webpack": "^5.91.0", "webpack-merge": "^4.1.3" } } diff --git a/scripts/generator.js b/scripts/generator.js index 5075f336..bbd90c36 100644 --- a/scripts/generator.js +++ b/scripts/generator.js @@ -8,7 +8,6 @@ const moment = require('moment'); const { AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_KEY, PUBLISHER_RENDER_URL } = require('../constants'); -const CLIENT_URL = PUBLISHER_RENDER_URL; const RENDER_TIMEOUT = 10 * 60 * 1000; const MAX_RENDER_ATTEMPTS = 3; const SCALE = 96 / 72; @@ -18,7 +17,7 @@ const cwd = process.cwd(); const pdfOutputDir = path.join(cwd, 'output'); -const pdfPath = id => path.join(pdfOutputDir, `${id}.pdf`); +const pdfPath = (id) => path.join(pdfOutputDir, `${id}.pdf`); async function initialize() { browser = await puppeteer.launch({ @@ -49,13 +48,13 @@ async function renderComponent(options) { await page.exposeFunction('serverLog', log); - page.on('error', error => { + page.on('error', (error) => { page.close(); browser.close(); onError(error); }); - page.on('console', message => { + page.on('console', (message) => { const { url, lineNumber, columnNumber } = message.location(); if (['error', 'warning', 'log'].includes(message.type())) { onInfo( @@ -72,10 +71,14 @@ async function renderComponent(options) { timeout: RENDER_TIMEOUT, }); - const { error = null, width, height } = await page.evaluate( + const { + error = null, + width, + height, + } = await page.evaluate( () => - new Promise(resolve => { - window.callPhantom = opts => resolve(opts); + new Promise((resolve) => { + window.callPhantom = (opts) => resolve(opts); }), ); @@ -123,9 +126,9 @@ async function generate(options) { onInfo('Creating new browser instance'); await initialize(); } - const timeout = new Promise((resolve, reject) => - setTimeout(reject, RENDER_TIMEOUT, new Error('Render timeout')), - ); + const timeout = new Promise((resolve, reject) => { + setTimeout(reject, RENDER_TIMEOUT, new Error('Render timeout')); + }); const posterUploaded = await Promise.race([renderComponent(options), timeout]); const uploadFailed = !posterUploaded && AZURE_STORAGE_ACCOUNT && AZURE_STORAGE_KEY; diff --git a/scripts/store.js b/scripts/store.js index 3ea31e34..364f2234 100644 --- a/scripts/store.js +++ b/scripts/store.js @@ -22,7 +22,7 @@ cleanup(() => { function convertKeys(object, converter) { const obj = {}; - Object.keys(object).forEach(key => { + Object.keys(object).forEach((key) => { obj[converter(key)] = object[key]; }); return obj; @@ -46,7 +46,7 @@ async function getBuilds() { .orderBy('build.created_at', 'desc') .groupBy('build.id'); - return rows.map(row => convertKeys(row, camelCase)); + return rows.map((row) => convertKeys(row, camelCase)); } async function getBuild({ id }) { @@ -87,8 +87,8 @@ async function getBuild({ id }) { .groupBy('poster.id'); const build = convertKeys(buildRow, camelCase); - const posters = posterRows.map(row => convertKeys(row, camelCase)); - return Object.assign({}, build, { posters }); + const posters = posterRows.map((row) => convertKeys(row, camelCase)); + return { ...build, posters }; } async function addBuild({ title }) { @@ -101,16 +101,12 @@ async function addBuild({ title }) { } async function updateBuild({ id, status }) { - await knex('build') - .where({ id }) - .update({ status, updated_at: knex.fn.now() }); + await knex('build').where({ id }).update({ status, updated_at: knex.fn.now() }); return { id }; } async function removeBuild({ id }) { - await knex('build') - .where({ id }) - .update({ status: 'REMOVED', updated_at: knex.fn.now() }); + await knex('build').where({ id }).update({ status: 'REMOVED', updated_at: knex.fn.now() }); return { id }; } @@ -145,17 +141,13 @@ async function addPoster({ buildId, component, template, props, order }) { ), order, }); - await knex('build') - .where('id', buildId) - .update({ updated_at: knex.fn.now() }); + await knex('build').where('id', buildId).update({ updated_at: knex.fn.now() }); return { id }; } async function updatePoster({ id, status }) { - await knex('poster') - .where({ id }) - .update({ status, updated_at: knex.fn.now() }); + await knex('poster').where({ id }).update({ status, updated_at: knex.fn.now() }); return { id }; } @@ -164,9 +156,7 @@ async function removePoster({ id }) { .returning('build_id') .where({ id }) .update({ status: 'REMOVED', updated_at: knex.fn.now() }); - await knex('build') - .where('id', buildId[0].build_id) - .update({ updated_at: knex.fn.now() }); + await knex('build').where('id', buildId[0].build_id).update({ updated_at: knex.fn.now() }); return { id }; } @@ -197,11 +187,7 @@ async function getTemplateImage(slot) { return emptySlot; } - const dbImg = await knex - .select('*') - .from('template_images') - .where({ name: imageName }) - .first(); + const dbImg = await knex.select('*').from('template_images').where({ name: imageName }).first(); if (dbImg) { return merge({}, slot, { image: dbImg }); @@ -216,19 +202,17 @@ async function getTemplateImages(template) { } return merge({}, template, { - areas: await pMap(template.areas, async area => + areas: await pMap(template.areas, async (area) => merge({}, area, { - slots: await pMap(area.slots, slot => getTemplateImage(slot)), + slots: await pMap(area.slots, (slot) => getTemplateImage(slot)), }), ), }); } async function getTemplates() { - const templates = await knex('template') - .select('*') - .orderBy('created_at', 'asc'); - return pMap(templates, template => getTemplateImages(template)); + const templates = await knex('template').select('*').orderBy('created_at', 'asc'); + return pMap(templates, (template) => getTemplateImages(template)); } async function addTemplate({ label }) { @@ -241,11 +225,7 @@ async function addTemplate({ label }) { } async function getTemplate({ id }, withImages = true) { - const templateRow = await knex - .select('*') - .from('template') - .where({ id }) - .first(); + const templateRow = await knex.select('*').from('template').where({ id }).first(); if (!withImages) { return templateRow; @@ -256,7 +236,7 @@ async function getTemplate({ id }, withImages = true) { // Not exported. Saves the passed images into the database. async function saveAreaImages(slots) { - return pMap(slots, async slot => { + return pMap(slots, async (slot) => { if (!slot.image) { return slot; } @@ -297,7 +277,7 @@ async function saveTemplate(template) { const { id } = template; const existingTemplate = await getTemplate({ id }, false); - const savedAreas = await pMap(template.areas, async area => + const savedAreas = await pMap(template.areas, async (area) => merge({}, area, { slots: await saveAreaImages(area.slots), }), @@ -325,37 +305,31 @@ async function removeTemplate({ id }) { throw error; } - await knex('template') - .where({ id }) - .del(); + await knex('template').where({ id }).del(); return { id }; } async function getImages() { - return knex('template_images') - .select('*') - .orderBy('updated_at', 'asc'); + return knex('template_images').select('*').orderBy('updated_at', 'asc'); } function templateHasImage(template, imageName) { - return template.areas.some(area => - area.slots.some(slot => get(slot, 'image.name', '_') === imageName), + return template.areas.some((area) => + area.slots.some((slot) => get(slot, 'image.name', '_') === imageName), ); } async function removeImage({ name }) { const templates = await knex('template').select('*'); - if (templates.some(template => templateHasImage(template, name))) { + if (templates.some((template) => templateHasImage(template, name))) { const error = new Error(`Image '${name}' is in use in one or more templates!`); error.status = 400; throw error; } - await knex('template_images') - .where({ name }) - .del(); + await knex('template_images').where({ name }).del(); return { name }; } @@ -398,9 +372,9 @@ async function getStopInfo({ stopId, date }) { const stopData = await response.json(); const { stop } = stopData.data; - const routeSegments = flatMap(stop.siblings.nodes, node => node.routeSegments.nodes); - const routeIds = routeSegments.map(routeSegment => routeSegment.routeId); - const modes = flatMap(routeSegments, node => node.route.nodes.map(route => route.mode)); + const routeSegments = flatMap(stop.siblings.nodes, (node) => node.routeSegments.nodes); + const routeIds = routeSegments.map((routeSegment) => routeSegment.routeId); + const modes = flatMap(routeSegments, (node) => node.route.nodes.map((route) => route.mode)); const city = stop.shortId.match(/^[a-zA-Z]*/)[0]; // Get the first letters of the id. const { stopZone } = stop; diff --git a/src/components/a3Timetable/a3TableRows.js b/src/components/a3Timetable/a3TableRows.js index bf65b6cc..13c8e2fa 100644 --- a/src/components/a3Timetable/a3TableRows.js +++ b/src/components/a3Timetable/a3TableRows.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Row, WrappingRow } from 'components/util'; -import { sortBy, keys, pickBy, groupBy } from 'lodash'; +import { sortBy, keys, pickBy } from 'lodash'; import { trimRouteId } from 'util/domain'; import classnames from 'classnames'; import TableHeader from './a3TableHeader'; @@ -9,7 +9,7 @@ import RouteDiagram from 'components/routeDiagram/routeDiagramContainer'; import styles from './a3TableRows.css'; -const Departure = props => ( +const Departure = (props) => (
{props.minutes < 10 && '0'} @@ -30,11 +30,11 @@ Departure.propTypes = { note: PropTypes.string, }; -const TableRow = props => ( +const TableRow = (props) => (
{props.hours}
- {sortBy(props.departures, a => a.minutes).map((departure, index) => ( + {sortBy(props.departures, (a) => a.minutes).map((departure, index) => ( ))} @@ -61,7 +61,7 @@ TableRow.propTypes = { departures: PropTypes.arrayOf(PropTypes.shape(Departure.propTypes)).isRequired, }; -const TableRows = props => { +const TableRows = (props) => { // Split columns to multiple divs if there's a day divider. const indexesToSplit = keys(pickBy(props.departures, 'segment')); const diagramExists = keys(pickBy(props.departures, 'diagram')); @@ -69,7 +69,7 @@ const TableRows = props => { indexesToSplit.push(props.departures.length); let startIndex = 0; - indexesToSplit.forEach(splitIndex => { + indexesToSplit.forEach((splitIndex) => { const splitHere = parseInt(splitIndex, 10); const departuresChunk = props.departures.slice(startIndex, splitHere); startIndex = splitIndex; @@ -104,7 +104,8 @@ const TableRows = props => { className={classnames(styles.a3container, { [styles.wide]: props.useWide, [styles.diagram]: diagramExists.length > 0, - })}> + })} + > {departureSegments.map((departureSegment, index) => { if (departureSegment.diagram) { if (!props.diagram) { @@ -133,7 +134,8 @@ const TableRows = props => { key={`tableRows_${index}`} className={classnames(styles.a3root, { [styles.summer]: props.isSummerTimetable, - })}> + })} + > {content}
); diff --git a/src/components/a3Timetable/a3Timetable.js b/src/components/a3Timetable/a3Timetable.js index 2072cf6f..060cdf1e 100644 --- a/src/components/a3Timetable/a3Timetable.js +++ b/src/components/a3Timetable/a3Timetable.js @@ -18,6 +18,120 @@ const SEGMENT_NAMES = { sundays: 'Sunnuntai', }; +const defaultColumns = (props) => { + const { weekdays, saturdays, sundays } = props; + const segments = [weekdays, saturdays, sundays]; + const departuresLength = segments + .flat() + .reduce((acc, element) => acc + element.departures.length, 0); + const useSingleColumn = departuresLength < SINGLE_COLUMN_MIN_DEPARTURES; + let departures = []; + for (let i = 0; i < segments.length; i++) { + const segment = segments[i]; + if (segment.length > 0) { + if (useSingleColumn) { + departures = departures.concat(segment); + } else { + departures.push(segment); + } + } + } + + return useSingleColumn ? [departures] : departures; +}; + +const getZoneLetterStyle = (zone) => ({ + transform: + zone === 'B' + ? 'translate(calc(-50%), calc(-50% + 2px))' + : zone === 'C' + ? 'translate(calc(-50% - 2px), calc(-50% + 2px))' + : zone === 'D' + ? 'translate(calc(-50% + 2px), calc(-50% + 2px))' + : 'translate(-50%, -50%)', // No px adjustments for zone A and the "else" case. +}); + +const getNotes = (notes, symbols) => { + const parsedNotes = []; + symbols.forEach((symbol) => { + notes.forEach((note) => { + if (note.substring(0, symbol.length) === symbol && !parsedNotes.includes(note)) { + parsedNotes.push(note); + } + }); + }); + return parsedNotes; +}; + +const nullOrEmpty = (arr) => !arr || arr.length === 0; + +const rowsByHour = (rowDepartures) => { + const departuresByHour = groupBy( + rowDepartures, + (departure) => (departure.isNextDay ? 24 : 0) + departure.hours, + ); + const rows = Object.entries(departuresByHour).map(([hours, departures]) => ({ + hour: hours, + departures, + })); + + const isEqualDepartureHour = (a, b) => { + if (!a || !b) { + return false; + } + if (a.length !== b.length) { + return false; + } + + for (let i = 0; i < a.length; i++) { + const curA = a[i]; + const curB = b[i]; + + if (!curA || !curB) { + return false; + } + + if (curA.minutes !== curB.minutes) { + return false; + } + if (curA.note !== curB.note) { + return false; + } + } + return true; + }; + + const formatHour = (hour) => `${hour % 24 < 10 ? '0' : ''}${hour % 24}`; + + const getDuplicateCutOff = (startIndex, rowArray) => { + const startRow = rowArray[startIndex]; + let cutOffIndex = startIndex; + for (let i = startIndex; i < rowArray.length; i++) { + const cur = rowArray[i]; + if (!isEqualDepartureHour(startRow.departures, cur.departures)) { + return cutOffIndex; + } + cutOffIndex = i; + } + return cutOffIndex; + }; + + const rowsByHourArr = []; + for (let i = 0; i < rows.length; i++) { + const cutOff = getDuplicateCutOff(i, rows); + const hours = + rows[i].hour === rows[cutOff].hour + ? `${formatHour(rows[i].hour)}` + : `${formatHour(rows[i].hour)}-${formatHour(rows[cutOff].hour)}`; + rowsByHourArr.push({ + hour: hours, + departures: rows[i].departures, + }); + i = cutOff; + } + return rowsByHourArr; +}; + class Timetable extends Component { constructor(props) { super(props); @@ -31,14 +145,13 @@ class Timetable extends Component { }, diagramRetries: this.props.groupedRows ? MAX_DIAGRAM_RETRIES : 0, }; + renderQueue.add(this); } componentDidMount() { - renderQueue.add(this); - let departures = []; - const weekdays = this.rowsByHour(this.props.weekdays); - const saturdays = this.rowsByHour(this.props.saturdays); - const sundays = this.rowsByHour(this.props.sundays); + const weekdays = rowsByHour(this.props.weekdays); + const saturdays = rowsByHour(this.props.saturdays); + const sundays = rowsByHour(this.props.sundays); if (weekdays.length > 0) { weekdays[0].segment = SEGMENT_NAMES.weekdays; @@ -49,9 +162,9 @@ class Timetable extends Component { if (sundays.length > 0) { sundays[0].segment = SEGMENT_NAMES.sundays; } - departures = this.props.groupedRows + const departures = this.props.groupedRows ? this.props.groupedRows - : this.defaultColumns({ weekdays, saturdays, sundays }); + : defaultColumns({ weekdays, saturdays, sundays }); this.setState({ weekdays: { @@ -59,14 +172,11 @@ class Timetable extends Component { groupedRows: departures, }, }); - renderQueue.onEmpty(error => !error && this.updateLayout(), { - ignore: this, - }); } componentDidUpdate() { window.setTimeout(() => { - renderQueue.onEmpty(error => !error && this.updateLayout(), { + renderQueue.onEmpty((error) => !error && this.updateLayout(), { ignore: this, }); }, 50); @@ -76,120 +186,6 @@ class Timetable extends Component { renderQueue.remove(this, { error: new Error(error) }); } - defaultColumns = props => { - const { weekdays, saturdays, sundays } = props; - const segments = [weekdays, saturdays, sundays]; - const departuresLength = segments - .flat() - .reduce((acc, element) => acc + element.departures.length, 0); - const useSingleColumn = departuresLength < SINGLE_COLUMN_MIN_DEPARTURES; - let departures = []; - for (let i = 0; i < segments.length; i++) { - const segment = segments[i]; - if (segment.length > 0) { - if (useSingleColumn) { - departures = departures.concat(segment); - } else { - departures.push(segment); - } - } - } - - return useSingleColumn ? [departures] : departures; - }; - - getZoneLetterStyle = zone => ({ - transform: - zone === 'B' - ? 'translate(calc(-50%), calc(-50% + 2px))' - : zone === 'C' - ? 'translate(calc(-50% - 2px), calc(-50% + 2px))' - : zone === 'D' - ? 'translate(calc(-50% + 2px), calc(-50% + 2px))' - : 'translate(-50%, -50%)', // No px adjustments for zone A and the "else" case. - }); - - getNotes = (notes, symbols) => { - const parsedNotes = []; - symbols.forEach(symbol => { - notes.forEach(note => { - if (note.substring(0, symbol.length) === symbol && !parsedNotes.includes(note)) { - parsedNotes.push(note); - } - }); - }); - return parsedNotes; - }; - - nullOrEmpty = arr => !arr || arr.length === 0; - - rowsByHour = rowDepartures => { - const departuresByHour = groupBy( - rowDepartures, - departure => (departure.isNextDay ? 24 : 0) + departure.hours, - ); - const rows = Object.entries(departuresByHour).map(([hours, departures]) => ({ - hour: hours, - departures, - })); - - const isEqualDepartureHour = (a, b) => { - if (!a || !b) { - return false; - } - if (a.length !== b.length) { - return false; - } - - for (let i = 0; i < a.length; i++) { - const curA = a[i]; - const curB = b[i]; - - if (!curA || !curB) { - return false; - } - - if (curA.minutes !== curB.minutes) { - return false; - } - if (curA.note !== curB.note) { - return false; - } - } - return true; - }; - - const formatHour = hour => `${hour % 24 < 10 ? '0' : ''}${hour % 24}`; - - const getDuplicateCutOff = (startIndex, rowArray) => { - const startRow = rowArray[startIndex]; - let cutOffIndex = startIndex; - for (let i = startIndex; i < rowArray.length; i++) { - const cur = rowArray[i]; - if (!isEqualDepartureHour(startRow.departures, cur.departures)) { - return cutOffIndex; - } - cutOffIndex = i; - } - return cutOffIndex; - }; - - const rowsByHourArr = []; - for (let i = 0; i < rows.length; i++) { - const cutOff = getDuplicateCutOff(i, rows); - const hours = - rows[i].hour === rows[cutOff].hour - ? `${formatHour(rows[i].hour)}` - : `${formatHour(rows[i].hour)}-${formatHour(rows[cutOff].hour)}`; - rowsByHourArr.push({ - hour: hours, - departures: rows[i].departures, - }); - i = cutOff; - } - return rowsByHourArr; - }; - hasOverflow() { if (!this.content) { console.log('no content'); @@ -237,14 +233,12 @@ class Timetable extends Component { break; } } - renderQueue.remove(this); this.props.updateHook(weekdays.groupedRows); return; } // If everything ok and using diagram remove process from queue and return if (allOk && this.state.useDiagram) { - renderQueue.remove(this); this.props.updateHook(weekdays.groupedRows); return; } @@ -308,17 +302,20 @@ class Timetable extends Component { weekdays, }); } + window.setTimeout(() => { + renderQueue.remove(this); + }, 1000); } render() { const allNullOrEmpty = - this.nullOrEmpty(this.props.weekdays) && - this.nullOrEmpty(this.props.saturdays) && - this.nullOrEmpty(this.props.sundays); + nullOrEmpty(this.props.weekdays) && + nullOrEmpty(this.props.saturdays) && + nullOrEmpty(this.props.sundays); if (allNullOrEmpty) { return null; } - const weekdaysRows = this.rowsByHour(this.state.weekdays); + const weekdaysRows = rowsByHour(this.state.weekdays); const chunkedRows = chunk(this.state.weekdays.groupedRows, COLUMNS_PER_PAGE); // Header is excluded from first page. const contentContainerStyle = { height: PAGE_HEIGHT - 97 }; @@ -336,32 +333,32 @@ class Timetable extends Component { [styles.a3]: true, [styles.standalone]: this.props.standalone, [styles.greyscale]: this.props.greyscale, - })}> + })} + > {this.props.standalone && ( - + <>
Vyöhyke
Zon/Zone
- + {this.props.stopZone}
-
+ )} {weekdaysRows.length > 0 && (
{ + ref={(ref) => { this.content = ref; - }}> - {chunkedRows.map((chunkedRow, index) => { + }} + > + {chunkedRows.map((chunkedRow) => { // Use wider timetable columns: // - If last column is an empty column // - If three columns and last one is empty @@ -382,7 +379,7 @@ class Timetable extends Component { return (
- {chunkedRow.map(rows => ( + {chunkedRow.map((rows) => ( } {this.props.showNotes && - this.getNotes(this.props.notes, this.props.specialSymbols).map(note => ( + getNotes(this.props.notes, this.props.specialSymbols).map(note => (
{note}
diff --git a/src/components/a3Timetable/a3TimetableContainer.js b/src/components/a3Timetable/a3TimetableContainer.js index 76757b3f..49c92044 100644 --- a/src/components/a3Timetable/a3TimetableContainer.js +++ b/src/components/a3Timetable/a3TimetableContainer.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; -import { graphql } from 'react-apollo'; -import gql from 'graphql-tag'; +import { gql } from '@apollo/client'; +import { graphql } from '@apollo/client/react/hoc'; import mapProps from 'recompose/mapProps'; import compose from 'recompose/compose'; import find from 'lodash/find'; @@ -17,7 +17,7 @@ import A3Timetable from './a3Timetable'; function filterDepartures(departures, routeSegments, routeFilter) { return departures .filter( - departure => + (departure) => !isDropOffOnly( find(routeSegments, { routeId: departure.routeId, @@ -25,16 +25,16 @@ function filterDepartures(departures, routeSegments, routeFilter) { }), ), ) - .filter(departure => filterRoute({ routeId: departure.routeId, filter: routeFilter })); + .filter((departure) => filterRoute({ routeId: departure.routeId, filter: routeFilter })); } function groupDepartures(departures) { return { - weekdays: departures.filter(departure => - departure.dayType.some(day => ['Ma', 'Ti', 'Ke', 'To', 'Pe'].includes(day)), + weekdays: departures.filter((departure) => + departure.dayType.some((day) => ['Ma', 'Ti', 'Ke', 'To', 'Pe'].includes(day)), ), - saturdays: departures.filter(departure => departure.dayType.includes('La')), - sundays: departures.filter(departure => departure.dayType.includes('Su')), + saturdays: departures.filter((departure) => departure.dayType.includes('La')), + sundays: departures.filter((departure) => departure.dayType.includes('Su')), }; } @@ -69,7 +69,7 @@ function getNotes(isSummerTimetable) { (noteType.includes('V') || noteType.includes(isSummerTimetable ? 'K' : 'T')) ); }) - .map(note => { + .map((note) => { const noteText = note.noteText || ''; return noteText.replace(/^(p|pe)(\s=)?\s/, 'pe = ').replace('’s', `'s`); }) @@ -80,6 +80,7 @@ function getNotes(isSummerTimetable) { const timetableQuery = gql` query timetableQuery($stopId: String!, $date: Date!) { stop: stopByStopId(stopId: $stopId) { + nodeId nameFi nameSe shortId @@ -87,6 +88,7 @@ const timetableQuery = gql` stopZone siblings { nodes { + nodeId routeSegments: routeSegmentsForDate(date: $date) { nodes { routeId @@ -162,12 +164,12 @@ function modifyNote(departureNote) { } } -const propsMapper = mapProps(props => { - let departures = flatMap(props.data.stop.siblings.nodes, stop => +const propsMapper = mapProps((props) => { + let departures = flatMap(props.data.stop.siblings.nodes, (stop) => filterDepartures(stop.departures.nodes, stop.routeSegments.nodes, props.routeFilter), ); - let notes = flatMap(props.data.stop.siblings.nodes, stop => + let notes = flatMap(props.data.stop.siblings.nodes, (stop) => flatMap(stop.routeSegments.nodes, getNotes(props.isSummerTimetable)), ); // if (props.data.stop.siblings.nodes.some(stop => @@ -182,15 +184,15 @@ const propsMapper = mapProps(props => { // Search for routes with two different destinations from the same stop and add notes for them Object.values( groupBy( - flatMap(props.data.stop.siblings.nodes, stop => stop.routeSegments.nodes).filter( - route => route.hasRegularDayDepartures && !isDropOffOnly(route), + flatMap(props.data.stop.siblings.nodes, (stop) => stop.routeSegments.nodes).filter( + (route) => route.hasRegularDayDepartures && !isDropOffOnly(route), ), - route => route.routeId, + (route) => route.routeId, ), ) - .filter(routes => routes.length > 1) - .forEach(directions => - directions.forEach(direction => { + .filter((routes) => routes.length > 1) + .forEach((directions) => + directions.forEach((direction) => { const noteSymbol = `${trimRouteId(direction.routeId)}${'*'.repeat(direction.direction)}`; if (!specialSymbols.includes(noteSymbol)) { specialSymbols.push(noteSymbol); @@ -204,17 +206,17 @@ const propsMapper = mapProps(props => { }), ); - departures.forEach(departure => { + departures.forEach((departure) => { if (departure.note && !specialSymbols.includes(departure.note)) { specialSymbols.push(departure.note); } }); - if (departures.some(departure => departure.routeId.includes('H'))) { + if (departures.some((departure) => departure.routeId.includes('H'))) { specialSymbols.push('H'); } - departures = departures.map(departure => ({ + departures = departures.map((departure) => ({ ...departure, note: modifyNote( [ @@ -229,13 +231,13 @@ const propsMapper = mapProps(props => { const { weekdays, saturdays, sundays } = pick(groupDepartures(departures), props.segments); const dateBegin = props.dateBegin || - flatMap(props.data.stop.siblings.nodes, stop => - stop.departures.nodes.map(departure => departure.dateBegin), + flatMap(props.data.stop.siblings.nodes, (stop) => + stop.departures.nodes.map((departure) => departure.dateBegin), ).sort((a, b) => b.localeCompare(a))[0]; const dateEnd = props.dateEnd || - flatMap(props.data.stop.siblings.nodes, stop => - stop.departures.nodes.map(departure => departure.dateEnd), + flatMap(props.data.stop.siblings.nodes, (stop) => + stop.departures.nodes.map((departure) => departure.dateEnd), ).sort((a, b) => a.localeCompare(b))[0]; return { weekdays, diff --git a/src/components/a3stopPoster/a3StopPosterContainer.js b/src/components/a3stopPoster/a3StopPosterContainer.js index 23e060e6..55d67226 100644 --- a/src/components/a3stopPoster/a3StopPosterContainer.js +++ b/src/components/a3stopPoster/a3StopPosterContainer.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; -import { graphql } from 'react-apollo'; -import gql from 'graphql-tag'; +import { gql } from '@apollo/client'; +import { graphql } from '@apollo/client/react/hoc'; import compose from 'recompose/compose'; import withProps from 'recompose/withProps'; import flatMap from 'lodash/flatMap'; @@ -13,9 +13,11 @@ import A3StopPoster from './a3stopPoster'; const stopPosterQuery = gql` query stopPosterQuery($stopId: String!, $date: Date!) { stop: stopByStopId(stopId: $stopId) { + nodeId shortId siblings { nodes { + nodeId routeSegments: routeSegmentsForDate(date: $date) { nodes { routeId @@ -39,27 +41,27 @@ const stopPosterQuery = gql` } `; -const propsMapper = withProps(props => { - const routeSegments = flatMap(props.data.stop.siblings.nodes, node => +const propsMapper = withProps((props) => { + const routeSegments = flatMap(props.data.stop.siblings.nodes, (node) => node.routeSegments.nodes - .filter(routeSegment => routeSegment.hasRegularDayDepartures) - .filter(routeSegment => !isNumberVariant(routeSegment.routeId)) - .filter(routeSegment => !isDropOffOnly(routeSegment)) - .filter(routeSegment => + .filter((routeSegment) => routeSegment.hasRegularDayDepartures) + .filter((routeSegment) => !isNumberVariant(routeSegment.routeId)) + .filter((routeSegment) => !isDropOffOnly(routeSegment)) + .filter((routeSegment) => filterRoute({ routeId: routeSegment.routeId, filter: props.routeFilter }), ), ); - const routeIds = routeSegments.map(routeSegment => trimRouteId(routeSegment.routeId)); - const modes = flatMap(routeSegments, node => node.route.nodes.map(route => route.mode)); + const routeIds = routeSegments.map((routeSegment) => trimRouteId(routeSegment.routeId)); + const modes = flatMap(routeSegments, (node) => node.route.nodes.map((route) => route.mode)); return { shortId: props.data.stop.shortId, hasRoutes: routeIds.length > 0, isTrunkStop: routeSegments.some( - routeSegment => routeSegment.line.nodes && routeSegment.line.nodes[0].trunkRoute === '1', + (routeSegment) => routeSegment.line.nodes && routeSegment.line.nodes[0].trunkRoute === '1', ), - isTramStop: modes.some(mode => mode === 'TRAM'), + isTramStop: modes.some((mode) => mode === 'TRAM'), }; }); diff --git a/src/components/a3stopPoster/a3header.js b/src/components/a3stopPoster/a3header.js index fa4b610d..43ff248b 100644 --- a/src/components/a3stopPoster/a3header.js +++ b/src/components/a3stopPoster/a3header.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { chunk, cloneDeep, sortBy } from 'lodash'; -import { Row, Column, InlineSVG } from 'components/util'; +import { chunk, sortBy } from 'lodash'; +import { Row, Column } from 'components/util'; import a3headerContainer from './a3headerContainer'; import renderQueue from 'util/renderQueue'; import { getColor } from 'util/domain'; @@ -12,16 +12,6 @@ import styles from './a3header.css'; const MAX_COLUMNS = 5; class A3Header extends Component { - static propTypes = { - routes: PropTypes.arrayOf( - PropTypes.shape({ - routeId: PropTypes.string.isRequired, - destinationFi: PropTypes.string.isRequired, - destinationSe: PropTypes.string, - }), - ).isRequired, - }; - constructor(props) { super(props); this.state = { @@ -71,10 +61,10 @@ class A3Header extends Component { render() { const routesPerColumn = Math.ceil(this.props.routes.length / MAX_COLUMNS); const routeColumns = chunk( - sortBy(this.props.routes, route => !route.trunkRoute), + sortBy(this.props.routes, (route) => !route.trunkRoute), routesPerColumn, ); - const routeIds = this.props.routes.map(route => route.routeId); + const routeIds = this.props.routes.map((route) => route.routeId); const zone = this.props.stop.stopZone; @@ -82,9 +72,10 @@ class A3Header extends Component { return (
{ + ref={(ref) => { this.root = ref; - }}> + }} + >
{this.props.stop.nameFi}
{this.props.stop.nameSe}
@@ -102,7 +93,8 @@ class A3Header extends Component {
+ style={{ color: getColor(route) }} + >
{route.destinationFi + (route.viaFi ? ` kautta ${route.viaFi}` : '')}
@@ -147,6 +139,13 @@ class A3Header extends Component { A3Header.propTypes = { stop: PropTypes.object.isRequired, variables: PropTypes.object.isRequired, + routes: PropTypes.arrayOf( + PropTypes.shape({ + routeId: PropTypes.string.isRequired, + destinationFi: PropTypes.string.isRequired, + destinationSe: PropTypes.string, + }), + ).isRequired, }; export default a3headerContainer(A3Header); diff --git a/src/components/a3stopPoster/a3headerContainer.js b/src/components/a3stopPoster/a3headerContainer.js index 95aa6f7d..44c06a03 100644 --- a/src/components/a3stopPoster/a3headerContainer.js +++ b/src/components/a3stopPoster/a3headerContainer.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; -import { graphql } from 'react-apollo'; -import gql from 'graphql-tag'; +import { gql } from '@apollo/client'; +import { graphql } from '@apollo/client/react/hoc'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import flatMap from 'lodash/flatMap'; @@ -12,12 +12,14 @@ import routeCompare from 'util/routeCompare'; const routesQuery = gql` query routesQuery($stopId: String!, $date: Date!) { stop: stopByStopId(stopId: $stopId) { + nodeId nameFi nameSe shortId stopZone siblings { nodes { + nodeId routeSegments: routeSegmentsForDate(date: $date) { nodes { routeId @@ -45,18 +47,18 @@ const routesQuery = gql` } `; -const propsMapper = mapProps(props => ({ +const propsMapper = mapProps((props) => ({ variables: props.data.variables, stop: props.data.stop, - routes: flatMap(props.data.stop.siblings.nodes, node => + routes: flatMap(props.data.stop.siblings.nodes, (node) => node.routeSegments.nodes - .filter(routeSegment => routeSegment.hasRegularDayDepartures === true) - .filter(routeSegment => !isNumberVariant(routeSegment.routeId)) - .filter(routeSegment => !isDropOffOnly(routeSegment)) - .filter(routeSegment => + .filter((routeSegment) => routeSegment.hasRegularDayDepartures === true) + .filter((routeSegment) => !isNumberVariant(routeSegment.routeId)) + .filter((routeSegment) => !isDropOffOnly(routeSegment)) + .filter((routeSegment) => filterRoute({ routeId: routeSegment.routeId, filter: props.routeFilter }), ) - .map(routeSegment => ({ + .map((routeSegment) => ({ ...routeSegment.route.nodes[0], viaFi: routeSegment.viaFi, viaSe: routeSegment.viaSe, @@ -69,7 +71,7 @@ const propsMapper = mapProps(props => ({ const hoc = compose(graphql(routesQuery), apolloWrapper(propsMapper)); -export default component => { +export default (component) => { const A3HeaderContainer = hoc(component); A3HeaderContainer.propTypes = { diff --git a/src/components/a3stopPoster/a3stopPoster.js b/src/components/a3stopPoster/a3stopPoster.js index 2a15d635..c72016eb 100644 --- a/src/components/a3stopPoster/a3stopPoster.js +++ b/src/components/a3stopPoster/a3stopPoster.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { JustifiedColumn } from 'components/util'; import renderQueue from 'util/renderQueue'; import { colorsByMode } from 'util/domain'; -import { chunk } from 'lodash'; +import { chunk, isEqual } from 'lodash'; import Timetable from 'components/a3Timetable/a3TimetableContainer'; import Header from './a3header'; @@ -29,12 +29,12 @@ class A3StopPoster extends Component { diagramOptions: defaultDiagramOptions, pageCount: 1, }; + renderQueue.add(this); } componentDidMount() { - renderQueue.add(this); this.updateLayout(); - renderQueue.onEmpty(error => !error && this.updateLayout(), { + renderQueue.onEmpty((error) => !error && this.updateLayout(), { ignore: this, }); } @@ -43,7 +43,7 @@ class A3StopPoster extends Component { if (this.hasOverflow()) { this.updateLayout(); } - renderQueue.onEmpty(error => !error && this.updateLayout(), { + renderQueue.onEmpty((error) => !error && this.updateLayout(), { ignore: this, }); } @@ -52,12 +52,13 @@ class A3StopPoster extends Component { renderQueue.remove(this, { error: new Error(error) }); } - updateHook = groupedRows => { - const chunkedRows = chunk(groupedRows, 3); - const pageCount = chunkedRows.length; - - this.setState({ groupedRows, pageCount }); - this.updateLayout(); + updateHook = (groupedRows) => { + if (!isEqual(groupedRows, this.state.groupedRows)) { + const chunkedRows = chunk(groupedRows, 3); + const pageCount = chunkedRows.length; + this.setState({ groupedRows, pageCount }); + this.updateLayout(); + } }; updateLayout() { @@ -74,7 +75,7 @@ class A3StopPoster extends Component { }); return; } - renderQueue.remove(this, { error: new Error('Failed to remove routes overflow') }); + this.onError('Failed to remove routes overflow'); return; } window.setTimeout(() => { @@ -102,31 +103,13 @@ class A3StopPoster extends Component { isSummerTimetable, dateBegin, dateEnd, + routeFilter, } = this.props; if (!hasRoutesProp) { return null; } - const printAsA3 = true; - const StopPosterTimetable = props => ( - - ); const containerStyle = {}; if (isTrunkStop) { containerStyle['--background'] = colorsByMode.TRUNK; @@ -137,26 +120,34 @@ class A3StopPoster extends Component { diagramOptions: this.state.diagramOptions, stopId, date, - routeFilter: this.props.routeFilter, - printAsA3, + routeFilter, + printAsA3: true, }; return (
{ + ref={(ref) => { this.content = ref; - }}> + }} + >
- { + renderQueue.onEmpty((error) => { if (error) { App.handleError(error); return; @@ -73,7 +79,7 @@ class App extends Component { try { params = qs.parse(window.location.search, { ignoreQueryPrefix: true, - decoder: str => { + decoder: (str) => { // Make booleans booleans again // qs encodes booleans to strings, we need to make sure that they are real booleans. if (str === 'true') { @@ -108,9 +114,10 @@ class App extends Component { return (
{ + ref={(ref) => { this.root = ref; - }}> + }} + > diff --git a/src/components/labelPlacement/costFunctions.js b/src/components/labelPlacement/costFunctions.js index b3c37289..ef5ffc01 100644 --- a/src/components/labelPlacement/costFunctions.js +++ b/src/components/labelPlacement/costFunctions.js @@ -65,7 +65,13 @@ function getOverlapCost(positions, indexes) { * @param {boolean} */ function hasIntersectingLines(a, b) { - return !!segseg(a.x, a.y, a.x + a.cx, a.y + a.cy, b.x, b.y, b.x + b.cx, b.y + b.cy); + return segseg( + [NaN, NaN], + [a.x, a.y], + [a.x + a.cx, a.y + a.cy], + [b.x, b.y], + [b.x + b.cx, b.y + b.cy], + ); } /** @@ -111,10 +117,14 @@ function getFixedIntersectionCost(positions, indexes) { const bl = [b.left, b.top + b.height]; const br = [b.left + b.width, b.top + b.height]; - const p1 = segseg(a0, a1, tl, tr); - const p2 = segseg(a0, a1, tl, bl); - const p3 = segseg(a0, a1, bl, br); - const p4 = segseg(a0, a1, tr, br); + const p1 = [NaN, NaN]; + segseg(p1, a0, a1, tl, tr); + const p2 = [NaN, NaN]; + segseg(p2, a0, a1, tl, bl); + const p3 = [NaN, NaN]; + segseg(p3, a0, a1, bl, br); + const p4 = [NaN, NaN]; + segseg(p4, a0, a1, tr, br); const intersections = [p1, p2, p3, p4].filter(p => Array.isArray(p)); diff --git a/src/components/labelPlacement/itemFixed.js b/src/components/labelPlacement/itemFixed.js index c9c4d9e7..18e86539 100644 --- a/src/components/labelPlacement/itemFixed.js +++ b/src/components/labelPlacement/itemFixed.js @@ -1,3 +1,5 @@ +/* eslint-disable react/no-unused-class-component-methods */ + import React, { Component } from 'react'; import PropTypes from 'prop-types'; @@ -31,10 +33,11 @@ class ItemFixed extends Component { } return (
{ + ref={(ref) => { this.root = ref; }} - style={style}> + style={style} + > {this.props.children}
); diff --git a/src/components/labelPlacement/itemPositioned.js b/src/components/labelPlacement/itemPositioned.js index 4ba45eca..8abf66cc 100644 --- a/src/components/labelPlacement/itemPositioned.js +++ b/src/components/labelPlacement/itemPositioned.js @@ -1,3 +1,5 @@ +/* eslint-disable react/no-unused-class-component-methods */ + import React, { Component } from 'react'; import PropTypes from 'prop-types'; @@ -27,10 +29,11 @@ class ItemPositioned extends Component { const style = { ...this.state, position: 'absolute' }; return (
{ + ref={(ref) => { this.root = ref; }} - style={style}> + style={style} + > {this.props.children}
); diff --git a/src/components/lineTimetable/lineTableHeader.js b/src/components/lineTimetable/lineTableHeader.js index 2b0fb808..e2011c55 100644 --- a/src/components/lineTimetable/lineTableHeader.js +++ b/src/components/lineTimetable/lineTableHeader.js @@ -1,10 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; import styles from './lineTableHeader.css'; -const LineTableHeader = props => { +const LineTableHeader = (props) => { const { stop } = props; return (
diff --git a/src/components/lineTimetable/lineTimetableContainer.js b/src/components/lineTimetable/lineTimetableContainer.js index 12fcb80e..31734df7 100644 --- a/src/components/lineTimetable/lineTimetableContainer.js +++ b/src/components/lineTimetable/lineTimetableContainer.js @@ -1,7 +1,6 @@ -/* eslint-disable no-undef */ import PropTypes from 'prop-types'; -import { graphql } from 'react-apollo'; -import gql from 'graphql-tag'; +import { gql } from '@apollo/client'; +import { graphql } from '@apollo/client/react/hoc'; import mapProps from 'recompose/mapProps'; import compose from 'recompose/compose'; import { filter, find } from 'lodash'; @@ -50,6 +49,7 @@ const lineQuery = gql` } } stop: stopByStopId { + nodeId stopId lat lon @@ -95,18 +95,18 @@ const departureQuery = gql` } `; -const filterTimedStopsListFromLineQuery = props => { - const routeForSelectedDirection = find(props.data.line.routes.nodes, route => { +const filterTimedStopsListFromLineQuery = (props) => { + const routeForSelectedDirection = find(props.data.line.routes.nodes, (route) => { return route.direction === props.routeDirection; }); const stopList = routeForSelectedDirection.routeSegments.nodes; - const filteredStopsList = filter(stopList, stop => { + const filteredStopsList = filter(stopList, (stop) => { return stop.stopIndex <= 1 || stop.timingStopType > 0; }); return { timedStops: filteredStopsList, allStops: stopList }; }; -const lineQueryMapper = mapProps(props => { +const lineQueryMapper = mapProps((props) => { const { dateBegin, dateEnd, routeDirection, showPrintBtn, lang } = props; const { line } = props.data; const { timedStops, allStops } = filterTimedStopsListFromLineQuery(props); @@ -125,13 +125,13 @@ const lineQueryMapper = mapProps(props => { }; }); -const departuresMapper = mapProps(props => { +const departuresMapper = mapProps((props) => { console.log(props); const departures = props.data.departures.nodes; - const departuresByStop = props.timedStops.map(timedStop => { + const departuresByStop = props.timedStops.map((timedStop) => { const stopDepartures = departures.filter( - departure => departure.stopId === timedStop.stop.stopId, + (departure) => departure.stopId === timedStop.stop.stopId, ); return { stop: timedStop.stop, diff --git a/src/components/map/customMap.js b/src/components/map/customMap.js index f93ad1a2..2d654da7 100644 --- a/src/components/map/customMap.js +++ b/src/components/map/customMap.js @@ -10,35 +10,13 @@ import { sizedSvg } from '../../util/sizedSvg'; const MAP_MIN_HEIGHT = 500; class CustomMap extends Component { - static propTypes = { - stopId: PropTypes.string.isRequired, - date: PropTypes.string.isRequired, - // eslint-disable-next-line react/require-default-props - isSummerTimetable: PropTypes.bool, - // eslint-disable-next-line react/require-default-props - template: PropTypes.any, - setMapHeight: PropTypes.func.isRequired, - mapZoneSymbols: PropTypes.bool, - mapZones: PropTypes.bool, - showSalesPoint: PropTypes.bool, - minimapZoneSymbols: PropTypes.bool, - minimapZones: PropTypes.bool, - legend: PropTypes.bool, - }; - - static defaultProps = { - mapZoneSymbols: false, - mapZones: false, - showSalesPoint: false, - minimapZoneSymbols: false, - minimapZones: false, - legend: false, - }; - - state = { - mapWidth: -1, - mapHeight: -1, - }; + constructor(props) { + super(props); + this.state = { + mapWidth: -1, + mapHeight: -1, + }; + } componentDidMount() { renderQueue.add(this); @@ -141,7 +119,8 @@ class CustomMap extends Component { width: '100%', height: wrapperHeight, }} - ref={measureRef}> + ref={measureRef} + > {renderMap === 'svg' ? ( ) : renderMap === 'local' ? ( @@ -166,4 +145,29 @@ class CustomMap extends Component { } } +CustomMap.propTypes = { + stopId: PropTypes.string.isRequired, + date: PropTypes.string.isRequired, + // eslint-disable-next-line react/require-default-props + isSummerTimetable: PropTypes.bool, + // eslint-disable-next-line react/require-default-props + template: PropTypes.any, + setMapHeight: PropTypes.func.isRequired, + mapZoneSymbols: PropTypes.bool, + mapZones: PropTypes.bool, + showSalesPoint: PropTypes.bool, + minimapZoneSymbols: PropTypes.bool, + minimapZones: PropTypes.bool, + legend: PropTypes.bool, +}; + +CustomMap.defaultProps = { + mapZoneSymbols: false, + mapZones: false, + showSalesPoint: false, + minimapZoneSymbols: false, + minimapZones: false, + legend: false, +}; + export default CustomMap; diff --git a/src/components/map/stopMap.js b/src/components/map/stopMap.js index f148bc4b..ee0de9b2 100644 --- a/src/components/map/stopMap.js +++ b/src/components/map/stopMap.js @@ -42,19 +42,19 @@ const INFO_MARGIN_LEFT = 44; const Attribution = () =>
© OpenStreetMap
; -const LocationSymbol = props => ( +const LocationSymbol = (props) => (
); -const getSalesPointIcon = type => ( +const getSalesPointIcon = (type) => ( ); -const getZoneIcon = zone => { +const getZoneIcon = (zone) => { switch (zone) { case 'A': return ; @@ -73,7 +73,7 @@ const getZoneIcon = zone => { } }; -const ZoneLabel = props => ( +const ZoneLabel = () => (
Vyöhyke Zon/Zone @@ -81,7 +81,7 @@ const ZoneLabel = props => (
); -const ZoneSymbol = props => ( +const ZoneSymbol = (props) => (
{getZoneIcon(props.zone)}
@@ -97,10 +97,10 @@ ZoneSymbol.propTypes = { zone: PropTypes.string.isRequired, }; -const getSymbolForEachZone = projectedSymbolsWithDistance => { +const getSymbolForEachZone = (projectedSymbolsWithDistance) => { const zones = []; const uniqueSymbols = []; - projectedSymbolsWithDistance.forEach(symbol => { + projectedSymbolsWithDistance.forEach((symbol) => { if (!zones || !zones.includes(symbol.zone)) { zones.push(symbol.zone); uniqueSymbols.push(symbol); @@ -110,8 +110,8 @@ const getSymbolForEachZone = projectedSymbolsWithDistance => { }; const calculateSymbolDistancesFromStops = (stops, symbols) => { - const symbolsWithStopDistances = symbols.map(symbol => { - const stopDistances = stops.map(stop => { + const symbolsWithStopDistances = symbols.map((symbol) => { + const stopDistances = stops.map((stop) => { const xDif = Math.abs(symbol.sy - stop.x); const yDif = Math.abs(symbol.sx - stop.y); return yDif + xDif; @@ -131,8 +131,8 @@ const calculateSymbolDistancesFromStops = (stops, symbols) => { const getLegend = (stops, projectedSalesPoints, subwayEntrances) => { const modes = []; - stops.forEach(stop => { - stop.routes.forEach(route => { + stops.forEach((stop) => { + stop.routes.forEach((route) => { const { mode } = route; if (mode && !modes.includes(mode)) { modes.push(mode); @@ -143,7 +143,7 @@ const getLegend = (stops, projectedSalesPoints, subwayEntrances) => { }); }); - const legendContent = modes.map(mode => { + const legendContent = modes.map((mode) => { switch (mode) { case 'BUS': return ( @@ -254,7 +254,7 @@ const getLegend = (stops, projectedSalesPoints, subwayEntrances) => { }; }; -const StopMap = props => { +const StopMap = (props) => { const mapStyle = { width: props.mapOptions.width, height: props.mapOptions.height, @@ -272,20 +272,20 @@ const StopMap = props => { // Filter out stops that are behind the mini map const stops = props.nearbyStops.filter( - stop => stop.x < miniMapStyle.left || stop.y < miniMapStyle.top, + (stop) => stop.x < miniMapStyle.left || stop.y < miniMapStyle.top, ); // Filter out zone symbols that are behind the mini map // Added extra buffed width because zone symbols might be cut half by minimap const projectedSymbols = props.projectedSymbols.filter( - symbol => + (symbol) => symbol.sx < miniMapStyle.top - ZONE_SYMBOL_MAP_PADDING || symbol.sy < miniMapStyle.left - ZONE_SYMBOL_MAP_PADDING_EXTRA, ); // Avoid map edges so zone symbol and text is fully visible const projectedSymbolsInBbox = projectedSymbols.filter( - symbol => + (symbol) => symbol.sy > ZONE_SYMBOL_MAP_PADDING && symbol.sx > ZONE_SYMBOL_MAP_PADDING && symbol.sy < mapStyle.width - ZONE_SYMBOL_MAP_PADDING_EXTRA && @@ -338,7 +338,7 @@ const StopMap = props => { {stops.map((stop, index) => ( @@ -348,9 +348,10 @@ const StopMap = props => { {!isTerminal && ( + left={props.currentStop.x - STOP_RADIUS} + > @@ -360,7 +361,8 @@ const StopMap = props => { {!isTerminal && ( + left={props.currentStop.x - LOCATION_RADIUS} + >
Olet tässä
@@ -384,7 +386,8 @@ const StopMap = props => { x={stop.x} y={stop.y} distance={25} - angle={stop.calculatedHeading}> + angle={stop.calculatedHeading} + > ))} @@ -392,7 +395,8 @@ const StopMap = props => { {nearestSalePoint && ( + left={nearestSalePoint.x - SALES_POINT_RADIUS} + >
{salesPointIcon} @@ -451,7 +455,8 @@ const StopMap = props => { top: miniMarkerOffsetTop, left: miniMarkerOffsetLeft, position: 'absolute', - }}> + }} + >
diff --git a/src/components/map/stopMapContainer.js b/src/components/map/stopMapContainer.js index cb3bb8d0..8532ff4e 100644 --- a/src/components/map/stopMapContainer.js +++ b/src/components/map/stopMapContainer.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; -import { graphql } from 'react-apollo'; -import gql from 'graphql-tag'; +import { gql } from '@apollo/client'; +import { graphql } from '@apollo/client/react/hoc'; import mapProps from 'recompose/mapProps'; import compose from 'recompose/compose'; import flatMap from 'lodash/flatMap'; @@ -58,6 +58,7 @@ const nearbyItemsQuery = gql` nameSe stops { nodes { + nodeId calculatedHeading platform routeSegments: routeSegmentsForDate(date: $date) { @@ -99,16 +100,16 @@ const nearbyItemsQuery = gql` } `; -const stopsMapper = stopGroup => ({ +const stopsMapper = (stopGroup) => ({ ...stopGroup, // Assume all stops face the same way calculatedHeading: stopGroup.stops.nodes[0].calculatedHeading, - routes: flatMap(stopGroup.stops.nodes, node => + routes: flatMap(stopGroup.stops.nodes, (node) => node.routeSegments.nodes - .filter(routeSegment => routeSegment.hasRegularDayDepartures === true) - .filter(routeSegment => !isNumberVariant(routeSegment.routeId)) - .filter(routeSegment => !isDropOffOnly(routeSegment)) - .map(routeSegment => { + .filter((routeSegment) => routeSegment.hasRegularDayDepartures === true) + .filter((routeSegment) => !isNumberVariant(routeSegment.routeId)) + .filter((routeSegment) => !isDropOffOnly(routeSegment)) + .map((routeSegment) => { const mergedRouteSegment = mergeRouteSegments( routeSegment, get(routeSegment, 'route.nodes[0].routeSegments.nodes', []), @@ -133,14 +134,15 @@ const stopsMapper = stopGroup => ({ }; }), ).sort(routeCompare), + stops: stopGroup.stops.nodes, }); -const nearbyItemsMapper = mapProps(props => { +const nearbyItemsMapper = mapProps((props) => { const stops = props.data.stopGroups.nodes // Merge properties from mode-specific stops .map(stopsMapper) // Filter out stops with no departures - .filter(stop => !!stop.routes.length); + .filter((stop) => !!stop.routes.length); const { projectedStops, @@ -172,7 +174,7 @@ const nearbyItemsMapper = mapProps(props => { // Calculate distances to sale points and get the nearest one const nearestSalePoint = props.showSalesPoint ? projectedSalePoints - .map(sp => { + .map((sp) => { // Euclidean distance const distance = haversine( { latitude: sp.lat, longitude: sp.lon }, @@ -185,7 +187,7 @@ const nearbyItemsMapper = mapProps(props => { : null; const projectedSalesPoints = []; - props.salePoints.forEach(salePoint => { + props.salePoints.forEach((salePoint) => { if ( salePoint.lon > minLon && salePoint.lon < maxLon && @@ -240,17 +242,19 @@ const nearbyItemsMapper = mapProps(props => { const mapPositionQuery = gql` query mapPositionQuery($stopId: String!) { stop: stopByStopId(stopId: $stopId) { + nodeId lat lon } terminal: terminalByTerminalId(terminalId: $stopId) { + nodeId lat lon } } `; -const mapInterestsMapper = mapProps(props => { +const mapInterestsMapper = mapProps((props) => { const { stop, terminal } = props.data; // Use either stop or terminal information, depending on which query has succeeded. const longitude = stop ? stop.lon : terminal.lon; @@ -288,11 +292,11 @@ const mapInterestsMapper = mapProps(props => { const getSalePoints = () => fetch(process.env.SALES_POINT_DATA_URL, { method: 'GET' }) - .then(response => response.json()) - .then(data => + .then((response) => response.json()) + .then((data) => data.features - .filter(sp => SALE_POINT_TYPES.includes(sp.properties.Tyyppi)) - .map(sp => { + .filter((sp) => SALE_POINT_TYPES.includes(sp.properties.Tyyppi)) + .map((sp) => { const { properties } = sp; const { coordinates } = sp.geometry; const [lon, lat] = coordinates; @@ -307,7 +311,7 @@ const getSalePoints = () => }), ); -const fetchOSMObjects = async props => { +const fetchOSMObjects = async (props) => { let results; try { const osmData = await fetch( @@ -322,7 +326,7 @@ const fetchOSMObjects = async props => { return results; }; -const osmPointsMapper = mapProps(props => { +const osmPointsMapper = mapProps((props) => { const subwayEntrances = fetchOSMObjects(props); return { ...props, @@ -330,7 +334,7 @@ const osmPointsMapper = mapProps(props => { }; }); -const salePointsMapper = mapProps(props => { +const salePointsMapper = mapProps((props) => { // If sales points are not configured, do not fetch them but return empty array const salePoints = props.showSalesPoint || props.legend ? getSalePoints() : Promise.resolve([]); return { diff --git a/src/components/qrCode.js b/src/components/qrCode.js index d6ab87e0..07124b56 100644 --- a/src/components/qrCode.js +++ b/src/components/qrCode.js @@ -42,9 +42,9 @@ class QrCode extends Component { style={{ display: 'block', width: '100%' }} src={this.state.src} onLoad={() => renderQueue.remove(this)} - onError={() => - renderQueue.remove(this, { error: new Error('Failed to render QR code') }) - } + onError={() => { + renderQueue.remove(this, { error: new Error('Failed to render QR code') }); + }} alt="" /> )} diff --git a/src/components/routeDiagram/routeDiagramContainer.js b/src/components/routeDiagram/routeDiagramContainer.js index eda32e1d..6a484106 100644 --- a/src/components/routeDiagram/routeDiagramContainer.js +++ b/src/components/routeDiagram/routeDiagramContainer.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; -import { graphql } from 'react-apollo'; -import gql from 'graphql-tag'; +import { gql } from '@apollo/client'; +import { graphql } from '@apollo/client/react/hoc'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import flatMap from 'lodash/flatMap'; @@ -15,10 +15,12 @@ const routeDiagramQuery = gql` query routeDiagramQuery($stopIds: [String]!, $date: Date!) { stops: getStopsByIds(stopIds: $stopIds) { nodes { + nodeId shortId stopZone siblings { nodes { + nodeId routeSegments: routeSegmentsForDate(date: $date) { nodes { routeId @@ -35,6 +37,7 @@ const routeDiagramQuery = gql` nodes { stopIndex stopByStopId { + nodeId nameFi nameSe shortId @@ -53,8 +56,10 @@ const routeDiagramQuery = gql` } } terminalByTerminalId { + nodeId siblings { nodes { + nodeId modes(date: $date) { nodes } @@ -81,27 +86,27 @@ const nodeToStop = ({ stopByStopId }) => { const transferModes = flatMap( terminalByTerminalId.siblings.nodes, // Filter out bus terminals, until we have more specs how to handle those - sibling => sibling.modes.nodes.filter(mode => mode !== 'BUS'), + (sibling) => sibling.modes.nodes.filter((mode) => mode !== 'BUS'), ); return { ...stop, transferModes }; }; -const propsMapper = mapProps(props => { - const routes = flatMap(props.data.stops.nodes, s => - flatMap(s.siblings.nodes, stop => +const propsMapper = mapProps((props) => { + const routes = flatMap(props.data.stops.nodes, (s) => + flatMap(s.siblings.nodes, (stop) => stop.routeSegments.nodes // Select regular routes that allow boarding from current stop - .filter(routeSegment => routeSegment.hasRegularDayDepartures === true) - .filter(routeSegment => !isNumberVariant(routeSegment.routeId)) - .filter(routeSegment => !isDropOffOnly(routeSegment)) - .filter(routeSegment => + .filter((routeSegment) => routeSegment.hasRegularDayDepartures === true) + .filter((routeSegment) => !isNumberVariant(routeSegment.routeId)) + .filter((routeSegment) => !isDropOffOnly(routeSegment)) + .filter((routeSegment) => filterRoute({ routeId: routeSegment.routeId, filter: props.routeFilter }), ) - .map(routeSegment => ({ + .map((routeSegment) => ({ ...routeSegment.route.nodes[0], routeId: trimRouteId(routeSegment.routeId), // List all stops (including drop-off only) for each route - stops: sortBy(routeSegment.nextStops.nodes, node => node.stopIndex).map(nodeToStop), + stops: sortBy(routeSegment.nextStops.nodes, (node) => node.stopIndex).map(nodeToStop), })), ), ); diff --git a/src/components/stopPoster/headerContainer.js b/src/components/stopPoster/headerContainer.js index a013de84..afd4b3a5 100644 --- a/src/components/stopPoster/headerContainer.js +++ b/src/components/stopPoster/headerContainer.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; -import { graphql } from 'react-apollo'; -import gql from 'graphql-tag'; +import { gql } from '@apollo/client'; +import { graphql } from '@apollo/client/react/hoc'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import apolloWrapper from 'util/apolloWrapper'; @@ -10,16 +10,19 @@ import Header from './header'; const headerQuery = gql` query headerQuery($stopId: String!) { stop: stopByStopId(stopId: $stopId) { + nodeId shortId nameFi nameSe stopZone } terminal: terminalByTerminalId(terminalId: $stopId) { + nodeId nameFi nameSe stops: stopsByTerminalId { nodes { + nodeId stopZone } } @@ -27,7 +30,7 @@ const headerQuery = gql` } `; -const propsMapper = mapProps(props => { +const propsMapper = mapProps((props) => { const { stop, terminal } = props.data; // Get the details from stop or terminal depending on how the query returned data const { shortId, nameFi, nameSe } = stop || terminal; diff --git a/src/components/stopPoster/routes.js b/src/components/stopPoster/routes.js index 24f32907..85bd9194 100644 --- a/src/components/stopPoster/routes.js +++ b/src/components/stopPoster/routes.js @@ -12,25 +12,6 @@ import styles from './routes.css'; const MAX_COLUMNS = 6; class Routes extends Component { - static propTypes = { - routes: PropTypes.arrayOf( - PropTypes.shape({ - routeId: PropTypes.string.isRequired, - destinationFi: PropTypes.string.isRequired, - destinationSe: PropTypes.string, - }), - ).isRequired, - platformInfo: PropTypes.bool, - betterLayoutAvailable: PropTypes.bool, - triggerAnotherLayout: PropTypes.func, - }; - - static defaultProps = { - platformInfo: false, - betterLayoutAvailable: false, - triggerAnotherLayout: () => {}, - }; - constructor(props) { super(props); this.state = { columns: MAX_COLUMNS }; @@ -58,7 +39,7 @@ class Routes extends Component { if (this.hasOverflow()) { if (this.state.columns > 1) { renderQueue.add(this); - this.setState(state => ({ columns: state.columns - 1 })); + this.setState((state) => ({ columns: state.columns - 1 })); return; } if (!this.props.betterLayoutAvailable) { @@ -72,15 +53,16 @@ class Routes extends Component { render() { const routesPerColumn = Math.ceil(this.props.routes.length / this.state.columns); const routeColumns = chunk( - sortBy(this.props.routes, route => !route.trunkRoute), + sortBy(this.props.routes, (route) => !route.trunkRoute), routesPerColumn, ); return (
{ + ref={(ref) => { this.root = ref; - }}> + }} + > {routeColumns.map((routes, i) => ( @@ -136,4 +118,23 @@ class Routes extends Component { } } +Routes.propTypes = { + routes: PropTypes.arrayOf( + PropTypes.shape({ + routeId: PropTypes.string.isRequired, + destinationFi: PropTypes.string.isRequired, + destinationSe: PropTypes.string, + }), + ).isRequired, + platformInfo: PropTypes.bool, + betterLayoutAvailable: PropTypes.bool, + triggerAnotherLayout: PropTypes.func, +}; + +Routes.defaultProps = { + platformInfo: false, + betterLayoutAvailable: false, + triggerAnotherLayout: () => {}, +}; + export default routesContainer(Routes); diff --git a/src/components/stopPoster/routesContainer.js b/src/components/stopPoster/routesContainer.js index ebee56bd..fc475229 100644 --- a/src/components/stopPoster/routesContainer.js +++ b/src/components/stopPoster/routesContainer.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; -import { graphql } from 'react-apollo'; -import gql from 'graphql-tag'; +import { gql } from '@apollo/client'; +import { graphql } from '@apollo/client/react/hoc'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import flatMap from 'lodash/flatMap'; @@ -14,9 +14,11 @@ import routeCompare from 'util/routeCompare'; const routesQuery = gql` query stopPosterQuery($stopId: String!, $date: Date!) { stop: stopByStopId(stopId: $stopId) { + nodeId shortId siblings { nodes { + nodeId platform routeSegments: routeSegmentsForDate(date: $date) { nodes { @@ -45,21 +47,21 @@ const routesQuery = gql` } `; -const propsMapper = mapProps(props => { +const propsMapper = mapProps((props) => { const { data, routeFilter, ...propsToForward } = props; const stops = flatMap( - data.stop.siblings.nodes.map(s => + data.stop.siblings.nodes.map((s) => s.routeSegments.nodes - .map(routeSegment => ({ ...routeSegment, platform: s.platform })) - .filter(routeSegment => routeSegment.hasRegularDayDepartures === true) - .filter(routeSegment => !isNumberVariant(routeSegment.routeId)) - .filter(routeSegment => !isDropOffOnly(routeSegment)) - .filter(routeSegment => + .map((routeSegment) => ({ ...routeSegment, platform: s.platform })) + .filter((routeSegment) => routeSegment.hasRegularDayDepartures === true) + .filter((routeSegment) => !isNumberVariant(routeSegment.routeId)) + .filter((routeSegment) => !isDropOffOnly(routeSegment)) + .filter((routeSegment) => filterRoute({ routeId: routeSegment.routeId, filter: routeFilter }), ), ), ); - const routes = stops.map(routeSegment => ({ + const routes = stops.map((routeSegment) => ({ ...routeSegment.route.nodes[0], viaFi: routeSegment.viaFi, viaSe: routeSegment.viaSe, @@ -70,14 +72,14 @@ const propsMapper = mapProps(props => { })); // Group similar routes and place the platforminfo in the list - const routesGrouped = Object.values(groupBy(routes, r => r.routeId + r.destinationFi)) - .map(r => + const routesGrouped = Object.values(groupBy(routes, (r) => r.routeId + r.destinationFi)) + .map((r) => r.reduce((prev, curr) => ({ ...prev, platforms: prev.platforms.concat(curr.platform) }), { ...r[0], platforms: [], }), ) - .map(r => ({ ...r, platforms: compact(r.platforms).sort() })) + .map((r) => ({ ...r, platforms: compact(r.platforms).sort() })) .sort(routeCompare); return { @@ -88,7 +90,7 @@ const propsMapper = mapProps(props => { const hoc = compose(graphql(routesQuery), apolloWrapper(propsMapper)); -export default component => { +export default (component) => { const RoutesContainer = hoc(component); RoutesContainer.propTypes = { diff --git a/src/components/stopPoster/stopPoster.js b/src/components/stopPoster/stopPoster.js index 0229d247..fe8b216f 100644 --- a/src/components/stopPoster/stopPoster.js +++ b/src/components/stopPoster/stopPoster.js @@ -90,13 +90,13 @@ class StopPoster extends Component { }); } - renderQueue.onEmpty(error => !error && this.updateLayout(), { + renderQueue.onEmpty((error) => !error && this.updateLayout(), { ignore: this, }); } componentDidUpdate() { - renderQueue.onEmpty(error => !error && this.updateLayout(), { + renderQueue.onEmpty((error) => !error && this.updateLayout(), { ignore: this, }); } @@ -125,10 +125,10 @@ class StopPoster extends Component { removeAdsFromTemplate(ads) { const { template } = this.state; const removedAds = []; - ads.slots.forEach(slot => { + ads.slots.forEach((slot) => { if (slot.image) removedAds.push(slot); }); - template.areas.find(t => t.key === 'ads').slots = []; + template.areas.find((t) => t.key === 'ads').slots = []; this.setState({ removedAds, template, @@ -144,7 +144,7 @@ class StopPoster extends Component { // Remove ads from template and try to add them later when there's no overflow. // i.e when this.state.adsPhase = true if (this.state.template && !this.state.removedAds) { - const ads = get(this.state.template, 'areas', []).find(t => t.key === 'ads'); + const ads = get(this.state.template, 'areas', []).find((t) => t.key === 'ads'); if (ads.slots.length > 0) { this.removeAdsFromTemplate(ads); return; @@ -154,7 +154,7 @@ class StopPoster extends Component { // If we get overflow we remove ad from template. if (this.state.adsPhase) { const { template, removedAds } = this.state; - const ads = get(template, 'areas', []).find(t => t.key === 'ads'); + const ads = get(template, 'areas', []).find((t) => t.key === 'ads'); if ( !removedAds || @@ -170,7 +170,7 @@ class StopPoster extends Component { ads.slots.push(removedAds.pop()); } - template.areas.find(t => t.key === 'ads').slots = ads.slots; + template.areas.find((t) => t.key === 'ads').slots = ads.slots; window.setTimeout(() => { this.setState({ @@ -251,7 +251,7 @@ class StopPoster extends Component { const template = cloneDeep(this.state.template); const mapTemplate = template - ? get(template, 'areas', []).find(t => t.key === 'map' || t.key === 'tram') + ? get(template, 'areas', []).find((t) => t.key === 'map' || t.key === 'tram') : null; if (mapTemplate) { for (let i = 0; i < template.areas.length; i++) { @@ -287,7 +287,7 @@ class StopPoster extends Component { if (this.state.template && this.state.removedAds.length > 0) { const { template } = this.state; - const svg = get(template, 'areas', []).find(t => t.key === 'map').slots[0]; + const svg = get(template, 'areas', []).find((t) => t.key === 'map').slots[0]; // If using svg postpone adsPhase untill we have mapHeight. if (!svg.image) { this.setState({ adsPhase: true }); @@ -336,7 +336,6 @@ class StopPoster extends Component { const { template, - mapHeight, hasRoutesOnTop, hasDiagram, hasStretchedLeftColumn, @@ -345,13 +344,13 @@ class StopPoster extends Component { hasColumnTimetable, } = this.state; - const src = get(template, 'areas', []).find(t => t.key === 'tram'); + const src = get(template, 'areas', []).find((t) => t.key === 'tram'); const tramImage = get(src, 'slots[0].image.svg', ''); // Use diagram, if it's available, except on tram and lightrail stops with tramimage const useTramDiagram = (isTramStop || isLightRail) && tramImage; const useDiagram = hasDiagram && !useTramDiagram; - const StopPosterTimetable = props => ( + const StopPosterTimetable = (props) => (
{ + ref={(ref) => { this.content = ref; - }}> + }} + > {hasRoutes && hasRoutesOnTop && ( @@ -409,19 +409,16 @@ class StopPoster extends Component { t.key === 'ads') : {}} + template={ + template ? get(template, 'areas', []).find((t) => t.key === 'ads') : {} + } />
- {({ - measureRef, - contentRect: { - client: { height: rightColumnHeight }, - }, - }) => ( + {({ measureRef }) => (
{!hasColumnTimetable && (
@@ -449,7 +446,7 @@ class StopPoster extends Component { isSummerTimetable={isSummerTimetable} template={ template - ? get(template, 'areas', []).find(t => t.key === 'map') + ? get(template, 'areas', []).find((t) => t.key === 'map') : template // null if template is loading, false if no template } mapZoneSymbols={mapZoneSymbols} @@ -479,7 +476,7 @@ class StopPoster extends Component {
t.key === 'footer') : {}} + template={template ? get(template, 'areas', []).find((t) => t.key === 'footer') : {}} shortId={shortId} isTrunkStop={isTrunkStop} /> diff --git a/src/components/stopPoster/stopPosterContainer.js b/src/components/stopPoster/stopPosterContainer.js index cdf0c161..a94c6025 100644 --- a/src/components/stopPoster/stopPosterContainer.js +++ b/src/components/stopPoster/stopPosterContainer.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; -import { graphql } from 'react-apollo'; -import gql from 'graphql-tag'; +import { gql } from '@apollo/client'; +import { graphql } from '@apollo/client/react/hoc'; import compose from 'recompose/compose'; import withProps from 'recompose/withProps'; import flatMap from 'lodash/flatMap'; @@ -13,9 +13,11 @@ import StopPoster from './stopPoster'; const stopPosterQuery = gql` query stopPosterQuery($stopId: String!, $date: Date!) { stop: stopByStopId(stopId: $stopId) { + nodeId shortId siblings { nodes { + nodeId routeSegments: routeSegmentsForDate(date: $date) { nodes { routeId @@ -39,27 +41,27 @@ const stopPosterQuery = gql` } `; -const propsMapper = withProps(props => { - const routeSegments = flatMap(props.data.stop.siblings.nodes, node => +const propsMapper = withProps((props) => { + const routeSegments = flatMap(props.data.stop.siblings.nodes, (node) => node.routeSegments.nodes - .filter(routeSegment => routeSegment.hasRegularDayDepartures) - .filter(routeSegment => !isNumberVariant(routeSegment.routeId)) - .filter(routeSegment => !isDropOffOnly(routeSegment)) - .filter(routeSegment => + .filter((routeSegment) => routeSegment.hasRegularDayDepartures) + .filter((routeSegment) => !isNumberVariant(routeSegment.routeId)) + .filter((routeSegment) => !isDropOffOnly(routeSegment)) + .filter((routeSegment) => filterRoute({ routeId: routeSegment.routeId, filter: props.routeFilter }), ), ); - const routeIds = routeSegments.map(routeSegment => trimRouteId(routeSegment.routeId)); - const modes = flatMap(routeSegments, node => node.route.nodes.map(route => route.mode)); + const routeIds = routeSegments.map((routeSegment) => trimRouteId(routeSegment.routeId)); + const modes = flatMap(routeSegments, (node) => node.route.nodes.map((route) => route.mode)); return { shortId: props.data.stop.shortId, hasRoutes: routeIds.length > 0, isTrunkStop: routeSegments.some( - routeSegment => routeSegment.line.nodes && routeSegment.line.nodes[0].trunkRoute === '1', + (routeSegment) => routeSegment.line.nodes && routeSegment.line.nodes[0].trunkRoute === '1', ), - isTramStop: modes.some(mode => mode === 'TRAM'), - isLightRail: modes.some(mode => mode === 'L_RAIL'), + isTramStop: modes.some((mode) => mode === 'TRAM'), + isLightRail: modes.some((mode) => mode === 'L_RAIL'), }; }); diff --git a/src/components/stopPoster/terminalPoster.js b/src/components/stopPoster/terminalPoster.js index 4d2bb9bd..c1e0e65d 100644 --- a/src/components/stopPoster/terminalPoster.js +++ b/src/components/stopPoster/terminalPoster.js @@ -88,13 +88,13 @@ class TerminalPoster extends Component { }); } - renderQueue.onEmpty(error => !error && this.updateLayout(), { + renderQueue.onEmpty((error) => !error && this.updateLayout(), { ignore: this, }); } componentDidUpdate() { - renderQueue.onEmpty(error => !error && this.updateLayout(), { + renderQueue.onEmpty((error) => !error && this.updateLayout(), { ignore: this, }); } @@ -124,10 +124,10 @@ class TerminalPoster extends Component { removeAdsFromTemplate(ads) { const { template } = this.state; const removedAds = []; - ads.slots.forEach(slot => { + ads.slots.forEach((slot) => { if (slot.image) removedAds.push(slot); }); - template.areas.find(t => t.key === 'ads').slots = []; + template.areas.find((t) => t.key === 'ads').slots = []; this.setState({ removedAds, template, @@ -143,7 +143,7 @@ class TerminalPoster extends Component { // Remove ads from template and try to add them later when there's no overflow. // i.e when this.state.adsPhase = true if (this.state.template && !this.state.removedAds) { - const ads = get(this.state.template, 'areas', []).find(t => t.key === 'ads'); + const ads = get(this.state.template, 'areas', []).find((t) => t.key === 'ads'); if (ads.slots.length > 0) { this.removeAdsFromTemplate(ads); return; @@ -153,7 +153,7 @@ class TerminalPoster extends Component { // If we get overflow we remove ad from template. if (this.state.adsPhase) { const { template, removedAds } = this.state; - const ads = get(template, 'areas', []).find(t => t.key === 'ads'); + const ads = get(template, 'areas', []).find((t) => t.key === 'ads'); if ( !removedAds || @@ -169,7 +169,7 @@ class TerminalPoster extends Component { ads.slots.push(removedAds.pop()); } - template.areas.find(t => t.key === 'ads').slots = ads.slots; + template.areas.find((t) => t.key === 'ads').slots = ads.slots; window.setTimeout(() => { this.setState({ @@ -253,7 +253,7 @@ class TerminalPoster extends Component { const template = cloneDeep(this.state.template); const mapTemplate = template - ? get(template, 'areas', []).find(t => t.key === 'map' || t.key === 'tram') + ? get(template, 'areas', []).find((t) => t.key === 'map' || t.key === 'tram') : null; if (mapTemplate) { for (let i = 0; i < template.areas.length; i++) { @@ -283,7 +283,7 @@ class TerminalPoster extends Component { if (this.state.template && this.state.removedAds && this.state.removedAds.length > 0) { const { template } = this.state; - const svg = get(template, 'areas', []).find(t => t.key === 'map').slots[0]; + const svg = get(template, 'areas', []).find((t) => t.key === 'map').slots[0]; // If using svg postpone adsPhase untill we have mapHeight. if (!svg.image) { this.setState({ adsPhase: true }); @@ -353,15 +353,15 @@ class TerminalPoster extends Component { } = this.state; const { isTramStop } = this.props; - const src = get(template, 'areas', []).find(t => t.key === 'tram'); + const src = get(template, 'areas', []).find((t) => t.key === 'tram'); const tramImage = get(src, 'slots[0].image.svg', ''); let useDiagram = this.state.hasDiagram; if (isTramStop && tramImage) useDiagram = false; - const TerminalPosterTimetable = props => ( - - {stops.map(id => ( + const TerminalPosterTimetable = (props) => ( + <> + {stops.map((id) => (
))} -
+ ); return ( @@ -388,14 +388,16 @@ class TerminalPoster extends Component { className={classNames(styles.root, { [styles.smallTerminalPoster]: isSmallTerminalPoster, })} - style={isTrunkStop ? trunkStopStyle : null}> + style={isTrunkStop ? trunkStopStyle : null} + >
{ + ref={(ref) => { this.content = ref; - }}> + }} + > {hasRoutes && hasRoutesOnTop && ( t.key === 'ads') : {} + template ? get(template, 'areas', []).find((t) => t.key === 'ads') : {} } />
@@ -460,7 +462,7 @@ class TerminalPoster extends Component { isSummerTimetable={isSummerTimetable} template={ template - ? get(template, 'areas', []).find(t => t.key === 'map') + ? get(template, 'areas', []).find((t) => t.key === 'map') : template // null if template is loading, false if no template } mapZoneSymbols={mapZoneSymbols} @@ -491,7 +493,7 @@ class TerminalPoster extends Component {