├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── dist ├── network-animator.LICENSES.json └── network-animator.js ├── docker-compose.yml ├── docs ├── README.md ├── classes │ ├── BoundingBox.md │ ├── Config.md │ ├── Projection.md │ ├── Rotation.md │ ├── SvgAnimator.md │ └── Vector.md └── interfaces │ ├── SvgAbstractTimedDrawableAttributes.md │ ├── SvgCrumpledImageAttributes.md │ ├── SvgKenImageAttributes.md │ ├── SvgLabelAttributes.md │ ├── SvgLineAttributes.md │ ├── SvgStationAttributes.md │ └── SvgTrainAttributes.md ├── examples ├── cologne-sbahn.svg ├── ice-network.css ├── ice-network.png ├── ice-network.svg ├── map_1920.png ├── map_LICENSE.txt ├── trains.css ├── trains.svg ├── travel-times-fernverkehr.css └── travel-times-fernverkehr.svg ├── package-lock.json ├── package.json ├── screentest ├── _screentest.spec.js ├── cologne-sbahn.svg_1000.png ├── ice-network.svg_2022.png ├── trains.svg_1000.png └── travel-times-fernverkehr.svg_2030-12.png ├── src ├── Animator.ts ├── ArrivalDepartureTime.ts ├── BoundingBox.ts ├── Config.ts ├── DrawableSorter.ts ├── Gravitator.ts ├── Instant.ts ├── LineGroup.ts ├── Network.ts ├── PreferredTrack.ts ├── Projection.ts ├── Rotation.ts ├── Utils.ts ├── Vector.ts ├── Zoomer.ts ├── drawables │ ├── AbstractTimedDrawable.ts │ ├── CrumpledImage.ts │ ├── GenericTimedDrawable.ts │ ├── KenImage.ts │ ├── Label.ts │ ├── Line.ts │ ├── Station.ts │ ├── TimedDrawable.ts │ └── Train.ts ├── main.ts └── svg │ ├── SvgAbstractTimedDrawable.ts │ ├── SvgAnimator.ts │ ├── SvgApi.ts │ ├── SvgCrumpledImage.ts │ ├── SvgGenericTimedDrawable.ts │ ├── SvgKenImage.ts │ ├── SvgLabel.ts │ ├── SvgLine.ts │ ├── SvgNetwork.ts │ ├── SvgStation.ts │ ├── SvgTrain.ts │ └── SvgUtils.ts ├── test ├── BoundingBox.spec.ts ├── DrawableSorter.spec.ts ├── Instant.spec.ts ├── LineGroup.spec.ts ├── Network.spec.ts ├── PreferredTrack.spec.ts ├── Rotation.spec.ts ├── SvgUtils.spec.ts ├── Utils.spec.ts ├── Vector.spec.ts ├── Zoomer.spec.ts └── drawables │ ├── Label.spec.ts │ ├── Line.spec.ts │ └── Station.spec.ts ├── timecut-parallel.sh ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | output 3 | out 4 | videos 5 | working 6 | .nyc_output 7 | *.log 8 | script.md 9 | *-diff.png 10 | *-new.png 11 | network-animator.min.js 12 | network-animator.js.map -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alekzonder/puppeteer:latest 2 | 3 | USER root 4 | 5 | RUN apt-get update && apt-get install -yq ffmpeg 6 | RUN yarn global add timecut 7 | RUN yarn global add jest 8 | RUN yarn add puppeteer-screenshot-tester 9 | 10 | USER pptruser -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Traines 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /dist/network-animator.LICENSES.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "delaunator", 4 | "version": "5.0.0", 5 | "author": "Vladimir Agafonkin", 6 | "repository": "https://github.com/mapbox/delaunator", 7 | "source": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", 8 | "license": "ISC", 9 | "licenseText": "ISC License\n\nCopyright (c) 2017, Mapbox\n\nPermission to use, copy, modify, and/or distribute this software for any purpose\nwith or without fee is hereby granted, provided that the above copyright notice\nand this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS\nOF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER\nTORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF\nTHIS SOFTWARE.\n" 10 | }, 11 | { 12 | "name": "expose-loader", 13 | "version": "1.0.3", 14 | "author": "Tobias Koppers @sokra", 15 | "repository": "https://github.com/webpack-contrib/expose-loader", 16 | "source": "https://registry.npmjs.org/expose-loader/-/expose-loader-1.0.3.tgz", 17 | "license": "MIT", 18 | "licenseText": "Copyright JS Foundation and other contributors\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" 19 | }, 20 | { 21 | "name": "fmin", 22 | "version": "0.0.2", 23 | "author": "Ben Frederickson", 24 | "repository": "https://github.com/benfred/fmin", 25 | "source": "https://registry.npmjs.org/fmin/-/fmin-0.0.2.tgz", 26 | "license": "BSD-3-Clause", 27 | "licenseText": "Copyright 2016, Ben Frederickson\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of the author nor the names of contributors may be used to\n endorse or promote products derived from this software without specific prior\n written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" 28 | } 29 | ] -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.0" 2 | 3 | services: 4 | network-animator-render: 5 | build: ./ 6 | container_name: network-animator-render 7 | volumes: 8 | - ./dist/:/app/dist/ 9 | - ./examples/:/app/examples/ 10 | - ./output/:/app/output/ 11 | - ./screentest/:/app/screentest/ 12 | shm_size: 1G 13 | entrypoint: "timecut examples/ice-network.svg --start 5 --duration=450 --viewport=3840,2160 --fps=60 --pipe-mode --launch-arguments='--no-sandbox --disable-setuid-sandbox --allow-file-access-from-files' --pix-fmt=yuv420p --output=output/out.mp4" 14 | 15 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | transport-network-animator 2 | 3 | # transport-network-animator 4 | 5 | ## Table of contents 6 | 7 | ### Classes 8 | 9 | - [BoundingBox](classes/BoundingBox.md) 10 | - [Config](classes/Config.md) 11 | - [Projection](classes/Projection.md) 12 | - [Rotation](classes/Rotation.md) 13 | - [SvgAnimator](classes/SvgAnimator.md) 14 | - [Vector](classes/Vector.md) 15 | 16 | ### Interfaces 17 | 18 | - [SvgAbstractTimedDrawableAttributes](interfaces/SvgAbstractTimedDrawableAttributes.md) 19 | - [SvgCrumpledImageAttributes](interfaces/SvgCrumpledImageAttributes.md) 20 | - [SvgKenImageAttributes](interfaces/SvgKenImageAttributes.md) 21 | - [SvgLabelAttributes](interfaces/SvgLabelAttributes.md) 22 | - [SvgLineAttributes](interfaces/SvgLineAttributes.md) 23 | - [SvgStationAttributes](interfaces/SvgStationAttributes.md) 24 | - [SvgTrainAttributes](interfaces/SvgTrainAttributes.md) 25 | -------------------------------------------------------------------------------- /docs/classes/BoundingBox.md: -------------------------------------------------------------------------------- 1 | [transport-network-animator](../README.md) / BoundingBox 2 | 3 | # Class: BoundingBox 4 | 5 | ## Table of contents 6 | 7 | ### Constructors 8 | 9 | - [constructor](BoundingBox.md#constructor) 10 | 11 | ### Properties 12 | 13 | - [br](BoundingBox.md#br) 14 | - [tl](BoundingBox.md#tl) 15 | 16 | ### Accessors 17 | 18 | - [dimensions](BoundingBox.md#dimensions) 19 | 20 | ### Methods 21 | 22 | - [add](BoundingBox.md#add) 23 | - [calculateBoundingBoxForZoom](BoundingBox.md#calculateboundingboxforzoom) 24 | - [isNull](BoundingBox.md#isnull) 25 | - [from](BoundingBox.md#from) 26 | 27 | ## Constructors 28 | 29 | ### constructor 30 | 31 | • **new BoundingBox**(`tl`, `br`) 32 | 33 | #### Parameters 34 | 35 | | Name | Type | 36 | | :------ | :------ | 37 | | `tl` | [`Vector`](Vector.md) | 38 | | `br` | [`Vector`](Vector.md) | 39 | 40 | #### Defined in 41 | 42 | [BoundingBox.ts:4](https://github.com/traines-source/transport-network-animator/blob/master/src/BoundingBox.ts#L4) 43 | 44 | ## Properties 45 | 46 | ### br 47 | 48 | • **br**: [`Vector`](Vector.md) 49 | 50 | ___ 51 | 52 | ### tl 53 | 54 | • **tl**: [`Vector`](Vector.md) 55 | 56 | ## Accessors 57 | 58 | ### dimensions 59 | 60 | • `get` **dimensions**(): [`Vector`](Vector.md) 61 | 62 | #### Returns 63 | 64 | [`Vector`](Vector.md) 65 | 66 | #### Defined in 67 | 68 | [BoundingBox.ts:11](https://github.com/traines-source/transport-network-animator/blob/master/src/BoundingBox.ts#L11) 69 | 70 | ## Methods 71 | 72 | ### add 73 | 74 | ▸ **add**(...`coords`): `void` 75 | 76 | #### Parameters 77 | 78 | | Name | Type | 79 | | :------ | :------ | 80 | | `...coords` | [`Vector`](Vector.md)[] | 81 | 82 | #### Returns 83 | 84 | `void` 85 | 86 | #### Defined in 87 | 88 | [BoundingBox.ts:29](https://github.com/traines-source/transport-network-animator/blob/master/src/BoundingBox.ts#L29) 89 | 90 | ___ 91 | 92 | ### calculateBoundingBoxForZoom 93 | 94 | ▸ **calculateBoundingBoxForZoom**(`percentX`, `percentY`): [`BoundingBox`](BoundingBox.md) 95 | 96 | #### Parameters 97 | 98 | | Name | Type | 99 | | :------ | :------ | 100 | | `percentX` | `number` | 101 | | `percentY` | `number` | 102 | 103 | #### Returns 104 | 105 | [`BoundingBox`](BoundingBox.md) 106 | 107 | #### Defined in 108 | 109 | [BoundingBox.ts:18](https://github.com/traines-source/transport-network-animator/blob/master/src/BoundingBox.ts#L18) 110 | 111 | ___ 112 | 113 | ### isNull 114 | 115 | ▸ **isNull**(): `boolean` 116 | 117 | #### Returns 118 | 119 | `boolean` 120 | 121 | #### Defined in 122 | 123 | [BoundingBox.ts:14](https://github.com/traines-source/transport-network-animator/blob/master/src/BoundingBox.ts#L14) 124 | 125 | ___ 126 | 127 | ### from 128 | 129 | ▸ `Static` **from**(`tl_x`, `tl_y`, `br_x`, `br_y`): [`BoundingBox`](BoundingBox.md) 130 | 131 | #### Parameters 132 | 133 | | Name | Type | 134 | | :------ | :------ | 135 | | `tl_x` | `number` | 136 | | `tl_y` | `number` | 137 | | `br_x` | `number` | 138 | | `br_y` | `number` | 139 | 140 | #### Returns 141 | 142 | [`BoundingBox`](BoundingBox.md) 143 | 144 | #### Defined in 145 | 146 | [BoundingBox.ts:7](https://github.com/traines-source/transport-network-animator/blob/master/src/BoundingBox.ts#L7) 147 | -------------------------------------------------------------------------------- /docs/classes/Projection.md: -------------------------------------------------------------------------------- 1 | [transport-network-animator](../README.md) / Projection 2 | 3 | # Class: Projection 4 | 5 | ## Table of contents 6 | 7 | ### Constructors 8 | 9 | - [constructor](Projection.md#constructor) 10 | 11 | ### Properties 12 | 13 | - [projections](Projection.md#projections) 14 | 15 | ### Accessors 16 | 17 | - [default](Projection.md#default) 18 | 19 | ### Methods 20 | 21 | - [project](Projection.md#project) 22 | 23 | ## Constructors 24 | 25 | ### constructor 26 | 27 | • **new Projection**(`_projection`) 28 | 29 | #### Parameters 30 | 31 | | Name | Type | 32 | | :------ | :------ | 33 | | `_projection` | `string` | 34 | 35 | #### Defined in 36 | 37 | [Projection.ts:8](https://github.com/traines-source/transport-network-animator/blob/master/src/Projection.ts#L8) 38 | 39 | ## Properties 40 | 41 | ### projections 42 | 43 | ▪ `Static` **projections**: `Object` 44 | 45 | The definitions of available projections, which can be added to. 46 | 47 | #### Index signature 48 | 49 | ▪ [name: `string`]: (`lonlat`: [`Vector`](Vector.md)) => [`Vector`](Vector.md) 50 | 51 | #### Defined in 52 | 53 | [Projection.ts:24](https://github.com/traines-source/transport-network-animator/blob/master/src/Projection.ts#L24) 54 | 55 | ## Accessors 56 | 57 | ### default 58 | 59 | • `Static` `get` **default**(): [`Projection`](Projection.md) 60 | 61 | The default projection as set by [Config.mapProjection](Config.md#mapprojection) 62 | 63 | #### Returns 64 | 65 | [`Projection`](Projection.md) 66 | 67 | #### Defined in 68 | 69 | [Projection.ts:17](https://github.com/traines-source/transport-network-animator/blob/master/src/Projection.ts#L17) 70 | 71 | ## Methods 72 | 73 | ### project 74 | 75 | ▸ **project**(`coords`): [`Vector`](Vector.md) 76 | 77 | Project the given coordinates to the target projection. 78 | 79 | #### Parameters 80 | 81 | | Name | Type | Description | 82 | | :------ | :------ | :------ | 83 | | `coords` | [`Vector`](Vector.md) | The coords in WGS84 / EPSG:4326 | 84 | 85 | #### Returns 86 | 87 | [`Vector`](Vector.md) 88 | 89 | #### Defined in 90 | 91 | [Projection.ts:35](https://github.com/traines-source/transport-network-animator/blob/master/src/Projection.ts#L35) 92 | -------------------------------------------------------------------------------- /docs/classes/Rotation.md: -------------------------------------------------------------------------------- 1 | [transport-network-animator](../README.md) / Rotation 2 | 3 | # Class: Rotation 4 | 5 | ## Table of contents 6 | 7 | ### Constructors 8 | 9 | - [constructor](Rotation.md#constructor) 10 | 11 | ### Accessors 12 | 13 | - [degrees](Rotation.md#degrees) 14 | - [name](Rotation.md#name) 15 | - [radians](Rotation.md#radians) 16 | 17 | ### Methods 18 | 19 | - [add](Rotation.md#add) 20 | - [delta](Rotation.md#delta) 21 | - [halfDirection](Rotation.md#halfdirection) 22 | - [isVertical](Rotation.md#isvertical) 23 | - [nearestRoundedInDirection](Rotation.md#nearestroundedindirection) 24 | - [normalize](Rotation.md#normalize) 25 | - [quarterDirection](Rotation.md#quarterdirection) 26 | - [from](Rotation.md#from) 27 | 28 | ## Constructors 29 | 30 | ### constructor 31 | 32 | • **new Rotation**(`_degrees`) 33 | 34 | #### Parameters 35 | 36 | | Name | Type | 37 | | :------ | :------ | 38 | | `_degrees` | `number` | 39 | 40 | #### Defined in 41 | 42 | [Rotation.ts:6](https://github.com/traines-source/transport-network-animator/blob/master/src/Rotation.ts#L6) 43 | 44 | ## Accessors 45 | 46 | ### degrees 47 | 48 | • `get` **degrees**(): `number` 49 | 50 | #### Returns 51 | 52 | `number` 53 | 54 | #### Defined in 55 | 56 | [Rotation.ts:23](https://github.com/traines-source/transport-network-animator/blob/master/src/Rotation.ts#L23) 57 | 58 | ___ 59 | 60 | ### name 61 | 62 | • `get` **name**(): `string` 63 | 64 | #### Returns 65 | 66 | `string` 67 | 68 | #### Defined in 69 | 70 | [Rotation.ts:14](https://github.com/traines-source/transport-network-animator/blob/master/src/Rotation.ts#L14) 71 | 72 | ___ 73 | 74 | ### radians 75 | 76 | • `get` **radians**(): `number` 77 | 78 | #### Returns 79 | 80 | `number` 81 | 82 | #### Defined in 83 | 84 | [Rotation.ts:27](https://github.com/traines-source/transport-network-animator/blob/master/src/Rotation.ts#L27) 85 | 86 | ## Methods 87 | 88 | ### add 89 | 90 | ▸ **add**(`that`): [`Rotation`](Rotation.md) 91 | 92 | #### Parameters 93 | 94 | | Name | Type | 95 | | :------ | :------ | 96 | | `that` | [`Rotation`](Rotation.md) | 97 | 98 | #### Returns 99 | 100 | [`Rotation`](Rotation.md) 101 | 102 | #### Defined in 103 | 104 | [Rotation.ts:31](https://github.com/traines-source/transport-network-animator/blob/master/src/Rotation.ts#L31) 105 | 106 | ___ 107 | 108 | ### delta 109 | 110 | ▸ **delta**(`that`): [`Rotation`](Rotation.md) 111 | 112 | #### Parameters 113 | 114 | | Name | Type | 115 | | :------ | :------ | 116 | | `that` | [`Rotation`](Rotation.md) | 117 | 118 | #### Returns 119 | 120 | [`Rotation`](Rotation.md) 121 | 122 | #### Defined in 123 | 124 | [Rotation.ts:40](https://github.com/traines-source/transport-network-animator/blob/master/src/Rotation.ts#L40) 125 | 126 | ___ 127 | 128 | ### halfDirection 129 | 130 | ▸ **halfDirection**(`relativeTo`, `splitAxis`): [`Rotation`](Rotation.md) 131 | 132 | #### Parameters 133 | 134 | | Name | Type | 135 | | :------ | :------ | 136 | | `relativeTo` | [`Rotation`](Rotation.md) | 137 | | `splitAxis` | [`Rotation`](Rotation.md) | 138 | 139 | #### Returns 140 | 141 | [`Rotation`](Rotation.md) 142 | 143 | #### Defined in 144 | 145 | [Rotation.ts:75](https://github.com/traines-source/transport-network-animator/blob/master/src/Rotation.ts#L75) 146 | 147 | ___ 148 | 149 | ### isVertical 150 | 151 | ▸ **isVertical**(): `boolean` 152 | 153 | #### Returns 154 | 155 | `boolean` 156 | 157 | #### Defined in 158 | 159 | [Rotation.ts:65](https://github.com/traines-source/transport-network-animator/blob/master/src/Rotation.ts#L65) 160 | 161 | ___ 162 | 163 | ### nearestRoundedInDirection 164 | 165 | ▸ **nearestRoundedInDirection**(`relativeTo`, `direction`): [`Rotation`](Rotation.md) 166 | 167 | #### Parameters 168 | 169 | | Name | Type | 170 | | :------ | :------ | 171 | | `relativeTo` | [`Rotation`](Rotation.md) | 172 | | `direction` | `number` | 173 | 174 | #### Returns 175 | 176 | [`Rotation`](Rotation.md) 177 | 178 | #### Defined in 179 | 180 | [Rotation.ts:92](https://github.com/traines-source/transport-network-animator/blob/master/src/Rotation.ts#L92) 181 | 182 | ___ 183 | 184 | ### normalize 185 | 186 | ▸ **normalize**(): [`Rotation`](Rotation.md) 187 | 188 | #### Returns 189 | 190 | [`Rotation`](Rotation.md) 191 | 192 | #### Defined in 193 | 194 | [Rotation.ts:54](https://github.com/traines-source/transport-network-animator/blob/master/src/Rotation.ts#L54) 195 | 196 | ___ 197 | 198 | ### quarterDirection 199 | 200 | ▸ **quarterDirection**(`relativeTo`): [`Rotation`](Rotation.md) 201 | 202 | #### Parameters 203 | 204 | | Name | Type | 205 | | :------ | :------ | 206 | | `relativeTo` | [`Rotation`](Rotation.md) | 207 | 208 | #### Returns 209 | 210 | [`Rotation`](Rotation.md) 211 | 212 | #### Defined in 213 | 214 | [Rotation.ts:69](https://github.com/traines-source/transport-network-animator/blob/master/src/Rotation.ts#L69) 215 | 216 | ___ 217 | 218 | ### from 219 | 220 | ▸ `Static` **from**(`direction`): [`Rotation`](Rotation.md) 221 | 222 | #### Parameters 223 | 224 | | Name | Type | 225 | | :------ | :------ | 226 | | `direction` | `string` | 227 | 228 | #### Returns 229 | 230 | [`Rotation`](Rotation.md) 231 | 232 | #### Defined in 233 | 234 | [Rotation.ts:10](https://github.com/traines-source/transport-network-animator/blob/master/src/Rotation.ts#L10) 235 | -------------------------------------------------------------------------------- /docs/classes/SvgAnimator.md: -------------------------------------------------------------------------------- 1 | [transport-network-animator](../README.md) / SvgAnimator 2 | 3 | # Class: SvgAnimator 4 | 5 | ## Hierarchy 6 | 7 | - `Animator` 8 | 9 | ↳ **`SvgAnimator`** 10 | 11 | ## Table of contents 12 | 13 | ### Constructors 14 | 15 | - [constructor](SvgAnimator.md#constructor) 16 | 17 | ### Properties 18 | 19 | - [EASE\_CUBIC](SvgAnimator.md#ease_cubic) 20 | - [EASE\_NONE](SvgAnimator.md#ease_none) 21 | - [EASE\_SINE](SvgAnimator.md#ease_sine) 22 | 23 | ### Methods 24 | 25 | - [animate](SvgAnimator.md#animate) 26 | - [ease](SvgAnimator.md#ease) 27 | - [from](SvgAnimator.md#from) 28 | - [now](SvgAnimator.md#now) 29 | - [requestFrame](SvgAnimator.md#requestframe) 30 | - [timePassed](SvgAnimator.md#timepassed) 31 | - [timeout](SvgAnimator.md#timeout) 32 | - [to](SvgAnimator.md#to) 33 | - [wait](SvgAnimator.md#wait) 34 | 35 | ## Constructors 36 | 37 | ### constructor 38 | 39 | • **new SvgAnimator**() 40 | 41 | #### Overrides 42 | 43 | Animator.constructor 44 | 45 | #### Defined in 46 | 47 | [svg/SvgAnimator.ts:5](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgAnimator.ts#L5) 48 | 49 | ## Properties 50 | 51 | ### EASE\_CUBIC 52 | 53 | ▪ `Static` **EASE\_CUBIC**: (`x`: `number`) => `number` 54 | 55 | #### Type declaration 56 | 57 | ▸ (`x`): `number` 58 | 59 | ##### Parameters 60 | 61 | | Name | Type | 62 | | :------ | :------ | 63 | | `x` | `number` | 64 | 65 | ##### Returns 66 | 67 | `number` 68 | 69 | #### Inherited from 70 | 71 | Animator.EASE\_CUBIC 72 | 73 | #### Defined in 74 | 75 | [Animator.ts:4](https://github.com/traines-source/transport-network-animator/blob/master/src/Animator.ts#L4) 76 | 77 | ___ 78 | 79 | ### EASE\_NONE 80 | 81 | ▪ `Static` **EASE\_NONE**: (`x`: `number`) => `number` 82 | 83 | #### Type declaration 84 | 85 | ▸ (`x`): `number` 86 | 87 | ##### Parameters 88 | 89 | | Name | Type | 90 | | :------ | :------ | 91 | | `x` | `number` | 92 | 93 | ##### Returns 94 | 95 | `number` 96 | 97 | #### Inherited from 98 | 99 | Animator.EASE\_NONE 100 | 101 | #### Defined in 102 | 103 | [Animator.ts:3](https://github.com/traines-source/transport-network-animator/blob/master/src/Animator.ts#L3) 104 | 105 | ___ 106 | 107 | ### EASE\_SINE 108 | 109 | ▪ `Static` **EASE\_SINE**: (`x`: `number`) => `number` 110 | 111 | #### Type declaration 112 | 113 | ▸ (`x`): `number` 114 | 115 | ##### Parameters 116 | 117 | | Name | Type | 118 | | :------ | :------ | 119 | | `x` | `number` | 120 | 121 | ##### Returns 122 | 123 | `number` 124 | 125 | #### Inherited from 126 | 127 | Animator.EASE\_SINE 128 | 129 | #### Defined in 130 | 131 | [Animator.ts:5](https://github.com/traines-source/transport-network-animator/blob/master/src/Animator.ts#L5) 132 | 133 | ## Methods 134 | 135 | ### animate 136 | 137 | ▸ **animate**(`durationMilliseconds`, `callback`): `void` 138 | 139 | #### Parameters 140 | 141 | | Name | Type | 142 | | :------ | :------ | 143 | | `durationMilliseconds` | `number` | 144 | | `callback` | (`x`: `number`, `isLast`: `boolean`) => `boolean` | 145 | 146 | #### Returns 147 | 148 | `void` 149 | 150 | #### Inherited from 151 | 152 | Animator.animate 153 | 154 | #### Defined in 155 | 156 | [Animator.ts:47](https://github.com/traines-source/transport-network-animator/blob/master/src/Animator.ts#L47) 157 | 158 | ___ 159 | 160 | ### ease 161 | 162 | ▸ **ease**(`ease`): `Animator` 163 | 164 | #### Parameters 165 | 166 | | Name | Type | 167 | | :------ | :------ | 168 | | `ease` | (`x`: `number`) => `number` | 169 | 170 | #### Returns 171 | 172 | `Animator` 173 | 174 | #### Inherited from 175 | 176 | Animator.ease 177 | 178 | #### Defined in 179 | 180 | [Animator.ts:34](https://github.com/traines-source/transport-network-animator/blob/master/src/Animator.ts#L34) 181 | 182 | ___ 183 | 184 | ### from 185 | 186 | ▸ **from**(`from`): `Animator` 187 | 188 | #### Parameters 189 | 190 | | Name | Type | 191 | | :------ | :------ | 192 | | `from` | `number` | 193 | 194 | #### Returns 195 | 196 | `Animator` 197 | 198 | #### Inherited from 199 | 200 | Animator.from 201 | 202 | #### Defined in 203 | 204 | [Animator.ts:19](https://github.com/traines-source/transport-network-animator/blob/master/src/Animator.ts#L19) 205 | 206 | ___ 207 | 208 | ### now 209 | 210 | ▸ `Protected` **now**(): `number` 211 | 212 | #### Returns 213 | 214 | `number` 215 | 216 | #### Overrides 217 | 218 | Animator.now 219 | 220 | #### Defined in 221 | 222 | [svg/SvgAnimator.ts:9](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgAnimator.ts#L9) 223 | 224 | ___ 225 | 226 | ### requestFrame 227 | 228 | ▸ `Protected` **requestFrame**(`callback`): `void` 229 | 230 | #### Parameters 231 | 232 | | Name | Type | 233 | | :------ | :------ | 234 | | `callback` | () => `void` | 235 | 236 | #### Returns 237 | 238 | `void` 239 | 240 | #### Overrides 241 | 242 | Animator.requestFrame 243 | 244 | #### Defined in 245 | 246 | [svg/SvgAnimator.ts:17](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgAnimator.ts#L17) 247 | 248 | ___ 249 | 250 | ### timePassed 251 | 252 | ▸ **timePassed**(`timePassed`): `Animator` 253 | 254 | #### Parameters 255 | 256 | | Name | Type | 257 | | :------ | :------ | 258 | | `timePassed` | `number` | 259 | 260 | #### Returns 261 | 262 | `Animator` 263 | 264 | #### Inherited from 265 | 266 | Animator.timePassed 267 | 268 | #### Defined in 269 | 270 | [Animator.ts:29](https://github.com/traines-source/transport-network-animator/blob/master/src/Animator.ts#L29) 271 | 272 | ___ 273 | 274 | ### timeout 275 | 276 | ▸ `Protected` **timeout**(`callback`, `delayMilliseconds`): `void` 277 | 278 | #### Parameters 279 | 280 | | Name | Type | 281 | | :------ | :------ | 282 | | `callback` | () => `void` | 283 | | `delayMilliseconds` | `number` | 284 | 285 | #### Returns 286 | 287 | `void` 288 | 289 | #### Overrides 290 | 291 | Animator.timeout 292 | 293 | #### Defined in 294 | 295 | [svg/SvgAnimator.ts:13](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgAnimator.ts#L13) 296 | 297 | ___ 298 | 299 | ### to 300 | 301 | ▸ **to**(`to`): `Animator` 302 | 303 | #### Parameters 304 | 305 | | Name | Type | 306 | | :------ | :------ | 307 | | `to` | `number` | 308 | 309 | #### Returns 310 | 311 | `Animator` 312 | 313 | #### Inherited from 314 | 315 | Animator.to 316 | 317 | #### Defined in 318 | 319 | [Animator.ts:24](https://github.com/traines-source/transport-network-animator/blob/master/src/Animator.ts#L24) 320 | 321 | ___ 322 | 323 | ### wait 324 | 325 | ▸ **wait**(`delayMilliseconds`, `callback`): `void` 326 | 327 | #### Parameters 328 | 329 | | Name | Type | 330 | | :------ | :------ | 331 | | `delayMilliseconds` | `number` | 332 | | `callback` | () => `void` | 333 | 334 | #### Returns 335 | 336 | `void` 337 | 338 | #### Inherited from 339 | 340 | Animator.wait 341 | 342 | #### Defined in 343 | 344 | [Animator.ts:39](https://github.com/traines-source/transport-network-animator/blob/master/src/Animator.ts#L39) 345 | -------------------------------------------------------------------------------- /docs/interfaces/SvgAbstractTimedDrawableAttributes.md: -------------------------------------------------------------------------------- 1 | [transport-network-animator](../README.md) / SvgAbstractTimedDrawableAttributes 2 | 3 | # Interface: SvgAbstractTimedDrawableAttributes 4 | 5 | There is no need to access this interface and its child interfaces and classes directly. 6 | The attributes documented here should be used in the SVG code as attributes to the respective SVG element tags, while converting the attribute name from `camelCase` to `data-kebap-case`. 7 | 8 | ## Hierarchy 9 | 10 | - **`SvgAbstractTimedDrawableAttributes`** 11 | 12 | ↳ [`SvgKenImageAttributes`](SvgKenImageAttributes.md) 13 | 14 | ↳ [`SvgCrumpledImageAttributes`](SvgCrumpledImageAttributes.md) 15 | 16 | ↳ [`SvgLabelAttributes`](SvgLabelAttributes.md) 17 | 18 | ↳ [`SvgLineAttributes`](SvgLineAttributes.md) 19 | 20 | ↳ [`SvgStationAttributes`](SvgStationAttributes.md) 21 | 22 | ↳ [`SvgTrainAttributes`](SvgTrainAttributes.md) 23 | 24 | ## Table of contents 25 | 26 | ### Properties 27 | 28 | - [from](SvgAbstractTimedDrawableAttributes.md#from) 29 | - [name](SvgAbstractTimedDrawableAttributes.md#name) 30 | - [to](SvgAbstractTimedDrawableAttributes.md#to) 31 | 32 | ## Properties 33 | 34 | ### from 35 | 36 | • **from**: `Instant` 37 | 38 | Indicates when this element shall appear. 39 | 40 | Pattern: `(?\d+) (?\d+)(? [\w-]+)?` e.g. `2020 5 noanim-nozoom` 41 | 42 | `epoch`: Epochs will be executed in order. Years can be used as epochs. 43 | `second`: Seconds reset to 0 with every epoch. 44 | `flag`: Optional. `reverse`, `noanim`, `nozoom`, `keepzoom`. Can be combined with `-`. 45 | 46 | See further explanations in root Readme. 47 | 48 | SVG: `data-from` 49 | 50 | #### Defined in 51 | 52 | [svg/SvgApi.ts:31](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L31) 53 | 54 | ___ 55 | 56 | ### name 57 | 58 | • **name**: `string` 59 | 60 | The name. In certain circumstances, this will be used a grouping identifier. 61 | 62 | SVG: `name`, the standard SVG name attribute 63 | 64 | #### Defined in 65 | 66 | [svg/SvgApi.ts:16](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L16) 67 | 68 | ___ 69 | 70 | ### to 71 | 72 | • **to**: `Instant` 73 | 74 | Indicates when this element shall disappear. 75 | 76 | Pattern: `(?\d+) (?\d+)(? [\w-]+)?` e.g. `2020 5 noanim-nozoom` 77 | 78 | `epoch`: Epochs will be executed in order. Years can be used as epochs. 79 | `second`: Seconds reset to 0 with every epoch. 80 | `flag`: Optional. `reverse`, `noanim`, `nozoom`, `keepzoom`. Can be combined with `-`. 81 | 82 | See further explanations in root Readme. 83 | 84 | SVG: `data-to` 85 | 86 | #### Defined in 87 | 88 | [svg/SvgApi.ts:46](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L46) 89 | -------------------------------------------------------------------------------- /docs/interfaces/SvgCrumpledImageAttributes.md: -------------------------------------------------------------------------------- 1 | [transport-network-animator](../README.md) / SvgCrumpledImageAttributes 2 | 3 | # Interface: SvgCrumpledImageAttributes 4 | 5 | A crumpled, i.e. distorted image, usually a background map, used in conjunction with the Gravitator. 6 | 7 | SVG: `foreignObject` 8 | 9 | **`example`** 10 | ``` 11 | 12 | ``` 13 | 14 | ## Hierarchy 15 | 16 | - [`SvgAbstractTimedDrawableAttributes`](SvgAbstractTimedDrawableAttributes.md) 17 | 18 | ↳ **`SvgCrumpledImageAttributes`** 19 | 20 | ## Table of contents 21 | 22 | ### Properties 23 | 24 | - [crumpledImage](SvgCrumpledImageAttributes.md#crumpledimage) 25 | - [from](SvgCrumpledImageAttributes.md#from) 26 | - [name](SvgCrumpledImageAttributes.md#name) 27 | - [to](SvgCrumpledImageAttributes.md#to) 28 | 29 | ## Properties 30 | 31 | ### crumpledImage 32 | 33 | • **crumpledImage**: `string` 34 | 35 | URL to an image to be used for crumpling. 36 | 37 | Required. 38 | 39 | SVG: `data-crumpled-image` 40 | 41 | #### Defined in 42 | 43 | [svg/SvgApi.ts:90](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L90) 44 | 45 | ___ 46 | 47 | ### from 48 | 49 | • **from**: `Instant` 50 | 51 | Indicates when this element shall appear. 52 | 53 | Pattern: `(?\d+) (?\d+)(? [\w-]+)?` e.g. `2020 5 noanim-nozoom` 54 | 55 | `epoch`: Epochs will be executed in order. Years can be used as epochs. 56 | `second`: Seconds reset to 0 with every epoch. 57 | `flag`: Optional. `reverse`, `noanim`, `nozoom`, `keepzoom`. Can be combined with `-`. 58 | 59 | See further explanations in root Readme. 60 | 61 | SVG: `data-from` 62 | 63 | #### Inherited from 64 | 65 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[from](SvgAbstractTimedDrawableAttributes.md#from) 66 | 67 | #### Defined in 68 | 69 | [svg/SvgApi.ts:31](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L31) 70 | 71 | ___ 72 | 73 | ### name 74 | 75 | • **name**: `string` 76 | 77 | The name. In certain circumstances, this will be used a grouping identifier. 78 | 79 | SVG: `name`, the standard SVG name attribute 80 | 81 | #### Inherited from 82 | 83 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[name](SvgAbstractTimedDrawableAttributes.md#name) 84 | 85 | #### Defined in 86 | 87 | [svg/SvgApi.ts:16](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L16) 88 | 89 | ___ 90 | 91 | ### to 92 | 93 | • **to**: `Instant` 94 | 95 | Indicates when this element shall disappear. 96 | 97 | Pattern: `(?\d+) (?\d+)(? [\w-]+)?` e.g. `2020 5 noanim-nozoom` 98 | 99 | `epoch`: Epochs will be executed in order. Years can be used as epochs. 100 | `second`: Seconds reset to 0 with every epoch. 101 | `flag`: Optional. `reverse`, `noanim`, `nozoom`, `keepzoom`. Can be combined with `-`. 102 | 103 | See further explanations in root Readme. 104 | 105 | SVG: `data-to` 106 | 107 | #### Inherited from 108 | 109 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[to](SvgAbstractTimedDrawableAttributes.md#to) 110 | 111 | #### Defined in 112 | 113 | [svg/SvgApi.ts:46](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L46) 114 | -------------------------------------------------------------------------------- /docs/interfaces/SvgKenImageAttributes.md: -------------------------------------------------------------------------------- 1 | [transport-network-animator](../README.md) / SvgKenImageAttributes 2 | 3 | # Interface: SvgKenImageAttributes 4 | 5 | An image with a Ken Burns effect, i.e. slowly zooming in on the image. 6 | 7 | SVG: `image` 8 | 9 | **`example`** 10 | ``` 11 | 12 | ``` 13 | 14 | ## Hierarchy 15 | 16 | - [`SvgAbstractTimedDrawableAttributes`](SvgAbstractTimedDrawableAttributes.md) 17 | 18 | ↳ **`SvgKenImageAttributes`** 19 | 20 | ## Table of contents 21 | 22 | ### Properties 23 | 24 | - [from](SvgKenImageAttributes.md#from) 25 | - [name](SvgKenImageAttributes.md#name) 26 | - [to](SvgKenImageAttributes.md#to) 27 | - [zoom](SvgKenImageAttributes.md#zoom) 28 | 29 | ## Properties 30 | 31 | ### from 32 | 33 | • **from**: `Instant` 34 | 35 | Indicates when this element shall appear. 36 | 37 | Pattern: `(?\d+) (?\d+)(? [\w-]+)?` e.g. `2020 5 noanim-nozoom` 38 | 39 | `epoch`: Epochs will be executed in order. Years can be used as epochs. 40 | `second`: Seconds reset to 0 with every epoch. 41 | `flag`: Optional. `reverse`, `noanim`, `nozoom`, `keepzoom`. Can be combined with `-`. 42 | 43 | See further explanations in root Readme. 44 | 45 | SVG: `data-from` 46 | 47 | #### Inherited from 48 | 49 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[from](SvgAbstractTimedDrawableAttributes.md#from) 50 | 51 | #### Defined in 52 | 53 | [svg/SvgApi.ts:31](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L31) 54 | 55 | ___ 56 | 57 | ### name 58 | 59 | • **name**: `string` 60 | 61 | The name. In certain circumstances, this will be used a grouping identifier. 62 | 63 | SVG: `name`, the standard SVG name attribute 64 | 65 | #### Inherited from 66 | 67 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[name](SvgAbstractTimedDrawableAttributes.md#name) 68 | 69 | #### Defined in 70 | 71 | [svg/SvgApi.ts:16](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L16) 72 | 73 | ___ 74 | 75 | ### to 76 | 77 | • **to**: `Instant` 78 | 79 | Indicates when this element shall disappear. 80 | 81 | Pattern: `(?\d+) (?\d+)(? [\w-]+)?` e.g. `2020 5 noanim-nozoom` 82 | 83 | `epoch`: Epochs will be executed in order. Years can be used as epochs. 84 | `second`: Seconds reset to 0 with every epoch. 85 | `flag`: Optional. `reverse`, `noanim`, `nozoom`, `keepzoom`. Can be combined with `-`. 86 | 87 | See further explanations in root Readme. 88 | 89 | SVG: `data-to` 90 | 91 | #### Inherited from 92 | 93 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[to](SvgAbstractTimedDrawableAttributes.md#to) 94 | 95 | #### Defined in 96 | 97 | [svg/SvgApi.ts:46](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L46) 98 | 99 | ___ 100 | 101 | ### zoom 102 | 103 | • **zoom**: [`Vector`](../classes/Vector.md) 104 | 105 | The center of where to zoom to, e.g. `60 50`. 106 | This influences both the direction of the zoom and how far to zoom in. 107 | The zoom factor will be kept as low as possible while ensuring that the image is covering the entire screen canvas at all times. 108 | 109 | Required. 110 | 111 | SVG: `data-zoom` 112 | 113 | #### Defined in 114 | 115 | [svg/SvgApi.ts:69](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L69) 116 | -------------------------------------------------------------------------------- /docs/interfaces/SvgLabelAttributes.md: -------------------------------------------------------------------------------- 1 | [transport-network-animator](../README.md) / SvgLabelAttributes 2 | 3 | # Interface: SvgLabelAttributes 4 | 5 | A Label for a Station or a Line. 6 | 7 | SVG: `text` 8 | 9 | **`example`** 10 | ``` 11 | Dublin 12 | ``` 13 | 14 | ## Hierarchy 15 | 16 | - [`SvgAbstractTimedDrawableAttributes`](SvgAbstractTimedDrawableAttributes.md) 17 | 18 | ↳ **`SvgLabelAttributes`** 19 | 20 | ## Table of contents 21 | 22 | ### Properties 23 | 24 | - [forLine](SvgLabelAttributes.md#forline) 25 | - [forStation](SvgLabelAttributes.md#forstation) 26 | - [from](SvgLabelAttributes.md#from) 27 | - [name](SvgLabelAttributes.md#name) 28 | - [to](SvgLabelAttributes.md#to) 29 | 30 | ## Properties 31 | 32 | ### forLine 33 | 34 | • **forLine**: `undefined` \| `string` 35 | 36 | If the Label should be for a Line, the name of the Line defined elsewhere in the SVG ([SvgLineAttributes.name](SvgLineAttributes.md#name)). 37 | The Label will appear at all termini Stations of the Line. One of forStation or forLine is required. 38 | 39 | SVG: `data-line` 40 | 41 | #### Defined in 42 | 43 | [svg/SvgApi.ts:118](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L118) 44 | 45 | ___ 46 | 47 | ### forStation 48 | 49 | • **forStation**: `undefined` \| `string` 50 | 51 | If the Label should be for a Station, the identifier of the Station defined elsewhere in the SVG ([SvgStationAttributes.id](SvgStationAttributes.md#id)). 52 | One of forStation or forLine is required. 53 | 54 | SVG: `data-station` 55 | 56 | #### Defined in 57 | 58 | [svg/SvgApi.ts:110](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L110) 59 | 60 | ___ 61 | 62 | ### from 63 | 64 | • **from**: `Instant` 65 | 66 | Indicates when this element shall appear. 67 | 68 | Pattern: `(?\d+) (?\d+)(? [\w-]+)?` e.g. `2020 5 noanim-nozoom` 69 | 70 | `epoch`: Epochs will be executed in order. Years can be used as epochs. 71 | `second`: Seconds reset to 0 with every epoch. 72 | `flag`: Optional. `reverse`, `noanim`, `nozoom`, `keepzoom`. Can be combined with `-`. 73 | 74 | See further explanations in root Readme. 75 | 76 | SVG: `data-from` 77 | 78 | #### Inherited from 79 | 80 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[from](SvgAbstractTimedDrawableAttributes.md#from) 81 | 82 | #### Defined in 83 | 84 | [svg/SvgApi.ts:31](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L31) 85 | 86 | ___ 87 | 88 | ### name 89 | 90 | • **name**: `string` 91 | 92 | The name. In certain circumstances, this will be used a grouping identifier. 93 | 94 | SVG: `name`, the standard SVG name attribute 95 | 96 | #### Inherited from 97 | 98 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[name](SvgAbstractTimedDrawableAttributes.md#name) 99 | 100 | #### Defined in 101 | 102 | [svg/SvgApi.ts:16](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L16) 103 | 104 | ___ 105 | 106 | ### to 107 | 108 | • **to**: `Instant` 109 | 110 | Indicates when this element shall disappear. 111 | 112 | Pattern: `(?\d+) (?\d+)(? [\w-]+)?` e.g. `2020 5 noanim-nozoom` 113 | 114 | `epoch`: Epochs will be executed in order. Years can be used as epochs. 115 | `second`: Seconds reset to 0 with every epoch. 116 | `flag`: Optional. `reverse`, `noanim`, `nozoom`, `keepzoom`. Can be combined with `-`. 117 | 118 | See further explanations in root Readme. 119 | 120 | SVG: `data-to` 121 | 122 | #### Inherited from 123 | 124 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[to](SvgAbstractTimedDrawableAttributes.md#to) 125 | 126 | #### Defined in 127 | 128 | [svg/SvgApi.ts:46](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L46) 129 | -------------------------------------------------------------------------------- /docs/interfaces/SvgLineAttributes.md: -------------------------------------------------------------------------------- 1 | [transport-network-animator](../README.md) / SvgLineAttributes 2 | 3 | # Interface: SvgLineAttributes 4 | 5 | An animated Line. 6 | 7 | SVG: `path` 8 | 9 | **`example`** 10 | ``` 11 | 12 | ``` 13 | 14 | ## Hierarchy 15 | 16 | - [`SvgAbstractTimedDrawableAttributes`](SvgAbstractTimedDrawableAttributes.md) 17 | 18 | ↳ **`SvgLineAttributes`** 19 | 20 | ## Table of contents 21 | 22 | ### Properties 23 | 24 | - [animOrder](SvgLineAttributes.md#animorder) 25 | - [beckStyle](SvgLineAttributes.md#beckstyle) 26 | - [from](SvgLineAttributes.md#from) 27 | - [name](SvgLineAttributes.md#name) 28 | - [speed](SvgLineAttributes.md#speed) 29 | - [stops](SvgLineAttributes.md#stops) 30 | - [to](SvgLineAttributes.md#to) 31 | - [weight](SvgLineAttributes.md#weight) 32 | 33 | ## Properties 34 | 35 | ### animOrder 36 | 37 | • **animOrder**: `undefined` \| [`Rotation`](../classes/Rotation.md) 38 | 39 | If set, indicates the geographical animation order, e.g. from north to south, 40 | instead of animating elements with the same name and for the same Instant by the order in which they appear in the SVG. 41 | e.g. `n`, `sw`. 42 | 43 | SVG: `data-anim-order` 44 | 45 | #### Defined in 46 | 47 | [svg/SvgApi.ts:177](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L177) 48 | 49 | ___ 50 | 51 | ### beckStyle 52 | 53 | • **beckStyle**: `boolean` 54 | 55 | Whether to use the "Harry Beck style" for this Line segment. If set to false, overrides [Config.beckStyle](../classes/Config.md#beckstyle). 56 | 57 | SVG: `data-beck-style` 58 | 59 | #### Defined in 60 | 61 | [svg/SvgApi.ts:184](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L184) 62 | 63 | ___ 64 | 65 | ### from 66 | 67 | • **from**: `Instant` 68 | 69 | Indicates when this element shall appear. 70 | 71 | Pattern: `(?\d+) (?\d+)(? [\w-]+)?` e.g. `2020 5 noanim-nozoom` 72 | 73 | `epoch`: Epochs will be executed in order. Years can be used as epochs. 74 | `second`: Seconds reset to 0 with every epoch. 75 | `flag`: Optional. `reverse`, `noanim`, `nozoom`, `keepzoom`. Can be combined with `-`. 76 | 77 | See further explanations in root Readme. 78 | 79 | SVG: `data-from` 80 | 81 | #### Inherited from 82 | 83 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[from](SvgAbstractTimedDrawableAttributes.md#from) 84 | 85 | #### Defined in 86 | 87 | [svg/SvgApi.ts:31](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L31) 88 | 89 | ___ 90 | 91 | ### name 92 | 93 | • **name**: `string` 94 | 95 | The name. In certain circumstances, this will be used a grouping identifier. 96 | Attention: The SVG attribute is different for Lines! 97 | 98 | Required. 99 | 100 | SVG: `data-line` 101 | 102 | #### Overrides 103 | 104 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[name](SvgAbstractTimedDrawableAttributes.md#name) 105 | 106 | #### Defined in 107 | 108 | [svg/SvgApi.ts:140](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L140) 109 | 110 | ___ 111 | 112 | ### speed 113 | 114 | • **speed**: `undefined` \| `number` 115 | 116 | The animation speed of that Line segment. This overrides [Config.animSpeed](../classes/Config.md#animspeed). 117 | 118 | SVG: `data-speed` 119 | 120 | #### Defined in 121 | 122 | [svg/SvgApi.ts:168](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L168) 123 | 124 | ___ 125 | 126 | ### stops 127 | 128 | • **stops**: `Stop`[] 129 | 130 | A space-separated list of Station identifiers, and, optionally, a preceding track info. 131 | 132 | Pattern: `((?[-+]\d*\*? )?(?\w+( |$)))+` e.g. `+1 Frankfurt - Hannover +2* Berlin` 133 | 134 | `stationId`: The identifier of a station defined elsewhere in the SVG ([SvgStationAttributes.id](SvgStationAttributes.md#id)). 135 | `trackInfo`: see [https://github.com/traines-source/transport-network-animator#tracks](https://github.com/traines-source/transport-network-animator#tracks) 136 | 137 | Required. 138 | 139 | SVG: `data-stops` 140 | 141 | #### Defined in 142 | 143 | [svg/SvgApi.ts:154](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L154) 144 | 145 | ___ 146 | 147 | ### to 148 | 149 | • **to**: `Instant` 150 | 151 | Indicates when this element shall disappear. 152 | 153 | Pattern: `(?\d+) (?\d+)(? [\w-]+)?` e.g. `2020 5 noanim-nozoom` 154 | 155 | `epoch`: Epochs will be executed in order. Years can be used as epochs. 156 | `second`: Seconds reset to 0 with every epoch. 157 | `flag`: Optional. `reverse`, `noanim`, `nozoom`, `keepzoom`. Can be combined with `-`. 158 | 159 | See further explanations in root Readme. 160 | 161 | SVG: `data-to` 162 | 163 | #### Inherited from 164 | 165 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[to](SvgAbstractTimedDrawableAttributes.md#to) 166 | 167 | #### Defined in 168 | 169 | [svg/SvgApi.ts:46](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L46) 170 | 171 | ___ 172 | 173 | ### weight 174 | 175 | • **weight**: `undefined` \| `number` 176 | 177 | The graph weight of that Line segment, used for Gravitator. 178 | 179 | `data-weight` 180 | 181 | #### Defined in 182 | 183 | [svg/SvgApi.ts:161](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L161) 184 | -------------------------------------------------------------------------------- /docs/interfaces/SvgStationAttributes.md: -------------------------------------------------------------------------------- 1 | [transport-network-animator](../README.md) / SvgStationAttributes 2 | 3 | # Interface: SvgStationAttributes 4 | 5 | A Station through which Lines can run. 6 | 7 | SVG: `rect` 8 | 9 | **`example`** 10 | ``` 11 | 12 | ``` 13 | 14 | ## Hierarchy 15 | 16 | - [`SvgAbstractTimedDrawableAttributes`](SvgAbstractTimedDrawableAttributes.md) 17 | 18 | ↳ **`SvgStationAttributes`** 19 | 20 | ## Table of contents 21 | 22 | ### Properties 23 | 24 | - [baseCoords](SvgStationAttributes.md#basecoords) 25 | - [from](SvgStationAttributes.md#from) 26 | - [id](SvgStationAttributes.md#id) 27 | - [labelDir](SvgStationAttributes.md#labeldir) 28 | - [lonLat](SvgStationAttributes.md#lonlat) 29 | - [name](SvgStationAttributes.md#name) 30 | - [rotation](SvgStationAttributes.md#rotation) 31 | - [to](SvgStationAttributes.md#to) 32 | 33 | ## Properties 34 | 35 | ### baseCoords 36 | 37 | • **baseCoords**: [`Vector`](../classes/Vector.md) 38 | 39 | The position of the station. 40 | 41 | SVG: standard `x` and `y` attributes 42 | 43 | #### Defined in 44 | 45 | [svg/SvgApi.ts:212](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L212) 46 | 47 | ___ 48 | 49 | ### from 50 | 51 | • **from**: `Instant` 52 | 53 | Indicates when this element shall appear. 54 | 55 | Pattern: `(?\d+) (?\d+)(? [\w-]+)?` e.g. `2020 5 noanim-nozoom` 56 | 57 | `epoch`: Epochs will be executed in order. Years can be used as epochs. 58 | `second`: Seconds reset to 0 with every epoch. 59 | `flag`: Optional. `reverse`, `noanim`, `nozoom`, `keepzoom`. Can be combined with `-`. 60 | 61 | See further explanations in root Readme. 62 | 63 | SVG: `data-from` 64 | 65 | #### Inherited from 66 | 67 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[from](SvgAbstractTimedDrawableAttributes.md#from) 68 | 69 | #### Defined in 70 | 71 | [svg/SvgApi.ts:31](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L31) 72 | 73 | ___ 74 | 75 | ### id 76 | 77 | • **id**: `string` 78 | 79 | The Station identifier. Attention! This is not the SVG `id` attribute! 80 | 81 | Required. 82 | 83 | SVG: `data-station` 84 | 85 | #### Defined in 86 | 87 | [svg/SvgApi.ts:205](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L205) 88 | 89 | ___ 90 | 91 | ### labelDir 92 | 93 | • **labelDir**: [`Rotation`](../classes/Rotation.md) 94 | 95 | The direction in which labels for that Station appear, e.g. `n`, `sw`. 96 | 97 | **`defaultvalue`** `n` 98 | 99 | SVG: `data-label-dir` 100 | 101 | #### Defined in 102 | 103 | [svg/SvgApi.ts:240](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L240) 104 | 105 | ___ 106 | 107 | ### lonLat 108 | 109 | • **lonLat**: `undefined` \| [`Vector`](../classes/Vector.md) 110 | 111 | The position of the station in WGS84 / EPSG:4326, i.e. GPS coordinates. 112 | Longitude and latitude are to be separated by a space, e.g. `28.9603 41.01`. 113 | This will be automatically projected to SVG coordinates using [Projection.default](../classes/Projection.md#default). 114 | If set, this overrides the [baseCoords](SvgStationAttributes.md#basecoords) coordinates. 115 | 116 | SVG: `data-lon-lat` 117 | 118 | #### Defined in 119 | 120 | [svg/SvgApi.ts:222](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L222) 121 | 122 | ___ 123 | 124 | ### name 125 | 126 | • **name**: `string` 127 | 128 | The name. In certain circumstances, this will be used a grouping identifier. 129 | 130 | SVG: `name`, the standard SVG name attribute 131 | 132 | #### Inherited from 133 | 134 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[name](SvgAbstractTimedDrawableAttributes.md#name) 135 | 136 | #### Defined in 137 | 138 | [svg/SvgApi.ts:16](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L16) 139 | 140 | ___ 141 | 142 | ### rotation 143 | 144 | • **rotation**: [`Rotation`](../classes/Rotation.md) 145 | 146 | The orientation of the station, e.g. `n`, `sw`. 147 | 148 | **`defaultvalue`** `n` 149 | 150 | SVG: `data-dir` 151 | 152 | #### Defined in 153 | 154 | [svg/SvgApi.ts:231](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L231) 155 | 156 | ___ 157 | 158 | ### to 159 | 160 | • **to**: `Instant` 161 | 162 | Indicates when this element shall disappear. 163 | 164 | Pattern: `(?\d+) (?\d+)(? [\w-]+)?` e.g. `2020 5 noanim-nozoom` 165 | 166 | `epoch`: Epochs will be executed in order. Years can be used as epochs. 167 | `second`: Seconds reset to 0 with every epoch. 168 | `flag`: Optional. `reverse`, `noanim`, `nozoom`, `keepzoom`. Can be combined with `-`. 169 | 170 | See further explanations in root Readme. 171 | 172 | SVG: `data-to` 173 | 174 | #### Inherited from 175 | 176 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[to](SvgAbstractTimedDrawableAttributes.md#to) 177 | 178 | #### Defined in 179 | 180 | [svg/SvgApi.ts:46](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L46) 181 | -------------------------------------------------------------------------------- /docs/interfaces/SvgTrainAttributes.md: -------------------------------------------------------------------------------- 1 | [transport-network-animator](../README.md) / SvgTrainAttributes 2 | 3 | # Interface: SvgTrainAttributes 4 | 5 | An animated Train that runs on a Line. 6 | 7 | SVG: `path` 8 | 9 | **`example`** 10 | ``` 11 | 12 | ``` 13 | 14 | ## Hierarchy 15 | 16 | - [`SvgAbstractTimedDrawableAttributes`](SvgAbstractTimedDrawableAttributes.md) 17 | 18 | ↳ **`SvgTrainAttributes`** 19 | 20 | ## Table of contents 21 | 22 | ### Properties 23 | 24 | - [from](SvgTrainAttributes.md#from) 25 | - [length](SvgTrainAttributes.md#length) 26 | - [name](SvgTrainAttributes.md#name) 27 | - [offset](SvgTrainAttributes.md#offset) 28 | - [stops](SvgTrainAttributes.md#stops) 29 | - [to](SvgTrainAttributes.md#to) 30 | 31 | ## Properties 32 | 33 | ### from 34 | 35 | • **from**: `Instant` 36 | 37 | Indicates when this element shall appear. 38 | 39 | Pattern: `(?\d+) (?\d+)(? [\w-]+)?` e.g. `2020 5 noanim-nozoom` 40 | 41 | `epoch`: Epochs will be executed in order. Years can be used as epochs. 42 | `second`: Seconds reset to 0 with every epoch. 43 | `flag`: Optional. `reverse`, `noanim`, `nozoom`, `keepzoom`. Can be combined with `-`. 44 | 45 | See further explanations in root Readme. 46 | 47 | SVG: `data-from` 48 | 49 | #### Inherited from 50 | 51 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[from](SvgAbstractTimedDrawableAttributes.md#from) 52 | 53 | #### Defined in 54 | 55 | [svg/SvgApi.ts:31](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L31) 56 | 57 | ___ 58 | 59 | ### length 60 | 61 | • **length**: `number` 62 | 63 | The length of the train (i.e. how many "carriages"). 64 | 65 | **`defaultvalue`** `2` 66 | 67 | SVG: `data-length` 68 | 69 | #### Defined in 70 | 71 | [svg/SvgApi.ts:286](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L286) 72 | 73 | ___ 74 | 75 | ### name 76 | 77 | • **name**: `string` 78 | 79 | The name, referring to a Line name defined elsewhere in the SVG ([SvgLineAttributes.name](SvgLineAttributes.md#name)). 80 | The Train will run on this Line. 81 | Attention: The SVG attribute is different for Trains! 82 | 83 | Required. 84 | 85 | SVG: `data-train` 86 | 87 | #### Overrides 88 | 89 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[name](SvgAbstractTimedDrawableAttributes.md#name) 90 | 91 | #### Defined in 92 | 93 | [svg/SvgApi.ts:263](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L263) 94 | 95 | ___ 96 | 97 | ### offset 98 | 99 | • **offset**: `number` 100 | 101 | Offset in minutes of the timetable given in [SvgTrainAttributes.stops](SvgTrainAttributes.md#stops). This is useful to more easily model a cyclic schedule. Instead of having to calculate manually the times for different trips of e.g. an hourly service, one can just repeat the same schedule definition and set an offset of e.g. 60 min. 102 | 103 | #### Defined in 104 | 105 | [svg/SvgApi.ts:291](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L291) 106 | 107 | ___ 108 | 109 | ### stops 110 | 111 | • **stops**: `Stop`[] 112 | 113 | Stops of the Line given above at which the Train is supposed to stop, with departure and arrival times in between. 114 | 115 | Pattern: `((?\w+)( (?[-+]\d+[-+]\d+) |$))+` e.g. `Berlin +11+50 Hannover +56+120 Frankfurt` 116 | 117 | `stationId`: The identifier of a station defined elsewhere in the SVG ([SvgStationAttributes.id](SvgStationAttributes.md#id)). Must be a stop of the Line on which the train runs. 118 | `depArrInfo`: see [https://github.com/traines-source/transport-network-animator#trains-beta](https://github.com/traines-source/transport-network-animator#trains-beta) 119 | 120 | Required. 121 | 122 | SVG: `data-stops` 123 | 124 | #### Defined in 125 | 126 | [svg/SvgApi.ts:277](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L277) 127 | 128 | ___ 129 | 130 | ### to 131 | 132 | • **to**: `Instant` 133 | 134 | Indicates when this element shall disappear. 135 | 136 | Pattern: `(?\d+) (?\d+)(? [\w-]+)?` e.g. `2020 5 noanim-nozoom` 137 | 138 | `epoch`: Epochs will be executed in order. Years can be used as epochs. 139 | `second`: Seconds reset to 0 with every epoch. 140 | `flag`: Optional. `reverse`, `noanim`, `nozoom`, `keepzoom`. Can be combined with `-`. 141 | 142 | See further explanations in root Readme. 143 | 144 | SVG: `data-to` 145 | 146 | #### Inherited from 147 | 148 | [SvgAbstractTimedDrawableAttributes](SvgAbstractTimedDrawableAttributes.md).[to](SvgAbstractTimedDrawableAttributes.md#to) 149 | 150 | #### Defined in 151 | 152 | [svg/SvgApi.ts:46](https://github.com/traines-source/transport-network-animator/blob/master/src/svg/SvgApi.ts#L46) 153 | -------------------------------------------------------------------------------- /examples/ice-network.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@400;700&display=swap'); 2 | 3 | /*rect.station { 4 | stroke: black; 5 | fill: white; 6 | rx: 5; 7 | stroke-width: 2; 8 | }*/ 9 | 10 | rect.station { 11 | stroke: black; 12 | fill: white; 13 | rx: 3; 14 | stroke-width: 1; 15 | } 16 | 17 | rect.station.helper { 18 | stroke: none; 19 | fill: none; 20 | } 21 | 22 | path { 23 | stroke: black; 24 | stroke-width: 3; 25 | fill: none; 26 | stroke-linecap: round; 27 | stroke-linejoin: round; 28 | } 29 | 30 | #elements text { 31 | visibility: hidden; 32 | } 33 | 34 | svg { 35 | background-color: white; 36 | font-family: 'Roboto Condensed', sans-serif; 37 | font-size: 16px; 38 | line-height: 16px; 39 | } 40 | 41 | .e2h { 42 | stroke-dasharray: 12 4; 43 | } 44 | 45 | .e4h { 46 | stroke-dasharray: 6 4; 47 | } 48 | 49 | .some { 50 | stroke-dasharray: 1 4; 51 | } 52 | 53 | #epoch-label-bg { 54 | fill:white; 55 | stroke: black; 56 | stroke-width: 5px; 57 | } 58 | 59 | #epoch-label { 60 | font-size: 70px; 61 | font-weight: bold; 62 | text-anchor: middle; 63 | dominant-baseline: middle; 64 | } 65 | 66 | .for-line { 67 | overflow: visible; 68 | white-space: nowrap; 69 | font-weight: bold; 70 | } 71 | 72 | .for-line * { 73 | display: inline-block; 74 | } 75 | 76 | .for-line div * { 77 | border-radius: 50%; 78 | width: 12px; 79 | font-size: 12px; 80 | line-height: 12px; 81 | padding: 2px; 82 | margin-right: 2px; 83 | text-align: center; 84 | } 85 | 86 | .ICE11, .ICE30, .ICE31 { 87 | stroke:#ee1d23; 88 | background-color: #ee1d23; 89 | color: white; 90 | } 91 | 92 | .ICE25 { 93 | stroke:#b0abd4; 94 | background-color: #b0abd4; 95 | } 96 | 97 | .ICE88, .ICE43, .ICE28 { 98 | stroke:#39b97a; 99 | background-color: #39b97a; 100 | color: white; 101 | } 102 | 103 | .ICE20, .ICE51 { 104 | stroke:#f5c700; 105 | background-color: #f5c700; 106 | } 107 | 108 | .ICE22, .ICE90 { 109 | stroke: #abe546; 110 | background-color: #abe546; 111 | } 112 | 113 | .ICE1 { 114 | stroke:#39b97a; 115 | background-color: #39b97a; 116 | color: white; 117 | } 118 | 119 | .ICE7 { 120 | stroke:#8dd7f7; 121 | background-color: #8dd7f7; 122 | } 123 | 124 | .ICE5 { 125 | stroke:#0095da; 126 | background-color: #0095da; 127 | color: white; 128 | } 129 | 130 | .ICE87, .ICE78, .ICE75, .ICE29 { 131 | stroke:#f48232; 132 | background-color: #f48232; 133 | color: white; 134 | } 135 | 136 | .ICE10, .ICE47 { 137 | stroke:#b41e8d; 138 | background-color: #b41e8d; 139 | color: white; 140 | } 141 | 142 | .ICE12, .ICE91 { 143 | stroke:#8dd7f7; 144 | background-color: #8dd7f7; 145 | } 146 | 147 | .ICE65 { 148 | stroke:#ee1d23; 149 | background-color: #ee1d23; 150 | color: white; 151 | } 152 | 153 | .ICE79 { 154 | stroke:#b0abd4; 155 | background-color: #b0abd4; 156 | } 157 | 158 | 159 | .ICE45, .ICE82, .ICE13, .ICE14 { 160 | stroke:#0095d9; 161 | background-color: #0095d9; 162 | color: white; 163 | } 164 | 165 | /*.ICE41 { 166 | stroke:#bcd646; 167 | background-color: #bcd646; 168 | }*/ 169 | 170 | .ICE21, .ICE55, .ICE49, .ICE76 { 171 | stroke:#a78b6b; 172 | background-color: #a78b6b; 173 | color: white; 174 | } 175 | 176 | .ICE18 { 177 | stroke: #d68b00; 178 | background-color: #d68b00; 179 | color: white; 180 | } 181 | 182 | .ICE42, .ICE15 { 183 | stroke:#f8abad; 184 | background-color: #f8abad; 185 | } 186 | 187 | .ICE83, .ICE41 { 188 | stroke: black; 189 | background-color: black; 190 | color: white; 191 | } 192 | 193 | .ICE50, .ICE50a, .ICE26, .ICE39 { 194 | stroke:#a8a9ad; 195 | background-color: #a8a9ad; 196 | color: white; 197 | } 198 | 199 | /* 200 | #epoch-label, #epoch-label-bg, text, foreignObject { 201 | display: none !important; 202 | } 203 | */ -------------------------------------------------------------------------------- /examples/ice-network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traines-source/transport-network-animator/b51892f72b451362eb032354ebbbc5a3388caa11/examples/ice-network.png -------------------------------------------------------------------------------- /examples/map_1920.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traines-source/transport-network-animator/b51892f72b451362eb032354ebbbc5a3388caa11/examples/map_1920.png -------------------------------------------------------------------------------- /examples/map_LICENSE.txt: -------------------------------------------------------------------------------- 1 | https://maps-for-free.com CC0 https://creativecommons.org/publicdomain/zero/1.0/ - © OpenStreetMap Contributors https://www.openstreetmap.org/copyright -------------------------------------------------------------------------------- /examples/trains.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@400;700&display=swap'); 2 | 3 | svg { 4 | background-color: white; 5 | font-family: 'Roboto Condensed', sans-serif; 6 | font-size: 16px; 7 | line-height: 16px; 8 | } 9 | 10 | rect.station { 11 | stroke: #263238; 12 | fill: white; 13 | rx: 5; 14 | stroke-width: 2; 15 | } 16 | 17 | #elements rect.helper, #elements path.helper { 18 | stroke: none; 19 | fill: none; 20 | } 21 | 22 | path { 23 | stroke: #263238; 24 | stroke-width: 3; 25 | fill: none; 26 | stroke-linecap: round; 27 | stroke-linejoin: round; 28 | } 29 | 30 | .unit path { 31 | stroke: none; 32 | } 33 | 34 | path.train.train-ICE { 35 | stroke: #cbd0cc; 36 | stroke-width: 10; 37 | marker-end: url(#ice-end); 38 | marker-start: url(#ice-start); 39 | marker-mid: url(#panto); 40 | } 41 | 42 | path.train.train-IC { 43 | stroke: #9da3a6; 44 | stroke-width: 10; 45 | marker-end: url(#ic-end); 46 | marker-start: url(#ic-start); 47 | marker-mid: url(#panto); 48 | } 49 | 50 | path.train.train-Regio { 51 | stroke: #B71C1C; 52 | stroke-width: 10; 53 | marker-end: url(#regio-end); 54 | marker-start: url(#regio-start); 55 | marker-mid: url(#panto); 56 | } 57 | 58 | .panto { 59 | stroke: #2e3032; 60 | stroke-width: 10; 61 | } 62 | 63 | .train-start { 64 | fill:#f7b500; 65 | } 66 | .train-end { 67 | fill: #ff2d21; 68 | } 69 | .lamp { 70 | fill: inherit; 71 | } 72 | .bumper, .black-windscreen { 73 | fill:#2e3032; 74 | } 75 | .trafficred { 76 | fill:#B71C1C; 77 | } 78 | .windowgrey, .windscreen { 79 | fill:#9da3a6; 80 | } 81 | .lightgrey, .icebody { 82 | fill:#cbd0cc; 83 | } 84 | .bluegrey, .cap { 85 | fill: #5b686d; 86 | } 87 | 88 | #elements text { 89 | visibility: hidden; 90 | } 91 | 92 | .for-line { 93 | overflow: visible; 94 | white-space: nowrap; 95 | font-weight: bold; 96 | } 97 | 98 | .for-line * { 99 | display: inline-block; 100 | } 101 | 102 | .for-line div * { 103 | border-radius: 50%; 104 | width: 12px; 105 | font-size: 12px; 106 | line-height: 12px; 107 | padding: 2px; 108 | margin-right: 2px; 109 | text-align: center; 110 | } 111 | 112 | .line-RE { 113 | stroke:#E53935; 114 | background-color: #E53935; 115 | color: white; 116 | } 117 | .line-ICE { 118 | stroke:#546E7A; 119 | background-color: #546E7A; 120 | color: white; 121 | } 122 | 123 | .underthrow { 124 | stroke: none; 125 | fill: #afafaf; 126 | } 127 | 128 | .platform { 129 | stroke: none; 130 | fill: #dbdbdb; 131 | } 132 | 133 | #custom-epoch-label, .custom-epoch-label { 134 | font-size: 30px; 135 | font-weight: bold; 136 | text-anchor: middle; 137 | dominant-baseline: middle; 138 | visibility: hidden; 139 | } -------------------------------------------------------------------------------- /examples/trains.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Hamburg 20 | Berlin 21 | Hannover 22 | Frankfurt 23 | 24 | 1 25 | 3 26 | 27 | 28 | 29 | 30 | 31 | 34 | 37 | 40 | 43 | 46 | 47 | 48 | 51 | 55 | 58 | 61 | 64 | 65 | 66 | 69 | 72 | 75 | 78 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 115 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /examples/travel-times-fernverkehr.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@400;700&display=swap'); 2 | 3 | #stations rect { 4 | stroke: black; 5 | fill: white; 6 | rx: 5; 7 | stroke-width: 2; 8 | } 9 | 10 | #stations rect.helper { 11 | stroke: none; 12 | fill: none; 13 | } 14 | 15 | path { 16 | stroke: black; 17 | stroke-width: 3; 18 | fill: none; 19 | stroke-linecap: round; 20 | stroke-linejoin: round; 21 | } 22 | 23 | #elements text { 24 | visibility: hidden; 25 | fill: white; 26 | text-shadow: 1px 1px 2px black; 27 | 28 | } 29 | 30 | svg { 31 | background-color: white; 32 | font-family: 'Roboto Condensed', sans-serif; 33 | font-size: 16px; 34 | line-height: 16px; 35 | } 36 | 37 | 38 | .e12h { 39 | stroke-width: 8; 40 | } 41 | 42 | .e1h { 43 | stroke-width: 6; 44 | } 45 | 46 | .e2h { 47 | stroke-width: 4; 48 | } 49 | 50 | .e4h { 51 | stroke-width: 2; 52 | } 53 | 54 | .some { 55 | stroke-width: 2; 56 | } 57 | 58 | #epoch-label-bg { 59 | fill:white; 60 | stroke: black; 61 | stroke-width: 5px; 62 | } 63 | 64 | #epoch-label { 65 | font-size: 70px; 66 | font-weight: bold; 67 | text-anchor: middle; 68 | dominant-baseline: middle; 69 | } 70 | 71 | .for-line { 72 | overflow: visible; 73 | white-space: nowrap; 74 | font-weight: bold; 75 | } 76 | 77 | .for-line * { 78 | display: inline-block; 79 | } 80 | 81 | .for-line div * { 82 | border-radius: 50%; 83 | width: 12px; 84 | font-size: 12px; 85 | line-height: 12px; 86 | padding: 2px; 87 | margin-right: 2px; 88 | text-align: center; 89 | } 90 | 91 | /* 92 | #epoch-label, #epoch-label-bg, text, foreignObject { 93 | display: none !important; 94 | } 95 | */ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transport-network-animator", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/network-animator.js", 6 | "directories": {}, 7 | "dependencies": { 8 | "delaunator": "^5.0.0", 9 | "fmin": "0.0.2" 10 | }, 11 | "devDependencies": { 12 | "@types/chai": "^4.2.22", 13 | "@types/delaunator": "^5.0.0", 14 | "@types/mocha": "^8.2.3", 15 | "@types/node": "^14.17.33", 16 | "chai": "^4.3.4", 17 | "expose-loader": "^1.0.0", 18 | "mocha": "^9.2.0", 19 | "nyc": "^15.1.0", 20 | "ts-loader": "^8.3.0", 21 | "ts-mockito": "^2.6.1", 22 | "ts-node": "^10.4.0", 23 | "typedoc": "^0.22.11", 24 | "typedoc-plugin-markdown": "^3.11.12", 25 | "typedoc-plugin-merge-modules": "^3.1.0", 26 | "typescript": "^4.5.5", 27 | "webpack": "^5.64.0", 28 | "webpack-cli": "^4.9.1", 29 | "webpack-license-plugin": "^4.2.1" 30 | }, 31 | "scripts": { 32 | "wp": "webpack", 33 | "wpw": "webpack --watch", 34 | "test": "mocha -r ts-node/register 'test/**/*.spec.ts'", 35 | "coverage": "nyc mocha -r ts-node/register 'test/**/*.spec.ts'", 36 | "screentest": "docker-compose run --rm -u $(id -u ${USER}):$(id -g ${USER}) --entrypoint 'jest --runInBand --detectOpenHandles --forceExit' network-animator-render /app/screentest/_screentest.spec.js", 37 | "docs": "typedoc --plugin typedoc-plugin-markdown --plugin typedoc-plugin-merge-modules --mergeModulesMergeMode project --out docs --readme none --excludePrivate --githubPages false --gitRevision master src/main.ts src/svg/SvgApi.ts", 38 | "build": "npm run wp && npm run test && npm run screentest && npm run docs" 39 | }, 40 | "author": "https://github.com/traines-source/", 41 | "license": "GPL-3.0", 42 | "private": true 43 | } 44 | -------------------------------------------------------------------------------- /screentest/_screentest.spec.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer') 2 | const ScreenshotTester = require('puppeteer-screenshot-tester') 3 | 4 | describe('screentest', () => { 5 | 6 | const tests = ['ice-network.svg#2022', 'travel-times-fernverkehr.svg#2030-12', 'trains.svg#1000', 'cologne-sbahn.svg#1000']; 7 | 8 | tests.forEach(test => { 9 | 10 | it(test, async () => { 11 | const tester = await ScreenshotTester(0.1); 12 | const browser = await puppeteer.launch({ 13 | args: [ 14 | '--no-sandbox', 15 | '--disable-setuid-sandbox', 16 | '--allow-file-access-from-files' 17 | ] 18 | }); 19 | const page = await browser.newPage(); 20 | await page.setViewport({width: 1920, height: 1080}); 21 | await page.goto('file:///app/examples/'+test, { waitUntil: 'networkidle0' }); 22 | await page.waitFor(2000); 23 | 24 | const result = await tester(page, test.replace('#', '_'), {saveNewImageOnError : true}); 25 | await browser.close(); 26 | expect(result).toBe(true); 27 | }, 60000); 28 | 29 | }); 30 | 31 | }) -------------------------------------------------------------------------------- /screentest/cologne-sbahn.svg_1000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traines-source/transport-network-animator/b51892f72b451362eb032354ebbbc5a3388caa11/screentest/cologne-sbahn.svg_1000.png -------------------------------------------------------------------------------- /screentest/ice-network.svg_2022.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traines-source/transport-network-animator/b51892f72b451362eb032354ebbbc5a3388caa11/screentest/ice-network.svg_2022.png -------------------------------------------------------------------------------- /screentest/trains.svg_1000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traines-source/transport-network-animator/b51892f72b451362eb032354ebbbc5a3388caa11/screentest/trains.svg_1000.png -------------------------------------------------------------------------------- /screentest/travel-times-fernverkehr.svg_2030-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traines-source/transport-network-animator/b51892f72b451362eb032354ebbbc5a3388caa11/screentest/travel-times-fernverkehr.svg_2030-12.png -------------------------------------------------------------------------------- /src/Animator.ts: -------------------------------------------------------------------------------- 1 | export abstract class Animator { 2 | 3 | static EASE_NONE: (x: number) => number = x => x; 4 | static EASE_CUBIC: (x: number) => number = x => x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; 5 | static EASE_SINE: (x: number) => number = x => -(Math.cos(Math.PI * x) - 1) / 2; 6 | 7 | private _from: number = 0; 8 | private _to: number = 1; 9 | private _timePassed: number = 0; 10 | private _ease: (x: number) => number = Animator.EASE_NONE; 11 | 12 | private callback: (x: number, isLast: boolean) => boolean = x => true; 13 | private startTime: number = 0; 14 | private durationMilliseconds: number = 0; 15 | 16 | constructor() { 17 | } 18 | 19 | public from(from: number): Animator { 20 | this._from = from; 21 | return this; 22 | } 23 | 24 | public to(to: number): Animator { 25 | this._to = to; 26 | return this; 27 | } 28 | 29 | public timePassed(timePassed: number): Animator { 30 | this._timePassed = timePassed; 31 | return this; 32 | } 33 | 34 | public ease(ease: (x: number) => number): Animator { 35 | this._ease = ease; 36 | return this; 37 | } 38 | 39 | public wait(delayMilliseconds: number, callback: () => void): void { 40 | if (delayMilliseconds > 0) { 41 | this.timeout(callback, delayMilliseconds); 42 | return; 43 | } 44 | callback(); 45 | } 46 | 47 | public animate(durationMilliseconds: number, callback: (x: number, isLast: boolean) => boolean): void { 48 | this.durationMilliseconds = durationMilliseconds; 49 | this.callback = callback; 50 | this.startTime = this.now(); 51 | this.frame(); 52 | } 53 | 54 | private frame() { 55 | const now = this.now(); 56 | let x = 1; 57 | if (this.durationMilliseconds > 0) { 58 | x = (now-this.startTime+this._timePassed) / this.durationMilliseconds; 59 | } 60 | x = Math.max(0, Math.min(1, x)); 61 | const y = this._from + (this._to-this._from) * this._ease(x); 62 | const cont = this.callback(y, x == 1); 63 | if (cont && x < 1) { 64 | this.requestFrame(() => this.frame()); 65 | } else if (!cont) { 66 | console.log('Stopped animation because callback returned false.'); 67 | } 68 | } 69 | 70 | protected abstract now(): number; 71 | 72 | protected abstract timeout(callback: () => void, delayMilliseconds: number): void; 73 | 74 | protected abstract requestFrame(callback: () => void): void; 75 | } 76 | -------------------------------------------------------------------------------- /src/ArrivalDepartureTime.ts: -------------------------------------------------------------------------------- 1 | export class ArrivalDepartureTime { 2 | private value: string; 3 | 4 | constructor(value: string) { 5 | this.value = value; 6 | } 7 | 8 | private parse(offset: number): number { 9 | const split = this.value.split(/([-+])/); 10 | return parseFloat(split[offset]) * (split[offset-1] == '-' ? -1 : 1) 11 | } 12 | 13 | get departure(): number { 14 | return this.parse(2); 15 | } 16 | 17 | get arrival(): number { 18 | return this.parse(4); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/BoundingBox.ts: -------------------------------------------------------------------------------- 1 | import { Vector } from "./Vector"; 2 | 3 | export class BoundingBox { 4 | constructor(public tl: Vector, public br: Vector) { 5 | } 6 | 7 | static from(tl_x: number, tl_y: number, br_x: number, br_y: number): BoundingBox { 8 | return new BoundingBox(new Vector(tl_x, tl_y), new Vector(br_x, br_y)); 9 | } 10 | 11 | get dimensions(): Vector { 12 | return this.tl.delta(this.br); 13 | } 14 | isNull() { 15 | return this.tl == Vector.NULL || this.br == Vector.NULL; 16 | } 17 | 18 | calculateBoundingBoxForZoom(percentX: number, percentY: number): BoundingBox { 19 | const bbox = this; 20 | const delta = bbox.dimensions; 21 | const relativeCenter = new Vector(percentX / 100, percentY / 100); 22 | const center = bbox.tl.add(new Vector(delta.x * relativeCenter.x, delta.y * relativeCenter.y)); 23 | const edgeDistance = new Vector(delta.x * Math.min(relativeCenter.x, 1 - relativeCenter.x), delta.y * Math.min(relativeCenter.y, 1 - relativeCenter.y)); 24 | const ratioPreservingEdgeDistance = new Vector(edgeDistance.y * delta.x / delta.y, edgeDistance.x * delta.y / delta.x); 25 | const minimalEdgeDistance = new Vector(Math.min(edgeDistance.x, ratioPreservingEdgeDistance.x), Math.min(edgeDistance.y, ratioPreservingEdgeDistance.y)); 26 | return new BoundingBox(center.add(new Vector(-minimalEdgeDistance.x, -minimalEdgeDistance.y)), center.add(minimalEdgeDistance)); 27 | } 28 | 29 | add(...coords: Vector[]) { 30 | for(let i=0; ielements[i]; 30 | let termini = [Vector.NULL, Vector.NULL]; 31 | if (element.path.length > 1) { 32 | termini = [element.path[0], element.path[element.path.length-1]]; 33 | } 34 | 35 | const proj1 = termini[0].signedLengthProjectedAt(direction); 36 | const proj2 = termini[1].signedLengthProjectedAt(direction); 37 | const reverse = proj1 < proj2; 38 | if (reverse) { 39 | termini.reverse(); 40 | } 41 | cache.push({ 42 | element: element, 43 | termini: termini, 44 | projection: Math.max(proj1, proj2), 45 | reverse: reverse, 46 | animationDuration: element.animationDurationSeconds 47 | }); 48 | } 49 | } 50 | return cache; 51 | } 52 | 53 | private orderByGeometricDirection(elements: TimedDrawable[], direction: Rotation, draw: boolean, animate: boolean): {delay: number, reverse: boolean}[] { 54 | const cache = this.buildSortableCache(elements, direction); 55 | cache.sort((a, b) => (a.projection < b.projection) ? 1 : -1); 56 | elements.splice(0, elements.length); 57 | 58 | const delays: {delay: number, reverse: boolean}[] = []; 59 | for (let i=0;i toIdx) { 83 | slice.reverse(); 84 | fromIdx = slice.length - 1 - fromIdx; 85 | toIdx = slice.length - 1 - toIdx; 86 | } 87 | return { path: slice, from: fromIdx, to: toIdx }; 88 | } 89 | 90 | private indexOf(array: Vector[], element: Vector) { 91 | for (let i=0; i { 113 | const lineTermini = l.termini; 114 | lineTermini.forEach(t => { 115 | if (!t.trackInfo.includes('*')) { 116 | if (candidates[t.stationId] == undefined) { 117 | candidates[t.stationId] = 1; 118 | } else { 119 | candidates[t.stationId]++; 120 | } 121 | } 122 | }); 123 | }); 124 | const termini: Stop[] = []; 125 | for (const [stationId, occurences] of Object.entries(candidates)) { 126 | if (occurences == 1) { 127 | termini.push(new Stop(stationId, '')); 128 | } 129 | } 130 | this._termini = termini; 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/PreferredTrack.ts: -------------------------------------------------------------------------------- 1 | import { LineAtStation } from "./drawables/Station"; 2 | 3 | export class PreferredTrack { 4 | private value: string; 5 | 6 | constructor(value: string) { 7 | this.value = value; 8 | } 9 | 10 | fromString(value: string): PreferredTrack { 11 | if (value != '') { 12 | return new PreferredTrack(value); 13 | } 14 | return this; 15 | } 16 | 17 | fromNumber(value: number): PreferredTrack { 18 | const prefix = value >= 0 ? '+' : ''; 19 | return new PreferredTrack(prefix + value); 20 | } 21 | 22 | fromExistingLineAtStation(atStation: LineAtStation | undefined) { 23 | if (atStation == undefined) { 24 | return this; 25 | } 26 | if(this.hasTrackNumber()) 27 | return this; 28 | return this.fromNumber(atStation.track); 29 | } 30 | 31 | keepOnlySign(): PreferredTrack { 32 | const v = this.value[0]; 33 | return new PreferredTrack(v == '-' ? v : '+'); 34 | } 35 | 36 | hasTrackNumber(): boolean { 37 | return this.value.length > 1; 38 | } 39 | 40 | get trackNumber(): number { 41 | return parseInt(this.value.replace('*', '')) 42 | } 43 | 44 | isPositive(): boolean { 45 | return this.value[0] != '-'; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/Projection.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "./Config"; 2 | import { Vector } from "./Vector"; 3 | 4 | 5 | export class Projection { 6 | private static _default: Projection; 7 | 8 | constructor(private _projection: string) { 9 | if (!(_projection in Projection.projections)) { 10 | throw new Error('Unknown projection: ' + _projection); 11 | } 12 | } 13 | 14 | /** 15 | * The default projection as set by {@link Config.mapProjection} 16 | */ 17 | public static get default(): Projection { 18 | return this._default || (this._default = new Projection(Config.default.mapProjection)); 19 | } 20 | 21 | /** 22 | * The definitions of available projections, which can be added to. 23 | */ 24 | static projections: { [name: string]: (lonlat: Vector) => Vector } = { 25 | 'epsg3857': lonlat => new Vector( 26 | 256/Math.PI*(lonlat.x/180*Math.PI+Math.PI), 27 | 256/Math.PI*(Math.PI-Math.log(Math.tan(Math.PI/4+lonlat.y/180*Math.PI/2))) 28 | ) 29 | }; 30 | 31 | /** 32 | * Project the given coordinates to the target projection. 33 | * @param coords The coords in WGS84 / EPSG:4326 34 | */ 35 | project(coords: Vector): Vector { 36 | return Projection.projections[this._projection](coords).scale(Config.default.mapProjectionScale).round(1); 37 | } 38 | 39 | 40 | } -------------------------------------------------------------------------------- /src/Rotation.ts: -------------------------------------------------------------------------------- 1 | import { Utils } from "./Utils"; 2 | 3 | export class Rotation { 4 | private static DIRS: { [id: string]: number } = {'sw': -135, 'w': -90, 'nw': -45, 'n': 0, 'ne': 45, 'e': 90, 'se': 135, 's': 180}; 5 | 6 | constructor(private _degrees: number) { 7 | 8 | } 9 | 10 | static from(direction: string): Rotation { 11 | return new Rotation(Rotation.DIRS[direction] || 0); 12 | } 13 | 14 | get name(): string { 15 | for (const [key, value] of Object.entries(Rotation.DIRS)) { 16 | if (Utils.equals(value, this.degrees)) { 17 | return key; 18 | } 19 | } 20 | return 'n'; 21 | } 22 | 23 | get degrees(): number { 24 | return this._degrees; 25 | } 26 | 27 | get radians(): number { 28 | return this.degrees / 180 * Math.PI; 29 | } 30 | 31 | add(that: Rotation): Rotation { 32 | let sum = this.degrees + that.degrees; 33 | if (sum <= -180) 34 | sum += 360; 35 | if (sum > 180) 36 | sum -= 360; 37 | return new Rotation(sum); 38 | } 39 | 40 | delta(that: Rotation): Rotation { 41 | let a = this.degrees; 42 | let b = that.degrees; 43 | let dist = b-a; 44 | if (Math.abs(dist) > 180) { 45 | if (a < 0) 46 | a += 360; 47 | if (b < 0) 48 | b += 360; 49 | dist = b-a; 50 | } 51 | return new Rotation(dist); 52 | } 53 | 54 | normalize(): Rotation { 55 | let dir = this.degrees; 56 | if (Utils.equals(dir, -90)) 57 | dir = 0; 58 | else if (dir < -90) 59 | dir += 180; 60 | else if (dir > 90) 61 | dir -= 180; 62 | return new Rotation(dir); 63 | } 64 | 65 | isVertical(): boolean { 66 | return this.degrees % 180 == 0; 67 | } 68 | 69 | quarterDirection(relativeTo: Rotation): Rotation { 70 | const deltaDir = relativeTo.delta(this).degrees; 71 | const deg = deltaDir < 0 ? Math.ceil((deltaDir-45)/90) : Math.floor((deltaDir+45)/90); 72 | return new Rotation(deg*90); 73 | } 74 | 75 | halfDirection(relativeTo: Rotation, splitAxis: Rotation): Rotation { 76 | const deltaDir = relativeTo.delta(this).degrees; 77 | let deg; 78 | if (splitAxis.isVertical()) { 79 | if (deltaDir < 0 && deltaDir >= -180) 80 | deg = -90; 81 | else 82 | deg = 90; 83 | } else { 84 | if (deltaDir < 90 && deltaDir >= -90) 85 | deg = 0; 86 | else 87 | deg = 180; 88 | } 89 | return new Rotation(deg); 90 | } 91 | 92 | nearestRoundedInDirection(relativeTo: Rotation, direction: number) { 93 | const ceiledOrFlooredOrientation = relativeTo.round(direction); 94 | const differenceInOrientation = Math.abs(ceiledOrFlooredOrientation.degrees - this.degrees) % 90; 95 | return this.add(new Rotation(Math.sign(direction)*differenceInOrientation)); 96 | } 97 | 98 | private round(direction: number): Rotation { 99 | const deg = this.degrees / 45; 100 | return new Rotation((direction >= 0 ? Math.ceil(deg) : Math.floor(deg)) * 45); 101 | } 102 | 103 | 104 | } -------------------------------------------------------------------------------- /src/Utils.ts: -------------------------------------------------------------------------------- 1 | export class Utils { 2 | static readonly IMPRECISION: number = 0.001; 3 | 4 | static equals(a: number, b: number): boolean { 5 | return Math.abs(a - b) < Utils.IMPRECISION; 6 | } 7 | 8 | static trilemma(int: number, options: [string, string, string]): string { 9 | if (Utils.equals(int, 0)) { 10 | return options[1]; 11 | } else if (int > 0) { 12 | return options[2]; 13 | } 14 | return options[0]; 15 | } 16 | 17 | static alphabeticId(a: string, b: string): string { 18 | if (a < b) 19 | return a + '_' + b; 20 | return b + '_' + a; 21 | } 22 | 23 | static ease(x: number) { 24 | return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; 25 | } 26 | } -------------------------------------------------------------------------------- /src/Vector.ts: -------------------------------------------------------------------------------- 1 | import { Rotation } from "./Rotation"; 2 | import { Utils } from "./Utils"; 3 | 4 | export class Vector { 5 | static UNIT: Vector = new Vector(0, -1); 6 | static NULL: Vector = new Vector(0, 0); 7 | 8 | constructor(private _x: number, private _y: number) { 9 | 10 | } 11 | 12 | static fromArray(arr: number[]) { 13 | return new Vector(arr[0], arr[1]); 14 | } 15 | 16 | get x(): number { 17 | return this._x; 18 | } 19 | 20 | get y(): number { 21 | return this._y; 22 | } 23 | 24 | get length(): number { 25 | return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2)); 26 | } 27 | 28 | withLength(length: number): Vector { 29 | const ratio = this.length != 0 ? length/this.length : 0; 30 | return new Vector(this.x*ratio, this.y*ratio); 31 | } 32 | 33 | signedLengthProjectedAt(direction: Rotation): number { 34 | const s = Vector.UNIT.rotate(direction); 35 | return this.dotProduct(s)/s.dotProduct(s); 36 | } 37 | 38 | add(that: Vector): Vector { 39 | return new Vector(this.x + that.x, this.y + that.y); 40 | } 41 | 42 | scale(factor: number) { 43 | return new Vector(this.x*factor, this.y*factor); 44 | } 45 | 46 | delta(that: Vector): Vector { 47 | return new Vector(that.x - this.x, that.y - this.y); 48 | } 49 | 50 | rotate(theta: Rotation): Vector { 51 | let rad: number = theta.radians; 52 | return new Vector(this.x * Math.cos(rad) - this.y * Math.sin(rad), this.x * Math.sin(rad) + this.y * Math.cos(rad)); 53 | } 54 | 55 | dotProduct(that: Vector): number { 56 | return this.x*that.x+this.y*that.y; 57 | } 58 | 59 | solveDeltaForIntersection(dir1: Vector, dir2: Vector): {a: number, b: number} { 60 | const delta: Vector = this; 61 | const swapZeroDivision = Utils.equals(dir2.y, 0); 62 | const x = swapZeroDivision ? 'y' : 'x'; 63 | const y = swapZeroDivision ? 'x' : 'y'; 64 | const denominator = (dir1[y]*dir2[x]-dir1[x]*dir2[y]); 65 | if (Utils.equals(denominator, 0)) { 66 | return {a: NaN, b: NaN}; 67 | } 68 | const a = (delta[y]*dir2[x]-delta[x]*dir2[y])/denominator; 69 | const b = (a*dir1[y]-delta[y])/dir2[y]; 70 | return {a, b}; 71 | } 72 | 73 | isDeltaMatchingParallel(dir1: Vector, dir2: Vector): boolean { 74 | const a = this.angle(dir1).degrees; 75 | const b = dir1.angle(dir2).degrees; 76 | return Utils.equals(a % 180, 0) && Utils.equals(b % 180, 0); 77 | } 78 | 79 | inclination(): Rotation { 80 | if (Utils.equals(this.x, 0)) 81 | return new Rotation(this.y > 0 ? 180 : 0); 82 | if (Utils.equals(this.y, 0)) 83 | return new Rotation(this.x > 0 ? 90 : -90); 84 | const adjacent = new Vector(0,-Math.abs(this.y)); 85 | return new Rotation(Math.sign(this.x)*Math.acos(this.dotProduct(adjacent)/adjacent.length/this.length)*180/Math.PI); 86 | } 87 | 88 | angle(other: Vector): Rotation { 89 | return this.inclination().delta(other.inclination()); 90 | } 91 | 92 | bothAxisMins(other: Vector) { 93 | if (this == Vector.NULL) 94 | return other; 95 | if (other == Vector.NULL) 96 | return this; 97 | return new Vector(this.x < other.x ? this.x : other.x, this.y < other.y ? this.y : other.y) 98 | } 99 | 100 | bothAxisMaxs(other: Vector) { 101 | if (this == Vector.NULL) 102 | return other; 103 | if (other == Vector.NULL) 104 | return this; 105 | return new Vector(this.x > other.x ? this.x : other.x, this.y > other.y ? this.y : other.y) 106 | } 107 | 108 | between(other: Vector, x: number) { 109 | const delta = this.delta(other); 110 | return this.add(delta.withLength(delta.length*x)); 111 | } 112 | 113 | equals(other: Vector) { 114 | return this.x == other.x && this.y == other.y; 115 | } 116 | 117 | round(decimals: number) { 118 | return new Vector(this.roundNumber(this.x, decimals), this.roundNumber(this.y, decimals)); 119 | } 120 | 121 | private roundNumber(n: number, decimals: number): number { 122 | const precision = Math.pow(10, decimals); 123 | return Math.round(n*precision)/precision; 124 | } 125 | } -------------------------------------------------------------------------------- /src/Zoomer.ts: -------------------------------------------------------------------------------- 1 | import { Instant } from "./Instant"; 2 | import { Vector } from "./Vector"; 3 | import { Rotation } from "./Rotation"; 4 | import { BoundingBox } from "./BoundingBox"; 5 | import { Config } from "./Config"; 6 | 7 | export class Zoomer { 8 | 9 | private boundingBox = new BoundingBox(Vector.NULL, Vector.NULL); 10 | private customDuration = -1; 11 | private resetFlag = false; 12 | 13 | constructor(private canvasSize: BoundingBox, private zoomMaxScale = 3) { 14 | } 15 | 16 | include(boundingBox: BoundingBox, from: Instant, to: Instant, draw: boolean, shouldAnimate: boolean, pad: boolean = true) { 17 | const now = draw ? from : to; 18 | if (now.flag.includes('keepzoom')) { 19 | this.resetFlag = false; 20 | } else { 21 | if (this.resetFlag) { 22 | this.doReset(); 23 | } 24 | if (shouldAnimate && !now.flag.includes('nozoom')) { 25 | if (pad && !boundingBox.isNull()) { 26 | boundingBox = this.paddedBoundingBox(boundingBox); 27 | } 28 | this.boundingBox.add(boundingBox.tl, boundingBox.br); 29 | } 30 | } 31 | } 32 | 33 | private enforcedBoundingBox(): BoundingBox { 34 | if (!this.boundingBox.isNull()) { 35 | const paddedBoundingBox = this.boundingBox; 36 | const zoomSize = paddedBoundingBox.dimensions; 37 | const canvasSize = this.canvasSize.dimensions; 38 | const minZoomSize = new Vector(canvasSize.x / this.zoomMaxScale, canvasSize.y / this.zoomMaxScale); 39 | const delta = zoomSize.delta(minZoomSize); 40 | const additionalSpacing = new Vector(Math.max(0, delta.x/2), Math.max(0, delta.y/2)) 41 | return new BoundingBox( 42 | paddedBoundingBox.tl.add(additionalSpacing.rotate(new Rotation(180))), 43 | paddedBoundingBox.br.add(additionalSpacing) 44 | ); 45 | } 46 | return this.boundingBox; 47 | } 48 | 49 | private paddedBoundingBox(boundingBox: BoundingBox): BoundingBox { 50 | const padding = Config.default.zoomPaddingFactor*Math.min(this.zoomMaxScale, 8); 51 | return new BoundingBox( 52 | boundingBox.tl.add(new Vector(-padding, -padding)), 53 | boundingBox.br.add(new Vector(padding, padding)) 54 | ); 55 | } 56 | 57 | get center(): Vector { 58 | const enforcedBoundingBox = this.enforcedBoundingBox(); 59 | if (!enforcedBoundingBox.isNull()) { 60 | return new Vector( 61 | Math.round((enforcedBoundingBox.tl.x + enforcedBoundingBox.br.x)/2), 62 | Math.round((enforcedBoundingBox.tl.y + enforcedBoundingBox.br.y)/2)); 63 | } 64 | return this.canvasSize.tl.between(this.canvasSize.br, 0.5); 65 | } 66 | 67 | get scale(): number { 68 | const enforcedBoundingBox = this.enforcedBoundingBox(); 69 | if (!enforcedBoundingBox.isNull()) { 70 | const zoomSize = enforcedBoundingBox.dimensions; 71 | const delta = this.canvasSize.dimensions; 72 | return Math.min(delta.x / zoomSize.x, delta.y / zoomSize.y); 73 | } 74 | return 1; 75 | } 76 | 77 | get duration(): number { 78 | if (this.customDuration == -1) { 79 | return Config.default.zoomDuration; 80 | } 81 | return this.customDuration; 82 | } 83 | 84 | private doReset() { 85 | this.boundingBox = new BoundingBox(Vector.NULL, Vector.NULL); 86 | this.customDuration = -1; 87 | this.resetFlag = false; 88 | } 89 | 90 | public reset() { 91 | this.resetFlag = true; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/drawables/AbstractTimedDrawable.ts: -------------------------------------------------------------------------------- 1 | import { TimedDrawable, Timed } from "./TimedDrawable"; 2 | import { BoundingBox } from "../BoundingBox"; 3 | import { Instant } from "../Instant"; 4 | 5 | export interface AbstractTimedDrawableAdapter extends Timed { 6 | name: string; 7 | boundingBox: BoundingBox; 8 | } 9 | 10 | export abstract class AbstractTimedDrawable implements TimedDrawable { 11 | 12 | constructor(protected adapter: AbstractTimedDrawableAdapter) { 13 | 14 | } 15 | 16 | private _from = this.adapter.from; 17 | private _to = this.adapter.to; 18 | private _name = this.adapter.name; 19 | private _boundingBox = this.adapter.boundingBox; 20 | 21 | get from(): Instant { 22 | return this._from; 23 | } 24 | 25 | get to(): Instant { 26 | return this._to; 27 | } 28 | 29 | get name(): string { 30 | return this._name; 31 | } 32 | 33 | get boundingBox(): BoundingBox { 34 | return this._boundingBox; 35 | } 36 | 37 | abstract draw(delay: number, animate: boolean, reverse: boolean): number; 38 | 39 | abstract erase(delay: number, animate: boolean, reverse: boolean): number; 40 | 41 | } -------------------------------------------------------------------------------- /src/drawables/CrumpledImage.ts: -------------------------------------------------------------------------------- 1 | import { Station } from "./Station"; 2 | import { Vector } from "../Vector"; 3 | import Delaunator from "delaunator"; 4 | import { AbstractTimedDrawableAdapter, AbstractTimedDrawable } from "./AbstractTimedDrawable"; 5 | 6 | export interface Vertex {currentCoords: () => Vector, startCoords: Vector}; 7 | export interface Triangle {a: Vertex, b: Vertex, c: Vertex}; 8 | 9 | export interface CrumpledImageAdapter extends AbstractTimedDrawableAdapter { 10 | draw(delaySeconds: number, animationDurationSeconds: number, triangles: Triangle[]): void; 11 | erase(delaySeconds: number): void; 12 | } 13 | 14 | export class CrumpledImage extends AbstractTimedDrawable { 15 | static EXTEND_BEYOND_CANVAS_FACTOR = 2; 16 | 17 | private vertices: Vertex[] = []; 18 | private triangles: Triangle[] = []; 19 | private canvasSides: { corner: Vector, side: Vector }[] = []; 20 | 21 | constructor(protected adapter: CrumpledImageAdapter) { 22 | super(adapter); 23 | } 24 | 25 | initialize(stations: Station[]) { 26 | this.vertices = stations.map(n => ({currentCoords: () => n.baseCoords, startCoords: n.baseCoords})); 27 | this.canvasSides = this.getCanvasSides(); 28 | let delaunay = this.getDelaunator(); 29 | 30 | this.setVerticesExtendedToCanvasBoundaries(delaunay.triangles, delaunay.halfedges); 31 | 32 | delaunay = this.getDelaunator(); 33 | this.setTriangles(delaunay.triangles); 34 | } 35 | 36 | private getDelaunator(): Delaunator { 37 | return Delaunator.from(this.vertices, n => n.startCoords.x, n => n.startCoords.y); 38 | } 39 | 40 | private getCanvasSides(): {corner: Vector, side: Vector}[] { 41 | const tl = this.adapter.boundingBox.tl; 42 | const br = this.adapter.boundingBox.br; 43 | const tr = new Vector(br.x, tl.y); 44 | const bl = new Vector(tl.x, br.y); 45 | const t = new Vector(tr.x-tl.x, 0); 46 | const r = new Vector(0, br.y-tr.y); 47 | const b = new Vector(bl.x-br.x, 0); 48 | const l = new Vector(0, tl.y-bl.y); 49 | return [ 50 | { corner: tl, side: t }, 51 | { corner: tr, side: r }, 52 | { corner: br, side: b }, 53 | { corner: bl, side: l } 54 | ]; 55 | } 56 | 57 | private setVerticesExtendedToCanvasBoundaries(triangles: Uint32Array, halfEdges: Int32Array) { 58 | for (let i = 0; i < halfEdges.length; i++) { 59 | if (halfEdges[i] == -1) { 60 | const c = this.vertices[triangles[i]]; 61 | const b = this.vertices[triangles[this.prevHalfedge(i)]]; 62 | const a = this.vertices[triangles[this.nextHalfedge(i)]]; 63 | 64 | const ba = b.startCoords.delta(a.startCoords); 65 | const bc = b.startCoords.delta(c.startCoords); 66 | const baFactor = this.factorToExtendVectorToCanvasBoundaries(b.startCoords, ba); 67 | const bcFactor = this.factorToExtendVectorToCanvasBoundaries(b.startCoords, bc); 68 | const aExtendedStartCoords = b.startCoords.add(ba.scale(baFactor)); 69 | const cExtendedStartCoords = b.startCoords.add(bc.scale(bcFactor)); 70 | 71 | this.vertices.push({ 72 | currentCoords: () => this.extendCurrentVector(b, a, baFactor), 73 | startCoords: aExtendedStartCoords 74 | }); 75 | this.vertices.push({ 76 | currentCoords: () => this.extendCurrentVector(b, c, bcFactor), 77 | startCoords: cExtendedStartCoords 78 | }); 79 | } 80 | } 81 | } 82 | 83 | private nextHalfedge(e: number) { 84 | return (e % 3 === 2) ? e - 2 : e + 1; 85 | } 86 | 87 | private prevHalfedge(e: number) { 88 | return (e % 3 === 0) ? e + 2 : e - 1; 89 | } 90 | 91 | private factorToExtendVectorToCanvasBoundaries(origin: Vector, extend: Vector): number { 92 | for (let i=0; i= 0 && solution.a <= 1 && solution.b >= 0) { 95 | return solution.b*CrumpledImage.EXTEND_BEYOND_CANVAS_FACTOR; 96 | } 97 | } 98 | throw new Error(extend + " does not seem to intersect with canvas boundaries, which is impossible."); 99 | } 100 | 101 | private extendCurrentVector(origin: Vertex, direction: Vertex, factor: number) { 102 | return origin.currentCoords().add(origin.currentCoords().delta(direction.currentCoords()).scale(factor)); 103 | } 104 | 105 | private setTriangles(triangles: Uint32Array) { 106 | for (let i = 0; i < triangles.length; i += 3) { 107 | this.triangles.push({ 108 | a: this.vertices[triangles[i]], 109 | b: this.vertices[triangles[i + 1]], 110 | c: this.vertices[triangles[i + 2]] 111 | }); 112 | } 113 | } 114 | 115 | draw(delay: number, animate: boolean, reverse: boolean): number { 116 | this.adapter.draw(delay, 0, []); 117 | return 0; 118 | } 119 | 120 | crumple(delay: number, animationDurationSeconds: number) { 121 | this.adapter.draw(delay, animationDurationSeconds, this.triangles); 122 | return 0; 123 | } 124 | 125 | erase(delay: number, animate: boolean, reverse: boolean): number { 126 | this.adapter.erase(delay); 127 | return 0; 128 | } 129 | } -------------------------------------------------------------------------------- /src/drawables/GenericTimedDrawable.ts: -------------------------------------------------------------------------------- 1 | import { AbstractTimedDrawable, AbstractTimedDrawableAdapter } from "./AbstractTimedDrawable"; 2 | import { Config } from "../Config"; 3 | 4 | export interface GenericTimedDrawableAdapter extends AbstractTimedDrawableAdapter { 5 | draw(delaySeconds: number, animationDurationSeconds: number): void; 6 | erase(delaySeconds: number, animationDurationSeconds: number): void; 7 | } 8 | 9 | export class GenericTimedDrawable extends AbstractTimedDrawable { 10 | 11 | constructor(protected adapter: GenericTimedDrawableAdapter) { 12 | super(adapter); 13 | } 14 | 15 | draw(delay: number, animate: boolean): number { 16 | this.adapter.draw(delay, animate ? Config.default.fadeDurationSeconds : 0); 17 | return 0; 18 | } 19 | 20 | erase(delay: number, animate: boolean, reverse: boolean): number { 21 | this.adapter.erase(delay, animate ? Config.default.fadeDurationSeconds : 0); 22 | return 0; 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/drawables/KenImage.ts: -------------------------------------------------------------------------------- 1 | import { BoundingBox } from "../BoundingBox"; 2 | import { Vector } from "../Vector"; 3 | import { Zoomer } from "../Zoomer"; 4 | import { Instant } from "../Instant"; 5 | import { AbstractTimedDrawable, AbstractTimedDrawableAdapter } from "./AbstractTimedDrawable"; 6 | 7 | export interface KenImageAdapter extends AbstractTimedDrawableAdapter { 8 | zoom: Vector; 9 | draw(delaySeconds: number, animationDurationSeconds: number, zoomCenter: Vector, zoomScale: number): void; 10 | erase(delaySeconds: number): void; 11 | } 12 | 13 | export class KenImage extends AbstractTimedDrawable { 14 | 15 | constructor(protected adapter: KenImageAdapter) { 16 | super(adapter); 17 | } 18 | 19 | draw(delay: number, animate: boolean): number { 20 | const zoomer = new Zoomer(this.boundingBox); 21 | zoomer.include(this.getZoomedBoundingBox(), Instant.BIG_BANG, Instant.BIG_BANG, true, true, false); 22 | this.adapter.draw(delay, !animate ? 0 : this.adapter.from.delta(this.adapter.to), zoomer.center, zoomer.scale); 23 | return 0; 24 | } 25 | 26 | erase(delay: number, animate: boolean, reverse: boolean): number { 27 | this.adapter.erase(delay); 28 | return 0; 29 | } 30 | 31 | private getZoomedBoundingBox(): BoundingBox { 32 | const bbox = this.adapter.boundingBox; 33 | 34 | const center = this.adapter.zoom; 35 | if (center != Vector.NULL) { 36 | const zoomBbox = bbox.calculateBoundingBoxForZoom(center.x, center.y); 37 | return zoomBbox; 38 | } 39 | return bbox; 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/drawables/Label.ts: -------------------------------------------------------------------------------- 1 | import { Station } from "./Station"; 2 | import { Rotation } from "../Rotation"; 3 | import { StationProvider } from "../Network"; 4 | import { Vector } from "../Vector"; 5 | import { AbstractTimedDrawable, AbstractTimedDrawableAdapter } from "./AbstractTimedDrawable"; 6 | import { Config } from "../Config"; 7 | 8 | export interface LabelAdapter extends AbstractTimedDrawableAdapter { 9 | forStation: string | undefined; 10 | forLine: string | undefined; 11 | draw(delaySeconds: number, animationDurationSeconds: number, textCoords: Vector, labelDir: Rotation, children: LabelAdapter[]): void; 12 | erase(delaySeconds: number, animationDurationSeconds: number): void; 13 | cloneForStation(stationId: string): LabelAdapter; 14 | } 15 | 16 | export class Label extends AbstractTimedDrawable { 17 | 18 | constructor(protected adapter: LabelAdapter, private stationProvider: StationProvider) { 19 | super(adapter); 20 | } 21 | 22 | children: Label[] = []; 23 | 24 | hasChildren(): boolean { 25 | return this.children.length > 0; 26 | } 27 | 28 | get name(): string { 29 | return this.adapter.forStation || this.adapter.forLine || ''; 30 | } 31 | 32 | get forStation(): Station { 33 | const s = this.stationProvider.stationById(this.adapter.forStation || ''); 34 | if (s == undefined) { 35 | throw new Error('Station with ID ' + this.adapter.forStation + ' is undefined'); 36 | } 37 | return s; 38 | } 39 | 40 | draw(delay: number, animate: boolean): number { 41 | const animationDurationSeconds = animate ? Config.default.fadeDurationSeconds : 0; 42 | if (this.adapter.forStation != undefined) { 43 | const station = this.forStation; 44 | station.addLabel(this); 45 | if (station.linesExisting()) { 46 | this.drawForStation(delay, animationDurationSeconds, station, false); 47 | } else { 48 | this.adapter.erase(delay, animate ? Config.default.fadeDurationSeconds : 0); 49 | } 50 | } else if (this.adapter.forLine != undefined) { 51 | const termini = this.stationProvider.lineGroupById(this.adapter.forLine).termini; 52 | termini.forEach(t => { 53 | const s = this.stationProvider.stationById(t.stationId); 54 | if (s != undefined) { 55 | let found = false; 56 | s.labels.forEach(l => { 57 | if (l.hasChildren()) { 58 | found = true; 59 | l.children.push(this); 60 | l.draw(delay, animate); 61 | } 62 | }); 63 | if (!found) { 64 | const newLabelForStation = new Label(this.adapter.cloneForStation(s.id), this.stationProvider); 65 | newLabelForStation.children.push(this); 66 | s.addLabel(newLabelForStation); 67 | newLabelForStation.draw(delay, animate); 68 | this.children.push(newLabelForStation); 69 | } 70 | } 71 | 72 | }); 73 | } else { 74 | this.adapter.draw(delay, animationDurationSeconds, Vector.NULL, Rotation.from('n'), []); 75 | } 76 | return 0; 77 | } 78 | 79 | private drawForStation(delaySeconds: number, animationDurationSeconds: number, station: Station, forLine: boolean) { 80 | const baseCoord = station.baseCoords; 81 | let yOffset = 0; 82 | for (let i=0; i0 ? 2 : 0); //TODO magic numbers 91 | const stationDir = station.rotation; 92 | const diffDir = labelDir.add(new Rotation(-stationDir.degrees)); 93 | const unitv = Vector.UNIT.rotate(diffDir); 94 | const anchor = new Vector(station.stationSizeForAxis('x', unitv.x), station.stationSizeForAxis('y', unitv.y)); 95 | const textCoords = baseCoord.add(anchor.rotate(stationDir)).add(new Vector(0, yOffset)); 96 | 97 | this.adapter.draw(delaySeconds, animationDurationSeconds, textCoords, labelDir, this.children.map(c => c.adapter)); 98 | } 99 | 100 | erase(delay: number, animate: boolean, reverse: boolean): number { 101 | if (this.adapter.forStation != undefined) { 102 | this.forStation.removeLabel(this); 103 | this.adapter.erase(delay, animate ? Config.default.fadeDurationSeconds : 0); 104 | } else if (this.adapter.forLine != undefined) { 105 | this.children.forEach(c => { 106 | c.erase(delay, animate, reverse); 107 | }); 108 | } else { 109 | this.adapter.erase(delay, animate ? Config.default.fadeDurationSeconds : 0); 110 | } 111 | return 0; 112 | } 113 | } -------------------------------------------------------------------------------- /src/drawables/Station.ts: -------------------------------------------------------------------------------- 1 | import { Vector } from "../Vector"; 2 | import { Rotation } from "../Rotation"; 3 | import { Line } from "./Line"; 4 | import { Utils } from "../Utils"; 5 | import { PreferredTrack } from "../PreferredTrack"; 6 | import { Label } from "./Label"; 7 | import { BoundingBox } from "../BoundingBox"; 8 | import { AbstractTimedDrawable, AbstractTimedDrawableAdapter } from "./AbstractTimedDrawable"; 9 | import { Projection } from "../Projection"; 10 | import { Config } from "../Config"; 11 | 12 | export interface StationAdapter extends AbstractTimedDrawableAdapter { 13 | baseCoords: Vector; 14 | lonLat: Vector | undefined; 15 | rotation: Rotation; 16 | labelDir: Rotation; 17 | id: string; 18 | draw(delaySeconds: number, animationDurationSeconds: number, getPositionBoundaries: () => {[id: string]: [number, number]}): void; 19 | erase(delaySeconds: number, animationDurationSeconds: number): void; 20 | move(delaySeconds: number, animationDurationSeconds: number, from: Vector, to: Vector, callback: () => void): void; 21 | } 22 | 23 | export class Stop { 24 | constructor(public stationId: string, public trackInfo: string) { 25 | 26 | } 27 | 28 | public coord: Vector | null = null; 29 | } 30 | 31 | export interface LineAtStation { 32 | line?: Line; 33 | axis: string; 34 | track: number; 35 | } 36 | 37 | export class Station extends AbstractTimedDrawable { 38 | 39 | private existingLines: {[id: string]: LineAtStation[]} = {x: [], y: []}; 40 | private existingLabels: Label[] = []; 41 | private phantom?: LineAtStation = undefined; 42 | rotation = this.adapter.rotation; 43 | labelDir = this.adapter.labelDir; 44 | id = this.adapter.id; 45 | 46 | constructor(protected adapter: StationAdapter) { 47 | super(adapter); 48 | if (this.adapter.lonLat != undefined) { 49 | this.adapter.baseCoords = Projection.default.project(this.adapter.lonLat); 50 | } 51 | } 52 | 53 | get baseCoords() { 54 | return this.adapter.baseCoords; 55 | } 56 | 57 | set baseCoords(baseCoords: Vector) { 58 | this.adapter.baseCoords = baseCoords; 59 | } 60 | 61 | get boundingBox() { 62 | return new BoundingBox(this.adapter.baseCoords, this.adapter.baseCoords); 63 | } 64 | 65 | addLine(line: Line, axis: string, track: number): void { 66 | this.phantom = undefined; 67 | this.existingLines[axis].push({line: line, axis: axis, track: track}); 68 | } 69 | 70 | removeLine(line: Line): void { 71 | this.removeLineAtAxis(line, this.existingLines.x); 72 | this.removeLineAtAxis(line, this.existingLines.y); 73 | } 74 | 75 | addLabel(label: Label): void { 76 | if (!this.existingLabels.includes(label)) 77 | this.existingLabels.push(label); 78 | } 79 | 80 | removeLabel(label: Label): void { 81 | let i = 0; 82 | while (i < this.existingLabels.length) { 83 | if (this.existingLabels[i] == label) { 84 | this.existingLabels.splice(i, 1); 85 | } else { 86 | i++; 87 | } 88 | } 89 | } 90 | 91 | get labels(): Label[] { 92 | return this.existingLabels; 93 | } 94 | 95 | private removeLineAtAxis(line: Line, existingLinesForAxis: LineAtStation[]): void { 96 | let i = 0; 97 | while (i < existingLinesForAxis.length) { 98 | if (existingLinesForAxis[i].line == line) { 99 | this.phantom = existingLinesForAxis[i]; 100 | existingLinesForAxis.splice(i, 1); 101 | } else { 102 | i++; 103 | } 104 | } 105 | } 106 | 107 | axisAndTrackForExistingLine(lineName: string): LineAtStation | undefined { 108 | const x = this.trackForLineAtAxis(lineName, this.existingLines.x); 109 | if (x != undefined) { 110 | return x; 111 | } 112 | const y = this.trackForLineAtAxis(lineName, this.existingLines.y); 113 | if (y != undefined) { 114 | return y; 115 | } 116 | return undefined; 117 | } 118 | 119 | private trackForLineAtAxis(lineName: string, existingLinesForAxis: LineAtStation[]): LineAtStation | undefined { 120 | let i = 0; 121 | while (i < existingLinesForAxis.length) { 122 | if (existingLinesForAxis[i].line?.name == lineName) { 123 | return existingLinesForAxis[i]; 124 | } 125 | i++; 126 | } 127 | return undefined; 128 | } 129 | 130 | assignTrack(axis: string, preferredTrack: PreferredTrack, line: Line): number { 131 | if (preferredTrack.hasTrackNumber()) { 132 | return preferredTrack.trackNumber; 133 | } 134 | if (this.phantom?.line?.name == line.name && this.phantom?.axis == axis) { 135 | return this.phantom?.track; 136 | } 137 | const positionBoundariesForAxis = this.positionBoundaries()[axis]; 138 | return preferredTrack.isPositive() ? positionBoundariesForAxis[1] + 1 : positionBoundariesForAxis[0] - 1; 139 | } 140 | 141 | rotatedTrackCoordinates(incomingDir: Rotation, assignedTrack: number): Vector { 142 | let newCoord: Vector; 143 | if (incomingDir.degrees % 180 == 0) { 144 | newCoord = new Vector(assignedTrack * Config.default.lineDistance, 0); 145 | } else { 146 | newCoord = new Vector(0, assignedTrack * Config.default.lineDistance); 147 | } 148 | newCoord = newCoord.rotate(this.rotation); 149 | newCoord = this.baseCoords.add(newCoord); 150 | return newCoord; 151 | } 152 | 153 | private positionBoundaries(): {[id: string]: [number, number]} { 154 | return { 155 | x: this.positionBoundariesForAxis(this.existingLines.x), 156 | y: this.positionBoundariesForAxis(this.existingLines.y) 157 | }; 158 | } 159 | 160 | private positionBoundariesForAxis(existingLinesForAxis: LineAtStation[]): [number, number] { 161 | if (existingLinesForAxis.length == 0) { 162 | return [1, -1]; 163 | } 164 | let left = 0; 165 | let right = 0; 166 | for (let i=0; i existingLinesForAxis[i].track) { 171 | left = existingLinesForAxis[i].track; 172 | } 173 | } 174 | return [left, right]; 175 | } 176 | 177 | draw(delaySeconds: number, animate: boolean): number { 178 | const station = this; 179 | this.existingLabels.forEach(l => l.draw(delaySeconds, animate)); 180 | const t = station.positionBoundaries(); 181 | this.adapter.draw(delaySeconds, animate ? Config.default.fadeDurationSeconds : 0, function() { return t; }); 182 | return 0; 183 | } 184 | 185 | move(delaySeconds: number, animationDurationSeconds: number, to: Vector) { 186 | const station = this; 187 | this.adapter.move(delaySeconds, animationDurationSeconds, this.baseCoords, to, () => station.existingLabels.forEach(l => l.draw(0, false))); 188 | } 189 | 190 | erase(delaySeconds: number, animate: boolean): number { 191 | this.adapter.erase(delaySeconds, animate ? Config.default.fadeDurationSeconds : 0); 192 | return 0; 193 | } 194 | 195 | stationSizeForAxis(axis: string, vector: number): number { 196 | if (Utils.equals(vector, 0)) 197 | return 0; 198 | const dir = Math.sign(vector); 199 | let dimen = this.positionBoundariesForAxis(this.existingLines[axis])[vector < 0 ? 0 : 1]; 200 | if (dir*dimen < 0) { 201 | dimen = 0; 202 | } 203 | return dimen * Config.default.lineDistance + dir * (Config.default.defaultStationDimen + Config.default.labelDistance); 204 | } 205 | 206 | linesExisting(): boolean { 207 | if (this.existingLines.x.length > 0 || this.existingLines.y.length > 0) { 208 | return true; 209 | } 210 | return false; 211 | } 212 | 213 | 214 | } -------------------------------------------------------------------------------- /src/drawables/TimedDrawable.ts: -------------------------------------------------------------------------------- 1 | import { Instant } from "../Instant"; 2 | import { BoundingBox } from "../BoundingBox"; 3 | 4 | export interface Timed { 5 | from: Instant; 6 | to: Instant; 7 | } 8 | 9 | export interface TimedDrawable extends Timed { 10 | name: string; 11 | boundingBox: BoundingBox; 12 | draw(delaySeconds: number, animate: boolean, reverse: boolean): number; 13 | erase(delaySeconds: number, animate: boolean, reverse: boolean): number; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/drawables/Train.ts: -------------------------------------------------------------------------------- 1 | import { Stop } from "./Station"; 2 | import { StationProvider } from "../Network"; 3 | import { Vector } from "../Vector"; 4 | import { ArrivalDepartureTime } from "../ArrivalDepartureTime"; 5 | import { AbstractTimedDrawableAdapter, AbstractTimedDrawable } from "./AbstractTimedDrawable"; 6 | import { Config } from "../Config"; 7 | 8 | export interface TrainAdapter extends AbstractTimedDrawableAdapter { 9 | stops: Stop[]; 10 | offset: number; 11 | draw(delaySeconds: number, animationDurationSeconds: number, follow: {path: Vector[], from: number, to: number}): void; 12 | move(delaySeconds: number, animationDurationSeconds: number, follow: {path: Vector[], from: number, to: number}): void; 13 | erase(delaySeconds: number, animationDurationSeconds: number): void; 14 | } 15 | 16 | export class Train extends AbstractTimedDrawable { 17 | 18 | constructor(protected adapter: TrainAdapter, private stationProvider: StationProvider, private config: Config) { 19 | super(adapter); 20 | } 21 | 22 | draw(delay: number, animate: boolean): number { 23 | const lineGroup = this.stationProvider.lineGroupById(this.name) 24 | const stops = this.adapter.stops; 25 | if (stops.length < 2) { 26 | throw new Error("Train " + this.name + " needs at least 2 stops"); 27 | } 28 | for (let i=1; i= animateFromInstant.epoch && instant.second >= animateFromInstant.second) 54 | animate = true; 55 | 56 | console.log(instant, 'time: ' + Math.floor(timePassed / 60) + ':' + timePassed % 60); 57 | 58 | network.drawTimedDrawablesAt(instant, animate); 59 | const next = network.nextInstant(instant); 60 | 61 | if (!(stopped && animate) && next) { 62 | const delta = instant.delta(next); 63 | timePassed += delta; 64 | const delay = animate ? delta : 0; 65 | const animator = new SvgAnimator(); 66 | animator.wait(delay*1000, () => slide(next, animate)); 67 | } else { 68 | console.log('Stopped.'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/svg/SvgAbstractTimedDrawable.ts: -------------------------------------------------------------------------------- 1 | import { Instant } from "../Instant"; 2 | import { Vector } from "../Vector"; 3 | import { BoundingBox } from "../BoundingBox"; 4 | import { AbstractTimedDrawableAdapter } from "../drawables/AbstractTimedDrawable"; 5 | import { SvgAbstractTimedDrawableAttributes } from "./SvgApi"; 6 | import { SvgAnimator } from "./SvgAnimator"; 7 | import { Animator } from "../Animator"; 8 | 9 | export class SvgAbstractTimedDrawable implements AbstractTimedDrawableAdapter, SvgAbstractTimedDrawableAttributes { 10 | 11 | constructor(protected element: SVGGraphicsElement) { 12 | 13 | } 14 | 15 | get name(): string { 16 | return this.element.getAttribute('name') || this.element.getAttribute('src') || ''; 17 | } 18 | 19 | get from(): Instant { 20 | return this.getInstant('from'); 21 | } 22 | 23 | get to(): Instant { 24 | return this.getInstant('to'); 25 | } 26 | 27 | get boundingBox(): BoundingBox { 28 | const lBox = this.element.getBBox(); 29 | if (document.getElementById('zoomable') != undefined) { 30 | const zoomable = document.getElementById('zoomable'); 31 | const zRect = zoomable.getBoundingClientRect(); 32 | const zBox = zoomable.getBBox(); 33 | const lRect = this.element.getBoundingClientRect(); 34 | const zScale = zBox.width/zRect.width; 35 | const x = (lRect.x-zRect.x)*zScale+zBox.x; 36 | const y = (lRect.y-zRect.y)*zScale+zBox.y; 37 | return new BoundingBox(new Vector(x, y), new Vector(x+lRect.width*zScale, y+lRect.height*zScale)); 38 | } 39 | return new BoundingBox(new Vector(lBox.x, lBox.y), new Vector(lBox.x+lBox.width, lBox.y+lBox.height)); 40 | } 41 | 42 | private getInstant(fromOrTo: string): Instant { 43 | if (this.element.dataset[fromOrTo] != undefined) { 44 | const arr = this.element.dataset[fromOrTo]?.split(/\s+/) 45 | if (arr != undefined) { 46 | return Instant.from(arr); 47 | } 48 | } 49 | return Instant.BIG_BANG; 50 | } 51 | 52 | protected show(fadeSeconds: number) { 53 | if (fadeSeconds == 0) { 54 | this.element.style.visibility = 'visible'; 55 | } else if (getComputedStyle(this.element).visibility != 'visible') { 56 | this.element.style.opacity = '0'; 57 | this.element.style.visibility = 'visible'; 58 | const animator = new SvgAnimator(); 59 | animator.ease(Animator.EASE_CUBIC).animate(fadeSeconds*1000, (x, isLast) => { 60 | this.element.style.opacity = x + ''; 61 | return true; 62 | }); 63 | } 64 | } 65 | 66 | protected hide(fadeSeconds: number) { 67 | if (fadeSeconds != 0) { 68 | this.element.style.opacity = '1'; 69 | const animator = new SvgAnimator(); 70 | animator.ease(Animator.EASE_CUBIC).from(1).to(0).animate(fadeSeconds*1000, (x, isLast) => { 71 | this.element.style.opacity = x + ''; 72 | if (isLast) { 73 | this.element.style.visibility = 'hidden'; 74 | } 75 | return true; 76 | }); 77 | } else { 78 | this.element.style.visibility = 'hidden'; 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/svg/SvgAnimator.ts: -------------------------------------------------------------------------------- 1 | import { Animator } from "../Animator"; 2 | 3 | export class SvgAnimator extends Animator { 4 | 5 | constructor() { 6 | super(); 7 | } 8 | 9 | protected now(): number { 10 | return performance.now(); 11 | } 12 | 13 | protected timeout(callback: () => void, delayMilliseconds: number): void { 14 | window.setTimeout(callback, delayMilliseconds); 15 | } 16 | 17 | protected requestFrame(callback: () => void): void { 18 | window.requestAnimationFrame(callback); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/svg/SvgCrumpledImage.ts: -------------------------------------------------------------------------------- 1 | import { Vector } from "../Vector"; 2 | import { SvgAnimator } from "./SvgAnimator"; 3 | import { SvgAbstractTimedDrawable } from "./SvgAbstractTimedDrawable"; 4 | import { BoundingBox } from "../BoundingBox"; 5 | import { CrumpledImageAdapter, Triangle } from "../drawables/CrumpledImage"; 6 | import { SvgCrumpledImageAttributes } from "./SvgApi"; 7 | 8 | export class SvgCrumpledImage extends SvgAbstractTimedDrawable implements CrumpledImageAdapter, SvgCrumpledImageAttributes { 9 | private canvas: HTMLCanvasElement; 10 | private canvasBoundingBox: BoundingBox | undefined; 11 | private img: HTMLImageElement; 12 | private imgDimen: Vector = Vector.NULL; 13 | 14 | 15 | constructor(protected element: SVGGraphicsElement) { 16 | super(element); 17 | this.canvas = document.createElementNS('http://www.w3.org/1999/xhtml','canvas'); 18 | this.img = document.createElementNS('http://www.w3.org/1999/xhtml', 'img'); 19 | this.setupImage(); 20 | } 21 | 22 | get boundingBox(): BoundingBox { 23 | if (this.canvasBoundingBox == undefined) { 24 | const r = this.element.getBBox(); 25 | this.canvasBoundingBox = new BoundingBox(new Vector(r.x, r.y), new Vector(r.x+r.width, r.y+r.height)); 26 | } 27 | return this.canvasBoundingBox; 28 | } 29 | 30 | get crumpledImage(): string { 31 | return this.element.dataset.crumpledImage || ""; 32 | } 33 | 34 | private setupImage() { 35 | this.img.src = this.crumpledImage; 36 | this.img.style.visibility = 'hidden'; 37 | 38 | this.canvas.setAttribute("width", this.boundingBox.dimensions.x+""); 39 | this.canvas.setAttribute("height", this.boundingBox.dimensions.y+""); 40 | this.element.appendChild(this.canvas); 41 | this.img.onload = e => { 42 | const ctx = this.canvas.getContext('2d'); 43 | this.imgDimen = new Vector(this.img.width, this.img.height); 44 | ctx?.drawImage(this.img, 0, 0, this.boundingBox.dimensions.x, this.boundingBox.dimensions.y); 45 | } 46 | this.element.appendChild(this.img); 47 | } 48 | 49 | draw(delaySeconds: number, animationDurationSeconds: number, triangles: Triangle[]): void { 50 | const animator = new SvgAnimator(); 51 | animator.wait(delaySeconds*1000, () => { 52 | this.element.style.visibility = 'visible'; 53 | const ctx = this.canvas.getContext('2d'); 54 | if (ctx != null && triangles.length > 0) { 55 | animator.animate(animationDurationSeconds*1000, (x, isLast) => { 56 | this.mapTriangles(triangles, ctx); 57 | return true; 58 | }); 59 | } 60 | }); 61 | } 62 | 63 | erase(delaySeconds: number): void { 64 | const animator = new SvgAnimator(); 65 | animator.wait(delaySeconds*1000, () => { 66 | this.element.style.visibility = 'hidden'; 67 | }); 68 | } 69 | 70 | private mapTriangles(triangles: Triangle[], ctx: CanvasRenderingContext2D) { 71 | ctx.clearRect(0, 0, this.boundingBox.dimensions.x, this.boundingBox.dimensions.y); 72 | for (let i=0;i this.svg2ImgCoords(c.startCoords)); 76 | const dst = tArr.map(c => this.svg2CanvasCoords(c.currentCoords())); 77 | this.drawTriangle(ctx, this.img, 78 | dst[0], dst[1], dst[2], 79 | src[0], src[1], src[2] 80 | ); 81 | } 82 | } 83 | 84 | private svg2ImgCoords(v: Vector) { 85 | const scaleX = this.imgDimen.x / this.boundingBox.dimensions.x; 86 | const scaleY = this.imgDimen.y / this.boundingBox.dimensions.y; 87 | const relSvg = this.svg2CanvasCoords(v); 88 | return new Vector(relSvg.x*scaleX, relSvg.y*scaleY); 89 | } 90 | 91 | private svg2CanvasCoords(v: Vector) { 92 | return this.boundingBox.tl.delta(v); 93 | } 94 | 95 | // inspired by http://tulrich.com/geekstuff/canvas/jsgl.js 96 | private drawTriangle(ctx: CanvasRenderingContext2D, im: HTMLImageElement, d0: Vector, d1: Vector, d2: Vector, s0: Vector, s1: Vector, s2: Vector) { 97 | ctx.save(); 98 | 99 | ctx.beginPath(); 100 | ctx.moveTo(d0.x, d0.y); 101 | ctx.lineTo(d1.x, d1.y); 102 | ctx.lineTo(d2.x, d2.y); 103 | ctx.closePath(); 104 | //ctx.stroke(); 105 | ctx.clip(); 106 | 107 | var denom = s0.x * (s2.y - s1.y) - s1.x * s2.y + s2.x * s1.y + (s1.x - s2.x) * s0.y; 108 | if (denom == 0) { 109 | return; 110 | } 111 | var m11 = -(s0.y * (d2.x - d1.x) - s1.y * d2.x + s2.y * d1.x + (s1.y - s2.y) * d0.x) / denom; 112 | var m12 = (s1.y * d2.y + s0.y * (d1.y - d2.y) - s2.y * d1.y + (s2.y - s1.y) * d0.y) / denom; 113 | var m21 = (s0.x * (d2.x - d1.x) - s1.x * d2.x + s2.x * d1.x + (s1.x - s2.x) * d0.x) / denom; 114 | var m22 = -(s1.x * d2.y + s0.x * (d1.y - d2.y) - s2.x * d1.y + (s2.x - s1.x) * d0.y) / denom; 115 | var dx = (s0.x * (s2.y * d1.x - s1.y * d2.x) + s0.y * (s1.x * d2.x - s2.x * d1.x) + (s2.x * s1.y - s1.x * s2.y) * d0.x) / denom; 116 | var dy = (s0.x * (s2.y * d1.y - s1.y * d2.y) + s0.y * (s1.x * d2.y - s2.x * d1.y) + (s2.x * s1.y - s1.x * s2.y) * d0.y) / denom; 117 | 118 | ctx.transform(m11, m12, m21, m22, dx, dy); 119 | ctx.drawImage(im, 0, 0); 120 | 121 | ctx.restore(); 122 | }; 123 | 124 | } -------------------------------------------------------------------------------- /src/svg/SvgGenericTimedDrawable.ts: -------------------------------------------------------------------------------- 1 | import { GenericTimedDrawableAdapter } from "../drawables/GenericTimedDrawable"; 2 | import { SvgAnimator } from "./SvgAnimator"; 3 | import { SvgAbstractTimedDrawable } from "./SvgAbstractTimedDrawable"; 4 | 5 | export class SvgGenericTimedDrawable extends SvgAbstractTimedDrawable implements GenericTimedDrawableAdapter { 6 | 7 | constructor(protected element: SVGGraphicsElement) { 8 | super(element); 9 | } 10 | 11 | draw(delaySeconds: number, animationDurationSeconds: number): void { 12 | const animator = new SvgAnimator(); 13 | animator.wait(delaySeconds*1000, () => { 14 | this.show(animationDurationSeconds); 15 | 16 | if (this.element.localName == 'g') { 17 | if (animationDurationSeconds == 0) { 18 | this.element.style.opacity = '1'; 19 | } 20 | if (this.element.onfocus != undefined) { 21 | this.element.focus(); 22 | } 23 | } 24 | }); 25 | } 26 | 27 | erase(delaySeconds: number, animationDurationSeconds: number): void { 28 | const animator = new SvgAnimator(); 29 | animator.wait(delaySeconds*1000, () => { 30 | this.hide(animationDurationSeconds); 31 | if (this.element.localName == 'g') { 32 | this.element.style.opacity = '0'; 33 | } 34 | }); 35 | } 36 | } -------------------------------------------------------------------------------- /src/svg/SvgKenImage.ts: -------------------------------------------------------------------------------- 1 | import { Vector } from "../Vector"; 2 | import { SvgAnimator } from "./SvgAnimator"; 3 | import { KenImageAdapter } from "../drawables/KenImage"; 4 | import { SvgAbstractTimedDrawable } from "./SvgAbstractTimedDrawable"; 5 | import { BoundingBox } from "../BoundingBox"; 6 | import { SvgKenImageAttributes } from "./SvgApi"; 7 | 8 | export class SvgKenImage extends SvgAbstractTimedDrawable implements KenImageAdapter, SvgKenImageAttributes { 9 | 10 | constructor(protected element: SVGGraphicsElement) { 11 | super(element); 12 | } 13 | 14 | get boundingBox(): BoundingBox { 15 | const r = this.element.getBBox(); 16 | return new BoundingBox(new Vector(r.x, r.y), new Vector(r.x+r.width, r.y+r.height)); 17 | } 18 | 19 | get zoom(): Vector { 20 | if (this.element.dataset['zoom'] != undefined) { 21 | const center = this.element.dataset['zoom'].split(' '); 22 | return new Vector(parseInt(center[0]) || 50, parseInt(center[1]) || 50); 23 | } 24 | return Vector.NULL; 25 | } 26 | 27 | draw(delaySeconds: number, animationDurationSeconds: number, zoomCenter: Vector, zoomScale: number): void { 28 | const animator = new SvgAnimator(); 29 | animator.wait(delaySeconds*1000, () => { 30 | this.element.style.visibility = 'visible'; 31 | if (animationDurationSeconds > 0) { 32 | const fromCenter = this.boundingBox.tl.between(this.boundingBox.br, 0.5) 33 | animator 34 | .animate(animationDurationSeconds*1000, (x, isLast) => this.animateFrame(x, isLast, fromCenter, zoomCenter, 1, zoomScale)); 35 | } 36 | }); 37 | } 38 | 39 | private animateFrame(x: number, isLast: boolean, fromCenter: Vector, toCenter: Vector, fromScale: number, toScale: number): boolean { 40 | if (!isLast) { 41 | const delta = fromCenter.delta(toCenter) 42 | const center = new Vector(delta.x * x, delta.y * x).add(fromCenter); 43 | const scale = (toScale - fromScale) * x + fromScale; 44 | this.updateZoom(center, scale); 45 | } else { 46 | this.updateZoom(toCenter, toScale); 47 | } 48 | return true; 49 | } 50 | 51 | private updateZoom(center: Vector, scale: number) { 52 | const zoomable = this.element; 53 | if (zoomable != undefined) { 54 | const origin = this.boundingBox.tl.between(this.boundingBox.br, 0.5); 55 | zoomable.style.transformOrigin = origin.x + 'px ' + origin.y + 'px'; 56 | zoomable.style.transform = 'scale(' + scale + ') translate(' + (origin.x - center.x) + 'px,' + (origin.y - center.y) + 'px)'; 57 | } 58 | } 59 | 60 | erase(delaySeconds: number): void { 61 | const animator = new SvgAnimator(); 62 | animator.wait(delaySeconds*1000, () => { 63 | this.element.style.visibility = 'hidden'; 64 | }); 65 | } 66 | } -------------------------------------------------------------------------------- /src/svg/SvgLabel.ts: -------------------------------------------------------------------------------- 1 | import { Rotation } from "../Rotation"; 2 | import { LabelAdapter } from "../drawables/Label"; 3 | import { Vector } from "../Vector"; 4 | import { Utils } from "../Utils"; 5 | import { BoundingBox } from "../BoundingBox"; 6 | import { SvgAnimator } from "./SvgAnimator"; 7 | import { SvgAbstractTimedDrawable } from "./SvgAbstractTimedDrawable"; 8 | import { Config } from "../Config"; 9 | import { SvgLabelAttributes } from "./SvgApi"; 10 | 11 | export class SvgLabel extends SvgAbstractTimedDrawable implements LabelAdapter, SvgLabelAttributes { 12 | 13 | constructor(protected element: SVGGraphicsElement) { 14 | super(element); 15 | } 16 | 17 | get forStation(): string | undefined { 18 | return this.element.dataset.station; 19 | } 20 | 21 | get forLine(): string | undefined { 22 | return this.element.dataset.line; 23 | } 24 | 25 | get boundingBox(): BoundingBox { 26 | if (this.element.style.visibility == 'visible') { 27 | return super.boundingBox; 28 | } 29 | return new BoundingBox(Vector.NULL, Vector.NULL); 30 | } 31 | 32 | draw(delaySeconds: number, animationDurationSeconds: number, textCoords: Vector, labelDir: Rotation, children: LabelAdapter[]): void { 33 | const animator = new SvgAnimator(); 34 | animator.wait(delaySeconds*1000, () => { 35 | if (textCoords != Vector.NULL) { 36 | this.setCoord(this.element, textCoords); 37 | if (children.length > 0) { 38 | this.drawLineLabels(animationDurationSeconds, labelDir, children); 39 | } else { 40 | this.drawStationLabel(animationDurationSeconds, labelDir); 41 | } 42 | } else { 43 | this.show(animationDurationSeconds); 44 | } 45 | }); 46 | } 47 | 48 | private translate(animationDurationSeconds: number, boxDimen: Vector, labelDir: Rotation) { 49 | const labelunitv = Vector.UNIT.rotate(labelDir); 50 | this.element.style.transform = 'translate(' 51 | + Utils.trilemma(labelunitv.x, [-boxDimen.x + 'px', -boxDimen.x/2 + 'px', '0px']) 52 | + ',' 53 | + Utils.trilemma(labelunitv.y, [-Config.default.labelHeight + 'px', -Config.default.labelHeight/2 + 'px', '0px']) // TODO magic numbers 54 | + ')'; 55 | this.show(animationDurationSeconds); 56 | } 57 | 58 | private drawLineLabels(animationDurationSeconds: number, labelDir: Rotation, children: LabelAdapter[]) { 59 | this.element.children[0].innerHTML = ''; 60 | children.forEach(c => { 61 | if (c instanceof SvgLabel) { 62 | this.drawLineLabel(c); 63 | } 64 | }) 65 | const scale = this.element.getBoundingClientRect().width/Math.max(this.element.getBBox().width, 1); 66 | const bbox = this.element.children[0].getBoundingClientRect(); 67 | this.translate(animationDurationSeconds, new Vector(bbox.width/scale, bbox.height/scale), labelDir); 68 | } 69 | 70 | private drawLineLabel(label: SvgLabel) { 71 | const lineLabel = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); 72 | lineLabel.className = label.classNames; 73 | lineLabel.innerHTML = label.text; 74 | this.element.children[0].appendChild(lineLabel); 75 | } 76 | 77 | private drawStationLabel(animationDurationSeconds: number, labelDir: Rotation) { 78 | if (!this.element.className.baseVal.includes('for-station')) 79 | this.element.className.baseVal += ' for-station'; 80 | this.element.style.dominantBaseline = 'hanging'; 81 | this.translate(animationDurationSeconds, new Vector(this.element.getBBox().width, this.element.getBBox().height), labelDir); 82 | } 83 | 84 | erase(delaySeconds: number, animationDurationSeconds: number): void { 85 | const animator = new SvgAnimator(); 86 | animator.wait(delaySeconds*1000, () => { 87 | this.hide(animationDurationSeconds); 88 | }); 89 | } 90 | 91 | private setCoord(element: any, coord: Vector): void { 92 | element.setAttribute('x', coord.x); 93 | element.setAttribute('y', coord.y); 94 | } 95 | 96 | get classNames(): string { 97 | return this.element.className.baseVal + ' ' + this.forLine; 98 | } 99 | 100 | get text(): string { 101 | return this.element.innerHTML; 102 | } 103 | 104 | cloneForStation(stationId: string): LabelAdapter { 105 | const lineLabel: SVGGraphicsElement = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'); 106 | lineLabel.className.baseVal += ' for-line'; 107 | lineLabel.dataset.station = stationId; 108 | lineLabel.setAttribute('width', '1'); 109 | const container = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); 110 | lineLabel.appendChild(container); 111 | 112 | this.element.parentElement?.appendChild(lineLabel); 113 | return new SvgLabel(lineLabel) 114 | } 115 | 116 | } -------------------------------------------------------------------------------- /src/svg/SvgLine.ts: -------------------------------------------------------------------------------- 1 | import { LineAdapter } from "../drawables/Line"; 2 | import { Vector } from "../Vector"; 3 | import { Stop } from "../drawables/Station"; 4 | import { BoundingBox } from "../BoundingBox"; 5 | import { SvgAnimator } from "./SvgAnimator"; 6 | import { SvgAbstractTimedDrawable } from "./SvgAbstractTimedDrawable"; 7 | import { SvgUtils } from "./SvgUtils"; 8 | import { Rotation } from "../Rotation"; 9 | import { SvgLineAttributes } from "./SvgApi"; 10 | 11 | export class SvgLine extends SvgAbstractTimedDrawable implements LineAdapter, SvgLineAttributes { 12 | 13 | private _stops: Stop[] = []; 14 | private _boundingBox = new BoundingBox(Vector.NULL, Vector.NULL); 15 | 16 | constructor(protected element: SVGPathElement) { 17 | super(element); 18 | } 19 | 20 | get name(): string { 21 | return this.element.dataset.line || ''; 22 | } 23 | 24 | get boundingBox(): BoundingBox { 25 | return this._boundingBox; 26 | } 27 | 28 | get weight(): number | undefined { 29 | if (this.element.dataset.weight == undefined) { 30 | return undefined; 31 | } 32 | return parseInt(this.element.dataset.weight); 33 | } 34 | 35 | get beckStyle(): boolean { 36 | if (this.element.dataset.beckStyle == undefined) { 37 | return true; 38 | } 39 | return this.element.dataset.beckStyle != 'false'; 40 | } 41 | 42 | get totalLength(): number { 43 | return this.element.getTotalLength(); 44 | } 45 | 46 | get termini(): Vector[] { 47 | const d = this.element.getAttribute('d'); 48 | return SvgUtils.readTermini(d || undefined); 49 | } 50 | 51 | get animOrder(): Rotation | undefined { 52 | if (this.element.dataset.animOrder == undefined) { 53 | return undefined; 54 | } 55 | return Rotation.from(this.element.dataset.animOrder); 56 | } 57 | 58 | get speed(): number | undefined { 59 | if (this.element.dataset.speed == undefined) { 60 | return undefined; 61 | } 62 | return parseInt(this.element.dataset.speed); 63 | } 64 | 65 | private updateBoundingBox(path: Vector[]): void { 66 | const b = super.boundingBox; 67 | this._boundingBox.tl = b.tl; 68 | this._boundingBox.br = b.br; 69 | } 70 | 71 | get stops(): Stop[] { 72 | if (this._stops.length == 0) { 73 | this._stops = SvgUtils.readStops(this.element.dataset.stops); 74 | } 75 | return this._stops; 76 | } 77 | 78 | draw(delaySeconds: number, animationDurationSeconds: number, reverse: boolean, path: Vector[], length: number, colorDeviation: number): void { 79 | this.element.style.visibility = 'hidden'; 80 | this.createPath(path); 81 | this.updateBoundingBox(path); 82 | 83 | const animator = new SvgAnimator(); 84 | animator.wait(delaySeconds * 1000, () => { 85 | this.element.className.baseVal += ' line ' + this.name; 86 | this.element.style.visibility = 'visible'; 87 | 88 | this.updateDasharray(length); 89 | if (colorDeviation != 0) { 90 | this.updateColor(colorDeviation); 91 | } 92 | if (animationDurationSeconds == 0) { 93 | length = 0; 94 | } 95 | const direction = reverse ? -1 : 1; 96 | animator 97 | .from(length*direction) 98 | .to(0) 99 | .animate(animationDurationSeconds * 1000, (x: number, isLast: boolean) => this.animateFrame(x, isLast)); 100 | }); 101 | } 102 | 103 | move(delaySeconds: number, animationDurationSeconds: number, from: Vector[], to: Vector[], colorFrom: number, colorTo: number) { 104 | this.updateBoundingBox(to); 105 | const animator = new SvgAnimator(); 106 | animator.wait(delaySeconds*1000, () => { 107 | animator.animate(animationDurationSeconds*1000, (x, isLast) => this.animateFrameVector(from, to, colorFrom, colorTo, x, isLast)); 108 | }); 109 | } 110 | 111 | erase(delaySeconds: number, animationDurationSeconds: number, reverse: boolean, length: number): void { 112 | const animator = new SvgAnimator(); 113 | animator.wait(delaySeconds * 1000, () => { 114 | let from = 0; 115 | if (animationDurationSeconds == 0) { 116 | from = length; 117 | } 118 | const direction = reverse ? -1 : 1; 119 | animator 120 | .from(from) 121 | .to(length*direction) 122 | .animate(animationDurationSeconds*1000, (x, isLast) => this.animateFrame(x, isLast)); 123 | }); 124 | } 125 | 126 | private createPath(path: Vector[]) { 127 | if (path.length == 0) { 128 | return; 129 | } 130 | const d = 'M' + path.map(v => v.x+','+v.y).join(' L'); 131 | this.element.setAttribute('d', d); 132 | } 133 | 134 | private updateDasharray(length: number) { 135 | let dashedPart = length + ''; 136 | if (this.element.dataset.dashInitial == undefined) { 137 | this.element.dataset.dashInitial = getComputedStyle(this.element).strokeDasharray.replace(/[^0-9\s,]+/g, ''); 138 | } 139 | if (this.element.dataset.dashInitial.length > 0) { 140 | let presetArray = this.element.dataset.dashInitial.split(/[\s,]+/); 141 | if (presetArray.length % 2 == 1) 142 | presetArray = presetArray.concat(presetArray); 143 | const presetLength = presetArray.map(a => parseInt(a) || 0).reduce((a, b) => a + b, 0); 144 | dashedPart = new Array(Math.ceil(length / presetLength + 1)).join(presetArray.join(' ') + ' ') + '0'; 145 | } 146 | this.element.style.strokeDasharray = dashedPart + ' ' + length; 147 | } 148 | 149 | private updateColor(deviation: number) { 150 | this.element.style.stroke = 'rgb(' + Math.max(0, deviation) * 256 + ', 0, ' + Math.min(0, deviation) * -256 + ')'; 151 | } 152 | 153 | private animateFrame(x: number, isLast: boolean): boolean { 154 | this.element.style.strokeDashoffset = x + ''; 155 | if (isLast && x != 0) { 156 | this.element.style.visibility = 'hidden'; 157 | } 158 | return true; 159 | } 160 | 161 | private animateFrameVector(from: Vector[], to: Vector[], colorFrom: number, colorTo: number, x: number, isLast: boolean): boolean { 162 | if (!isLast) { 163 | const interpolated = []; 164 | for (let i=0; ielement).parentElement; 81 | } 82 | return new Station(new SvgStation(element)); 83 | } else if (element.localName == 'text' && (element.dataset.station != undefined || element.dataset.line != undefined)) { 84 | return new Label(new SvgLabel(element), network); 85 | } else if (element.localName == 'image' && element.dataset.zoom != undefined) { 86 | return new KenImage(new SvgKenImage(element)); 87 | } else if (element.localName == 'foreignObject' && element.dataset.crumpledImage != undefined) { 88 | return new CrumpledImage(new SvgCrumpledImage(element)); 89 | } else if (element.dataset.from != undefined || element.dataset.to != undefined) { 90 | return new GenericTimedDrawable(new SvgGenericTimedDrawable(element)); 91 | } 92 | return null; 93 | } 94 | 95 | createVirtualStop(id: string, baseCoords: Vector, rotation: Rotation): Station { 96 | const helpStop = document.createElementNS(SvgNetwork.SVGNS, 'rect'); 97 | helpStop.setAttribute('data-station', id); 98 | helpStop.setAttribute('data-dir', rotation.name); 99 | this.setCoord(helpStop, baseCoords); 100 | helpStop.className.baseVal = 'helper'; 101 | this.parent?.appendChild(helpStop); 102 | return new Station(new SvgStation(helpStop)); 103 | } 104 | 105 | private setCoord(element: any, coord: Vector): void { 106 | element.setAttribute('x', coord.x); 107 | element.setAttribute('y', coord.y); 108 | } 109 | 110 | drawEpoch(epoch: string): void { 111 | const event = new CustomEvent('epoch', { detail: epoch }); 112 | document.dispatchEvent(event); 113 | 114 | let epochLabel; 115 | if (document.getElementById('epoch-label') != undefined) { 116 | epochLabel = document.getElementById('epoch-label'); 117 | epochLabel.textContent = epoch; 118 | } 119 | } 120 | 121 | zoomTo(zoomCenter: Vector, zoomScale: number, animationDurationSeconds: number) { 122 | const animator = new SvgAnimator(); 123 | const defaultBehaviour = animationDurationSeconds <= Config.default.zoomDuration; 124 | animator.wait(defaultBehaviour ? 0 : Config.default.zoomDuration * 1000, () => { 125 | const currentZoomCenter = this.currentZoomCenter; 126 | const currentZoomScale = this.currentZoomScale; 127 | animator 128 | .ease(defaultBehaviour ? SvgAnimator.EASE_CUBIC : SvgAnimator.EASE_NONE) 129 | .animate(animationDurationSeconds * 1000, (x, isLast) => { 130 | this.animateFrame(x, isLast, currentZoomCenter, zoomCenter, currentZoomScale, zoomScale); 131 | return true; 132 | }); 133 | this.currentZoomCenter = zoomCenter; 134 | this.currentZoomScale = zoomScale; 135 | }); 136 | } 137 | 138 | private animateFrame(x: number, isLast: boolean, fromCenter: Vector, toCenter: Vector, fromScale: number, toScale: number): void { 139 | if (!isLast) { 140 | const delta = fromCenter.delta(toCenter) 141 | const center = new Vector(delta.x * x, delta.y * x).add(fromCenter); 142 | const scale = (toScale - fromScale) * x + fromScale; 143 | this.updateZoom(center, scale); 144 | } else { 145 | this.updateZoom(toCenter, toScale); 146 | } 147 | } 148 | 149 | private updateZoom(center: Vector, scale: number) { 150 | const zoomable = document.getElementById('zoomable'); 151 | if (zoomable != undefined) { 152 | const origin = this.canvasSize.tl.between(this.canvasSize.br, 0.5); 153 | zoomable.style.transformOrigin = origin.x + 'px ' + origin.y + 'px'; 154 | zoomable.style.transform = 'scale(' + scale + ') translate(' + (origin.x - center.x) + 'px,' + (origin.y - center.y) + 'px)'; 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/svg/SvgStation.ts: -------------------------------------------------------------------------------- 1 | import { StationAdapter } from "../drawables/Station"; 2 | import { Vector } from "../Vector"; 3 | import { Rotation } from "../Rotation"; 4 | import { SvgAnimator } from "./SvgAnimator"; 5 | import { SvgAbstractTimedDrawable } from "./SvgAbstractTimedDrawable"; 6 | import { Config } from "../Config"; 7 | import { SvgStationAttributes } from "./SvgApi"; 8 | 9 | export class SvgStation extends SvgAbstractTimedDrawable implements StationAdapter, SvgStationAttributes { 10 | 11 | constructor(protected element: SVGRectElement) { 12 | super(element); 13 | } 14 | 15 | get id(): string { 16 | if (this.element.dataset.station != undefined) { 17 | return this.element.dataset.station; 18 | } 19 | throw new Error('Station needs to have a data-station identifier'); 20 | } 21 | 22 | get baseCoords(): Vector { 23 | return new Vector(parseFloat(this.element.getAttribute('x') || '') || 0, parseFloat(this.element.getAttribute('y') || '') || 0); 24 | } 25 | 26 | set baseCoords(baseCoords: Vector) { 27 | this.element.setAttribute('x', baseCoords.x + ''); 28 | this.element.setAttribute('y', baseCoords.y + ''); 29 | } 30 | 31 | get lonLat(): Vector | undefined { 32 | const str = this.element.dataset.lonLat?.split(' '); 33 | if (str == undefined) 34 | return undefined; 35 | return new Vector(parseFloat(str[0]), parseFloat(str[1])); 36 | } 37 | 38 | get rotation(): Rotation { 39 | return Rotation.from(this.element.dataset.dir || 'n'); 40 | } 41 | 42 | get labelDir(): Rotation { 43 | return Rotation.from(this.element.dataset.labelDir || 'n'); 44 | } 45 | 46 | draw(delaySeconds: number, animationDurationSeconds: number, getPositionBoundaries: () => {[id: string]: [number, number]}): void { 47 | const animator = new SvgAnimator(); 48 | animator.wait(delaySeconds*1000, () => { 49 | const positionBoundaries = getPositionBoundaries(); 50 | const stopDimen = [positionBoundaries.x[1] - positionBoundaries.x[0], positionBoundaries.y[1] - positionBoundaries.y[0]]; 51 | 52 | if (!this.element.className.baseVal.includes('station')) { 53 | this.element.className.baseVal += ' station ' + this.id; 54 | } 55 | stopDimen[0] < 0 && stopDimen[1] < 0 56 | ? this.hide(animationDurationSeconds) 57 | : this.show(animationDurationSeconds); 58 | 59 | this.element.setAttribute('width', (Math.max(stopDimen[0], 0) * Config.default.lineDistance + Config.default.defaultStationDimen) + ''); 60 | this.element.setAttribute('height', (Math.max(stopDimen[1], 0) * Config.default.lineDistance + Config.default.defaultStationDimen) + ''); 61 | this.updateTransformOrigin(); 62 | const x = Math.min(positionBoundaries.x[0], 0) * Config.default.lineDistance - Config.default.defaultStationDimen / 2; 63 | const y = Math.min(positionBoundaries.y[0], 0) * Config.default.lineDistance - Config.default.defaultStationDimen / 2; 64 | this.element.setAttribute('transform','rotate(' + this.rotation.degrees + ') translate(' + x + ',' + y + ')'); 65 | 66 | }); 67 | } 68 | 69 | private updateTransformOrigin() { 70 | this.element.setAttribute('transform-origin', this.baseCoords.x + ' ' + this.baseCoords.y); 71 | } 72 | 73 | move(delaySeconds: number, animationDurationSeconds: number, from: Vector, to: Vector, callback: () => void): void { 74 | const animator = new SvgAnimator(); 75 | animator.wait(delaySeconds*1000, () => { 76 | animator 77 | .animate(animationDurationSeconds*1000, (x, isLast) => this.animateFrameVector(x, isLast, from, to, callback)); 78 | }); 79 | } 80 | 81 | private animateFrameVector(x: number, isLast: boolean, from: Vector, to: Vector, callback: () => void): boolean { 82 | if (!isLast) { 83 | this.baseCoords = from.between(to, x); 84 | } else { 85 | this.baseCoords = to; 86 | } 87 | this.updateTransformOrigin(); 88 | callback(); 89 | return true; 90 | } 91 | 92 | erase(delaySeconds: number, animationDurationSeconds: number): void { 93 | const animator = new SvgAnimator(); 94 | animator.wait(delaySeconds*1000, () => { 95 | this.hide(animationDurationSeconds*1000); 96 | }); 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /src/svg/SvgTrain.ts: -------------------------------------------------------------------------------- 1 | import { Vector } from "../Vector"; 2 | import { Stop } from "../drawables/Station"; 3 | import { BoundingBox } from "../BoundingBox"; 4 | import { TrainAdapter } from "../drawables/Train"; 5 | import { Rotation } from "../Rotation"; 6 | import { SvgAnimator } from "./SvgAnimator"; 7 | import { SvgAbstractTimedDrawable } from "./SvgAbstractTimedDrawable"; 8 | import { SvgUtils } from "./SvgUtils"; 9 | import { Config } from "../Config"; 10 | import { SvgTrainAttributes } from "./SvgApi"; 11 | 12 | export class SvgTrain extends SvgAbstractTimedDrawable implements TrainAdapter, SvgTrainAttributes { 13 | 14 | private _stops: Stop[] = []; 15 | 16 | constructor(protected element: SVGPathElement) { 17 | super(element); 18 | } 19 | 20 | get name(): string { 21 | return this.element.dataset.train || ''; 22 | } 23 | 24 | get boundingBox(): BoundingBox { 25 | return new BoundingBox(Vector.NULL, Vector.NULL); 26 | } 27 | 28 | get length(): number { 29 | if (this.element.dataset.length == undefined) { 30 | return 2; 31 | } 32 | return parseInt(this.element.dataset.length); 33 | } 34 | 35 | get stops(): Stop[] { 36 | if (this._stops.length == 0) { 37 | this._stops = SvgUtils.readStops(this.element.dataset.stops); 38 | } 39 | return this._stops; 40 | } 41 | 42 | get offset(): number { 43 | if (this.element.dataset.offset == undefined) { 44 | return 0; 45 | } 46 | return parseInt(this.element.dataset.offset); 47 | } 48 | 49 | draw(delaySeconds: number, animationDurationSeconds: number, follow: { path: Vector[], from: number, to: number }): void { 50 | this.element.className.baseVal += ' train'; 51 | this.setPath(this.calcTrainHinges(this.getPathLength(follow).lengthToStart, follow.path)); 52 | if (!this.from.flag.includes('autoshow')) { 53 | const animator = new SvgAnimator(); 54 | animator.wait(delaySeconds*1000, () => { 55 | this.show(animationDurationSeconds); 56 | }); 57 | } 58 | } 59 | 60 | move(delaySeconds: number, animationDurationSeconds: number, follow: { path: Vector[], from: number, to: number }) { 61 | const animator = new SvgAnimator(); 62 | animator.wait(delaySeconds*1000, () => { 63 | this.show(animationDurationSeconds != 0 ? Config.default.fadeDurationSeconds : 0); 64 | const pathLength = this.getPathLength(follow); 65 | animator 66 | .ease(SvgAnimator.EASE_SINE) 67 | .from(pathLength.lengthToStart) 68 | .to(pathLength.lengthToStart+pathLength.totalBoundedLength) 69 | .timePassed(delaySeconds < 0 ? (-delaySeconds*1000) : 0) 70 | .animate(animationDurationSeconds*1000, (x, isLast) => this.animateFrame(x, isLast, follow.path, animationDurationSeconds)); 71 | }); 72 | } 73 | 74 | private getPathLength(follow: { path: Vector[], from: number, to: number }): { lengthToStart: number, totalBoundedLength: number } { 75 | let lengthToStart = 0; 76 | let totalBoundedLength = 0; 77 | for (let i = 0; i < follow.path.length - 1; i++) { 78 | const l = follow.path[i].delta(follow.path[i + 1]).length; 79 | if (i < follow.from) { 80 | lengthToStart += l; 81 | } else if (i < follow.to) { 82 | totalBoundedLength += l; 83 | } 84 | } 85 | return { lengthToStart: lengthToStart, totalBoundedLength: totalBoundedLength }; 86 | } 87 | 88 | private getPositionByLength(current: number, path: Vector[]): Vector { 89 | let thresh = 0; 90 | for (let i = 0; i < path.length - 1; i++) { 91 | const delta = path[i].delta(path[i + 1]); 92 | const l = delta.length; 93 | if (thresh + l >= current) { 94 | return path[i].between(path[i + 1], (current - thresh) / l).add(delta.rotate(new Rotation(90)).withLength(Config.default.trainTrackOffset)); 95 | } 96 | thresh += l; 97 | } 98 | return path[path.length - 1]; 99 | } 100 | 101 | erase(delaySeconds: number, animationDurationSeconds: number): void { 102 | const animator = new SvgAnimator(); 103 | animator.wait(delaySeconds*1000, () => { 104 | this.hide(animationDurationSeconds); 105 | }); 106 | } 107 | 108 | private setPath(path: Vector[]) { 109 | const d = 'M' + path.map(v => v.x + ',' + v.y).join(' L'); 110 | this.element.setAttribute('d', d); 111 | } 112 | 113 | private calcTrainHinges(front: number, path: Vector[]): Vector[] { 114 | const newTrain: Vector[] = []; 115 | for (let i = 0; i < this.length + 1; i++) { 116 | newTrain.push(this.getPositionByLength(front - i * Config.default.trainWagonLength, path)); 117 | } 118 | return newTrain; 119 | } 120 | 121 | private animateFrame(x: number, isLast: boolean, path: Vector[], animationDurationSeconds: number): boolean { 122 | const trainPath = this.calcTrainHinges(x, path); 123 | this.setPath(trainPath); 124 | if (isLast && this.from.flag.includes('autoshow')) { 125 | this.hide(animationDurationSeconds != 0 ? Config.default.fadeDurationSeconds : 0); 126 | } 127 | return true; 128 | } 129 | } -------------------------------------------------------------------------------- /src/svg/SvgUtils.ts: -------------------------------------------------------------------------------- 1 | import { Stop } from "../drawables/Station"; 2 | import { Vector } from "../Vector"; 3 | 4 | export class SvgUtils { 5 | 6 | static readStops(stopsString: string | undefined): Stop[] { 7 | const stops : Stop[] = []; 8 | const tokens = stopsString?.split(/\s+/) || []; 9 | let nextStop = new Stop('', ''); 10 | for (var i = 0; i < tokens?.length; i++) { 11 | if (tokens[i][0] != '-' && tokens[i][0] != '+' && tokens[i][0] != '*') { 12 | nextStop.stationId = tokens[i]; 13 | stops.push(nextStop); 14 | nextStop = new Stop('', ''); 15 | } else { 16 | nextStop.trackInfo = tokens[i]; 17 | } 18 | } 19 | return stops; 20 | } 21 | 22 | static readTermini(terminiString: string | undefined): Vector[] { 23 | const numbers = terminiString?.trim().split(/[^\d.]+/); 24 | if (numbers != undefined) { 25 | return [ 26 | new Vector(parseFloat(numbers[1]), parseFloat(numbers[2])), 27 | new Vector(parseFloat(numbers[numbers.length-2]), parseFloat(numbers[numbers.length-1])) 28 | ]; 29 | } 30 | return []; 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /test/BoundingBox.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { BoundingBox } from '../src/BoundingBox'; 3 | 4 | describe('BoundingBox', () => { 5 | it('whenCalculateBoundingBoxForZoom', () => { 6 | expect(BoundingBox.from(100, 200, 600, 500).calculateBoundingBoxForZoom(50, 50)).eql(BoundingBox.from(100, 200, 600, 500)); 7 | expect(BoundingBox.from(100, 200, 600, 500).calculateBoundingBoxForZoom(60, 50)).eql(BoundingBox.from(200, 230, 600, 470)); 8 | expect(BoundingBox.from(100, 200, 600, 500).calculateBoundingBoxForZoom(50, 10)).eql(BoundingBox.from(300, 200, 400, 260)); 9 | expect(BoundingBox.from(200, 100, 500, 600).calculateBoundingBoxForZoom(20, 90)).eql(BoundingBox.from(230, 500, 290, 600)); 10 | //expect(BoundingBox.from(0, 0, 300, 100).calculateBoundingBoxForZoom(20, 90)).approximately(BoundingBox.from(30, 80, 90, 100), 0.01); 11 | }) 12 | 13 | 14 | }) -------------------------------------------------------------------------------- /test/DrawableSorter.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { instance, mock, when, verify, anything } from 'ts-mockito'; 3 | import { DrawableSorter } from '../src/DrawableSorter'; 4 | import { Line } from '../src/drawables/Line'; 5 | import { Rotation } from '../src/Rotation'; 6 | import { Vector } from '../src/Vector'; 7 | import { Config } from '../src/Config'; 8 | 9 | describe('DrawableSorter', () => { 10 | 11 | it('givenDrawBoolean_thenReverseOnlyForFalse', () => { 12 | const underTest = new DrawableSorter(); 13 | 14 | const timedDrawable1: Line = instance(mock()); 15 | const timedDrawable2: Line = instance(mock()); 16 | const list = [timedDrawable1, timedDrawable2]; 17 | expect(underTest.sort(list, true, true)).eql([]); 18 | expect(list).eql([timedDrawable1, timedDrawable2]); 19 | expect(underTest.sort(list, false, true)).eql([]); 20 | expect(list).eql([timedDrawable2, timedDrawable1]); 21 | }) 22 | 23 | it('givenAnimOrderSE_thenSortDespiteDeleteAndDoImplicitReverse', () => { 24 | const underTest = new DrawableSorter(); 25 | 26 | const line1: Line = mock(); 27 | when(line1.animOrder).thenReturn(Rotation.from("se")); 28 | when(line1.path).thenReturn([new Vector(6, 5), new Vector(-10, 2)]); 29 | when(line1.animationDurationSeconds).thenReturn(1); 30 | when(line1.speed).thenReturn(Config.default.animSpeed); 31 | const line2: Line = mock(); 32 | when(line2.animOrder).thenReturn(Rotation.from("se")); 33 | when(line2.path).thenReturn([new Vector(7, 9), new Vector(10, 20)]); 34 | when(line2.animationDurationSeconds).thenReturn(2); 35 | when(line2.speed).thenReturn(Config.default.animSpeed); 36 | 37 | const l1 = instance(line1); 38 | const l2 = instance(line2); 39 | Object.setPrototypeOf(l1, Line.prototype); 40 | Object.setPrototypeOf(l2, Line.prototype); 41 | const list = [l1, l2]; 42 | const delays = underTest.sort(list, false, true); 43 | expect(delays[0]).eql({delay: 0, reverse: false}); 44 | expect(delays[1].delay).approximately(2+4/Config.default.animSpeed, 0.01); 45 | expect(delays[1].reverse).eql(true); 46 | expect(list).eql([l2, l1]); 47 | }) 48 | 49 | it('givenAnimOrderNorth_thenSort', () => { 50 | const underTest = new DrawableSorter(); 51 | 52 | const line1: Line = mock(); 53 | when(line1.animOrder).thenReturn(Rotation.from("n")); 54 | when(line1.path).thenReturn([new Vector(6, 5), new Vector(-10, 2)]); 55 | when(line1.animationDurationSeconds).thenReturn(1); 56 | when(line1.speed).thenReturn(Config.default.animSpeed); 57 | const line2: Line = mock(); 58 | when(line2.animOrder).thenReturn(Rotation.from("n")); 59 | when(line2.path).thenReturn([new Vector(10, 20), new Vector(3, 4)]); 60 | when(line2.animationDurationSeconds).thenReturn(2); 61 | when(line2.speed).thenReturn(Config.default.animSpeed); 62 | const line3: Line = mock(); 63 | when(line3.animOrder).thenReturn(Rotation.from("n")); 64 | when(line3.path).thenReturn([new Vector(-5, -4), new Vector(-10, 2)]); 65 | when(line3.animationDurationSeconds).thenReturn(3); 66 | when(line3.speed).thenReturn(Config.default.animSpeed); 67 | const line4: Line = mock(); 68 | when(line4.animOrder).thenReturn(Rotation.from("n")); 69 | when(line4.path).thenReturn([new Vector(-10, 3), new Vector(-10, 20)]); 70 | when(line4.animationDurationSeconds).thenReturn(4); 71 | when(line4.speed).thenReturn(Config.default.animSpeed); 72 | 73 | const l1 = instance(line1); 74 | const l2 = instance(line2); 75 | const l3 = instance(line3); 76 | const l4 = instance(line4); 77 | Object.setPrototypeOf(l1, Line.prototype); 78 | Object.setPrototypeOf(l2, Line.prototype); 79 | Object.setPrototypeOf(l3, Line.prototype); 80 | Object.setPrototypeOf(l4, Line.prototype); 81 | const list = [l1, l2, l3, l4]; 82 | expect(underTest.sort(list, true, true)).eql([{delay: 0, reverse: false}, {delay: 3, reverse: true}, {delay: 3+1/Config.default.animSpeed, reverse: false}, {delay: 4-1/Config.default.animSpeed, reverse: true}]); 83 | expect(list).eql([l3, l1, l4, l2]); 84 | }) 85 | 86 | it('givenNoTermini_thenFallback', () => { 87 | const underTest = new DrawableSorter(); 88 | 89 | const line1: Line = mock(); 90 | when(line1.animOrder).thenReturn(Rotation.from("n")); 91 | when(line1.path).thenReturn([]); 92 | when(line1.animationDurationSeconds).thenReturn(1); 93 | when(line1.speed).thenReturn(Config.default.animSpeed); 94 | const line2: Line = mock(); 95 | when(line2.animOrder).thenReturn(Rotation.from("n")); 96 | when(line2.path).thenReturn([new Vector(10, 20), new Vector(3, 4)]); 97 | when(line2.animationDurationSeconds).thenReturn(2); 98 | when(line2.speed).thenReturn(Config.default.animSpeed); 99 | 100 | const l1 = instance(line1); 101 | const l2 = instance(line2); 102 | Object.setPrototypeOf(l1, Line.prototype); 103 | Object.setPrototypeOf(l2, Line.prototype); 104 | const list = [l1, l2]; 105 | expect(underTest.sort(list, true, true)).eql([{delay: 0, reverse: false}, {delay: 1+4/Config.default.animSpeed, reverse: true}]); 106 | expect(list).eql([l1, l2]); 107 | }) 108 | 109 | 110 | }) -------------------------------------------------------------------------------- /test/Instant.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Instant } from '../src/Instant'; 3 | 4 | describe('Instant', () => { 5 | it('whenFrom', () => { 6 | expect(Instant.from(['1990', '1'])).eql(new Instant(1990, 1, '')); 7 | expect(Instant.from(['1990', '1', 'noanim'])).eql(new Instant(1990, 1, 'noanim')); 8 | }) 9 | 10 | it('whenDelta', () => { 11 | expect(new Instant(1990, 5, '').delta(new Instant(1990, 7, ''))).eql(2); 12 | expect(new Instant(1990, 5, '').delta(new Instant(1991, 7, 'noanim'))).eql(7); 13 | expect(new Instant(1990, 5, '').delta(new Instant(1990, 5, ''))).eql(0); 14 | expect(new Instant(1991, 5, 'reverse').delta(new Instant(1990, 5, ''))).eql(5); 15 | expect(new Instant(1990, 4, '').delta(new Instant(1990, 0, ''))).eql(-4); 16 | }) 17 | 18 | it('whenEquals', () => { 19 | expect(new Instant(1990, 0, '').equals(new Instant(1990, 0, 'noanim'))).eql(true); 20 | expect(new Instant(1990, 1, '').equals(new Instant(1990, 0, ''))).eql(false); 21 | expect(new Instant(1990, 0, '').equals(new Instant(1991, 0, ''))).eql(false); 22 | }) 23 | }) -------------------------------------------------------------------------------- /test/LineGroup.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { LineGroup } from '../src/LineGroup'; 3 | import { Stop } from '../src/drawables/Station'; 4 | import { Line } from '../src/drawables/Line'; 5 | import { instance, mock, when, anyNumber, anything } from 'ts-mockito'; 6 | 7 | describe('LineGroup', () => { 8 | 9 | it('whenTermini', () => { 10 | const g = new LineGroup(); 11 | 12 | let m: Line = mock(); 13 | when(m.termini).thenReturn([new Stop('a', ''), new Stop('b', '')]); 14 | const l1 = instance(m); 15 | g.addLine(l1); 16 | 17 | expect(g.termini).eql([new Stop('a', ''), new Stop('b', '')]); 18 | 19 | m = mock(); 20 | when(m.termini).thenReturn([new Stop('b', ''), new Stop('c', '')]); 21 | const l2 = instance(m); 22 | g.addLine(l2); 23 | 24 | expect(g.termini).eql([new Stop('a', ''), new Stop('c', '')]); 25 | 26 | m = mock(); 27 | when(m.termini).thenReturn([new Stop('b', ''), new Stop('a', ''), new Stop('g', 'g*')]); 28 | const l3 = instance(m); 29 | g.addLine(l3); 30 | 31 | expect(g.termini).eql([new Stop('c', '')]); 32 | 33 | g.removeLine(l3); 34 | 35 | expect(g.termini).eql([new Stop('a', ''), new Stop('c', '')]); 36 | 37 | g.removeLine(l1); 38 | 39 | expect(g.termini).eql([new Stop('b', ''), new Stop('c', '')]); 40 | }) 41 | 42 | it('whenTerminiWithFork', () => { 43 | const g = new LineGroup(); 44 | 45 | let m: Line = mock(); 46 | when(m.termini).thenReturn([new Stop('a', ''), new Stop('b', ''), new Stop('c', '')]); 47 | const l1 = instance(m); 48 | g.addLine(l1); 49 | 50 | m = mock(); 51 | when(m.termini).thenReturn([new Stop('b', ''), new Stop('d', '')]); 52 | const l2 = instance(m); 53 | g.addLine(l2); 54 | 55 | expect(g.termini).eql([new Stop('a', ''), new Stop('c', ''), new Stop('d', '')]); 56 | }) 57 | }) -------------------------------------------------------------------------------- /test/PreferredTrack.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { PreferredTrack } from '../src/PreferredTrack'; 3 | 4 | describe('PreferredTrack', () => { 5 | it('whenFromString', () => { 6 | expect(new PreferredTrack('-1').fromString('')).eql(new PreferredTrack('-1')); 7 | expect(new PreferredTrack('-1').fromString('2')).eql(new PreferredTrack('2')); 8 | expect(new PreferredTrack('-1').fromString('*0')).eql(new PreferredTrack('*0')); 9 | }) 10 | 11 | it('whenFromNumber', () => { 12 | expect(new PreferredTrack('-0').fromNumber(0)).eql(new PreferredTrack('+0')); 13 | expect(new PreferredTrack('-0').fromNumber(1)).eql(new PreferredTrack('+1')); 14 | expect(new PreferredTrack('-0').fromNumber(-2)).eql(new PreferredTrack('-2')); 15 | }) 16 | 17 | it('whenExistingFromLineAtStation', () => { 18 | expect(new PreferredTrack('-1').fromExistingLineAtStation(undefined)).eql(new PreferredTrack('-1')); 19 | expect(new PreferredTrack('-1').fromExistingLineAtStation({axis: 'x', track: 2})).eql(new PreferredTrack('-1')); 20 | expect(new PreferredTrack('-').fromExistingLineAtStation({axis: 'x', track: 2})).eql(new PreferredTrack('+2')); 21 | }) 22 | 23 | it('whenIsPositive', () => { 24 | expect(new PreferredTrack('-0').isPositive()).eql(false); 25 | expect(new PreferredTrack('+2').isPositive()).eql(true); 26 | expect(new PreferredTrack('*0').isPositive()).eql(true); 27 | expect(new PreferredTrack('-0*').isPositive()).eql(false); 28 | expect(new PreferredTrack('-4').isPositive()).eql(false); 29 | }) 30 | 31 | it('whenHasTrackNumber', () => { 32 | expect(new PreferredTrack('-0').hasTrackNumber()).eql(true); 33 | expect(new PreferredTrack('-').hasTrackNumber()).eql(false); 34 | expect(new PreferredTrack('*1').hasTrackNumber()).eql(true); 35 | expect(new PreferredTrack('1*').hasTrackNumber()).eql(true); 36 | }) 37 | 38 | it('whenGetTrackNumber', () => { 39 | expect(new PreferredTrack('-0').trackNumber).eql(-0); 40 | expect(new PreferredTrack('-1').trackNumber).eql(-1); 41 | expect(new PreferredTrack('+3').trackNumber).eql(3); 42 | expect(new PreferredTrack('*3').trackNumber).eql(3); 43 | expect(new PreferredTrack('-2*').trackNumber).eql(-2); 44 | }) 45 | 46 | it('whenKeepOnlySign', () => { 47 | expect(new PreferredTrack('-0').keepOnlySign()).eql(new PreferredTrack('-')); 48 | expect(new PreferredTrack('-1').keepOnlySign()).eql(new PreferredTrack('-')); 49 | expect(new PreferredTrack('+3').keepOnlySign()).eql(new PreferredTrack('+')); 50 | expect(new PreferredTrack('4').keepOnlySign()).eql(new PreferredTrack('+')); 51 | expect(new PreferredTrack('*3').keepOnlySign()).eql(new PreferredTrack('+')); 52 | expect(new PreferredTrack('-3*').keepOnlySign()).eql(new PreferredTrack('-')); 53 | }) 54 | }) -------------------------------------------------------------------------------- /test/SvgUtils.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { SvgUtils } from '../src/svg/SvgUtils'; 3 | import { Vector } from '../src/Vector'; 4 | 5 | describe('SvgUtils', () => { 6 | 7 | it('whenReadTermini', () => { 8 | const input = "M 2690475.7 1762814 L 2690478.7 1762793.1 L 2690486.9 1762452 L 2690477.8 1762435.6 L 2690473.5 1762429"; 9 | 10 | expect(SvgUtils.readTermini(input)).eql([new Vector(2690475.7, 1762814), new Vector(2690473.5, 1762429)]); 11 | }) 12 | }) -------------------------------------------------------------------------------- /test/Utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Utils } from '../src/Utils'; 3 | 4 | describe('Utils', () => { 5 | it('whenEquals', () => { 6 | expect(Utils.equals(0, 0.0001)).to.be.true; 7 | expect(Utils.equals(0, -0.0001)).to.be.true; 8 | expect(Utils.equals(0, -0.1)).to.be.false; 9 | }) 10 | 11 | it('whenTripleDecision', () => { 12 | expect(Utils.trilemma(-50, ['a', 'b', 'c'])).eql('a'); 13 | expect(Utils.trilemma(0.0001, ['a', 'b', 'c'])).eql('b'); 14 | expect(Utils.trilemma(1, ['a', 'b', 'c'])).eql('c'); 15 | }) 16 | 17 | it('whenAlphabeticId', () => { 18 | expect(Utils.alphabeticId('a', 'b')).eql('a_b'); 19 | expect(Utils.alphabeticId('c', 'b')).eql('b_c'); 20 | }) 21 | }) -------------------------------------------------------------------------------- /test/Zoomer.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Vector } from '../src/Vector'; 3 | import { Zoomer } from '../src/Zoomer'; 4 | import { Instant } from '../src/Instant'; 5 | import { BoundingBox } from "../src/BoundingBox"; 6 | 7 | describe('Zoomer', () => { 8 | 9 | it('whenInclude_givenValidBoxes', () => { 10 | const z = new Zoomer(new BoundingBox(new Vector(0, 0), new Vector(1000, 1000))); 11 | expect(z.center).eql(new Vector(500, 500)); 12 | expect(z.scale).eql(1); 13 | 14 | z.include(new BoundingBox(new Vector(100, 200), new Vector(300, 600)), Instant.BIG_BANG, Instant.BIG_BANG, true, false); 15 | expect(z.center).eql(new Vector(500, 500)); 16 | expect(z.scale).eql(1); 17 | 18 | z.include(new BoundingBox(new Vector(100, 200), new Vector(300, 600)), new Instant(1, 1, 'nozoom'), Instant.BIG_BANG, true, true); 19 | expect(z.center).eql(new Vector(500, 500)); 20 | expect(z.scale).eql(1); 21 | 22 | z.include(new BoundingBox(new Vector(100, 230), new Vector(300, 570)), new Instant(1, 1, ''), Instant.BIG_BANG, true, true); 23 | expect(z.center).eql(new Vector(200, 400)); 24 | expect(z.scale).lessThan(2); 25 | 26 | z.include(new BoundingBox(new Vector(100, 200), new Vector(800, 600)), new Instant(1, 1, ''), Instant.BIG_BANG, true, true); 27 | expect(z.center).eql(new Vector(450, 400)); 28 | expect(z.scale).lessThan(1.25); 29 | 30 | z.include(new BoundingBox(new Vector(100, 200), new Vector(800, 700)), new Instant(1, 1, ''), Instant.BIG_BANG, true, true); 31 | expect(z.center).eql(new Vector(450, 450)); 32 | expect(z.scale).lessThan(1.25); 33 | 34 | z.include(new BoundingBox(new Vector(100, 200), new Vector(900, 900)), new Instant(1, 1, 'nozoom'), Instant.BIG_BANG, true, true); 35 | expect(z.center).eql(new Vector(450, 450)); 36 | expect(z.scale).lessThan(1.25); 37 | }) 38 | 39 | it('whenInclude_givenInvalidBoxesYAxis', () => { 40 | const z = new Zoomer(new BoundingBox(new Vector(0, 0), new Vector(1000, 1000))); 41 | 42 | z.include(new BoundingBox(new Vector(100, 230), new Vector(200, 230)), new Instant(1, 1, ''), Instant.BIG_BANG, true, true); 43 | expect(z.center).eql(new Vector(150, 230)); 44 | expect(z.scale).lessThan(3); 45 | 46 | z.include(new BoundingBox(new Vector(300, 230), new Vector(300, 570)), new Instant(1, 1, ''), Instant.BIG_BANG, true, true); 47 | expect(z.center).eql(new Vector(200, 400)); 48 | expect(z.scale).lessThan(2); 49 | }) 50 | 51 | it('whenInclude_givenInvalidBoxesXAxis', () => { 52 | const z = new Zoomer(new BoundingBox(new Vector(0, 0), new Vector(1000, 1000))); 53 | 54 | z.include(new BoundingBox(new Vector(300, 230), new Vector(300, 570)), new Instant(1, 1, ''), Instant.BIG_BANG, true, true); 55 | expect(z.center).eql(new Vector(300, 400)); 56 | expect(z.scale).lessThan(2); 57 | }) 58 | 59 | it('whenInclude_givenInvalidBoxesTwoAxis', () => { 60 | const z = new Zoomer(new BoundingBox(new Vector(0, 0), new Vector(1000, 1000))); 61 | 62 | z.include(new BoundingBox(new Vector(100, 200), new Vector(100, 200)), new Instant(1, 1, ''), Instant.BIG_BANG, true, true); 63 | expect(z.center).eql(new Vector(100, 200)); 64 | expect(z.scale).approximately(3, 0.1); 65 | }) 66 | }) -------------------------------------------------------------------------------- /test/drawables/Label.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { LabelAdapter, Label } from '../../src/drawables/Label'; 3 | import {instance, mock, when, anything, verify, between} from 'ts-mockito'; 4 | import { StationProvider } from '../../src/Network'; 5 | import { Vector } from '../../src/Vector'; 6 | import { Rotation } from '../../src/Rotation'; 7 | import { Station } from '../../src/drawables/Station'; 8 | 9 | describe('Label', () => { 10 | let labelAdapter: LabelAdapter; 11 | let stationProvider: StationProvider; 12 | let station: Station; 13 | 14 | beforeEach(() => { 15 | stationProvider = mock(); 16 | labelAdapter = mock(); 17 | station = mock(); 18 | }) 19 | 20 | it('givenNoLinesExisting_thenCallErase', () => { 21 | when(labelAdapter.forStation).thenReturn('a'); 22 | 23 | when(station.rotation).thenReturn(Rotation.from('n')); 24 | when(station.labelDir).thenReturn(Rotation.from('n')); 25 | when(station.baseCoords).thenReturn(new Vector(30, 10)); 26 | when(station.linesExisting).thenReturn(() => false); 27 | when(station.stationSizeForAxis('x', 0)).thenReturn(0); 28 | when(station.stationSizeForAxis('y', -1)).thenReturn(-3); 29 | when(stationProvider.stationById('a')).thenReturn(instance(station)); 30 | const l = new Label(instance(labelAdapter), instance(stationProvider)); 31 | expect(l.draw(2, false)).eql(0); 32 | verify(labelAdapter.draw(anything(), anything(), anything(), anything(), anything())).never(); 33 | verify(labelAdapter.erase(anything(), anything())).called(); 34 | }) 35 | 36 | it('givenNorthStationWithNorthLabel', () => { 37 | when(labelAdapter.forStation).thenReturn('a'); 38 | when(labelAdapter.draw(anything(), anything(), anything(), anything(), anything())).thenCall((d: number, a: number, v: Vector, r: Rotation) => { 39 | expect(v).eql(new Vector(30, 7)); 40 | expect(r).eql(Rotation.from('n')); 41 | }); 42 | when(station.rotation).thenReturn(Rotation.from('n')); 43 | when(station.labelDir).thenReturn(Rotation.from('n')); 44 | when(station.baseCoords).thenReturn(new Vector(30, 10)); 45 | when(station.linesExisting).thenReturn(() => true); 46 | when(station.stationSizeForAxis('x', 0)).thenReturn(0); 47 | when(station.stationSizeForAxis('y', -1)).thenReturn(-3); 48 | when(stationProvider.stationById('a')).thenReturn(instance(station)); 49 | const l = new Label(instance(labelAdapter), instance(stationProvider)); 50 | expect(l.draw(2, false)).eql(0); 51 | }) 52 | 53 | it('givenNorthStationWithNorthEastLabel', () => { 54 | when(labelAdapter.forStation).thenReturn('a'); 55 | when(labelAdapter.draw(anything(), anything(), anything(), anything(), anything())).thenCall((d: number, a: number, v: Vector, r: Rotation) => { 56 | expect(v).eql(new Vector(15, 27)); 57 | expect(r).eql(Rotation.from('ne')); 58 | }); 59 | when(station.rotation).thenReturn(Rotation.from('n')); 60 | when(station.labelDir).thenReturn(Rotation.from('ne')); 61 | when(station.baseCoords).thenReturn(new Vector(10, 30)); 62 | when(station.linesExisting).thenReturn(() => true); 63 | when(station.stationSizeForAxis('x', between(0, 1))).thenReturn(5); 64 | when(station.stationSizeForAxis('y', between(-1, 0))).thenReturn(-3); 65 | when(stationProvider.stationById('a')).thenReturn(instance(station)); 66 | const l = new Label(instance(labelAdapter), instance(stationProvider)); 67 | expect(l.draw(2, false)).eql(0); 68 | }) 69 | 70 | it('givenNorthEastStationWithNorthWestLabel', () => { 71 | when(labelAdapter.forStation).thenReturn('a'); 72 | when(labelAdapter.draw(anything(), anything(), anything(), anything(), anything())).thenCall((d: number, a: number, v: Vector, r: Rotation) => { 73 | expect(v.x).greaterThan(12); 74 | expect(v.x).lessThan(13); 75 | expect(v.y).greaterThan(-1); 76 | expect(v.y).lessThan(0); 77 | expect(r).eql(Rotation.from('w')); 78 | }); 79 | when(station.rotation).thenReturn(Rotation.from('ne')); 80 | when(station.labelDir).thenReturn(Rotation.from('w')); 81 | when(station.baseCoords).thenReturn(new Vector(30, 10)); 82 | when(station.linesExisting).thenReturn(() => true); 83 | when(station.stationSizeForAxis('x', between(-1, 0))).thenReturn(-20); 84 | when(station.stationSizeForAxis('y', between(0, 1))).thenReturn(5); 85 | when(stationProvider.stationById('a')).thenReturn(instance(station)); 86 | const l = new Label(instance(labelAdapter), instance(stationProvider)); 87 | expect(l.draw(2, false)).eql(0); 88 | }) 89 | 90 | }) -------------------------------------------------------------------------------- /timecut-parallel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | LAUNCH_ARGUMENTS="--no-sandbox --disable-setuid-sandbox --allow-file-access-from-files" 4 | WORKERS=3 5 | START_TIME=0 6 | FILE_TYPE=mp4 7 | RESOLUTION="1920,1080,deviceScaleFactor=2" 8 | FPS=30 9 | 10 | set -e 11 | 12 | usage() { echo "Usage: $0 -i -o -d [-s ] [-w ]" 1>&2; exit 1; } 13 | 14 | while getopts ":i:o:d:s:w:r:f:" o; do 15 | case "${o}" in 16 | i) 17 | INPUT=${OPTARG} 18 | ;; 19 | o) 20 | OUTPUT_DIR=${OPTARG} 21 | ;; 22 | w) 23 | WORKERS=${OPTARG} 24 | ;; 25 | s) 26 | START_TIME=${OPTARG} 27 | ;; 28 | d) 29 | DURATION=${OPTARG} 30 | ;; 31 | r) 32 | RESOLUTION=${OPTARG} 33 | ;; 34 | f) 35 | FPS=${OPTARG} 36 | ;; 37 | *) 38 | usage 39 | ;; 40 | esac 41 | done 42 | 43 | if [ -z "${INPUT}" ] || [ -z "${OUTPUT_DIR}" ] || [ -z "${DURATION}" ]; then 44 | usage 45 | fi 46 | 47 | TIMECUT_ARGUMENTS="--viewport=$RESOLUTION --fps=$FPS --pipe-mode" 48 | SLICE_LENGTH=$(($DURATION/$WORKERS)) 49 | CURRENT_SLICE_LENGTH=$(($SLICE_LENGTH+$DURATION-$SLICE_LENGTH*$WORKERS)) 50 | CURRENT_SLICE_START=$START_TIME 51 | 52 | IDENTIFIER=$(basename ${INPUT}) 53 | 54 | CONTAINER_PREFIX="timecut-worker-" 55 | CONCAT_COMMAND="" 56 | 57 | DOCKERFILE="FROM alekzonder/puppeteer:latest\n 58 | 59 | USER root\n 60 | 61 | RUN apt-get update && apt-get install -yq ffmpeg\n 62 | RUN yarn global add timecut\n 63 | 64 | USER pptruser" 65 | 66 | echo -e "\n=================" | tee -a timecut-parallel.log 67 | echo "Building image..." 68 | DOCKER_IMAGE=$(echo -e $DOCKERFILE | docker build --quiet -) 69 | echo "Image built." 70 | 71 | docker network create -d bridge tna-network || echo "Network already existing (?)." 72 | docker run --name tnaserve -d -v $(pwd):/app/ --workdir /app/ --network tna-network python python -m http.server 3000 73 | 74 | echo "Started tnaserve." 75 | 76 | i=1 77 | while [ "$i" -le "$WORKERS" ]; do 78 | OUTPUT_FILE=/output/${IDENTIFIER}_${i}.$FILE_TYPE 79 | CONCAT_COMMAND="${CONCAT_COMMAND}file $OUTPUT_FILE\n" 80 | 81 | docker rm $CONTAINER_PREFIX$i &>/dev/null || echo "No existing container to delete." 82 | 83 | echo "Starting worker $i at $CURRENT_SLICE_START with duration ${CURRENT_SLICE_LENGTH} ..." | tee -a timecut-parallel.log 84 | 85 | docker run \ 86 | --name $CONTAINER_PREFIX$i \ 87 | -u $(id -u ${USER}):$(id -g ${USER}) \ 88 | -d \ 89 | -v $(pwd):/app/ \ 90 | -v $(realpath ${OUTPUT_DIR}):/output/ \ 91 | --shm-size=1G \ 92 | --entrypoint timecut \ 93 | --network tna-network \ 94 | ${DOCKER_IMAGE} \ 95 | ${INPUT} --start ${CURRENT_SLICE_START} --duration ${CURRENT_SLICE_LENGTH} ${TIMECUT_ARGUMENTS} --launch-arguments="${LAUNCH_ARGUMENTS}" --output=${OUTPUT_FILE} 96 | 97 | CURRENT_SLICE_START=$(($CURRENT_SLICE_START+$CURRENT_SLICE_LENGTH)) 98 | CURRENT_SLICE_LENGTH=$SLICE_LENGTH 99 | i=$(($i + 1)) 100 | 101 | sleep 40 102 | done 103 | 104 | wait_until_workers_finished() { 105 | 106 | echo "$(date) Waiting until workers for $IDENTIFIER are finished..." 107 | 108 | while true; do 109 | sleep 5 110 | DPS=$(docker ps --format '{{.Names}}') 111 | if [[ $DPS != *"timecut-worker-"* ]]; then 112 | echo "$(date) All workers finished, concatenating final video file..." 113 | docker rm -f tnaserve 114 | docker run \ 115 | --name $CONTAINER_PREFIX-concat \ 116 | -u $(id -u ${USER}):$(id -g ${USER}) \ 117 | --rm \ 118 | -v $(realpath ${OUTPUT_DIR}):/output/ \ 119 | --entrypoint /bin/bash \ 120 | ${DOCKER_IMAGE} \ 121 | -c "ffmpeg -f concat -y -safe 0 -i <(echo -e '$CONCAT_COMMAND') -c copy /output/$IDENTIFIER.$FILE_TYPE" 122 | 123 | echo "Concatenating to $OUTPUT_DIR$IDENTIFIER.$FILE_TYPE finished." 124 | break 125 | fi 126 | done 127 | } 128 | 129 | wait_until_workers_finished &>>timecut-parallel.log & 130 | echo "See logs in timecut-parallel.log and docker logs ${CONTAINER_PREFIX}*" 131 | 132 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | "lib": ["dom", "es2016", "es2017.object"], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./network-animator.js", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./dist/", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const LicensePlugin = require('webpack-license-plugin') 4 | const TerserPlugin = require('terser-webpack-plugin'); 5 | 6 | const defaultConfig = { 7 | entry: './src/main.ts', 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.tsx?$/, 12 | use: [ 13 | {loader: 'expose-loader', options: { exposes: [{globalName: 'TNA', override: true}]}}, 14 | {loader: 'ts-loader', options: {onlyCompileBundledFiles: true}} 15 | ], 16 | include: /src/, 17 | }, 18 | ], 19 | }, 20 | resolve: { 21 | extensions: [ '.ts', '.ts', '.js' ], 22 | }, 23 | }; 24 | 25 | module.exports = [ 26 | { 27 | ...defaultConfig, 28 | mode: 'development', 29 | devtool: 'source-map', 30 | output: { 31 | filename: 'network-animator.js', 32 | path: path.resolve(__dirname, 'dist'), 33 | }, 34 | }, 35 | { 36 | ...defaultConfig, 37 | mode: 'production', 38 | output: { 39 | filename: 'network-animator.min.js', 40 | path: path.resolve(__dirname, 'dist'), 41 | }, 42 | optimization: { 43 | minimizer: [new TerserPlugin({ 44 | extractComments: false, 45 | })], 46 | }, 47 | plugins: [ 48 | new LicensePlugin({ outputFilename: 'network-animator.LICENSES.json' }), 49 | new webpack.BannerPlugin({banner: 'https://github.com/traines-source/transport-network-animator MIT License. For vendor licenses, please see network-animator.LICENSES.json'}) 50 | ], 51 | }, 52 | ]; --------------------------------------------------------------------------------