├── src
├── html
│ ├── map.html
│ ├── status-popup.html
│ ├── small-screen-warning.html
│ ├── main.html
│ ├── menu.html
│ └── help-popup.html
├── images
│ ├── layers.png
│ ├── layers-2x.png
│ ├── add-point.svg
│ ├── delete-point.svg
│ ├── global-settings.svg
│ ├── help.svg
│ ├── redo.svg
│ ├── undo.svg
│ ├── edit-point.svg
│ ├── move-point.svg
│ ├── clear.svg
│ ├── point-info.svg
│ ├── rotate-device.svg
│ ├── zoom-to-fit.svg
│ └── multi-point-line.svg
├── css
│ ├── main.css
│ └── leaflet.css
└── js
│ ├── leaflet-polylinedecorator.js
│ ├── nmea.js
│ └── tests.js
├── requirements.txt
├── .github
└── workflows
│ ├── static-code-analysis.yml
│ ├── functional-tests.yml
│ └── deployment.yml
├── run-tests
├── .gitignore
└── README.md
/src/html/map.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/images/layers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dassencio/nmeagen/HEAD/src/images/layers.png
--------------------------------------------------------------------------------
/src/images/layers-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dassencio/nmeagen/HEAD/src/images/layers-2x.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | csscompressor==0.9.5
2 | html5validator==0.3.3
3 | htmlmin==0.1.12
4 | jsmin==3.0.1
5 |
--------------------------------------------------------------------------------
/src/html/status-popup.html:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/html/small-screen-warning.html:
--------------------------------------------------------------------------------
1 |
2 | Please rotate your device to landscape mode.
3 |
4 |
5 | In the current orientation, your screen's width is too small to use the NMEA
6 | Generator.
7 |
8 |
--------------------------------------------------------------------------------
/.github/workflows/static-code-analysis.yml:
--------------------------------------------------------------------------------
1 | name: Static code analysis
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 | workflow_dispatch:
9 |
10 | jobs:
11 | codeql:
12 | name: Scan code with CodeQL
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout repository
17 | uses: actions/checkout@v2
18 | with:
19 | fetch-depth: 2
20 |
21 | # For PRs, checkout the PR's head instead of the merge commit.
22 | - name: Checkout pull request head
23 | if: github.event_name == 'pull_request'
24 | run: git checkout HEAD^2
25 |
26 | - name: Initialize CodeQL
27 | uses: github/codeql-action/init@v1
28 | with:
29 | languages: python
30 | queries: security-and-quality
31 |
32 | - name: Perform CodeQL analysis
33 | uses: github/codeql-action/analyze@v1
34 |
--------------------------------------------------------------------------------
/.github/workflows/functional-tests.yml:
--------------------------------------------------------------------------------
1 | name: Functional tests
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 |
9 | jobs:
10 | build-and-test:
11 | name: Build and test
12 | runs-on: ${{matrix.os}}
13 | strategy:
14 | matrix:
15 | os: [ubuntu-latest, macos-latest]
16 | python-version: ["3.10", "3.11"]
17 |
18 | steps:
19 | - name: Checkout repository
20 | uses: actions/checkout@v2
21 |
22 | - name: Set up Python ${{matrix.python-version}}
23 | uses: actions/setup-python@v2
24 | with:
25 | python-version: ${{matrix.python-version}}
26 |
27 | - name: Install dependencies
28 | run: |
29 | python -m pip install --upgrade pip
30 | pip install -r requirements.txt
31 | npm install puppeteer
32 |
33 | - name: Build NMEA Generator
34 | run: ./build -o test.html
35 |
36 | - name: Run functional tests
37 | run: ./run-tests test.html
38 |
--------------------------------------------------------------------------------
/.github/workflows/deployment.yml:
--------------------------------------------------------------------------------
1 | name: Deployment
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | workflow_dispatch:
7 |
8 | jobs:
9 | deploy-to-github-pages:
10 | name: Deploy to GitHub Pages
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout repository
15 | uses: actions/checkout@v2
16 |
17 | - name: Set up Python 3.8
18 | uses: actions/setup-python@v2
19 | with:
20 | python-version: 3.8
21 |
22 | - name: Install dependencies
23 | run: |
24 | python -m pip install --upgrade pip
25 | pip install -r requirements.txt
26 |
27 | - name: Build NMEA Generator
28 | run: |
29 | mkdir app
30 | ./build -o app/index.html
31 |
32 | - name: Deploy NMEA Generator to GitHub Pages
33 | env:
34 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
35 | uses: crazy-max/ghaction-github-pages@v2
36 | with:
37 | build_dir: app
38 | commit_message: Deploy build from ${{github.sha}}
39 | fqdn: nmeagen.org
40 | target_branch: gh-pages
41 |
--------------------------------------------------------------------------------
/run-tests:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const path = require('path');
4 | const puppeteer = require('puppeteer');
5 | const { setTimeout } = require('node:timers/promises');
6 |
7 | (async () => {
8 | const browser = await puppeteer.launch();
9 | const page = await browser.newPage();
10 | const fileName = process.argv[2] || 'index.html';
11 |
12 | // Open NMEA Generator inside a headless browser.
13 | try {
14 | await page.goto(`file:${path.join(__dirname, fileName)}`);
15 | await setTimeout(1000);
16 | } catch (error) {
17 | console.error(`Could not open file: '${fileName}'.`);
18 | process.exit(1);
19 | }
20 |
21 | // Use console messages to detect test failure/success.
22 | page.on('console', (message) => {
23 | const testResult = message.text();
24 | if (testResult.startsWith('[FAIL]')) {
25 | console.error(testResult);
26 | process.exit(1);
27 | }
28 | console.log(testResult);
29 | });
30 |
31 | // Tests are executed by invoking runTests() inside the browser.
32 | await page.evaluate('runTests()');
33 | await browser.close();
34 | })();
35 |
--------------------------------------------------------------------------------
/src/html/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | NMEA Generator
5 |
6 |
7 |
11 |
15 |
16 |
17 |
21 |
22 |
23 | __NMEAGEN__MENU_HTML__
24 | __NMEAGEN__MAP_HTML__
25 | __NMEAGEN__STATUS_POPUP_HTML__
26 | __NMEAGEN__HELP_POPUP_HTML__
27 | __NMEAGEN__SMALL_SCREEN_WARNING_HTML__
28 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
106 | # Visual Studio Code project settings
107 | .eslintrc.json
108 | .vscode
109 | jsconfig.json
110 |
111 | # Output HTML file
112 | index.html
113 |
114 | # Node.js packages
115 | node_modules/
116 | package-lock.json
117 |
--------------------------------------------------------------------------------
/src/images/add-point.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
39 |
41 |
42 |
44 | image/svg+xml
45 |
47 |
48 |
49 |
50 |
51 |
56 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/images/delete-point.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
39 |
41 |
42 |
44 | image/svg+xml
45 |
47 |
48 |
49 |
50 |
51 |
56 |
62 |
66 |
71 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 | 
4 |
5 | # Description
6 |
7 | NMEA Generator is a drawing tool for generating GPS logs in NMEA format. Being
8 | a web application consisting of a single HTML file, it can be opened in a web
9 | browser and used directly without the need for any installation or configuration
10 | work.
11 |
12 | This project contains the script (written in Python 3) used to build the
13 | NMEA Generator. An example of a publicly available instance of this tool can be
14 | found on [nmeagen.org](https://nmeagen.org).
15 |
16 | # License
17 |
18 | All files from this project are licensed under the GPLv3 except for those
19 | belonging to the following projects:
20 |
21 | - [jQuery](https://jquery.com/): licensed under the
22 | [MIT license](https://github.com/jquery/jquery/blob/master/LICENSE.txt).
23 | - [Leaflet](https://leafletjs.com/): licensed under the
24 | [2-clause BSD license](https://github.com/Leaflet/Leaflet/blob/master/LICENSE).
25 | - [Leaflet.PolylineDecorator](https://github.com/bbecquet/Leaflet.PolylineDecorator):
26 | licensed under the [MIT license](https://github.com/bbecquet/Leaflet.PolylineDecorator/blob/master/LICENSE).
27 | - [Leaflet Search](https://github.com/stefanocudini/leaflet-search): licensed
28 | under the [MIT license](https://github.com/stefanocudini/leaflet-search/blob/master/license.txt).
29 | - [nmea-0183](https://github.com/nherment/node-nmea): licensed under the
30 | [MIT license](https://github.com/nherment/node-nmea/blob/master/LICENSE).
31 |
32 | See the [`LICENSE`](https://github.com/dassencio/nmeagen/tree/master/LICENSE)
33 | file for more information.
34 |
35 | # Required modules
36 |
37 | All Python modules needed to build the NMEA Generator are listed in the
38 | [`requirements.txt`](https://github.com/dassencio/nmeagen/tree/master/requirements.txt)
39 | file. You can install them with the following command:
40 |
41 | pip3 install -r requirements.txt
42 |
43 | # Usage instructions
44 |
45 | To build the NMEA Generator, simply run the following command:
46 |
47 | ./build
48 |
49 | This will compile the NMEA Generator application as an HTML file named
50 | `index.html` on the current working directory. If you wish to specify another
51 | name or location for the output HTML file, use the `-o` option:
52 |
53 | ./build -o /path/to/nmeagen.html
54 |
55 | # Running the functional tests
56 |
57 | The NMEA Generator contains a set of functional tests which can be executed in
58 | a web browser or in a terminal.
59 |
60 | To execute the tests in a web browser, load the NMEA Generator and then
61 | invoke the `runTests()` function in the browser's console.
62 |
63 | To execute the tests in a terminal, run:
64 |
65 | npm install puppeteer
66 | ./run-tests /path/to/nmeagen.html
67 |
68 | # Contributors & contact information
69 |
70 | Diego Assencio / diego@assencio.com
71 |
--------------------------------------------------------------------------------
/src/images/global-settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
15 |
17 |
18 |
20 | image/svg+xml
21 |
23 |
24 |
25 |
26 |
27 |
29 |
49 |
54 |
55 |
--------------------------------------------------------------------------------
/src/images/help.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
44 |
46 |
47 |
49 | image/svg+xml
50 |
52 |
53 |
54 |
55 |
56 |
61 |
67 |
72 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/src/images/redo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
29 |
35 |
36 |
44 |
50 |
51 |
52 |
74 |
76 |
77 |
79 | image/svg+xml
80 |
82 |
83 |
84 |
85 |
86 |
91 |
103 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/src/images/undo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
29 |
35 |
36 |
44 |
50 |
51 |
52 |
74 |
76 |
77 |
79 | image/svg+xml
80 |
82 |
83 |
84 |
85 |
86 |
91 |
104 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/src/images/edit-point.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
29 |
34 |
35 |
43 |
49 |
50 |
51 |
69 |
71 |
72 |
74 | image/svg+xml
75 |
77 |
78 |
79 |
80 |
81 |
86 |
92 |
98 |
104 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/src/images/move-point.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
29 |
34 |
35 |
43 |
49 |
50 |
51 |
69 |
71 |
72 |
74 | image/svg+xml
75 |
77 |
78 |
79 |
80 |
81 |
86 |
92 |
98 |
104 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/src/images/clear.svg:
--------------------------------------------------------------------------------
1 |
2 | image/svg+xml
--------------------------------------------------------------------------------
/src/images/point-info.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
29 |
34 |
35 |
43 |
49 |
50 |
51 |
69 |
71 |
72 |
74 | image/svg+xml
75 |
77 |
78 |
79 |
80 |
81 |
86 |
92 |
97 |
105 |
110 |
115 |
120 |
125 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/src/html/menu.html:
--------------------------------------------------------------------------------
1 |
196 |
--------------------------------------------------------------------------------
/src/html/help-popup.html:
--------------------------------------------------------------------------------
1 |
158 |
--------------------------------------------------------------------------------
/src/images/rotate-device.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
33 |
41 |
47 |
48 |
49 |
71 |
73 |
74 |
76 | image/svg+xml
77 |
79 |
80 |
81 |
82 |
83 |
88 |
94 |
107 |
115 |
123 |
132 |
140 |
149 |
158 |
167 |
174 |
175 |
176 |
--------------------------------------------------------------------------------
/src/images/zoom-to-fit.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
29 |
35 |
36 |
44 |
50 |
51 |
59 |
65 |
66 |
74 |
80 |
81 |
89 |
95 |
96 |
97 |
120 |
122 |
123 |
125 | image/svg+xml
126 |
128 |
129 |
130 |
131 |
132 |
137 |
143 |
149 |
155 |
161 |
167 |
173 |
179 |
180 |
181 |
--------------------------------------------------------------------------------
/src/images/multi-point-line.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
29 |
34 |
35 |
43 |
48 |
49 |
57 |
63 |
64 |
72 |
78 |
79 |
87 |
93 |
94 |
95 |
113 |
115 |
116 |
118 | image/svg+xml
119 |
121 |
122 |
123 |
124 |
125 |
130 |
136 |
142 |
148 |
154 |
160 |
166 |
172 |
178 |
179 |
180 |
--------------------------------------------------------------------------------
/src/css/main.css:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | *
3 | * GLOBAL VARIABLES
4 | *
5 | ******************************************************************************/
6 |
7 | :root {
8 | --component-active-border-color: rgb(80, 80, 80);
9 | --component-default-border-color: rgb(170, 170, 170);
10 | --component-default-text-color: rgb(45, 45, 45);
11 | --component-disabled-border-color: rgb(200, 200, 200);
12 | --component-disabled-text-color: rgb(125, 125, 125);
13 | --component-focus-hover-border-color: rgb(140, 140, 140);
14 | --gray-component-active-background-color: rgb(220, 220, 220);
15 | --gray-component-default-background-color: rgb(245, 245, 245);
16 | --gray-component-disabled-background-color: rgb(250, 250, 250);
17 | --gray-component-focus-hover-background-color: rgb(235, 235, 235);
18 | --search-box-height: 42px;
19 | --search-box-max-width: calc(100vw - 355px);
20 | --search-box-width: 400px;
21 | --section-border-color: rgb(190, 190, 190);
22 | --white-component-active-background-color: rgb(235, 235, 235);
23 | --white-component-disabled-background-color: rgb(240, 240, 240);
24 | --white-component-focus-hover-background-color: rgb(245, 245, 245);
25 | --white-component-selected-background-color: palegreen;
26 | }
27 |
28 | /*******************************************************************************
29 | *
30 | * GLOBAL STYLES
31 | *
32 | ******************************************************************************/
33 |
34 | * {
35 | box-sizing: border-box;
36 | color: var(--component-default-text-color);
37 | font-family: Arial, sans-serif;
38 | font-size: 14px;
39 | font-weight: normal;
40 | margin: 0;
41 | outline: none;
42 | padding: 0;
43 | text-align: left;
44 | }
45 | html,
46 | body {
47 | background-color: black;
48 | height: 100%;
49 | width: 100%;
50 | }
51 | a {
52 | color: rgb(0, 70, 241);
53 | text-decoration: none;
54 | }
55 | a:focus,
56 | a:hover {
57 | color: dodgerblue;
58 | text-decoration: underline;
59 | }
60 | button {
61 | align-items: center;
62 | background-color: var(--gray-component-default-background-color);
63 | border: 1px solid var(--component-default-border-color);
64 | cursor: pointer;
65 | display: flex;
66 | flex-direction: row;
67 | justify-content: center;
68 | user-select: none;
69 | }
70 | button[disabled] {
71 | background-color: var(--gray-component-disabled-background-color);
72 | border: 1px solid var(--component-disabled-border-color);
73 | cursor: default;
74 | }
75 | button:not([disabled]):focus,
76 | button:not([disabled]):hover {
77 | background-color: var(--gray-component-focus-hover-background-color);
78 | border: 1px solid var(--component-focus-hover-border-color);
79 | }
80 | button:not([disabled]):active {
81 | background-color: var(--gray-component-active-background-color);
82 | border: 1px solid var(--component-active-border-color);
83 | }
84 | button::-moz-focus-inner {
85 | border: 0;
86 | }
87 | button[disabled] * {
88 | opacity: 0.3;
89 | }
90 | input[type="text"] {
91 | background-color: white;
92 | border-radius: 0;
93 | border: 1px solid var(--component-default-border-color);
94 | }
95 | input[type="text"][disabled] {
96 | background-color: var(--white-component-disabled-background-color);
97 | border: 1px solid var(--component-disabled-border-color);
98 | color: var(--component-disabled-text-color);
99 | }
100 | input[type="text"]:not([disabled]):focus,
101 | input[type="text"]:not([disabled]):hover {
102 | border: 1px solid var(--component-focus-hover-border-color);
103 | }
104 | input[type="text"].invalid-value {
105 | background-color: pink;
106 | border: 1px solid crimson;
107 | }
108 | p {
109 | margin-top: 15px;
110 | text-align: justify;
111 | }
112 | table {
113 | border-collapse: collapse;
114 | width: 100%;
115 | }
116 | td {
117 | vertical-align: middle;
118 | }
119 | [unclickable] {
120 | cursor: default;
121 | pointer-events: none;
122 | user-select: none;
123 | }
124 |
125 | /*******************************************************************************
126 | *
127 | * MENU
128 | *
129 | ******************************************************************************/
130 |
131 | #menu {
132 | background-color: white;
133 | border-right: 1px solid var(--component-default-border-color);
134 | float: left;
135 | height: 100%;
136 | overflow-y: auto;
137 | padding: 0 10px;
138 | width: 291px;
139 | }
140 | #menu > div {
141 | padding: 18px 10px;
142 | }
143 | #menu > div:not(:last-child) {
144 | border-bottom: 1px solid var(--section-border-color);
145 | }
146 | @media only screen and (max-width: 800px) {
147 | #menu {
148 | padding: 0 5px;
149 | width: 257px;
150 | }
151 | #menu > div {
152 | padding: 10px 5px;
153 | }
154 | }
155 | #menu input[type="text"] {
156 | padding: 4px;
157 | width: 100%;
158 | }
159 | #menu th {
160 | padding-right: 10px;
161 | white-space: nowrap;
162 | }
163 | #menu tr:not(:first-child) > td,
164 | #menu tr:not(:first-child) > th {
165 | padding-top: 12px;
166 | }
167 | #current-position td {
168 | text-align: right;
169 | }
170 | #tools button {
171 | background-color: white;
172 | border-radius: 3px;
173 | border: none;
174 | justify-content: flex-start;
175 | width: 100%;
176 | }
177 | #tools button:focus,
178 | #tools button:hover {
179 | background-color: var(--white-component-focus-hover-background-color);
180 | }
181 | #tools button:active {
182 | background: var(--white-component-active-background-color);
183 | }
184 | #tools button.selected {
185 | background: var(--white-component-selected-background-color);
186 | }
187 | #tools button img {
188 | height: 36px;
189 | margin-left: 6px;
190 | width: 36px;
191 | }
192 | #tools button span {
193 | line-height: 47px;
194 | padding-left: 15px;
195 | }
196 | #edit-point-box,
197 | #multi-point-line-box,
198 | #global-settings-box {
199 | display: none;
200 | }
201 | #extra-tools {
202 | display: flex;
203 | flex-direction: row;
204 | justify-content: space-between;
205 | }
206 | #extra-tools button {
207 | height: 36px;
208 | width: 56px;
209 | }
210 | #extra-tools button img {
211 | height: 24px;
212 | width: 24px;
213 | }
214 | #file-tools button {
215 | height: 35px;
216 | text-align: center;
217 | width: 100%;
218 | }
219 | #file-tools button:not(:first-child) {
220 | margin-top: 10px;
221 | }
222 | #file-tools input[type="file"] {
223 | display: none;
224 | }
225 |
226 | /*******************************************************************************
227 | *
228 | * HELP AND STATUS POPUPS
229 | *
230 | ******************************************************************************/
231 |
232 | #help-popup,
233 | #status-popup {
234 | background-color: white;
235 | border-radius: 3px;
236 | display: none;
237 | left: 50%;
238 | max-height: calc(100vh - 20px);
239 | max-width: 640px;
240 | overflow-y: auto;
241 | padding: 25px 25px 0 25px;
242 | position: fixed;
243 | top: 50%;
244 | transform: translate(-50%, -50%);
245 | width: calc(100vw - 120px);
246 | }
247 | /* Firefox ignores bottom padding (https://stackoverflow.com/q/29986977). */
248 | #help-popup > :last-child,
249 | #status-popup > :last-child {
250 | padding-bottom: 25px;
251 | }
252 | .popup-footnote {
253 | font-size: 12px;
254 | margin-top: 20px;
255 | opacity: 0.6;
256 | text-align: center;
257 | }
258 | .popup-title {
259 | border-bottom: 1px solid var(--section-border-color);
260 | font-size: 20px;
261 | padding-bottom: 5px;
262 | }
263 |
264 | /*******************************************************************************
265 | *
266 | * MAP
267 | *
268 | ******************************************************************************/
269 |
270 | #map {
271 | background-color: gainsboro;
272 | height: 100%;
273 | }
274 | .leaflet-popup-content-wrapper,
275 | .leaflet-popup-tip {
276 | border: 1px solid var(--component-default-border-color);
277 | box-shadow: none;
278 | }
279 | .leaflet-popup-content {
280 | margin: 15px;
281 | }
282 | .leaflet-popup-close-button {
283 | display: none;
284 | }
285 | #close-point-info-popup {
286 | color: var(--component-default-border-color);
287 | cursor: pointer;
288 | font-family: monospace;
289 | font-size: 20px;
290 | line-height: 20px;
291 | position: absolute;
292 | right: 5px;
293 | top: 2px;
294 | user-select: none;
295 | }
296 | #close-point-info-popup:hover,
297 | #close-point-info-popup:focus {
298 | color: var(--component-focus-hover-border-color);
299 | }
300 | #close-point-info-popup:active {
301 | color: var(--component-active-border-color);
302 | }
303 | table.point-info-popup td,
304 | table.point-info-popup th {
305 | font-size: 12px;
306 | }
307 | table.point-info-popup td {
308 | padding-left: 15px;
309 | }
310 | table.point-info-popup tr:not(:first-child) > td,
311 | table.point-info-popup tr:not(:first-child) > th {
312 | padding-top: 10px;
313 | }
314 | .leaflet-popup-tip-container {
315 | /* Make popup tip have a correct border. */
316 | bottom: -19px !important;
317 | }
318 | .leaflet-control-attribution a {
319 | font-size: 11px;
320 | }
321 |
322 | /*******************************************************************************
323 | *
324 | * SEARCH BOX
325 | *
326 | ******************************************************************************/
327 |
328 | .leaflet-control-search {
329 | margin: 15px 0 0 15px !important;
330 | max-width: var(--search-box-max-width);
331 | width: var(--search-box-width);
332 | }
333 | @media only screen and (max-width: 800px) {
334 | .leaflet-control-search {
335 | margin: 10px 0 0 10px !important;
336 | }
337 | }
338 | .leaflet-control-search .search-input {
339 | border-radius: 0;
340 | border: 1px solid var(--component-default-border-color);
341 | height: var(--search-box-height);
342 | margin: 0;
343 | max-width: var(--search-box-max-width) !important;
344 | padding: 10px calc(var(--search-box-height) + 2px) 10px 10px;
345 | width: var(--search-box-width) !important;
346 | }
347 | .leaflet-control-search .search-cancel {
348 | background: url(__NMEAGEN__CLEAR_SVG__) no-repeat;
349 | height: calc(var(--search-box-height) - 2px);
350 | margin: 0;
351 | position: absolute;
352 | right: 1px;
353 | top: 1px;
354 | visibility: visible;
355 | width: calc(var(--search-box-height) - 2px);
356 | }
357 | .leaflet-control-search .search-button,
358 | .leaflet-control-search .search-cancel > span {
359 | display: none;
360 | }
361 | .leaflet-control-search .search-cancel:focus,
362 | .leaflet-control-search .search-cancel:hover {
363 | background-color: var(--white-component-focus-hover-background-color);
364 | }
365 | .leaflet-control-search .search-tooltip {
366 | max-height: calc(100vh - 90px);
367 | width: 100%;
368 | }
369 | .leaflet-control-search .search-tip {
370 | background-color: white;
371 | border-radius: 0;
372 | border: 1px solid var(--section-border-color);
373 | border-top-width: 0;
374 | color: var(--component-default-text-color);
375 | line-height: calc(var(--search-box-height) - 4px);
376 | margin: 0;
377 | overflow: hidden;
378 | padding: 0 10px;
379 | text-overflow: ellipsis;
380 | white-space: nowrap;
381 | }
382 | .leaflet-control-search .search-tip:focus,
383 | .leaflet-control-search .search-tip:hover,
384 | .leaflet-control-search .search-tip-select {
385 | background-color: var(--white-component-focus-hover-background-color);
386 | }
387 | .leaflet-control-search .search-tip:hover {
388 | cursor: pointer;
389 | }
390 | .leaflet-control-search .search-tip:active {
391 | background-color: var(--white-component-active-background-color);
392 | }
393 |
394 | /*******************************************************************************
395 | *
396 | * ZOOM AND LAYER CONTROLS
397 | *
398 | ******************************************************************************/
399 |
400 | .leaflet-control-zoom,
401 | .leaflet-control-layers {
402 | border-radius: 0 !important;
403 | border: none !important;
404 | margin: 15px 15px 0 0 !important;
405 | }
406 | @media only screen and (max-width: 800px) {
407 | .leaflet-control-zoom,
408 | .leaflet-control-layers {
409 | margin: 10px 10px 0 0 !important;
410 | }
411 | }
412 | .leaflet-control-zoom-in,
413 | .leaflet-control-zoom-out {
414 | border-radius: 0 !important;
415 | border: 1px solid var(--component-default-border-color) !important;
416 | box-sizing: content-box;
417 | color: var(--component-default-text-color) !important;
418 | font-weight: normal;
419 | text-decoration: none !important;
420 | }
421 | /* By default, the "+" icon has no bottom border (only the "-" icon has it). */
422 | .leaflet-control-zoom-in {
423 | border-bottom: none !important;
424 | }
425 | /* When the "+" icon is focused/hovered over, its bottom border is drawn. */
426 | .leaflet-control-zoom-in:not(.leaflet-disabled):focus,
427 | .leaflet-control-zoom-in:not(.leaflet-disabled):hover,
428 | .leaflet-control-zoom-out:not(.leaflet-disabled):focus,
429 | .leaflet-control-zoom-out:not(.leaflet-disabled):hover {
430 | background-color: var(
431 | --white-component-focus-hover-background-color
432 | ) !important;
433 | border: 1px solid var(--component-focus-hover-border-color) !important;
434 | }
435 | /*
436 | * The :focus pseudoselector is necessary here to prevent the "+" icon from
437 | * temporarily losing its bottom border during the transition to the maximum
438 | * possible zoom level.
439 | */
440 | .leaflet-control-zoom-in.leaflet-disabled:focus,
441 | .leaflet-control-zoom-in.leaflet-disabled:hover,
442 | .leaflet-control-zoom-out.leaflet-disabled:focus,
443 | .leaflet-control-zoom-out.leaflet-disabled:hover {
444 | border: 1px solid var(--component-default-border-color) !important;
445 | }
446 | /* When the "+" icon is active, its bottom border is drawn. */
447 | .leaflet-control-zoom-in:not(.leaflet-disabled):active,
448 | .leaflet-control-zoom-out:not(.leaflet-disabled):active {
449 | background-color: var(--white-component-active-background-color) !important;
450 | border: 1px solid var(--component-active-border-color) !important;
451 | }
452 | /* Whenever the "+" icon has a bottom border, the "-" icon has no top border. */
453 | .leaflet-control-zoom-in:active + .leaflet-control-zoom-out,
454 | .leaflet-control-zoom-in:focus + .leaflet-control-zoom-out,
455 | .leaflet-control-zoom-in:hover + .leaflet-control-zoom-out {
456 | border-top: none !important;
457 | }
458 | .leaflet-disabled {
459 | background-color: var(--white-component-disabled-background-color) !important;
460 | color: var(--component-disabled-text-color) !important;
461 | user-select: none;
462 | }
463 | .leaflet-control-layers-toggle,
464 | .leaflet-control-layers-expanded {
465 | border-radius: 0 !important;
466 | border: 1px solid var(--component-default-border-color) !important;
467 | box-sizing: content-box;
468 | padding: 0;
469 | }
470 | .leaflet-control-layers-toggle:focus,
471 | .leaflet-control-layers-toggle:hover,
472 | .leaflet-control-layers-expanded:focus,
473 | .leaflet-control-layers-expanded:hover {
474 | border: 1px solid var(--component-focus-hover-border-color) !important;
475 | }
476 | .leaflet-control-layers-toggle:active,
477 | .leaflet-control-layers-expanded:active {
478 | border: 1px solid var(--component-active-border-color) !important;
479 | }
480 | .leaflet-control-layers-toggle {
481 | background-image: url(__NMEAGEN__LAYERS_PNG__);
482 | }
483 | .leaflet-retina .leaflet-control-layers-toggle {
484 | background-image: url(__NMEAGEN__LAYERS_2X_PNG__);
485 | }
486 | .leaflet-control-layers-list label {
487 | display: block;
488 | }
489 | .leaflet-control-layers-list label:focus,
490 | .leaflet-control-layers-list label:hover {
491 | background-color: var(--white-component-focus-hover-background-color);
492 | }
493 | .leaflet-control-layers-list label:active {
494 | background-color: var(--white-component-active-background-color);
495 | }
496 | .leaflet-control-layers-list input[type="radio"] {
497 | display: none;
498 | }
499 | .leaflet-control-layers-list input[type="radio"]:checked + span {
500 | background-color: var(--white-component-selected-background-color);
501 | }
502 | .leaflet-control-layers-list span {
503 | cursor: pointer;
504 | display: block;
505 | line-height: 24px;
506 | padding: 4px 8px;
507 | user-select: none;
508 | width: 100%;
509 | }
510 |
511 | /*******************************************************************************
512 | *
513 | * SMALL SCREEN WARNING
514 | *
515 | ******************************************************************************/
516 |
517 | #small-screen-warning {
518 | display: none;
519 | }
520 | @media only screen and (max-width: 500px) {
521 | #small-screen-warning {
522 | align-items: center;
523 | background-color: white;
524 | display: flex;
525 | flex-direction: column;
526 | font-size: 20px;
527 | height: 100vh;
528 | justify-content: center;
529 | left: 0;
530 | padding: 0 20px;
531 | position: fixed;
532 | text-align: center;
533 | top: 0;
534 | width: 100vw;
535 | z-index: 9999;
536 | }
537 | }
538 |
--------------------------------------------------------------------------------
/src/css/leaflet.css:
--------------------------------------------------------------------------------
1 | /* required styles */
2 |
3 | .leaflet-pane,
4 | .leaflet-tile,
5 | .leaflet-marker-icon,
6 | .leaflet-marker-shadow,
7 | .leaflet-tile-container,
8 | .leaflet-pane > svg,
9 | .leaflet-pane > canvas,
10 | .leaflet-zoom-box,
11 | .leaflet-image-layer,
12 | .leaflet-layer {
13 | position: absolute;
14 | left: 0;
15 | top: 0;
16 | }
17 | .leaflet-container {
18 | overflow: hidden;
19 | }
20 | .leaflet-tile,
21 | .leaflet-marker-icon,
22 | .leaflet-marker-shadow {
23 | -webkit-user-select: none;
24 | -moz-user-select: none;
25 | user-select: none;
26 | -webkit-user-drag: none;
27 | }
28 | /* Prevents IE11 from highlighting tiles in blue */
29 | .leaflet-tile::selection {
30 | background: transparent;
31 | }
32 | /* Safari renders non-retina tile on retina better with this, but Chrome is worse */
33 | .leaflet-safari .leaflet-tile {
34 | image-rendering: -webkit-optimize-contrast;
35 | }
36 | /* hack that prevents hw layers "stretching" when loading new tiles */
37 | .leaflet-safari .leaflet-tile-container {
38 | width: 1600px;
39 | height: 1600px;
40 | -webkit-transform-origin: 0 0;
41 | }
42 | .leaflet-marker-icon,
43 | .leaflet-marker-shadow {
44 | display: block;
45 | }
46 | /* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
47 | /* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
48 | .leaflet-container .leaflet-overlay-pane svg,
49 | .leaflet-container .leaflet-marker-pane img,
50 | .leaflet-container .leaflet-shadow-pane img,
51 | .leaflet-container .leaflet-tile-pane img,
52 | .leaflet-container img.leaflet-image-layer,
53 | .leaflet-container .leaflet-tile {
54 | max-width: none !important;
55 | max-height: none !important;
56 | }
57 |
58 | .leaflet-container.leaflet-touch-zoom {
59 | -ms-touch-action: pan-x pan-y;
60 | touch-action: pan-x pan-y;
61 | }
62 | .leaflet-container.leaflet-touch-drag {
63 | -ms-touch-action: pinch-zoom;
64 | /* Fallback for FF which doesn't support pinch-zoom */
65 | touch-action: none;
66 | touch-action: pinch-zoom;
67 | }
68 | .leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
69 | -ms-touch-action: none;
70 | touch-action: none;
71 | }
72 | .leaflet-container {
73 | -webkit-tap-highlight-color: transparent;
74 | }
75 | .leaflet-container a {
76 | -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
77 | }
78 | .leaflet-tile {
79 | filter: inherit;
80 | visibility: hidden;
81 | }
82 | .leaflet-tile-loaded {
83 | visibility: inherit;
84 | }
85 | .leaflet-zoom-box {
86 | width: 0;
87 | height: 0;
88 | -moz-box-sizing: border-box;
89 | box-sizing: border-box;
90 | z-index: 800;
91 | }
92 | /* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
93 | .leaflet-overlay-pane svg {
94 | -moz-user-select: none;
95 | }
96 |
97 | .leaflet-pane { z-index: 400; }
98 |
99 | .leaflet-tile-pane { z-index: 200; }
100 | .leaflet-overlay-pane { z-index: 400; }
101 | .leaflet-shadow-pane { z-index: 500; }
102 | .leaflet-marker-pane { z-index: 600; }
103 | .leaflet-tooltip-pane { z-index: 650; }
104 | .leaflet-popup-pane { z-index: 700; }
105 |
106 | .leaflet-map-pane canvas { z-index: 100; }
107 | .leaflet-map-pane svg { z-index: 200; }
108 |
109 | .leaflet-vml-shape {
110 | width: 1px;
111 | height: 1px;
112 | }
113 | .lvml {
114 | behavior: url(#default#VML);
115 | display: inline-block;
116 | position: absolute;
117 | }
118 |
119 |
120 | /* control positioning */
121 |
122 | .leaflet-control {
123 | position: relative;
124 | z-index: 800;
125 | pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
126 | pointer-events: auto;
127 | }
128 | .leaflet-top,
129 | .leaflet-bottom {
130 | position: absolute;
131 | z-index: 1000;
132 | pointer-events: none;
133 | }
134 | .leaflet-top {
135 | top: 0;
136 | }
137 | .leaflet-right {
138 | right: 0;
139 | }
140 | .leaflet-bottom {
141 | bottom: 0;
142 | }
143 | .leaflet-left {
144 | left: 0;
145 | }
146 | .leaflet-control {
147 | float: left;
148 | clear: both;
149 | }
150 | .leaflet-right .leaflet-control {
151 | float: right;
152 | }
153 | .leaflet-top .leaflet-control {
154 | margin-top: 10px;
155 | }
156 | .leaflet-bottom .leaflet-control {
157 | margin-bottom: 10px;
158 | }
159 | .leaflet-left .leaflet-control {
160 | margin-left: 10px;
161 | }
162 | .leaflet-right .leaflet-control {
163 | margin-right: 10px;
164 | }
165 |
166 |
167 | /* zoom and fade animations */
168 |
169 | .leaflet-fade-anim .leaflet-tile {
170 | will-change: opacity;
171 | }
172 | .leaflet-fade-anim .leaflet-popup {
173 | opacity: 0;
174 | -webkit-transition: opacity 0.2s linear;
175 | -moz-transition: opacity 0.2s linear;
176 | transition: opacity 0.2s linear;
177 | }
178 | .leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
179 | opacity: 1;
180 | }
181 | .leaflet-zoom-animated {
182 | -webkit-transform-origin: 0 0;
183 | -ms-transform-origin: 0 0;
184 | transform-origin: 0 0;
185 | }
186 | .leaflet-zoom-anim .leaflet-zoom-animated {
187 | will-change: transform;
188 | }
189 | .leaflet-zoom-anim .leaflet-zoom-animated {
190 | -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
191 | -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
192 | transition: transform 0.25s cubic-bezier(0,0,0.25,1);
193 | }
194 | .leaflet-zoom-anim .leaflet-tile,
195 | .leaflet-pan-anim .leaflet-tile {
196 | -webkit-transition: none;
197 | -moz-transition: none;
198 | transition: none;
199 | }
200 |
201 | .leaflet-zoom-anim .leaflet-zoom-hide {
202 | visibility: hidden;
203 | }
204 |
205 |
206 | /* cursors */
207 |
208 | .leaflet-interactive {
209 | cursor: pointer;
210 | }
211 | .leaflet-grab {
212 | cursor: -webkit-grab;
213 | cursor: -moz-grab;
214 | cursor: grab;
215 | }
216 | .leaflet-crosshair,
217 | .leaflet-crosshair .leaflet-interactive {
218 | cursor: crosshair;
219 | }
220 | .leaflet-popup-pane,
221 | .leaflet-control {
222 | cursor: auto;
223 | }
224 | .leaflet-dragging .leaflet-grab,
225 | .leaflet-dragging .leaflet-grab .leaflet-interactive,
226 | .leaflet-dragging .leaflet-marker-draggable {
227 | cursor: move;
228 | cursor: -webkit-grabbing;
229 | cursor: -moz-grabbing;
230 | cursor: grabbing;
231 | }
232 |
233 | /* marker & overlays interactivity */
234 | .leaflet-marker-icon,
235 | .leaflet-marker-shadow,
236 | .leaflet-image-layer,
237 | .leaflet-pane > svg path,
238 | .leaflet-tile-container {
239 | pointer-events: none;
240 | }
241 |
242 | .leaflet-marker-icon.leaflet-interactive,
243 | .leaflet-image-layer.leaflet-interactive,
244 | .leaflet-pane > svg path.leaflet-interactive,
245 | svg.leaflet-image-layer.leaflet-interactive path {
246 | pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
247 | pointer-events: auto;
248 | }
249 |
250 | /* visual tweaks */
251 |
252 | .leaflet-container {
253 | background: #ddd;
254 | outline: 0;
255 | }
256 | .leaflet-container a {
257 | color: #0078A8;
258 | }
259 | .leaflet-container a.leaflet-active {
260 | outline: 2px solid orange;
261 | }
262 | .leaflet-zoom-box {
263 | border: 2px dotted #38f;
264 | background: rgba(255,255,255,0.5);
265 | }
266 |
267 |
268 | /* general typography */
269 | .leaflet-container {
270 | font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
271 | }
272 |
273 |
274 | /* general toolbar styles */
275 |
276 | .leaflet-bar {
277 | box-shadow: 0 1px 5px rgba(0,0,0,0.65);
278 | border-radius: 4px;
279 | }
280 | .leaflet-bar a,
281 | .leaflet-bar a:hover {
282 | background-color: #fff;
283 | border-bottom: 1px solid #ccc;
284 | width: 26px;
285 | height: 26px;
286 | line-height: 26px;
287 | display: block;
288 | text-align: center;
289 | text-decoration: none;
290 | color: black;
291 | }
292 | .leaflet-bar a,
293 | .leaflet-control-layers-toggle {
294 | background-position: 50% 50%;
295 | background-repeat: no-repeat;
296 | display: block;
297 | }
298 | .leaflet-bar a:hover {
299 | background-color: #f4f4f4;
300 | }
301 | .leaflet-bar a:first-child {
302 | border-top-left-radius: 4px;
303 | border-top-right-radius: 4px;
304 | }
305 | .leaflet-bar a:last-child {
306 | border-bottom-left-radius: 4px;
307 | border-bottom-right-radius: 4px;
308 | border-bottom: none;
309 | }
310 | .leaflet-bar a.leaflet-disabled {
311 | cursor: default;
312 | background-color: #f4f4f4;
313 | color: #bbb;
314 | }
315 |
316 | .leaflet-touch .leaflet-bar a {
317 | width: 30px;
318 | height: 30px;
319 | line-height: 30px;
320 | }
321 | .leaflet-touch .leaflet-bar a:first-child {
322 | border-top-left-radius: 2px;
323 | border-top-right-radius: 2px;
324 | }
325 | .leaflet-touch .leaflet-bar a:last-child {
326 | border-bottom-left-radius: 2px;
327 | border-bottom-right-radius: 2px;
328 | }
329 |
330 | /* zoom control */
331 |
332 | .leaflet-control-zoom-in,
333 | .leaflet-control-zoom-out {
334 | font: bold 18px 'Lucida Console', Monaco, monospace;
335 | text-indent: 1px;
336 | }
337 |
338 | .leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
339 | font-size: 22px;
340 | }
341 |
342 |
343 | /* layers control */
344 |
345 | .leaflet-control-layers {
346 | box-shadow: 0 1px 5px rgba(0,0,0,0.4);
347 | background: #fff;
348 | border-radius: 5px;
349 | }
350 | .leaflet-control-layers-toggle {
351 | background-image: url(images/layers.png);
352 | width: 36px;
353 | height: 36px;
354 | }
355 | .leaflet-retina .leaflet-control-layers-toggle {
356 | background-image: url(images/layers-2x.png);
357 | background-size: 26px 26px;
358 | }
359 | .leaflet-touch .leaflet-control-layers-toggle {
360 | width: 44px;
361 | height: 44px;
362 | }
363 | .leaflet-control-layers .leaflet-control-layers-list,
364 | .leaflet-control-layers-expanded .leaflet-control-layers-toggle {
365 | display: none;
366 | }
367 | .leaflet-control-layers-expanded .leaflet-control-layers-list {
368 | display: block;
369 | position: relative;
370 | }
371 | .leaflet-control-layers-expanded {
372 | padding: 6px 10px 6px 6px;
373 | color: #333;
374 | background: #fff;
375 | }
376 | .leaflet-control-layers-scrollbar {
377 | overflow-y: scroll;
378 | overflow-x: hidden;
379 | padding-right: 5px;
380 | }
381 | .leaflet-control-layers-selector {
382 | margin-top: 2px;
383 | position: relative;
384 | top: 1px;
385 | }
386 | .leaflet-control-layers label {
387 | display: block;
388 | }
389 | .leaflet-control-layers-separator {
390 | height: 0;
391 | border-top: 1px solid #ddd;
392 | margin: 5px -10px 5px -6px;
393 | }
394 |
395 | /* Default icon URLs */
396 | .leaflet-default-icon-path {
397 | background-image: url(images/marker-icon.png);
398 | }
399 |
400 |
401 | /* attribution and scale controls */
402 |
403 | .leaflet-container .leaflet-control-attribution {
404 | background: #fff;
405 | background: rgba(255, 255, 255, 0.7);
406 | margin: 0;
407 | }
408 | .leaflet-control-attribution,
409 | .leaflet-control-scale-line {
410 | padding: 0 5px;
411 | color: #333;
412 | }
413 | .leaflet-control-attribution a {
414 | text-decoration: none;
415 | }
416 | .leaflet-control-attribution a:hover {
417 | text-decoration: underline;
418 | }
419 | .leaflet-container .leaflet-control-attribution,
420 | .leaflet-container .leaflet-control-scale {
421 | font-size: 11px;
422 | }
423 | .leaflet-left .leaflet-control-scale {
424 | margin-left: 5px;
425 | }
426 | .leaflet-bottom .leaflet-control-scale {
427 | margin-bottom: 5px;
428 | }
429 | .leaflet-control-scale-line {
430 | border: 2px solid #777;
431 | border-top: none;
432 | line-height: 1.1;
433 | padding: 2px 5px 1px;
434 | font-size: 11px;
435 | white-space: nowrap;
436 | overflow: hidden;
437 | -moz-box-sizing: border-box;
438 | box-sizing: border-box;
439 |
440 | background: #fff;
441 | background: rgba(255, 255, 255, 0.5);
442 | }
443 | .leaflet-control-scale-line:not(:first-child) {
444 | border-top: 2px solid #777;
445 | border-bottom: none;
446 | margin-top: -2px;
447 | }
448 | .leaflet-control-scale-line:not(:first-child):not(:last-child) {
449 | border-bottom: 2px solid #777;
450 | }
451 |
452 | .leaflet-touch .leaflet-control-attribution,
453 | .leaflet-touch .leaflet-control-layers,
454 | .leaflet-touch .leaflet-bar {
455 | box-shadow: none;
456 | }
457 | .leaflet-touch .leaflet-control-layers,
458 | .leaflet-touch .leaflet-bar {
459 | border: 2px solid rgba(0,0,0,0.2);
460 | background-clip: padding-box;
461 | }
462 |
463 |
464 | /* popup */
465 |
466 | .leaflet-popup {
467 | position: absolute;
468 | text-align: center;
469 | margin-bottom: 20px;
470 | }
471 | .leaflet-popup-content-wrapper {
472 | padding: 1px;
473 | text-align: left;
474 | border-radius: 12px;
475 | }
476 | .leaflet-popup-content {
477 | margin: 13px 19px;
478 | line-height: 1.4;
479 | }
480 | .leaflet-popup-content p {
481 | margin: 18px 0;
482 | }
483 | .leaflet-popup-tip-container {
484 | width: 40px;
485 | height: 20px;
486 | position: absolute;
487 | left: 50%;
488 | margin-left: -20px;
489 | overflow: hidden;
490 | pointer-events: none;
491 | }
492 | .leaflet-popup-tip {
493 | width: 17px;
494 | height: 17px;
495 | padding: 1px;
496 |
497 | margin: -10px auto 0;
498 |
499 | -webkit-transform: rotate(45deg);
500 | -moz-transform: rotate(45deg);
501 | -ms-transform: rotate(45deg);
502 | transform: rotate(45deg);
503 | }
504 | .leaflet-popup-content-wrapper,
505 | .leaflet-popup-tip {
506 | background: white;
507 | color: #333;
508 | box-shadow: 0 3px 14px rgba(0,0,0,0.4);
509 | }
510 | .leaflet-container a.leaflet-popup-close-button {
511 | position: absolute;
512 | top: 0;
513 | right: 0;
514 | padding: 4px 4px 0 0;
515 | border: none;
516 | text-align: center;
517 | width: 18px;
518 | height: 14px;
519 | font: 16px/14px Tahoma, Verdana, sans-serif;
520 | color: #c3c3c3;
521 | text-decoration: none;
522 | font-weight: bold;
523 | background: transparent;
524 | }
525 | .leaflet-container a.leaflet-popup-close-button:hover {
526 | color: #999;
527 | }
528 | .leaflet-popup-scrolled {
529 | overflow: auto;
530 | border-bottom: 1px solid #ddd;
531 | border-top: 1px solid #ddd;
532 | }
533 |
534 | .leaflet-oldie .leaflet-popup-content-wrapper {
535 | zoom: 1;
536 | }
537 | .leaflet-oldie .leaflet-popup-tip {
538 | width: 24px;
539 | margin: 0 auto;
540 |
541 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
542 | filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
543 | }
544 | .leaflet-oldie .leaflet-popup-tip-container {
545 | margin-top: -1px;
546 | }
547 |
548 | .leaflet-oldie .leaflet-control-zoom,
549 | .leaflet-oldie .leaflet-control-layers,
550 | .leaflet-oldie .leaflet-popup-content-wrapper,
551 | .leaflet-oldie .leaflet-popup-tip {
552 | border: 1px solid #999;
553 | }
554 |
555 |
556 | /* div icon */
557 |
558 | .leaflet-div-icon {
559 | background: #fff;
560 | border: 1px solid #666;
561 | }
562 |
563 |
564 | /* Tooltip */
565 | /* Base styles for the element that has a tooltip */
566 | .leaflet-tooltip {
567 | position: absolute;
568 | padding: 6px;
569 | background-color: #fff;
570 | border: 1px solid #fff;
571 | border-radius: 3px;
572 | color: #222;
573 | white-space: nowrap;
574 | -webkit-user-select: none;
575 | -moz-user-select: none;
576 | -ms-user-select: none;
577 | user-select: none;
578 | pointer-events: none;
579 | box-shadow: 0 1px 3px rgba(0,0,0,0.4);
580 | }
581 | .leaflet-tooltip.leaflet-clickable {
582 | cursor: pointer;
583 | pointer-events: auto;
584 | }
585 | .leaflet-tooltip-top:before,
586 | .leaflet-tooltip-bottom:before,
587 | .leaflet-tooltip-left:before,
588 | .leaflet-tooltip-right:before {
589 | position: absolute;
590 | pointer-events: none;
591 | border: 6px solid transparent;
592 | background: transparent;
593 | content: "";
594 | }
595 |
596 | /* Directions */
597 |
598 | .leaflet-tooltip-bottom {
599 | margin-top: 6px;
600 | }
601 | .leaflet-tooltip-top {
602 | margin-top: -6px;
603 | }
604 | .leaflet-tooltip-bottom:before,
605 | .leaflet-tooltip-top:before {
606 | left: 50%;
607 | margin-left: -6px;
608 | }
609 | .leaflet-tooltip-top:before {
610 | bottom: 0;
611 | margin-bottom: -12px;
612 | border-top-color: #fff;
613 | }
614 | .leaflet-tooltip-bottom:before {
615 | top: 0;
616 | margin-top: -12px;
617 | margin-left: -6px;
618 | border-bottom-color: #fff;
619 | }
620 | .leaflet-tooltip-left {
621 | margin-left: -6px;
622 | }
623 | .leaflet-tooltip-right {
624 | margin-left: 6px;
625 | }
626 | .leaflet-tooltip-left:before,
627 | .leaflet-tooltip-right:before {
628 | top: 50%;
629 | margin-top: -6px;
630 | }
631 | .leaflet-tooltip-left:before {
632 | right: 0;
633 | margin-right: -12px;
634 | border-left-color: #fff;
635 | }
636 | .leaflet-tooltip-right:before {
637 | left: 0;
638 | margin-left: -12px;
639 | border-right-color: #fff;
640 | }
641 |
--------------------------------------------------------------------------------
/src/js/leaflet-polylinedecorator.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('leaflet')) :
3 | typeof define === 'function' && define.amd ? define(['leaflet'], factory) :
4 | (factory(global.L));
5 | }(this, (function (L$1) { 'use strict';
6 |
7 | L$1 = L$1 && L$1.hasOwnProperty('default') ? L$1['default'] : L$1;
8 |
9 | // functional re-impl of L.Point.distanceTo,
10 | // with no dependency on Leaflet for easier testing
11 | function pointDistance(ptA, ptB) {
12 | var x = ptB.x - ptA.x;
13 | var y = ptB.y - ptA.y;
14 | return Math.sqrt(x * x + y * y);
15 | }
16 |
17 | var computeSegmentHeading = function computeSegmentHeading(a, b) {
18 | return (Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI + 90 + 360) % 360;
19 | };
20 |
21 | var asRatioToPathLength = function asRatioToPathLength(_ref, totalPathLength) {
22 | var value = _ref.value,
23 | isInPixels = _ref.isInPixels;
24 | return isInPixels ? value / totalPathLength : value;
25 | };
26 |
27 | function parseRelativeOrAbsoluteValue(value) {
28 | if (typeof value === 'string' && value.indexOf('%') !== -1) {
29 | return {
30 | value: parseFloat(value) / 100,
31 | isInPixels: false
32 | };
33 | }
34 | var parsedValue = value ? parseFloat(value) : 0;
35 | return {
36 | value: parsedValue,
37 | isInPixels: parsedValue > 0
38 | };
39 | }
40 |
41 | var pointsEqual = function pointsEqual(a, b) {
42 | return a.x === b.x && a.y === b.y;
43 | };
44 |
45 | function pointsToSegments(pts) {
46 | return pts.reduce(function (segments, b, idx, points) {
47 | // this test skips same adjacent points
48 | if (idx > 0 && !pointsEqual(b, points[idx - 1])) {
49 | var a = points[idx - 1];
50 | var distA = segments.length > 0 ? segments[segments.length - 1].distB : 0;
51 | var distAB = pointDistance(a, b);
52 | segments.push({
53 | a: a,
54 | b: b,
55 | distA: distA,
56 | distB: distA + distAB,
57 | heading: computeSegmentHeading(a, b)
58 | });
59 | }
60 | return segments;
61 | }, []);
62 | }
63 |
64 | function projectPatternOnPointPath(pts, pattern) {
65 | // 1. split the path into segment infos
66 | var segments = pointsToSegments(pts);
67 | var nbSegments = segments.length;
68 | if (nbSegments === 0) {
69 | return [];
70 | }
71 |
72 | var totalPathLength = segments[nbSegments - 1].distB;
73 |
74 | var offset = asRatioToPathLength(pattern.offset, totalPathLength);
75 | var endOffset = asRatioToPathLength(pattern.endOffset, totalPathLength);
76 | var repeat = asRatioToPathLength(pattern.repeat, totalPathLength);
77 |
78 | var repeatIntervalPixels = totalPathLength * repeat;
79 | var startOffsetPixels = offset > 0 ? totalPathLength * offset : 0;
80 | var endOffsetPixels = endOffset > 0 ? totalPathLength * endOffset : 0;
81 |
82 | // 2. generate the positions of the pattern as offsets from the path start
83 | var positionOffsets = [];
84 | var positionOffset = startOffsetPixels;
85 | do {
86 | positionOffsets.push(positionOffset);
87 | positionOffset += repeatIntervalPixels;
88 | } while (repeatIntervalPixels > 0 && positionOffset < totalPathLength - endOffsetPixels);
89 |
90 | // 3. projects offsets to segments
91 | var segmentIndex = 0;
92 | var segment = segments[0];
93 | return positionOffsets.map(function (positionOffset) {
94 | // find the segment matching the offset,
95 | // starting from the previous one as offsets are ordered
96 | while (positionOffset > segment.distB && segmentIndex < nbSegments - 1) {
97 | segmentIndex++;
98 | segment = segments[segmentIndex];
99 | }
100 |
101 | var segmentRatio = (positionOffset - segment.distA) / (segment.distB - segment.distA);
102 | return {
103 | pt: interpolateBetweenPoints(segment.a, segment.b, segmentRatio),
104 | heading: segment.heading
105 | };
106 | });
107 | }
108 |
109 | /**
110 | * Finds the point which lies on the segment defined by points A and B,
111 | * at the given ratio of the distance from A to B, by linear interpolation.
112 | */
113 | function interpolateBetweenPoints(ptA, ptB, ratio) {
114 | if (ptB.x !== ptA.x) {
115 | return {
116 | x: ptA.x + ratio * (ptB.x - ptA.x),
117 | y: ptA.y + ratio * (ptB.y - ptA.y)
118 | };
119 | }
120 | // special case where points lie on the same vertical axis
121 | return {
122 | x: ptA.x,
123 | y: ptA.y + (ptB.y - ptA.y) * ratio
124 | };
125 | }
126 |
127 | (function() {
128 | // save these original methods before they are overwritten
129 | var proto_initIcon = L.Marker.prototype._initIcon;
130 | var proto_setPos = L.Marker.prototype._setPos;
131 |
132 | var oldIE = (L.DomUtil.TRANSFORM === 'msTransform');
133 |
134 | L.Marker.addInitHook(function () {
135 | var iconOptions = this.options.icon && this.options.icon.options;
136 | var iconAnchor = iconOptions && this.options.icon.options.iconAnchor;
137 | if (iconAnchor) {
138 | iconAnchor = (iconAnchor[0] + 'px ' + iconAnchor[1] + 'px');
139 | }
140 | this.options.rotationOrigin = this.options.rotationOrigin || iconAnchor || 'center bottom' ;
141 | this.options.rotationAngle = this.options.rotationAngle || 0;
142 |
143 | // Ensure marker keeps rotated during dragging
144 | this.on('drag', function(e) { e.target._applyRotation(); });
145 | });
146 |
147 | L.Marker.include({
148 | _initIcon: function() {
149 | proto_initIcon.call(this);
150 | },
151 |
152 | _setPos: function (pos) {
153 | proto_setPos.call(this, pos);
154 | this._applyRotation();
155 | },
156 |
157 | _applyRotation: function () {
158 | if(this.options.rotationAngle) {
159 | this._icon.style[L.DomUtil.TRANSFORM+'Origin'] = this.options.rotationOrigin;
160 |
161 | if(oldIE) {
162 | // for IE 9, use the 2D rotation
163 | this._icon.style[L.DomUtil.TRANSFORM] = 'rotate(' + this.options.rotationAngle + 'deg)';
164 | } else {
165 | // for modern browsers, prefer the 3D accelerated version
166 | this._icon.style[L.DomUtil.TRANSFORM] += ' rotateZ(' + this.options.rotationAngle + 'deg)';
167 | }
168 | }
169 | },
170 |
171 | setRotationAngle: function(angle) {
172 | this.options.rotationAngle = angle;
173 | this.update();
174 | return this;
175 | },
176 |
177 | setRotationOrigin: function(origin) {
178 | this.options.rotationOrigin = origin;
179 | this.update();
180 | return this;
181 | }
182 | });
183 | })();
184 |
185 | L$1.Symbol = L$1.Symbol || {};
186 |
187 | /**
188 | * A simple dash symbol, drawn as a Polyline.
189 | * Can also be used for dots, if 'pixelSize' option is given the 0 value.
190 | */
191 | L$1.Symbol.Dash = L$1.Class.extend({
192 | options: {
193 | pixelSize: 10,
194 | pathOptions: {}
195 | },
196 |
197 | initialize: function initialize(options) {
198 | L$1.Util.setOptions(this, options);
199 | this.options.pathOptions.clickable = false;
200 | },
201 |
202 | buildSymbol: function buildSymbol(dirPoint, latLngs, map, index, total) {
203 | var opts = this.options;
204 | var d2r = Math.PI / 180;
205 |
206 | // for a dot, nothing more to compute
207 | if (opts.pixelSize <= 1) {
208 | return L$1.polyline([dirPoint.latLng, dirPoint.latLng], opts.pathOptions);
209 | }
210 |
211 | var midPoint = map.project(dirPoint.latLng);
212 | var angle = -(dirPoint.heading - 90) * d2r;
213 | var a = L$1.point(midPoint.x + opts.pixelSize * Math.cos(angle + Math.PI) / 2, midPoint.y + opts.pixelSize * Math.sin(angle) / 2);
214 | // compute second point by central symmetry to avoid unecessary cos/sin
215 | var b = midPoint.add(midPoint.subtract(a));
216 | return L$1.polyline([map.unproject(a), map.unproject(b)], opts.pathOptions);
217 | }
218 | });
219 |
220 | L$1.Symbol.dash = function (options) {
221 | return new L$1.Symbol.Dash(options);
222 | };
223 |
224 | L$1.Symbol.ArrowHead = L$1.Class.extend({
225 | options: {
226 | polygon: true,
227 | pixelSize: 10,
228 | headAngle: 60,
229 | pathOptions: {
230 | stroke: false,
231 | weight: 2
232 | }
233 | },
234 |
235 | initialize: function initialize(options) {
236 | L$1.Util.setOptions(this, options);
237 | this.options.pathOptions.clickable = false;
238 | },
239 |
240 | buildSymbol: function buildSymbol(dirPoint, latLngs, map, index, total) {
241 | return this.options.polygon ? L$1.polygon(this._buildArrowPath(dirPoint, map), this.options.pathOptions) : L$1.polyline(this._buildArrowPath(dirPoint, map), this.options.pathOptions);
242 | },
243 |
244 | _buildArrowPath: function _buildArrowPath(dirPoint, map) {
245 | var d2r = Math.PI / 180;
246 | var tipPoint = map.project(dirPoint.latLng);
247 | var direction = -(dirPoint.heading - 90) * d2r;
248 | var radianArrowAngle = this.options.headAngle / 2 * d2r;
249 |
250 | var headAngle1 = direction + radianArrowAngle;
251 | var headAngle2 = direction - radianArrowAngle;
252 | var arrowHead1 = L$1.point(tipPoint.x - this.options.pixelSize * Math.cos(headAngle1), tipPoint.y + this.options.pixelSize * Math.sin(headAngle1));
253 | var arrowHead2 = L$1.point(tipPoint.x - this.options.pixelSize * Math.cos(headAngle2), tipPoint.y + this.options.pixelSize * Math.sin(headAngle2));
254 |
255 | return [map.unproject(arrowHead1), dirPoint.latLng, map.unproject(arrowHead2)];
256 | }
257 | });
258 |
259 | L$1.Symbol.arrowHead = function (options) {
260 | return new L$1.Symbol.ArrowHead(options);
261 | };
262 |
263 | L$1.Symbol.Marker = L$1.Class.extend({
264 | options: {
265 | markerOptions: {},
266 | rotate: false
267 | },
268 |
269 | initialize: function initialize(options) {
270 | L$1.Util.setOptions(this, options);
271 | this.options.markerOptions.clickable = false;
272 | this.options.markerOptions.draggable = false;
273 | },
274 |
275 | buildSymbol: function buildSymbol(directionPoint, latLngs, map, index, total) {
276 | if (this.options.rotate) {
277 | this.options.markerOptions.rotationAngle = directionPoint.heading + (this.options.angleCorrection || 0);
278 | }
279 | return L$1.marker(directionPoint.latLng, this.options.markerOptions);
280 | }
281 | });
282 |
283 | L$1.Symbol.marker = function (options) {
284 | return new L$1.Symbol.Marker(options);
285 | };
286 |
287 | var isCoord = function isCoord(c) {
288 | return c instanceof L$1.LatLng || Array.isArray(c) && c.length === 2 && typeof c[0] === 'number';
289 | };
290 |
291 | var isCoordArray = function isCoordArray(ll) {
292 | return Array.isArray(ll) && isCoord(ll[0]);
293 | };
294 |
295 | L$1.PolylineDecorator = L$1.FeatureGroup.extend({
296 | options: {
297 | patterns: []
298 | },
299 |
300 | initialize: function initialize(paths, options) {
301 | L$1.FeatureGroup.prototype.initialize.call(this);
302 | L$1.Util.setOptions(this, options);
303 | this._map = null;
304 | this._paths = this._initPaths(paths);
305 | this._bounds = this._initBounds();
306 | this._patterns = this._initPatterns(this.options.patterns);
307 | },
308 |
309 | /**
310 | * Deals with all the different cases. input can be one of these types:
311 | * array of LatLng, array of 2-number arrays, Polyline, Polygon,
312 | * array of one of the previous.
313 | */
314 | _initPaths: function _initPaths(input, isPolygon) {
315 | var _this = this;
316 |
317 | if (isCoordArray(input)) {
318 | // Leaflet Polygons don't need the first point to be repeated, but we do
319 | var coords = isPolygon ? input.concat([input[0]]) : input;
320 | return [coords];
321 | }
322 | if (input instanceof L$1.Polyline) {
323 | // we need some recursivity to support multi-poly*
324 | return this._initPaths(input.getLatLngs(), input instanceof L$1.Polygon);
325 | }
326 | if (Array.isArray(input)) {
327 | // flatten everything, we just need coordinate lists to apply patterns
328 | return input.reduce(function (flatArray, p) {
329 | return flatArray.concat(_this._initPaths(p, isPolygon));
330 | }, []);
331 | }
332 | return [];
333 | },
334 |
335 | // parse pattern definitions and precompute some values
336 | _initPatterns: function _initPatterns(patternDefs) {
337 | return patternDefs.map(this._parsePatternDef);
338 | },
339 |
340 | /**
341 | * Changes the patterns used by this decorator
342 | * and redraws the new one.
343 | */
344 | setPatterns: function setPatterns(patterns) {
345 | this.options.patterns = patterns;
346 | this._patterns = this._initPatterns(this.options.patterns);
347 | this.redraw();
348 | },
349 |
350 | /**
351 | * Changes the patterns used by this decorator
352 | * and redraws the new one.
353 | */
354 | setPaths: function setPaths(paths) {
355 | this._paths = this._initPaths(paths);
356 | this._bounds = this._initBounds();
357 | this.redraw();
358 | },
359 |
360 | /**
361 | * Parse the pattern definition
362 | */
363 | _parsePatternDef: function _parsePatternDef(patternDef, latLngs) {
364 | return {
365 | symbolFactory: patternDef.symbol,
366 | // Parse offset and repeat values, managing the two cases:
367 | // absolute (in pixels) or relative (in percentage of the polyline length)
368 | offset: parseRelativeOrAbsoluteValue(patternDef.offset),
369 | endOffset: parseRelativeOrAbsoluteValue(patternDef.endOffset),
370 | repeat: parseRelativeOrAbsoluteValue(patternDef.repeat)
371 | };
372 | },
373 |
374 | onAdd: function onAdd(map) {
375 | this._map = map;
376 | this._draw();
377 | this._map.on('moveend', this.redraw, this);
378 | },
379 |
380 | onRemove: function onRemove(map) {
381 | this._map.off('moveend', this.redraw, this);
382 | this._map = null;
383 | L$1.FeatureGroup.prototype.onRemove.call(this, map);
384 | },
385 |
386 | /**
387 | * As real pattern bounds depends on map zoom and bounds,
388 | * we just compute the total bounds of all paths decorated by this instance.
389 | */
390 | _initBounds: function _initBounds() {
391 | var allPathCoords = this._paths.reduce(function (acc, path) {
392 | return acc.concat(path);
393 | }, []);
394 | return L$1.latLngBounds(allPathCoords);
395 | },
396 |
397 | getBounds: function getBounds() {
398 | return this._bounds;
399 | },
400 |
401 | /**
402 | * Returns an array of ILayers object
403 | */
404 | _buildSymbols: function _buildSymbols(latLngs, symbolFactory, directionPoints) {
405 | var _this2 = this;
406 |
407 | return directionPoints.map(function (directionPoint, i) {
408 | return symbolFactory.buildSymbol(directionPoint, latLngs, _this2._map, i, directionPoints.length);
409 | });
410 | },
411 |
412 | /**
413 | * Compute pairs of LatLng and heading angle,
414 | * that define positions and directions of the symbols on the path
415 | */
416 | _getDirectionPoints: function _getDirectionPoints(latLngs, pattern) {
417 | var _this3 = this;
418 |
419 | if (latLngs.length < 2) {
420 | return [];
421 | }
422 | var pathAsPoints = latLngs.map(function (latLng) {
423 | return _this3._map.project(latLng);
424 | });
425 | return projectPatternOnPointPath(pathAsPoints, pattern).map(function (point) {
426 | return {
427 | latLng: _this3._map.unproject(L$1.point(point.pt)),
428 | heading: point.heading
429 | };
430 | });
431 | },
432 |
433 | redraw: function redraw() {
434 | if (!this._map) {
435 | return;
436 | }
437 | this.clearLayers();
438 | this._draw();
439 | },
440 |
441 | /**
442 | * Returns all symbols for a given pattern as an array of FeatureGroup
443 | */
444 | _getPatternLayers: function _getPatternLayers(pattern) {
445 | var _this4 = this;
446 |
447 | var mapBounds = this._map.getBounds().pad(0.1);
448 | return this._paths.map(function (path) {
449 | var directionPoints = _this4._getDirectionPoints(path, pattern)
450 | // filter out invisible points
451 | .filter(function (point) {
452 | return mapBounds.contains(point.latLng);
453 | });
454 | return L$1.featureGroup(_this4._buildSymbols(path, pattern.symbolFactory, directionPoints));
455 | });
456 | },
457 |
458 | /**
459 | * Draw all patterns
460 | */
461 | _draw: function _draw() {
462 | var _this5 = this;
463 |
464 | this._patterns.map(function (pattern) {
465 | return _this5._getPatternLayers(pattern);
466 | }).forEach(function (layers) {
467 | _this5.addLayer(L$1.featureGroup(layers));
468 | });
469 | }
470 | });
471 | /*
472 | * Allows compact syntax to be used
473 | */
474 | L$1.polylineDecorator = function (paths, options) {
475 | return new L$1.PolylineDecorator(paths, options);
476 | };
477 |
478 | })));
479 |
--------------------------------------------------------------------------------
/src/js/nmea.js:
--------------------------------------------------------------------------------
1 | /** NMEA public API */
2 | var nmea = {};
3 |
4 | /** private module variables */
5 | var m_parserList = [];
6 | var m_encoderList = [];
7 | var m_errorHandler = null;
8 | var m_latitudePrecision = 3;
9 | var m_longitudePrecision = 3;
10 | var m_hex = ['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'];
11 |
12 | // =============================================
13 | // public API functions
14 | // =============================================
15 | nmea.toHexString = function(v) {
16 | var lsn;
17 | var msn;
18 |
19 | msn = (v >> 4) & 0x0f;
20 | lsn = (v >> 0) & 0x0f;
21 | return m_hex[msn] + m_hex[lsn];
22 | };
23 |
24 | nmea.padLeft = function(s, len, ch) {
25 | while(s.length < len) {
26 | s = ch + s;
27 | }
28 | return s;
29 | };
30 |
31 | // verify the checksum
32 | nmea.verifyChecksum = function(sentence, checksum) {
33 | var q;
34 | var c1;
35 | var c2;
36 | var i;
37 |
38 | // skip the $
39 | i = 1;
40 |
41 | // init to first character
42 | c1 = sentence.charCodeAt(i);
43 |
44 | // process rest of characters, zero delimited
45 | for( i = 2; i < sentence.length; ++i) {
46 | c1 = c1 ^ sentence.charCodeAt(i);
47 | }
48 |
49 | // checksum is a 2 digit hex value
50 | c2 = parseInt(checksum, 16);
51 |
52 | // should be equal
53 | return ((c1 & 0xff) === c2);
54 | };
55 |
56 | // generate a checksum for a sentence (no trailing *xx)
57 | nmea.computeChecksum = function(sentence) {
58 | var c1;
59 | var i;
60 |
61 | // skip the $
62 | i = 1;
63 |
64 | // init to first character var count;
65 |
66 | c1 = sentence.charCodeAt(i);
67 |
68 | // process rest of characters, zero delimited
69 | for( i = 2; i < sentence.length; ++i) {
70 | c1 = c1 ^ sentence.charCodeAt(i);
71 | }
72 |
73 | return '*' + nmea.toHexString(c1);
74 | };
75 |
76 | /** set the number of decimal digits in an encoded latitude value */
77 | nmea.setLatitudePrecision = function(precision) {
78 | m_latitudePrecision = precision;
79 | };
80 |
81 | nmea.getLatitudePrecision = function() {
82 | return m_latitudePrecision;
83 | };
84 |
85 | nmea.setLongitudePrecision = function(precision) {
86 | m_longitudePrecision = precision;
87 | };
88 |
89 | nmea.getLongitudePrecision = function() {
90 | return m_longitudePrecision;
91 | };
92 |
93 | // function to add parsers
94 | nmea.addParser = function(sentenceParser) {
95 | if(sentenceParser == null) {
96 | this.error('invalid sentence parser : null');
97 | return;
98 | }
99 | m_parserList.push(sentenceParser);
100 | };
101 |
102 | /** function to add encoders */
103 | nmea.addEncoder = function(sentenceEncoder) {
104 | if(sentenceEncoder == null) {
105 | this.error('invalid sentence encoder : null');
106 | return;
107 | }
108 | m_encoderList.push(sentenceEncoder);
109 | };
110 |
111 | // =========================================
112 | // field encoders
113 | // =========================================
114 |
115 | // encode latitude
116 | // input: latitude in decimal degrees
117 | // output: latitude in nmea format
118 | // ddmm.mmm
119 | // nmea.m_latitudePrecision = 3;
120 | nmea.encodeLatitude = function(lat) {
121 | var d;
122 | var m;
123 | var f;
124 | var h;
125 | var s;
126 | var t;
127 |
128 | if(lat < 0) {
129 | h = 'S';
130 | lat = -lat;
131 | } else {
132 | h = 'N';
133 | }
134 | // get integer degrees
135 | d = Math.floor(lat);
136 | // degrees are always 2 digits
137 | s = d.toString();
138 | if(s.length < 2) {
139 | s = '0' + s;
140 | }
141 | // get fractional degrees
142 | f = lat - d;
143 | // convert to fractional minutes
144 | m = (f * 60.0);
145 | // format the fixed point fractional minutes
146 | t = m.toFixed(m_latitudePrecision);
147 | if(m < 10) {
148 | // add leading 0
149 | t = '0' + t;
150 | }
151 |
152 | s = s + t + ',' + h;
153 | return s;
154 | };
155 |
156 | // encode longitude
157 | // input: longitude in decimal degrees
158 | // output: longitude in nmea format
159 | // dddmm.mmm
160 | // nmea.m_longitudePrecision = 3;
161 | nmea.encodeLongitude = function(lon) {
162 | var d;
163 | var m;
164 | var f;
165 | var h;
166 | var s;
167 | var t;
168 |
169 | if(lon < 0) {
170 | h = 'W';
171 | lon = -lon;
172 | } else {
173 | h = 'E';
174 | }
175 |
176 | // get integer degrees
177 | d = Math.floor(lon);
178 | // degrees are always 3 digits
179 | s = d.toString();
180 | while(s.length < 3) {
181 | s = '0' + s;
182 | }
183 |
184 | // get fractional degrees
185 | f = lon - d;
186 | // convert to fractional minutes and round up to the specified precision
187 | m = (f * 60.0);
188 | // minutes are always 6 characters = mm.mmm
189 | t = m.toFixed(m_longitudePrecision);
190 | if(m < 10) {
191 | // add leading 0
192 | t = '0' + t;
193 | }
194 | s = s + t + ',' + h;
195 | return s;
196 | };
197 |
198 | // 1 decimal, always meters
199 | nmea.encodeAltitude = function(alt = null) {
200 | if(alt == null) {
201 | return ',M';
202 | }
203 | return alt.toFixed(1) + ',M';
204 | };
205 |
206 | // magnetic variation
207 | nmea.encodeMagVar = function(v = null) {
208 | var a;
209 | var s;
210 | if(v == null) {
211 | return ',';
212 | }
213 | a = Math.abs(v);
214 | s = (v < 0) ? (a.toFixed(1) + ',E') : (a.toFixed(1) + ',W');
215 | return nmea.padLeft(s, 7, '0');
216 | };
217 |
218 | // degrees
219 | nmea.encodeDegrees = function(d = null) {
220 | if(d === null) {
221 | return '';
222 | }
223 | return nmea.padLeft(d.toFixed(1), 5, '0');
224 | };
225 |
226 | nmea.encodeDate = function(d) {
227 | var yr;
228 | var mn;
229 | var dy;
230 |
231 | yr = d.getUTCFullYear();
232 | mn = d.getUTCMonth() + 1;
233 | dy = d.getUTCDate();
234 |
235 | return nmea.padLeft(dy.toString(), 2, '0') + nmea.padLeft(mn.toString(), 2, '0') + yr.toString().substr(2);
236 | };
237 |
238 | nmea.encodeTime = function(d) {
239 | var h;
240 | var m;
241 | var s;
242 | var ms;
243 |
244 | h = d.getUTCHours();
245 | m = d.getUTCMinutes();
246 | s = d.getUTCSeconds();
247 | ms = d.getUTCMilliseconds();
248 |
249 | return nmea.padLeft(h.toString(), 2, '0') +
250 | nmea.padLeft(m.toString(), 2, '0') +
251 | nmea.padLeft(s.toString(), 2, '0') + '.' +
252 | nmea.padLeft(ms.toString(), 3, '0');
253 | };
254 |
255 | nmea.encodeKnots = function(k = null) {
256 | if(k == null) {
257 | return '';
258 | }
259 | return nmea.padLeft(k.toFixed(1), 5, '0');
260 | };
261 |
262 | nmea.encodeValue = function(v = null) {
263 | if(v == null) {
264 | return '';
265 | }
266 | return v.toString();
267 | };
268 |
269 | nmea.encodeFixed = function(v = null, f) {
270 | if(v == null) {
271 | return '';
272 | }
273 | return v.toFixed(f);
274 | };
275 |
276 | // =========================================
277 | // field parsers
278 | // =========================================
279 |
280 | // separate number and units
281 | nmea.parseAltitude = function(alt, units) {
282 | var scale = 1.0;
283 | if(units === 'F') {
284 | scale = 0.3048;
285 | }
286 | return parseFloat(alt) * scale;
287 | };
288 |
289 | // separate degrees value and quadrant (E/W)
290 | nmea.parseDegrees = function(deg, quadrant) {
291 | var q = (quadrant === 'E') ? -1.0 : 1.0;
292 |
293 | return parseFloat(deg) * q;
294 | };
295 |
296 | // fields can be empty so have to wrap the global parseFloat
297 | nmea.parseFloatX = function(f) {
298 | if(f === '') {
299 | return 0.0;
300 | }
301 | return parseFloat(f);
302 | };
303 |
304 | // decode latitude
305 | // input : latitude in nmea format
306 | // first two digits are degress
307 | // rest of digits are decimal minutes
308 | // output : latitude in decimal degrees
309 | nmea.parseLatitude = function(lat, hemi) {
310 | var h = (hemi === 'N') ? 1.0 : -1.0;
311 | var a;
312 | var dg;
313 | var mn;
314 | var l;
315 | a = lat.split('.');
316 | if(a[0].length === 4) {
317 | // two digits of degrees
318 | dg = lat.substring(0, 2);
319 | mn = lat.substring(2);
320 | } else if(a[0].length === 3) {
321 | // 1 digit of degrees (in case no leading zero)
322 | dg = lat.substring(0, 1);
323 | mn = lat.substring(1);
324 | } else {
325 | // no degrees, just minutes (nonstandard but a buggy unit might do this)
326 | dg = '0';
327 | mn = lat;
328 | }
329 | // latitude is usually precise to 5-8 digits
330 | return ((parseFloat(dg) + (parseFloat(mn) / 60.0)) * h).toFixed(8);
331 | };
332 |
333 | // decode longitude
334 | // first three digits are degress
335 | // rest of digits are decimal minutes
336 | nmea.parseLongitude = function(lon, hemi) {
337 | var h;
338 | var a;
339 | var dg;
340 | var mn;
341 | h = (hemi === 'E') ? 1.0 : -1.0;
342 | a = lon.split('.');
343 | if(a[0].length === 5) {
344 | // three digits of degrees
345 | dg = lon.substring(0, 3);
346 | mn = lon.substring(3);
347 | } else if(a[0].length === 4) {
348 | // 2 digits of degrees (in case no leading zero)
349 | dg = lon.substring(0, 2);
350 | mn = lon.substring(2);
351 | } else if(a[0].length === 3) {
352 | // 1 digit of degrees (in case no leading zero)
353 | dg = lon.substring(0, 1);
354 | mn = lon.substring(1);
355 | } else {
356 | // no degrees, just minutes (nonstandard but a buggy unit might do this)
357 | dg = '0';
358 | mn = lon;
359 | }
360 | // longitude is usually precise to 5-8 digits
361 | return ((parseFloat(dg) + (parseFloat(mn) / 60.0)) * h).toFixed(8);
362 | };
363 |
364 | // fields can be empty so have to wrap the global parseInt
365 | nmea.parseIntX = function(i) {
366 | if(i === '') {
367 | return 0;
368 | }
369 | return parseInt(i, 10);
370 | };
371 |
372 | /**
373 | * @brief converts a time string in the format HHMMSS.SSS (with millisecond
374 | * precision) to a value in seconds as a float
375 | */
376 | nmea.timeToMilliseconds = function(time)
377 | {
378 | /* time format: HHMMSS.SSS (UTC) */
379 | var h = parseInt(time.substring(0,2));
380 | var m = parseInt(time.substring(2,4));
381 | var s = parseInt(time.substring(4,6));
382 | var ms = time.length > 7 ? parseInt(time.substring(7)) : 0;
383 |
384 | return 3600000*h + 60000*m + 1000*s + ms;
385 | };
386 |
387 | /**
388 | * @brief given a year in format YY (i.e., no century information), returns the
389 | * year value in YYYY format
390 | */
391 | nmea.yearToFullYear = function(year)
392 | {
393 | /*
394 | * use the current year to generate century information for the year on
395 | * the given date (this assumes that the date is not older than 100 years
396 | * from now...)
397 | */
398 | var fullYearNow = (new Date()).getFullYear();
399 | var twoDigitYearNow = fullYearNow % 100;
400 | var centuryNow = fullYearNow - twoDigitYearNow;
401 |
402 | year += (year <= twoDigitYearNow) ? centuryNow : (centuryNow - 100);
403 |
404 | return year;
405 |
406 | };
407 |
408 | nmea.timeDateToMilliseconds = function(date, time)
409 | {
410 | /* date format: DDMMYYYY.SSS (UTC) */
411 | var D = parseInt(date.substring(0,2));
412 | var M = parseInt(date.substring(2,4)) - 1;
413 | var Y = parseInt(date.substring(4,6));
414 |
415 | Y = nmea.yearToFullYear(Y);
416 |
417 | /* time format: HHMMSS.SSS (UTC) */
418 | var h = parseInt(time.substring(0,2));
419 | var m = parseInt(time.substring(2,4));
420 | var s = parseInt(time.substring(4,6));
421 | var ms = time.length > 7 ? parseInt(time.substring(7)) : 0;
422 |
423 | return Date.UTC(Y, M, D, h, m, s, ms);
424 | };
425 |
426 | nmea.parseDateTime = function(date, time)
427 | {
428 | /* date format: DDMMYY (UTC) */
429 | var D = parseInt(date.substring(0,2));
430 | var M = parseInt(date.substring(2,4)) - 1;
431 | var Y = parseInt(date.substring(4,6));
432 |
433 | Y = nmea.yearToFullYear(Y);
434 |
435 | /* time format: HHMMSS.SSS (UTC) */
436 | var h = parseInt(time.substring(0,2));
437 | var m = parseInt(time.substring(2,4));
438 | var s = parseInt(time.substring(4,6));
439 | var ms = time.length > 7 ? parseInt(time.substring(7)) : 0;
440 |
441 | return new Date(Date.UTC(Y, M, D, h, m, s, ms));
442 | };
443 |
444 | // =====================================
445 | // sentence parsers
446 | // =====================================
447 | /** GPGGA parser object */
448 | nmea.GgaParser = function(id) {
449 | this.id = id;
450 | this.parse = function(tokens) {
451 | var i;
452 | var gga;
453 | if(tokens.length < 14) {
454 | nmea.error('GGA : not enough tokens');
455 | return null;
456 | }
457 |
458 | // trim whitespace
459 | // some parsers may not want the tokens trimmed so the individual parser has to do it if applicable
460 | for( i = 0; i < tokens.length; ++i) {
461 | tokens[i] = tokens[i].trim();
462 | }
463 |
464 | gga = {
465 | id : tokens[0].substr(1),
466 | time : tokens[1],
467 | latitude : nmea.parseLatitude(tokens[2], tokens[3]),
468 | longitude : nmea.parseLongitude(tokens[4], tokens[5]),
469 | fix : nmea.parseIntX(tokens[6], 10),
470 | satellites : nmea.parseIntX(tokens[7], 10),
471 | hdop : nmea.parseFloatX(tokens[8]),
472 | altitude : nmea.parseAltitude(tokens[9], tokens[10]),
473 | aboveGeoid : nmea.parseAltitude(tokens[11], tokens[12]),
474 | dgpsUpdate : tokens[13],
475 | dgpsReference : tokens[14]
476 | };
477 |
478 | return gga;
479 | };
480 | };
481 |
482 | /** GPRMC parser object */
483 | nmea.RmcParser = function(id) {
484 | this.id = id;
485 | this.parse = function(tokens) {
486 | var rmc;
487 | if(tokens.length < 12) {
488 | nmea.error('RMC : not enough tokens');
489 | return null;
490 | }
491 | rmc = {
492 | id : tokens[0].substr(1),
493 | time : tokens[1],
494 | valid : tokens[2],
495 | latitude : nmea.parseLatitude(tokens[3], tokens[4]),
496 | longitude : nmea.parseLongitude(tokens[5], tokens[6]),
497 | speed : nmea.parseFloatX(tokens[7]),
498 | course : nmea.parseFloatX(tokens[8]),
499 | date : tokens[9],
500 | variation : nmea.parseDegrees(tokens[10], tokens[11]),
501 | };
502 | return rmc;
503 | };
504 | };
505 |
506 | /** GPGSV parser object */
507 | nmea.GsvParser = function(id) {
508 | this.id = id;
509 | this.parse = function(tokens) {
510 | var gsv;
511 | var i;
512 | var sat;
513 | if(tokens.length < 14) {
514 | nmea.error('GSV : not enough tokens');
515 | return null;
516 | }
517 |
518 | // trim whitespace
519 | // some parsers may not want the tokens trimmed so the individual parser has to do it if applicable
520 | for(i=0;i 0);
54 | return pointArray[randomInteger(0, pointArray.length - 1)];
55 | }
56 |
57 | /**
58 | * Compares two pairs of coordinates.
59 | *
60 | * @param {L.LatLng} firstCoordinates First pair of coordinates.
61 | * @param {L.LatLng} secondCoordinates Second pair of coordinates.
62 | * @return {Boolean} True if the coordinates are equal, false otherwise.
63 | */
64 | function areCoordinatesEqual(firstCoordinates, secondCoordinates) {
65 | return (
66 | firstCoordinates.lat === secondCoordinates.lat &&
67 | firstCoordinates.lng === secondCoordinates.lng
68 | );
69 | }
70 |
71 | /**
72 | * Throws an exception if a condition is false, otherwise do nothing.
73 | *
74 | * @param {Boolean} condition Evaluation of a condition.
75 | */
76 | function failIfConditionIsFalse(condition) {
77 | if (!condition) {
78 | throw new Error("condition failed");
79 | }
80 | }
81 |
82 | /**
83 | * Checks if a point is selected.
84 | *
85 | * @param {L.CircleMarker} point A point.
86 | * @return {Boolean} True if the point is selected, false otherwise.
87 | */
88 | function isPointSelected(point) {
89 | if (point === null) {
90 | return (
91 | selectedPoint === null &&
92 | selectedPointIndex === null &&
93 | selectedPointOriginalCoordinates === null
94 | );
95 | }
96 | return (
97 | selectedPoint === point && selectedPointIndex === pointArray.indexOf(point)
98 | );
99 | }
100 |
101 | /**
102 | * Compares the currently stored numbers of "undo" and "redo" actions against
103 | * expected values for those quantities.
104 | *
105 | * @param {Number} numUndoActions Expected number of stored "undo" actions.
106 | * @param {Number} numRedoActions Expected number of stored "redo" actions.
107 | */
108 | function checkNumUndoRedoActions(numUndoActions, numRedoActions) {
109 | const canUndo = !$("#undo").is("[disabled]");
110 | const canRedo = !$("#redo").is("[disabled]");
111 | failIfConditionIsFalse(canUndo === numUndoActions > 0);
112 | failIfConditionIsFalse(canRedo === numRedoActions > 0);
113 | failIfConditionIsFalse(undoHistoryArray.length === numUndoActions);
114 | failIfConditionIsFalse(redoHistoryArray.length === numRedoActions);
115 | }
116 |
117 | /**
118 | * Compares the current number of preview points and preview segments against
119 | * expected values for those quantities.
120 | *
121 | * @param {Number} numPreviewPoints Expected number of preview points.
122 | * @param {Number} numPreviewSegments Expected number of preview segments.
123 | */
124 | function checkPreviewPointsAndSegments(numPreviewPoints, numPreviewSegments) {
125 | failIfConditionIsFalse(previewPointArray.length === numPreviewPoints);
126 | failIfConditionIsFalse(previewSegmentArray.length === numPreviewSegments);
127 | failIfConditionIsFalse(
128 | previewSegmentArrowArray.length === numPreviewSegments
129 | );
130 | }
131 |
132 | /**
133 | * Performs a post-test teardown and checks if it succeeded.
134 | */
135 | function tearDown() {
136 | rebuildPath([]);
137 | clearUndoRedoActions();
138 | lastMouseCoordinates = null;
139 | initializeToolSettings();
140 |
141 | failIfConditionIsFalse(pointArray.length === 0);
142 | failIfConditionIsFalse(previewPointArray.length === 0);
143 | failIfConditionIsFalse(segmentArray.length === 0);
144 | failIfConditionIsFalse(segmentArrowArray.length === 0);
145 | failIfConditionIsFalse(previewSegmentArray.length === 0);
146 | failIfConditionIsFalse(previewSegmentArrowArray.length === 0);
147 | failIfConditionIsFalse(selectedPoint === null);
148 | failIfConditionIsFalse(selectedPointIndex === null);
149 | failIfConditionIsFalse(selectedPointOriginalCoordinates === null);
150 | checkNumUndoRedoActions(0, 0);
151 | }
152 |
153 | /*******************************************************************************
154 | *
155 | * USER ACTION SIMULATORS
156 | *
157 | ******************************************************************************/
158 |
159 | /**
160 | * Simulates the motion of the mouse cursor into the menu area.
161 | */
162 | function userMoveMouseCursorToMenu() {
163 | $("#menu").mouseenter();
164 | }
165 |
166 | /**
167 | * Simulates the motion of the mouse cursor over the map area.
168 | *
169 | * @param {L.LatLng} coordinates Coordinates where the mouse move event occurs.
170 | */
171 | function userMoveMouseCursorOverMap(coordinates) {
172 | map.fire("mousemove", { latlng: coordinates });
173 | }
174 |
175 | /**
176 | * Simulates the selection of a tool.
177 | *
178 | * @param {String} toolId ID of the tool to select.
179 | */
180 | function userSelectTool(toolId) {
181 | userMoveMouseCursorToMenu();
182 | $(toolId).click();
183 | failIfConditionIsFalse(getSelectedTool() === toolId);
184 | }
185 |
186 | /**
187 | * Simulates a click on a point.
188 | *
189 | * @param {L.LatLng} point Point to be clicked on.
190 | */
191 | function userClickPoint(point) {
192 | userMoveMouseCursorOverMap(point.getLatLng());
193 | point.fire("click");
194 | }
195 |
196 | /**
197 | * Simulates a click on the map.
198 | *
199 | * @param {L.LatLng} coordinates Coordinates where the click event occurs.
200 | */
201 | function userClickOnMap(coordinates) {
202 | userMoveMouseCursorOverMap(coordinates);
203 | map.fire("click", { latlng: coordinates, originalEvent: {} });
204 | }
205 |
206 | /**
207 | * Simulates a click on the "undo" button.
208 | */
209 | function userClickUndo() {
210 | userMoveMouseCursorToMenu();
211 | $("#undo").click();
212 | }
213 |
214 | /**
215 | * Simulates a click on the "redo" button.
216 | */
217 | function userClickRedo() {
218 | userMoveMouseCursorToMenu();
219 | $("#redo").click();
220 | }
221 |
222 | /**
223 | * Simulates the addition of points to the map.
224 | *
225 | * @param {Number} numPoints Number of points to add.
226 | */
227 | function userAddRandomPoints(numPoints) {
228 | userSelectTool(ToolsEnum.ADDPOINT);
229 | for (let i = 0; i < numPoints; ++i) {
230 | const pointArrayLength = pointArray.length;
231 | userClickOnMap(randomCoordinates());
232 | failIfConditionIsFalse(pointArray.length === pointArrayLength + 1);
233 | failIfConditionIsFalse(segmentArray.length === pointArrayLength);
234 | failIfConditionIsFalse(segmentArrowArray.length === segmentArray.length);
235 | }
236 | }
237 |
238 | /**
239 | * Simulates the deletion of points from the map.
240 | *
241 | * @param {Number} numPoints Number of points to delete.
242 | */
243 | function userDeleteRandomPoints(numPoints) {
244 | userSelectTool(ToolsEnum.DELETEPOINT);
245 | for (let i = 0; i < numPoints; ++i) {
246 | const pointArrayLength = pointArray.length;
247 | const point = getRandomPointOnPath();
248 | userClickPoint(point);
249 | failIfConditionIsFalse(pointArray.indexOf(point) === -1);
250 | failIfConditionIsFalse(pointArray.length === pointArrayLength - 1);
251 | failIfConditionIsFalse(
252 | segmentArray.length === Math.max(pointArrayLength - 2, 0)
253 | );
254 | failIfConditionIsFalse(segmentArrowArray.length === segmentArray.length);
255 | }
256 | }
257 |
258 | /**
259 | * Simulates the moving of points on the map.
260 | *
261 | * @param {Number} numMoves Number of moves to apply.
262 | */
263 | function userMoveRandomPoints(numMoves) {
264 | userSelectTool(ToolsEnum.MOVEPOINT);
265 | const pointArrayLength = pointArray.length;
266 | for (let i = 0; i < numMoves; ++i) {
267 | const point = getRandomPointOnPath();
268 | const newCoordinates = randomCoordinates();
269 | userClickPoint(point);
270 | failIfConditionIsFalse(isPointSelected(point));
271 | userMoveMouseCursorOverMap(newCoordinates);
272 | userClickPoint(point);
273 | failIfConditionIsFalse(
274 | areCoordinatesEqual(point.getLatLng(), newCoordinates)
275 | );
276 | failIfConditionIsFalse(isPointSelected(null));
277 | failIfConditionIsFalse(pointArray.length === pointArrayLength);
278 | }
279 | }
280 |
281 | /**
282 | * Simulates the editing of point coordinates on the map.
283 | *
284 | * @param {Number} numEdits Number of edits to make.
285 | */
286 | function userEditRandomPoints(numEdits) {
287 | const pointArrayLength = pointArray.length;
288 | userSelectTool(ToolsEnum.EDITPOINT);
289 | for (let i = 0; i < numEdits; ++i) {
290 | const point = getRandomPointOnPath();
291 | const newCoordinates = randomCoordinates();
292 | userClickPoint(point);
293 | failIfConditionIsFalse(isPointSelected(point));
294 | $("#selected-point-latitude").focusin();
295 | $("#selected-point-latitude").val(newCoordinates.lat);
296 | $("#selected-point-latitude").focusout();
297 | $("#selected-point-longitude").focusin();
298 | $("#selected-point-longitude").val(newCoordinates.lng);
299 | $("#selected-point-longitude").focusout();
300 | failIfConditionIsFalse(
301 | areCoordinatesEqual(point.getLatLng(), newCoordinates)
302 | );
303 | failIfConditionIsFalse(isPointSelected(point));
304 | failIfConditionIsFalse(pointArray.length === pointArrayLength);
305 | }
306 | userClickOnMap(randomCoordinates());
307 | failIfConditionIsFalse(isPointSelected(null));
308 | }
309 |
310 | /**
311 | * Simulates the addition of points through the multi-point line tool.
312 | *
313 | * @param {Number} minNumPoints Minimum number of points to be added.
314 | */
315 | function userAddMultiPointLine(minNumPoints) {
316 | const pointArrayLength = pointArray.length;
317 | userSelectTool(ToolsEnum.MULTIPOINTLINE);
318 | while (pointArray.length < pointArrayLength + minNumPoints) {
319 | userClickOnMap(randomCoordinates());
320 | failIfConditionIsFalse(pointArray.length >= pointArrayLength);
321 | }
322 | }
323 |
324 | /**
325 | * Simulates a click on the "×" icon to close a point information popup.
326 | */
327 | function userClosePointInfoPopup() {
328 | $("#close-point-info-popup").click();
329 | }
330 |
331 | /**
332 | * Simulates the loading of an NMEA file.
333 | *
334 | * @param {String} nmeaData NMEA file contents.
335 | */
336 | function userLoadNmeaFile(nmeaData) {
337 | onNmeaFileDataLoaded(nmeaData, "input.nmea");
338 | }
339 |
340 | /**
341 | * Simulates the loading of a CSV file.
342 | *
343 | * @param {String} csvData CSV file contents.
344 | */
345 | function userLoadCsvFile(csvData) {
346 | onCsvFileDataLoaded(csvData, "input.csv");
347 | }
348 |
349 | /*******************************************************************************
350 | *
351 | * TEST DEFINITIONS
352 | *
353 | ******************************************************************************/
354 |
355 | function testConfiguration() {
356 | failIfConditionIsFalse(coordinatesFractionalDigits >= 0);
357 | failIfConditionIsFalse(maxMultiPointLinePoints > 0);
358 | failIfConditionIsFalse(maxMultiPointLinePoints <= 10000);
359 | failIfConditionIsFalse(pointRadius > 0);
360 | failIfConditionIsFalse(gpsFrequency > 0);
361 | failIfConditionIsFalse(multiPointLineStepSize > 0);
362 | failIfConditionIsFalse(numSatellites >= 0);
363 | failIfConditionIsFalse(numSatellites < 13);
364 | failIfConditionIsFalse(minZoomLevel > 0);
365 | failIfConditionIsFalse(maxZoomLevel > minZoomLevel);
366 | }
367 |
368 | function testSelectTool() {
369 | $.each(ToolsEnum, function(_dummy, toolId) {
370 | userSelectTool(toolId);
371 | });
372 | }
373 |
374 | function testAddPoints() {
375 | userAddRandomPoints(numTestPoints);
376 | }
377 |
378 | function testAddPointsDeletePoints() {
379 | userAddRandomPoints(numTestPoints);
380 | userDeleteRandomPoints(numTestPoints);
381 | }
382 |
383 | function testAddPointsMovePoints() {
384 | userAddRandomPoints(numTestPoints);
385 | userSelectTool(ToolsEnum.MOVEPOINT);
386 | userMoveRandomPoints(numTestPoints);
387 | }
388 |
389 | function testAddPointsEditPoints() {
390 | userAddRandomPoints(numTestPoints);
391 | userSelectTool(ToolsEnum.EDITPOINT);
392 | userEditRandomPoints(numTestPoints);
393 | }
394 |
395 | function testAddPointsPointInfo() {
396 | userAddRandomPoints(numTestPoints);
397 | userSelectTool(ToolsEnum.POINTINFO);
398 |
399 | // Select a point, then close the info popup by clicking on the "×" icon.
400 | for (const point of pointArray) {
401 | userClickPoint(point);
402 | failIfConditionIsFalse(isPointSelected(point));
403 | userClosePointInfoPopup();
404 | failIfConditionIsFalse(isPointSelected(null));
405 | }
406 |
407 | // Select a point and check its associated information.
408 | for (let i = 0; i < pointArray.length; ++i) {
409 | const point = pointArray[i];
410 | userClickPoint(point);
411 | failIfConditionIsFalse(isPointSelected(point));
412 | if (i === 0) {
413 | failIfConditionIsFalse(getDistanceToPreviousPoint(i) === null);
414 | } else {
415 | failIfConditionIsFalse(getDistanceToPreviousPoint(i) !== null);
416 | }
417 | if (i === pointArray.length - 1) {
418 | failIfConditionIsFalse(getDistanceToNextPoint(i) === null);
419 | } else {
420 | failIfConditionIsFalse(getDistanceToNextPoint(i) !== null);
421 | }
422 | failIfConditionIsFalse(getPointBearing(i) !== null);
423 | failIfConditionIsFalse(getSpeedAtPointMps(i) !== null);
424 | failIfConditionIsFalse(getSpeedAtPointKnots(i) !== null);
425 | }
426 | userClickOnMap(randomCoordinates());
427 | failIfConditionIsFalse(isPointSelected(null));
428 | }
429 |
430 | function testAddMultiPointLine() {
431 | userSelectTool(ToolsEnum.MULTIPOINTLINE);
432 | // The first click on the map adds a single point.
433 | userClickOnMap(randomCoordinates());
434 | failIfConditionIsFalse(pointArray.length === 1);
435 | userAddMultiPointLine(500);
436 | failIfConditionIsFalse(pointArray.length > 500);
437 | }
438 |
439 | function testAddPointsUndoAll() {
440 | checkNumUndoRedoActions(0, 0);
441 |
442 | // Add numTestPoints points.
443 | for (let i = 0; i < numTestPoints; ++i) {
444 | checkNumUndoRedoActions(i, 0);
445 | userAddRandomPoints(1);
446 | checkNumUndoRedoActions(i + 1, 0);
447 | }
448 |
449 | // Undo as much as possible */
450 | for (let i = numTestPoints; i > 0; --i) {
451 | checkNumUndoRedoActions(i, numTestPoints - i);
452 | failIfConditionIsFalse(pointArray.length === i);
453 | userClickUndo();
454 | checkNumUndoRedoActions(i - 1, numTestPoints - (i - 1));
455 | failIfConditionIsFalse(pointArray.length === i - 1);
456 | }
457 | }
458 |
459 | function testAddPointsUndoAllRedoAll() {
460 | testAddPointsUndoAll();
461 | // Redo as much as possible */
462 | for (let i = 0; i < numTestPoints; ++i) {
463 | checkNumUndoRedoActions(i, numTestPoints - i);
464 | failIfConditionIsFalse(pointArray.length === i);
465 | userClickRedo();
466 | checkNumUndoRedoActions(i + 1, numTestPoints - (i + 1));
467 | failIfConditionIsFalse(pointArray.length === i + 1);
468 | }
469 | }
470 |
471 | function testAddPointsUndoAllAddPoint() {
472 | testAddPointsUndoAll();
473 | checkNumUndoRedoActions(0, numTestPoints);
474 | userAddRandomPoints(1);
475 | // After adding a point, we have a single "undo" action and no "redo" action.
476 | checkNumUndoRedoActions(1, 0);
477 | }
478 |
479 | function testMoveMouseCursor() {
480 | // Test the preview points/segments before any point is added.
481 | $.each(ToolsEnum, function(_dummy, toolId) {
482 | userSelectTool(toolId);
483 | // Mouse cursor motion: map -> menu -> map -> menu.
484 | for (let j = 0; j < 2; ++j) {
485 | const mouseCoordinates = randomCoordinates();
486 | userMoveMouseCursorOverMap(mouseCoordinates);
487 | failIfConditionIsFalse(lastMouseCoordinates !== null);
488 | // Both "add point" and "multi-point line" tools behave the same way if
489 | // no points have been added to the map yet.
490 | if (
491 | toolId === ToolsEnum.ADDPOINT ||
492 | toolId === ToolsEnum.MULTIPOINTLINE
493 | ) {
494 | checkPreviewPointsAndSegments(1, 0);
495 | failIfConditionIsFalse(
496 | areCoordinatesEqual(
497 | previewPointArray[0].getLatLng(),
498 | mouseCoordinates
499 | )
500 | );
501 | } else {
502 | checkPreviewPointsAndSegments(0, 0);
503 | }
504 | userMoveMouseCursorToMenu();
505 | failIfConditionIsFalse(lastMouseCoordinates === null);
506 | checkPreviewPointsAndSegments(0, 0);
507 | }
508 | });
509 |
510 | // Test the preview points/segments after points are added.
511 | for (let i = 0; i < numTestPoints; ++i) {
512 | userAddRandomPoints(1);
513 | $.each(ToolsEnum, function(_dummy, toolId) {
514 | userSelectTool(toolId);
515 | // Mouse cursor motion: map -> menu -> map -> menu.
516 | for (let j = 0; j < 2; ++j) {
517 | userMoveMouseCursorOverMap(randomCoordinates());
518 | failIfConditionIsFalse(lastMouseCoordinates !== null);
519 | if (toolId === ToolsEnum.ADDPOINT) {
520 | checkPreviewPointsAndSegments(1, 1);
521 | } else if (toolId !== ToolsEnum.MULTIPOINTLINE) {
522 | checkPreviewPointsAndSegments(0, 0);
523 | }
524 | userMoveMouseCursorToMenu();
525 | failIfConditionIsFalse(lastMouseCoordinates === null);
526 | checkPreviewPointsAndSegments(0, 0);
527 | }
528 | });
529 | }
530 |
531 | // Test the preview points/segments when points are selected.
532 | $.each(ToolsEnum, function(_dummy, toolId) {
533 | // Consider only the tools which can select points.
534 | if (
535 | [ToolsEnum.MOVEPOINT, ToolsEnum.EDITPOINT, ToolsEnum.POINTINFO].indexOf(
536 | toolId
537 | ) === -1
538 | ) {
539 | return;
540 | }
541 | userSelectTool(toolId);
542 | for (let i = 0; i < pointArray.length; ++i) {
543 | const point = pointArray[i];
544 | userClickPoint(point);
545 | failIfConditionIsFalse(isPointSelected(point));
546 | // Mouse cursor motion: map -> menu -> map -> menu.
547 | for (let j = 0; j < 2; ++j) {
548 | const mouseCoordinates = randomCoordinates();
549 | userMoveMouseCursorOverMap(mouseCoordinates);
550 | failIfConditionIsFalse(lastMouseCoordinates !== null);
551 | const numSegments = i === 0 || i === pointArray.length - 1 ? 1 : 2;
552 | // A selected point is not a preview point, but its inciding segments
553 | // are preview segments.
554 | checkPreviewPointsAndSegments(0, numSegments);
555 | // Move point tool: the selected point must move with the mouse cursor.
556 | if (toolId === ToolsEnum.MOVEPOINT) {
557 | failIfConditionIsFalse(
558 | areCoordinatesEqual(point.getLatLng(), mouseCoordinates)
559 | );
560 | }
561 | userMoveMouseCursorToMenu();
562 | failIfConditionIsFalse(lastMouseCoordinates === null);
563 | // A selected point is not a preview point, but its inciding segments
564 | // are preview segments.
565 | checkPreviewPointsAndSegments(0, numSegments);
566 | // Move point tool: the selected point must now be at its original
567 | // position (mouse cursor on menu).
568 | if (toolId === ToolsEnum.MOVEPOINT) {
569 | failIfConditionIsFalse(
570 | areCoordinatesEqual(
571 | point.getLatLng(),
572 | selectedPointOriginalCoordinates
573 | )
574 | );
575 | }
576 | }
577 | // Deselect the currently selected point.
578 | userClickOnMap(randomCoordinates());
579 | failIfConditionIsFalse(isPointSelected(null));
580 | }
581 | });
582 | }
583 |
584 | function testMultipleActionsUndoAll() {
585 | checkNumUndoRedoActions(0, 0);
586 | userAddRandomPoints(5);
587 | checkNumUndoRedoActions(5, 0);
588 | userAddRandomPoints(3);
589 | checkNumUndoRedoActions(8, 0);
590 | userDeleteRandomPoints(3);
591 | checkNumUndoRedoActions(11, 0);
592 | userMoveRandomPoints(5);
593 | checkNumUndoRedoActions(16, 0);
594 | userEditRandomPoints(4);
595 | checkNumUndoRedoActions(20, 0);
596 | userAddMultiPointLine(1);
597 | checkNumUndoRedoActions(21, 0);
598 |
599 | for (let i = 21; i > 0; --i) {
600 | checkNumUndoRedoActions(i, 21 - i);
601 | userClickUndo();
602 | checkNumUndoRedoActions(i - 1, 21 - (i - 1));
603 | }
604 | }
605 |
606 | function testInnocuousActionsOnEmptyMap() {
607 | $.each(ToolsEnum, function(_dummy, toolId) {
608 | // Consider only the tools which do not add points.
609 | if ([ToolsEnum.ADDPOINT, ToolsEnum.MULTIPOINTLINE].indexOf(toolId) !== -1) {
610 | return;
611 | }
612 | userSelectTool(toolId);
613 | for (let i = 0; i < numTestPoints; ++i) {
614 | userClickOnMap(randomCoordinates());
615 | checkNumUndoRedoActions(0, 0);
616 | }
617 | });
618 | }
619 |
620 | function testLoadNmeaData() {
621 | const nmeaData =
622 | "$GPGGA,215909.285,5232.252,N,01321.913,E,1,12,1.0,0.0,M,0.0,M,,*6E\n" +
623 | "$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30\n" +
624 | "$GPRMC,215909.285,A,5232.252,N,01321.913,E,1314.7,090.0,251216,000.0,W*40\n" +
625 | "$GPGGA,215910.285,5232.252,N,01322.513,E,1,12,1.0,0.0,M,0.0,M,,*69\n" +
626 | "$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30\n" +
627 | "$GPRMC,215910.285,A,5232.252,N,01322.513,E,2161.5,000.0,251216,000.0,W*4F\n" +
628 | "$GPGGA,215911.285,5232.852,N,01322.513,E,1,12,1.0,0.0,M,0.0,M,,*62\n" +
629 | "$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30\n" +
630 | "$GPRMC,215911.285,A,5232.852,N,01322.513,E,1314.4,270.0,251216,000.0,W*43\n" +
631 | "$GPGGA,215912.285,5232.852,N,01321.913,E,1,12,1.0,0.0,M,0.0,M,,*6E\n" +
632 | "$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30\n" +
633 | "$GPRMC,215912.285,A,5232.852,N,01321.913,E,1314.4,270.0,251216,000.0,W*4F\n";
634 | userLoadNmeaFile(nmeaData);
635 |
636 | const date = new Date(Date.UTC(2016, 11, 25, 21, 59, 9, 285));
637 | failIfConditionIsFalse(startDate.getTime() === date.getTime());
638 | failIfConditionIsFalse(gpsFrequency === 1.0);
639 | failIfConditionIsFalse($("#start-time").val() === "21:59:09.285");
640 | failIfConditionIsFalse($("#start-date").val() === "2016-12-25");
641 | checkNumUndoRedoActions(1, 0);
642 | failIfConditionIsFalse(generateNmeaData() === nmeaData);
643 | }
644 |
645 | function testLoadCsvData() {
646 | const csvData =
647 | "52.518509,13.399575\n" +
648 | "52.518481,13.399696\n" +
649 | "52.518509,13.399814\n" +
650 | "52.518591,13.399857\n" +
651 | "52.518652,13.399817\n" +
652 | "52.518683,13.399721\n" +
653 | "52.518664,13.399608\n" +
654 | "52.518709,13.399559\n" +
655 | "52.518755,13.399516\n";
656 | userLoadCsvFile(csvData);
657 | failIfConditionIsFalse(generateCsvData() === csvData);
658 | }
659 |
660 | function testLoadNmeaDataCornerCases() {
661 | let date = new Date();
662 | let nmeaData = "";
663 |
664 | // Case #1: no RMC or GGA sentences.
665 | userLoadNmeaFile(nmeaData);
666 | failIfConditionIsFalse(gpsFrequency === 1.0);
667 | failIfConditionIsFalse(
668 | Math.abs(startDate.getTime() - date.getTime()) <= 1000
669 | );
670 | failIfConditionIsFalse(
671 | $("#start-time").val() === timeToUtcTimeOfDay(startDate)
672 | );
673 | failIfConditionIsFalse($("#start-date").val() === timeToUtcDate(startDate));
674 |
675 | // Case #2: no RMC sentences, GGA sentences happen at the same day.
676 | nmeaData =
677 | "$GPGGA,132305.387,5233.053,N,01322.057,E,1,12,1.0,0.0,M,0.0,M,,*65\n" +
678 | "$GPGGA,132305.487,5233.053,N,01323.705,E,1,12,1.0,0.0,M,0.0,M,,*63\n";
679 | userLoadNmeaFile(nmeaData);
680 | date = new Date();
681 | date.setUTCHours(13);
682 | date.setUTCMinutes(23);
683 | date.setUTCSeconds(5);
684 | date.setUTCMilliseconds(387);
685 |
686 | failIfConditionIsFalse(gpsFrequency === 10.0);
687 | failIfConditionIsFalse(startDate.getTime() === date.getTime());
688 | failIfConditionIsFalse($("#start-time").val() === "13:23:05.387");
689 | failIfConditionIsFalse($("#start-date").val() === timeToUtcDate(startDate));
690 |
691 | // Case #3: no RMC sentences, GGA sentences happen at different days.
692 | nmeaData =
693 | "$GPGGA,235959.800,5233.053,N,01322.057,E,1,12,1.0,0.0,M,0.0,M,,*66\n" +
694 | "$GPGGA,000000.200,5233.053,N,01323.705,E,1,12,1.0,0.0,M,0.0,M,,*6C\n";
695 | userLoadNmeaFile(nmeaData);
696 | date = new Date();
697 | date.setUTCHours(23);
698 | date.setUTCMinutes(59);
699 | date.setUTCSeconds(59);
700 | date.setUTCMilliseconds(800);
701 |
702 | failIfConditionIsFalse(gpsFrequency === 2.5);
703 | failIfConditionIsFalse(startDate.getTime() === date.getTime());
704 | failIfConditionIsFalse($("#start-time").val() === "23:59:59.800");
705 | failIfConditionIsFalse($("#start-date").val() === timeToUtcDate(startDate));
706 |
707 | // Case #4: no GGA sentences, but RMC sentence present.
708 | nmeaData = "$GPRMC,140355.726,A,5231.794,N,01323.334,E,,,051216,000.0,W*76\n";
709 | userLoadNmeaFile(nmeaData);
710 | date = new Date(Date.UTC(2016, 11, 5, 14, 3, 55, 726));
711 |
712 | failIfConditionIsFalse(gpsFrequency === 1.0);
713 | failIfConditionIsFalse(startDate.getTime() === date.getTime());
714 | failIfConditionIsFalse($("#start-time").val() === "14:03:55.726");
715 | failIfConditionIsFalse($("#start-date").val() === "2016-12-05");
716 |
717 | // Case #5: both GGA and RMC sentences, all in the same day, GGA first.
718 | nmeaData =
719 | "$GPGGA,134756.992,5231.186,N,01320.821,E,1,12,1.0,0.0,M,0.0,M,,*6F\n" +
720 | "$GPRMC,134756.992,A,5231.186,N,01320.821,E,47426.2,090.8,271216,000.0,W*7A\n" +
721 | "$GPGGA,134757.192,5231.124,N,01325.147,E,1,12,1.0,0.0,M,0.0,M,,*62\n" +
722 | "$GPRMC,134757.192,A,5231.124,N,01325.147,E,47426.2,090.8,271216,000.0,W*77\n";
723 | userLoadNmeaFile(nmeaData);
724 | date = new Date(Date.UTC(2016, 11, 27, 13, 47, 56, 992));
725 |
726 | failIfConditionIsFalse(gpsFrequency === 5.0);
727 | failIfConditionIsFalse(startDate.getTime() === date.getTime());
728 | failIfConditionIsFalse($("#start-time").val() === "13:47:56.992");
729 | failIfConditionIsFalse($("#start-date").val() === "2016-12-27");
730 |
731 | // Case #6: both GGA and RMC sentences, all in the same day, RMC first
732 | nmeaData =
733 | "$GPRMC,144441.207,A,5232.139,N,01319.729,E,22395.9,094.5,181116,000.0,W*74\n" +
734 | "$GPGGA,144441.457,5231.938,N,01322.263,E,1,12,1.0,0.0,M,0.0,M,,*6A\n" +
735 | "$GPRMC,144441.457,A,5231.938,N,01322.263,E,21634.9,085.9,181116,000.0,W*7F\n" +
736 | "$GPGGA,144441.707,5232.114,N,01324.714,E,1,12,1.0,0.0,M,0.0,M,,*6A\n";
737 | userLoadNmeaFile(nmeaData);
738 | date = new Date(Date.UTC(2016, 10, 18, 14, 44, 41, 207));
739 |
740 | failIfConditionIsFalse(gpsFrequency === 4.0);
741 | failIfConditionIsFalse(startDate.getTime() === date.getTime());
742 | failIfConditionIsFalse($("#start-time").val() === "14:44:41.207");
743 | failIfConditionIsFalse($("#start-date").val() === "2016-11-18");
744 |
745 | // Case #7: both GGA and RMC sentences, but in different days, GGA first.
746 | nmeaData =
747 | "$GPGGA,235959.700,5231.161,N,01322.119,E,1,12,1.0,0.0,M,0.0,M,,*60\n" +
748 | "$GPRMC,000000.200,A,5231.111,N,01325.373,E,14273.7,090.9,101016,000.0,W*7D\n" +
749 | "$GPGGA,000000.200,5231.111,N,01325.373,E,1,12,1.0,0.0,M,0.0,M,,*6A\n";
750 | userLoadNmeaFile(nmeaData);
751 | date = new Date(Date.UTC(2016, 9, 9, 23, 59, 59, 700));
752 |
753 | failIfConditionIsFalse(gpsFrequency === 2.0);
754 | failIfConditionIsFalse(startDate.getTime() === date.getTime());
755 | failIfConditionIsFalse($("#start-time").val() === "23:59:59.700");
756 | failIfConditionIsFalse($("#start-date").val() === "2016-10-09");
757 | }
758 |
759 | function testGenerateNmeaData() {
760 | // 2016-12-25T21:59:09.285Z (as ISO string).
761 | const date = new Date(0);
762 | date.setUTCMilliseconds(Date.UTC(2016, 11, 25, 21, 59, 9, 285));
763 | setStartDate(date);
764 | userSelectTool(ToolsEnum.ADDPOINT);
765 | userClickOnMap(L.latLng(52.537525, 13.365224));
766 | userClickOnMap(L.latLng(52.537525, 13.375224));
767 | userClickOnMap(L.latLng(52.547525, 13.375224));
768 | userClickOnMap(L.latLng(52.547525, 13.365224));
769 | const nmeaData =
770 | "$GPGGA,215909.285,5232.252,N,01321.913,E,1,12,1.0,0.0,M,0.0,M,,*6E\n" +
771 | "$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30\n" +
772 | "$GPRMC,215909.285,A,5232.252,N,01321.913,E,1314.7,090.0,251216,000.0,W*40\n" +
773 | "$GPGGA,215910.285,5232.252,N,01322.513,E,1,12,1.0,0.0,M,0.0,M,,*69\n" +
774 | "$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30\n" +
775 | "$GPRMC,215910.285,A,5232.252,N,01322.513,E,2161.5,000.0,251216,000.0,W*4F\n" +
776 | "$GPGGA,215911.285,5232.852,N,01322.513,E,1,12,1.0,0.0,M,0.0,M,,*62\n" +
777 | "$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30\n" +
778 | "$GPRMC,215911.285,A,5232.852,N,01322.513,E,1314.4,270.0,251216,000.0,W*43\n" +
779 | "$GPGGA,215912.285,5232.852,N,01321.913,E,1,12,1.0,0.0,M,0.0,M,,*6E\n" +
780 | "$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0*30\n" +
781 | "$GPRMC,215912.285,A,5232.852,N,01321.913,E,1314.4,270.0,251216,000.0,W*4F\n";
782 | failIfConditionIsFalse(generateNmeaData() === nmeaData);
783 | }
784 |
785 | function runTests() {
786 | const tests = {
787 | "Configuration parameters": testConfiguration,
788 | "Tool selection": testSelectTool,
789 | "Add points": testAddPoints,
790 | "Add points, delete points": testAddPointsDeletePoints,
791 | "Add points, move points": testAddPointsMovePoints,
792 | "Add points, edit points": testAddPointsEditPoints,
793 | "Add points, get point information": testAddPointsPointInfo,
794 | "Add multi-point line": testAddMultiPointLine,
795 | "Add points, undo all": testAddPointsUndoAll,
796 | "Add points, undo all, redo all": testAddPointsUndoAllRedoAll,
797 | "Add points, undo all, add point": testAddPointsUndoAllAddPoint,
798 | "Move mouse cursor, check preview points": testMoveMouseCursor,
799 | "Do multiple things, undo all": testMultipleActionsUndoAll,
800 | "Innocuous actions on empty map": testInnocuousActionsOnEmptyMap,
801 | "Load nmea data": testLoadNmeaData,
802 | "Load CSV data": testLoadCsvData,
803 | "Load nmea data with corner cases": testLoadNmeaDataCornerCases,
804 | "Generate nmea data": testGenerateNmeaData
805 | };
806 |
807 | for (const [testName, testFunction] of Object.entries(tests)) {
808 | try {
809 | testFunction();
810 | tearDown();
811 | console.log("[PASS] " + testName + ".");
812 | } catch (error) {
813 | console.error("[FAIL] " + testName + " (" + error + ").");
814 | console.error(error.stack);
815 | break;
816 | }
817 | }
818 | }
819 |
--------------------------------------------------------------------------------