├── .fixpackrc ├── .github └── workflows │ ├── conventional-pr-title.yml │ └── main.yml ├── .gitignore ├── .husky ├── commit-msg ├── post-checkout ├── post-merge ├── post-rebase └── pre-commit ├── .nvmrc ├── .prettierrc.js ├── .svgrrc ├── .whitesource ├── CHANGELOG.md ├── DEVELOP.md ├── LICENSE ├── README.md ├── __mocks__ ├── mapbox-gl.js └── resize-observer-polyfill.js ├── babel.config.js ├── commitlint.config.js ├── data ├── topic1.js └── topic2.js ├── doc ├── README.md └── doc-config.json ├── package.json ├── pull_request_template.md ├── renovate.json ├── scripts └── read-pkg-json.js ├── src ├── components │ ├── BaseLayerSwitcher │ │ ├── BaseLayerSwitcher.js │ │ ├── BaseLayerSwitcher.md.scss │ │ ├── BaseLayerSwitcher.scss │ │ ├── BaseLayerSwitcher.test.js │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── BaseLayerSwitcher.test.js.snap │ │ └── index.js │ ├── BasicMap │ │ ├── BasicMap.js │ │ ├── BasicMap.md.scss │ │ ├── BasicMap.test.js │ │ ├── README.md │ │ └── index.js │ ├── CanvasSaveButton │ │ ├── CanvasSaveButton.js │ │ ├── CanvasSaveButton.md.scss │ │ ├── CanvasSaveButton.test.js │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── CanvasSaveButton.test.js.snap │ │ └── index.js │ ├── Copyright │ │ ├── Copyright.js │ │ ├── Copyright.md.scss │ │ ├── Copyright.test.js │ │ ├── README.md │ │ └── index.js │ ├── FeatureExportButton │ │ ├── FeatureExportButton.js │ │ ├── FeatureExportButton.md.scss │ │ ├── FeatureExportButton.test.js │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── FeatureExportButton.test.js.snap │ │ └── index.js │ ├── FitExtent │ │ ├── FitExtent.js │ │ ├── FitExtent.md.scss │ │ ├── FitExtent.test.js │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── FitExtent.test.js.snap │ │ └── index.js │ ├── Geolocation │ │ ├── Geolocation.js │ │ ├── Geolocation.md.scss │ │ ├── Geolocation.scss │ │ ├── Geolocation.test.js │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── Geolocation.test.js.snap │ │ └── index.js │ ├── LayerTree │ │ ├── LayerTree.js │ │ ├── LayerTree.md.scss │ │ ├── LayerTree.scss │ │ ├── LayerTree.test.js │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── LayerTree.test.js.snap │ │ └── index.js │ ├── MousePosition │ │ ├── MousePosition.js │ │ ├── MousePosition.md.scss │ │ ├── MousePosition.test.js │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── MousePosition.test.js.snap │ │ └── index.js │ ├── NorthArrow │ │ ├── NorthArrow.js │ │ ├── NorthArrow.scss │ │ ├── NorthArrow.test.js │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── NorthArrow.test.js.snap │ │ └── index.js │ ├── Overlay │ │ ├── Overlay.js │ │ ├── Overlay.md.scss │ │ ├── Overlay.scss │ │ ├── Overlay.test.js │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── Overlay.test.js.snap │ │ └── index.js │ ├── Permalink │ │ ├── Permalink.js │ │ ├── Permalink.md.scss │ │ ├── Permalink.test.js │ │ ├── README.md │ │ └── index.js │ ├── Popup │ │ ├── Popup.js │ │ ├── Popup.md.scss │ │ ├── Popup.scss │ │ ├── Popup.test.js │ │ ├── README.md │ │ ├── __snapshots__ │ │ │ └── Popup.test.js.snap │ │ └── index.js │ ├── README.md │ ├── ResizeHandler │ │ ├── ResizeHandler.js │ │ ├── ResizeHandler.test.js │ │ └── index.js │ ├── RouteSchedule │ │ ├── README.md │ │ ├── RouteSchedule.js │ │ ├── RouteSchedule.md.scss │ │ ├── RouteSchedule.scss │ │ ├── RouteSchedule.test.js │ │ ├── __snapshots__ │ │ │ └── RouteSchedule.test.js.snap │ │ └── index.js │ ├── ScaleLine │ │ ├── README.md │ │ ├── ScaleLine.js │ │ ├── ScaleLine.scss │ │ ├── ScaleLine.test.js │ │ ├── __snapshots__ │ │ │ └── ScaleLine.test.js.snap │ │ └── index.js │ ├── StopsFinder │ │ ├── README.md │ │ ├── StopsFinder.js │ │ ├── StopsFinder.test.js │ │ ├── StopsFinderOption.js │ │ ├── __snapshots__ │ │ │ └── StopsFinder.test.js.snap │ │ └── index.js │ └── Zoom │ │ ├── README.md │ │ ├── Zoom.js │ │ ├── Zoom.md.scss │ │ ├── Zoom.scss │ │ ├── Zoom.test.js │ │ ├── __snapshots__ │ │ └── Zoom.test.js.snap │ │ └── index.js ├── images │ ├── RouteSchedule │ │ ├── firstStation.png │ │ ├── lastStation.png │ │ ├── line.png │ │ └── station.png │ ├── baselayer │ │ ├── baselayer.basebright.png │ │ ├── baselayer.osm.png │ │ ├── baselayer.travic.png │ │ ├── open.topo.map.png │ │ ├── osm.baselayer.hot.png │ │ └── osm.baselayer.png │ ├── favicon.png │ ├── geops_logo.png │ ├── geops_logo.svg │ ├── geops_qr.png │ ├── mots │ │ ├── bus_poi-blue-01.svg │ │ ├── bus_poi-grey-01.svg │ │ ├── bus_round-blue-01.svg │ │ ├── bus_round-grey-01.svg │ │ ├── bus_square-blue-01.svg │ │ ├── bus_square-grey-01.svg │ │ ├── cable_car_poi-blue-01.svg │ │ ├── cable_car_poi-grey-01.svg │ │ ├── cable_car_round-blue-01.svg │ │ ├── cable_car_round-grey-01.svg │ │ ├── cable_car_square-blue-01.svg │ │ ├── cable_car_square-grey-01.svg │ │ ├── ferry_poi-blue-01.svg │ │ ├── ferry_poi-grey-01.svg │ │ ├── ferry_round-blue-01.svg │ │ ├── ferry_round-grey-01.svg │ │ ├── ferry_square-blue-01.svg │ │ ├── ferry_square-grey-01.svg │ │ ├── funicular_round-blue-01.svg │ │ ├── funicular_round-grey-01.svg │ │ ├── funicular_square-blue-01.svg │ │ ├── gondola_round-blue-01.svg │ │ ├── rail_poi-blue-01.svg │ │ ├── rail_poi-grey-01.svg │ │ ├── rail_round-blue-01.svg │ │ ├── rail_round-grey-01.svg │ │ ├── rail_square-blue-01.svg │ │ ├── rail_square-grey-01.svg │ │ ├── subway_round blue-01.svg │ │ ├── subway_round-blue-01.svg │ │ ├── tram_poi-blue-01.svg │ │ ├── tram_poi-grey-01.svg │ │ ├── tram_round-blue-01.svg │ │ ├── tram_round-grey-01.svg │ │ ├── tram_square-blue-01.svg │ │ └── tram_square-grey-01.svg │ ├── northArrow.svg │ ├── northArrow.url.svg │ ├── northArrowCircle.svg │ └── northArrowCircle.url.svg ├── propTypes.js ├── setupTests.js ├── styleguidist │ ├── ComponentsList.js │ ├── StyleGuide.js │ └── styleguidist.css ├── themes │ ├── README.md │ └── default │ │ ├── components.scss │ │ ├── examples.scss │ │ ├── index.scss │ │ ├── mixins.scss │ │ └── variables.scss └── utils │ ├── GlobalsForOle.js │ ├── KML.js │ ├── KML.test.js │ ├── Styles.js │ ├── __snapshots__ │ ├── KML.test.js.snap.KML-readFeatures()-and-writeFeatures()-should-read-and-write-lineDash-and-fillPattern-style-for-polygon.canvas-image.png │ ├── getPolygonPattern.test.js.snap.getPolygonPattern()-render-pattern-2-(cross)-color-and-(light-blue)-opacity.canvas-image.png │ ├── getPolygonPattern.test.js.snap.getPolygonPattern()-render-pattern-3-(diagonal-line-from-bottom-left-tot-top-right)-with-color-(light-blue)-and-opacity.canvas-image.png │ └── getPolygonPattern.test.js.snap.getPolygonPattern()-render-pattern-4-(diagonal-line-from-top-left-to-bottom-right)-with-color-(light-blue)-and-opacity.canvas-image.png │ ├── getPolygonPattern.js │ ├── getPolygonPattern.test.js │ ├── timeUtils.js │ └── timeUtils.test.js ├── styleguide.config.js ├── stylelint.config.js └── yarn.lock /.fixpackrc: -------------------------------------------------------------------------------- 1 | { 2 | "sortToTop": [ 3 | "name", 4 | "license", 5 | "description", 6 | "version", 7 | "author", 8 | "homepage", 9 | "main", 10 | "module", 11 | "files", 12 | "exports", 13 | "proxy", 14 | "dependencies", 15 | "peerDependencies", 16 | "devDependencies", 17 | "resolutions", 18 | "scripts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/conventional-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Check PR title 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - reopened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: aslafy-z/conventional-pr-title-action@v3 15 | with: 16 | success-state: Title follows the specification. 17 | failure-state: Title does not follow the specification. 18 | context-name: conventional-pr-title 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version-file: '.nvmrc' 21 | 22 | - name: Run linting and tests 23 | run: | 24 | yarn install --frozen-lockfile 25 | yarn lint 26 | yarn test 27 | env: 28 | CI: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | .yalc 4 | yalc.lock 5 | .vscode 6 | styleguide 7 | coverage 8 | doc/build 9 | styleguide-build 10 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | yarn commitlint --edit $1 4 | -------------------------------------------------------------------------------- /.husky/post-checkout: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | [ -n "$CI" ] && exit 0 3 | 4 | yarn install --frozen-lockfile 5 | -------------------------------------------------------------------------------- /.husky/post-merge: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | yarn install --frozen-lockfile 4 | -------------------------------------------------------------------------------- /.husky/post-rebase: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | yarn install --frozen-lockfile 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sort && CI=true npx lint-staged 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /.svgrrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@svgr/plugin-jsx"] 3 | } -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | ########################################################## 2 | #### WhiteSource Integration configuration file #### 3 | ########################################################## 4 | 5 | # Configuration # 6 | #---------------# 7 | ws.repo.scan=true 8 | vulnerable.check.run.conclusion.level=failure 9 | -------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | # Develop 2 | 3 | ## Create new component 4 | 5 | Follow the [guidelines](https://github.com/geops/react-spatial/tree/master/src/components). 6 | 7 | ## Create new theme 8 | 9 | Follow the [guidelines](https://github.com/geops/react-spatial/tree/master/src/themes). 10 | 11 | ## Documentation 12 | 13 | We are using [react-styleguidist](https://react-styleguidist.js.org/). 14 | Documentation and examples are available [here](https://react-spatial.geops.de/). 15 | 16 | Build the doc: 17 | 18 | ```bash 19 | yarn doc 20 | ``` 21 | 22 | Run the doc on [`locahost:6060`](http://locahost:6060/): 23 | 24 | ```bash 25 | yarn start 26 | ``` 27 | 28 | ## Tests 29 | 30 | We are using [jest]([https://react-styleguidist.js.org/](https://jestjs.io/docs/en/getting-started.html)) and [enzyme]([https://github.com/airbnb/enzyme](https://airbnb.io/enzyme/)). 31 | 32 | Run the tests in watch mode: 33 | 34 | ```bash 35 | yarn test --watch 36 | ``` 37 | 38 | ## Coverage 39 | 40 | Run coverage: 41 | 42 | ```bash 43 | yarn coverage 44 | ``` 45 | 46 | Then open the file `coverage/lcov-report/index.html` in your browser. 47 | 48 | ## Publish on [npmjs.com](https://www.npmjs.com/package/react-spatial) 49 | 50 | Run publish: 51 | 52 | ```bash 53 | publish:public 54 | ``` 55 | You need to enter the new version number in the command line. 56 | Then the new version must be published on [npmjs.com](https://www.npmjs.com/package/react-spatial). 57 | 58 | ## Publish a development version on [npmjs.com](https://www.npmjs.com/package/react-spatial) 59 | 60 | This version WILL NOT be displayed to other in [npmjs.com](https://www.npmjs.com/package/react-spatial). 61 | 62 | Run publish: 63 | 64 | ```bash 65 | publish:beta 66 | ``` 67 | 68 | You need to enter the new version number in the command line. 69 | Append `-beta.0` to the current version or increase the beta number. 70 | Then the new version must be published on [npmjs.com](https://www.npmjs.com/package/react-spatial) with the tag beta. 71 | 72 | ## How to use SVG 73 | 74 | Before adding an SVG file in this folder please clean it using [svgo](https://www.npmjs.com/package/svgo) or [svgomg](https://jakearchibald.github.io/svgomg/). 75 | 76 | After optimization, verify you can use it Openlayers using this [code sandbox](https://codesandbox.io/s/5w5o4mqwlk). 77 | 78 | By default the svg loader will load the svg file as a react component if you want to load svg file as a base64 data URI use the extension .url.svg : 79 | 80 | ```javascript 81 | // Import as a React component: 82 | import NorthArrow from 'northArrow.svg'; 83 | // Use: 84 | 85 | // Import as a base64 data URI (or with an url the file is too big): 86 | import northArrow from 'northArrow.url.svg'; 87 | // northArrow = "data:image/svg+xml;base64,PHN2ZyB4...." 88 | ``` 89 | 90 | You could also use others loaders (ex: svg-inline-loader , svg-url-loader ...) . 91 | To use these loaders just adapt the extension of the svg file and add the loader config in styleguide.config.js, then you will be able to use it like this : 92 | 93 | ```javascript 94 | // Import as an inline svg: 95 | import northArrow from 'northArrow.svginline.svg'; 96 | // northArrow = " ... " 97 | 98 | // Import as an encoded inline svg data URI (or with an url the file is too big): 99 | import northArrow from 'northArrow.svgurl.svg'; 100 | // northArrow = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='192' ...%3E ... %3C/svg%3E" 101 | ``` 102 | 103 | ## How to use SVG with a dynamic size 104 | 105 | In case you want your SVG fits perfectly his parent div you need to remove `width` and `height` attributes of `` and replace it by a `viewBox` property, like this: 106 | 107 | Replace 108 | 109 | `` 110 | 111 | by 112 | 113 | `` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 geOps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-spatial 2 | 3 | [![npm](https://img.shields.io/npm/v/react-spatial.svg?style=flat-square)](https://www.npmjs.com/package/react-spatial) 4 | [![build](https://github.com/geops/react-spatial/workflows/main/badge.svg)](https://github.com/geops/react-spatial/actions?query=workflow%3Amain) 5 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 6 | ![Vercel](https://vercelbadge.vercel.app/api/geops/react-spatial) 7 | 8 | This library provides React components to build web applications and to visualize real-time geographical information based on [OpenLayers](https://openlayers.org/) and [Malibre GL](https://maplibre.org/maplibre-gl-js/). 9 | 10 | This library uses the [mobility-toolbox-js](https://mobility-toolbox-js.geops.io/) library. 11 | 12 | Documentation and examples at https://react-spatial.geops.io. 13 | 14 | ## Getting Started 15 | 16 | Install the [react-spatial](https://www.npmjs.com/package/react-spatial) package: 17 | 18 | ```bash 19 | yarn add mobility-toolbox-js mapbox-gl mapblibre-gl ol react-spatial 20 | ``` 21 | 22 | Your build pipeline needs to support ES6 modules and SASS. 23 | 24 | Import the main scss file in your project: 25 | 26 | ```bash 27 | import 'react-spatial/themes/default/index.scss'; 28 | ``` 29 | 30 | ## More 31 | 32 | - [Development](https://github.com/geops/react-spatial/tree/master/DEVELOP.md) 33 | - [Components](https://github.com/geops/react-spatial/tree/master/src/components) 34 | - [Themes](https://github.com/geops/react-spatial/tree/master/src/themes) 35 | 36 | ## Bugs 37 | 38 | Please use the [GitHub issue tracker](https://github.com/geops/react-spatial/issues) for all bugs and feature requests. Before creating a new issue, do a quick search to see if the problem has been reported already. 39 | -------------------------------------------------------------------------------- /__mocks__/mapbox-gl.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | class Map { 3 | isStyleLoaded() {} 4 | 5 | getCanvas() {} 6 | 7 | once() {} 8 | } 9 | module.exports = { 10 | Map, 11 | }; 12 | -------------------------------------------------------------------------------- /__mocks__/resize-observer-polyfill.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default class ResizeObserver { 3 | constructor(onResize) { 4 | ResizeObserver.onResize = onResize; 5 | } 6 | observe() {} 7 | unobserve() {} 8 | disconnect() {} 9 | } 10 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@babel/preset-env", "@babel/preset-react"], 3 | }; 4 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /data/topic1.js: -------------------------------------------------------------------------------- 1 | import Style from 'ol/style/Style'; 2 | import Circle from 'ol/style/Circle'; 3 | import Stroke from 'ol/style/Stroke'; 4 | import Fill from 'ol/style/Fill'; 5 | 6 | const topic1 = { 7 | id: 'topic1', 8 | name: 'Topic 1', 9 | visible: true, 10 | children: [ 11 | { 12 | name: 'OSM Baselayer', 13 | visible: true, 14 | isBaseLayer: true, 15 | data: { 16 | type: 'xyz', 17 | url: 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png', 18 | }, 19 | }, 20 | { 21 | name: 'Other layers', 22 | visible: true, 23 | type: 'parent', 24 | children: [ 25 | { 26 | name: 'Countries Borders', 27 | visible: false, 28 | data: { 29 | type: 'vectorLayer', 30 | url: 31 | 'https://openlayers.org/en/latest/examples/data/geojson/' + 32 | 'countries.geojson', 33 | }, 34 | }, 35 | { 36 | name: 'USA Population Density', 37 | visible: true, 38 | data: { 39 | type: 'wmts', 40 | url: 41 | 'https://services.arcgisonline.com/arcgis/rest/services/' + 42 | 'Demographics/USA_Population_Density/MapServer/WMTS/?layer=0' + 43 | '&style=default&tilematrixset=EPSG%3A3857&Service=WMTS&' + 44 | 'Request=GetTile&Version=1.0.0&Format=image%2Fpng&', 45 | projection: 'EPSG:3857', 46 | }, 47 | }, 48 | ], 49 | }, 50 | { 51 | name: 'Vector sample layers', 52 | visible: true, 53 | radioGroup: 'radio', 54 | children: [ 55 | { 56 | name: 'Points Samples', 57 | radioGroup: 'vectorLayers', 58 | visible: false, 59 | data: { 60 | style: new Style({ 61 | image: new Circle({ 62 | radius: 5, 63 | fill: new Fill({ 64 | color: '#ff0000', 65 | }), 66 | }), 67 | }), 68 | type: 'vectorLayer', 69 | url: 70 | 'https://raw.githubusercontent.com/openlayers/openlayers/' + 71 | '3c64018b3754cf605ea19cbbe4c8813304da2539/examples/data/geojson/' + 72 | 'point-samples.geojson', 73 | }, 74 | }, 75 | { 76 | name: 'Lines Samples', 77 | radioGroup: 'vectorLayers', 78 | visible: true, 79 | data: { 80 | style: new Style({ 81 | stroke: new Stroke({ 82 | color: '#ffcc33', 83 | width: 2, 84 | }), 85 | }), 86 | type: 'vectorLayer', 87 | url: 88 | 'https://raw.githubusercontent.com/openlayers/openlayers/' + 89 | '3c64018b3754cf605ea19cbbe4c8813304da2539/examples/data/geojson/' + 90 | 'line-samples.geojson', 91 | }, 92 | }, 93 | { 94 | name: 'Polygons Samples', 95 | radioGroup: 'vectorLayers', 96 | visible: false, 97 | data: { 98 | style: new Style({ 99 | stroke: new Stroke({ 100 | color: '#7dff8f', 101 | width: 3, 102 | }), 103 | fill: new Fill({ 104 | color: 'rgba(125, 255, 143, 0.2)', 105 | }), 106 | }), 107 | type: 'vectorLayer', 108 | url: 109 | 'https://raw.githubusercontent.com/openlayers/openlayers/' + 110 | '3c64018b3754cf605ea19cbbe4c8813304da2539/examples/data/geojson/' + 111 | 'polygon-samples.geojson', 112 | }, 113 | }, 114 | ], 115 | }, 116 | ], 117 | }; 118 | 119 | export default topic1; 120 | -------------------------------------------------------------------------------- /data/topic2.js: -------------------------------------------------------------------------------- 1 | const topic2 = { 2 | id: 'topic2', 3 | name: 'Topic 2', 4 | visible: false, 5 | children: [ 6 | { 7 | name: 'OSM Baselayer', 8 | visible: true, 9 | isBaseLayer: true, 10 | data: { 11 | type: 'xyz', 12 | url: 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png', 13 | }, 14 | }, 15 | { 16 | name: 'Countries Borders', 17 | visible: true, 18 | data: { 19 | type: 'vectorLayer', 20 | url: 21 | 'https://openlayers.org/en/latest/examples/data/geojson/' + 22 | 'countries.geojson', 23 | }, 24 | }, 25 | ], 26 | }; 27 | 28 | export default topic2; 29 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # Documentation in react-spatial 2 | 3 | Two documentation tools are combined: 4 | - react-styleguidist (https://github.com/styleguidist/react-styleguidist) 5 | - documentation.js (https://github.com/documentationjs/documentation) 6 | 7 | the folder published by netlify is doc/build (netlify.toml). 8 | 9 | ## Commands 10 | 11 | Styleguidist documentation is visible (with hot reload) with the cmd: 12 | 'yarn start' 13 | 14 | ## Configuration 15 | 16 | Variables shared among both documentation are written in /doc/doc-config.json 17 | 18 | - Styleguidist: 'styleguide.config.js 19 | one component need to be overwritten: 20 | - 'StyleGuideRenderer': to customize the style of the page, with our own 21 | header, ribbon footer, etc. 22 | -------------------------------------------------------------------------------- /doc/doc-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "react-spatial", 3 | "githubRepo": "https://github.com/geops/react-spatial" 4 | } 5 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | # How to 2 | 3 | 4 | 5 | # Others 6 | 7 | 8 | 9 | - [ ] It's not a hack or at least an unauthorized hack :). 10 | - [ ] The images added are optimized. 11 | - [ ] Everything in ticket description has been fixed. 12 | - [ ] The author of the MR has made its own review before assigning the reviewer. 13 | - [ ] The title means something for a human being and follows the [conventional commits](https://www.conventionalcommits.org/) specification. 14 | - [ ] The title contains [WIP] if it's necessary. 15 | - [ ] Labels applied. if it's a release? a hotfix? 16 | - [ ] Tests added. 17 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:js-lib", "schedule:monthly"], 3 | "assignees": ["@friedjoff"] 4 | } 5 | -------------------------------------------------------------------------------- /scripts/read-pkg-json.js: -------------------------------------------------------------------------------- 1 | const pjson = require('../package.json'); 2 | 3 | const { peerDependencies } = pjson; 4 | 5 | const packageKeys = Object.keys(peerDependencies); 6 | 7 | const arg = process.argv[2]; 8 | 9 | if (arg === 'add') { 10 | console.log(`yarn install --force`); 11 | } else if (arg === 'remove') { 12 | console.log( 13 | `rm -rf ${packageKeys.map((p) => `node_modules/${p}`).join(' ')}`, 14 | ); 15 | } else { 16 | console.log('echo "wrong argument."'); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/BaseLayerSwitcher/BaseLayerSwitcher.md.scss: -------------------------------------------------------------------------------- 1 | .rs-base-layer-example { 2 | position: relative; 3 | 4 | .rs-base-layer-switcher { 5 | position: absolute; 6 | bottom: 10px; 7 | left: 10px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/BaseLayerSwitcher/BaseLayerSwitcher.scss: -------------------------------------------------------------------------------- 1 | .rs-base-layer-switcher { 2 | position: relative; 3 | width: 100px; 4 | transition: 800ms width; 5 | overflow: hidden; 6 | display: flex; 7 | align-items: center; 8 | padding: 2px; 9 | pointer-events: none; 10 | 11 | &.rs-open { 12 | width: 600px; 13 | } 14 | 15 | .rs-base-layer-switcher-button { 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | justify-content: flex-end; 20 | height: 80px; 21 | width: 100px; 22 | min-width: 100px; 23 | margin-right: 4px; 24 | background-color: rgb(197 197 197); 25 | border: 2px solid white; 26 | box-sizing: border-box; 27 | pointer-events: auto; 28 | 29 | &.rs-opener { 30 | position: absolute; 31 | top: 2px; /* For IE */ 32 | left: 2px; /* For IE */ 33 | opacity: 1; 34 | z-index: 100; 35 | transition: 700ms opacity, 1000ms z-index; 36 | 37 | &.rs-open { 38 | opacity: 0; 39 | z-index: 0; 40 | } 41 | } 42 | } 43 | 44 | .rs-alt-text { 45 | flex-grow: 1; 46 | display: flex; 47 | justify-content: center; 48 | text-align: center; 49 | align-items: center; 50 | max-width: 80px; 51 | overflow: hidden; 52 | } 53 | 54 | .rs-base-layer-switcher-btn-wrapper { 55 | display: flex; 56 | align-items: center; 57 | } 58 | 59 | .rs-base-layer-switcher-close-btn { 60 | display: flex; 61 | flex-direction: column; 62 | justify-content: center; 63 | align-items: center; 64 | background-color: white; 65 | width: 24px; 66 | height: 24px; 67 | border-radius: 50px; 68 | pointer-events: auto; 69 | } 70 | 71 | .rs-open { 72 | &:focus, 73 | &:hover { 74 | outline: none; 75 | border: 2px solid red; 76 | } 77 | } 78 | 79 | .rs-base-layer-switcher-title { 80 | width: 100%; 81 | max-height: 12px; 82 | font-size: 10px; 83 | padding: 2px 0; 84 | color: white; 85 | background-color: rgb(0 0 0 / 70%); 86 | text-align: center; 87 | 88 | &.rs-active { 89 | font-weight: bold; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/components/BaseLayerSwitcher/BaseLayerSwitcher.test.js: -------------------------------------------------------------------------------- 1 | import { fireEvent, render } from "@testing-library/react"; 2 | import { Layer } from "mobility-toolbox-js/ol"; 3 | import React from "react"; 4 | 5 | import BaseLayerSwitcher from "./BaseLayerSwitcher"; 6 | 7 | describe("BaseLayerSwitcher", () => { 8 | let layers; 9 | const layerImages = { 10 | layerBar: "bar", 11 | layerFoo: "foo", 12 | layerFoobar: "foobar", 13 | }; 14 | 15 | beforeEach(() => { 16 | layers = [ 17 | new Layer({ 18 | name: "bl1", 19 | }), 20 | new Layer({ 21 | name: "bl2", 22 | visible: false, 23 | }), 24 | new Layer({ 25 | name: "bl3", 26 | visible: false, 27 | }), 28 | ]; 29 | }); 30 | 31 | describe("matches snapshots", () => { 32 | test("using default properties.", () => { 33 | const { container } = render( 34 | , 35 | ); 36 | expect(container.innerHTML).toMatchSnapshot(); 37 | }); 38 | }); 39 | 40 | test("the correct baselayer is visible on mount", () => { 41 | render(); 42 | expect(layers[0].visible).toBe(true); 43 | }); 44 | 45 | test("removes open class and switches layer on click", async () => { 46 | const { container } = render( 47 | , 48 | ); 49 | await fireEvent.click(container.querySelector(".rs-opener")); 50 | await fireEvent.click( 51 | container.querySelectorAll(".rs-base-layer-switcher-button")[3], 52 | ); 53 | expect(layers[2].visible).toBe(true); 54 | expect(!!container.querySelector(".rs-base-layer-switcher rs-open")).toBe( 55 | false, 56 | ); 57 | }); 58 | 59 | test("toggles base map instead of opening when only two base layers", async () => { 60 | const { container } = render( 61 | , 62 | ); 63 | expect(layers[0].visible).toBe(true); 64 | await fireEvent.click(container.querySelector(".rs-opener")); 65 | expect(layers[1].visible).toBe(true); 66 | expect(!!container.querySelector(".rs-base-layer-switcher rs-open")).toBe( 67 | false, 68 | ); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/components/BaseLayerSwitcher/README.md: -------------------------------------------------------------------------------- 1 | The following example demonstrates the use of BaseLayerSwitcher: 2 | 3 | ```jsx 4 | import React from 'react'; 5 | import Map from 'ol/Map'; 6 | import TileLayer from 'ol/layer/Tile'; 7 | import OSM from 'ol/source/OSM'; 8 | import { MapboxLayer, Layer } from 'mobility-toolbox-js/ol'; 9 | import BaseLayerSwitcher from 'react-spatial/components/BaseLayerSwitcher'; 10 | import BasicMap from 'react-spatial/components/BasicMap'; 11 | import osmImage from 'react-spatial/images/baselayer/baselayer.osm.png'; 12 | import travicImage from 'react-spatial/images/baselayer/baselayer.travic.png'; 13 | import basebrightImage from 'react-spatial/images/baselayer/baselayer.basebright.png'; 14 | 15 | const center = [1149722.7037660484, 6618091.313553318]; 16 | const map = new Map({ controls: [] }); 17 | const travicLayer = new MapboxLayer({ 18 | url: `https://maps.geops.io/styles/travic_v2/style.json?key=${apiKey}`, 19 | name: 'Travic', 20 | key: 'travic.baselayer', 21 | visible: true, 22 | }); 23 | 24 | const basebrightLayer = new MapboxLayer({ 25 | url: `https://maps.geops.io/styles/base_bright_v2/style.json?key=${apiKey}`, 26 | name: 'Base - Bright', 27 | key: 'basebright.baselayer', 28 | visible: false, 29 | }); 30 | 31 | const osmLayer = new Layer({ 32 | olLayer: new TileLayer({ 33 | source: new OSM(), 34 | }), 35 | name: 'OSM', 36 | key: 'osm.baselayer', 37 | visible: false, 38 | }); 39 | 40 | const layerImages = { 41 | 'travic.baselayer': travicImage, 42 | 'basebright.baselayer': basebrightImage, 43 | 'osm.baselayer': osmImage, 44 | }; 45 | 46 | const layers = [travicLayer, basebrightLayer, osmLayer]; 47 | 48 |
49 | 56 | 60 |
; 61 | ``` 62 | -------------------------------------------------------------------------------- /src/components/BaseLayerSwitcher/__snapshots__/BaseLayerSwitcher.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BaseLayerSwitcher matches snapshots using default properties. 1`] = ` 4 |
5 |
12 |
13 | Base layers 14 |
15 |
16 |
19 |
25 |
26 | bl1 27 |
28 | 29 | Source not found 30 | 31 |
32 |
33 |
36 |
42 |
43 | bl2 44 |
45 | 46 | Source not found 47 | 48 |
49 |
50 |
53 |
59 |
60 | bl3 61 |
62 | 63 | Source not found 64 | 65 |
66 |
67 |
73 | 81 | 82 | 83 | 84 |
85 |
86 | `; 87 | -------------------------------------------------------------------------------- /src/components/BaseLayerSwitcher/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./BaseLayerSwitcher"; 2 | -------------------------------------------------------------------------------- /src/components/BasicMap/BasicMap.md.scss: -------------------------------------------------------------------------------- 1 | .rs-map { 2 | position: relative; 3 | height: 300px; 4 | width: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/BasicMap/README.md: -------------------------------------------------------------------------------- 1 | 2 | The following example demonstrates the use of BasicMap. 3 | 4 | ```jsx 5 | import React from 'react'; 6 | import { MapboxLayer } from 'mobility-toolbox-js/ol'; 7 | import Tile from 'ol/layer/Tile'; 8 | import OSM from 'ol/source/OSM'; 9 | import BasicMap from 'react-spatial/components/BasicMap'; 10 | 11 | const layers = [ 12 | new MapboxLayer({ 13 | url: `https://maps.geops.io/styles/travic_v2/style.json?key=${apiKey}`, 14 | }) 15 | ]; 16 | 17 | ; 18 | ``` 19 | -------------------------------------------------------------------------------- /src/components/BasicMap/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./BasicMap"; 2 | -------------------------------------------------------------------------------- /src/components/CanvasSaveButton/CanvasSaveButton.md.scss: -------------------------------------------------------------------------------- 1 | .rs-canvas-save-button-example { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | 6 | button { 7 | display: flex; 8 | align-items: center; 9 | margin: 10px auto; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/CanvasSaveButton/README.md: -------------------------------------------------------------------------------- 1 | The following example demonstrates the use of CanvasSaveButton. 2 | 3 | ```jsx 4 | import React from 'react'; 5 | import { TiImage } from 'react-icons/ti'; 6 | import { geopsTheme } from '@geops/geops-ui'; 7 | import { ThemeProvider } from '@mui/material'; 8 | import Button from '@mui/material/Button'; 9 | import { Layer } from 'mobility-toolbox-js/ol'; 10 | import Tile from 'ol/layer/Tile'; 11 | import OSM from 'ol/source/OSM'; 12 | import Map from 'ol/Map'; 13 | import { toDegrees } from 'ol/math'; 14 | import CanvasSaveButton from 'react-spatial/components/CanvasSaveButton'; 15 | import BasicMap from 'react-spatial/components/BasicMap'; 16 | import geopsLogo from 'react-spatial/images/geops_logo.png'; 17 | import qrCode from 'react-spatial/images/geops_qr.png'; 18 | 19 | const map = new Map({ controls: [] }); 20 | 21 | const layers = [ 22 | new Layer({ 23 | olLayer: new Tile({ 24 | source: new OSM(), 25 | }), 26 | copyrights: '© layer-copyright', 27 | }), 28 | ]; 29 | 30 | 31 |
32 | 39 | { 44 | return layers[0].copyrights; 45 | }, 46 | background: true, 47 | }, 48 | northArrow: { 49 | rotation: () => { 50 | return toDegrees(map.getView().getRotation()); 51 | }, 52 | circled: true, 53 | }, 54 | logo: { 55 | src: geopsLogo, 56 | height: 22, 57 | width: 84, 58 | }, 59 | qrCode: { 60 | src: qrCode, 61 | height: 50, 62 | width: 50, 63 | }, 64 | }} 65 | > 66 | 67 | 68 |
69 |
70 | ``` 71 | -------------------------------------------------------------------------------- /src/components/CanvasSaveButton/__snapshots__/CanvasSaveButton.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CanvasSaveButton should match snapshot with a different attributes 1`] = ` 4 | 25 | `; 26 | 27 | exports[`CanvasSaveButton should match snapshot. 1`] = ` 28 | 49 | `; 50 | -------------------------------------------------------------------------------- /src/components/CanvasSaveButton/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./CanvasSaveButton"; 2 | -------------------------------------------------------------------------------- /src/components/Copyright/Copyright.js: -------------------------------------------------------------------------------- 1 | import { CopyrightControl } from "mobility-toolbox-js/ol"; 2 | import { Map } from "ol"; 3 | import PropTypes from "prop-types"; 4 | import React, { useEffect, useMemo, useState } from "react"; 5 | 6 | const propTypes = { 7 | /** 8 | * CSS class of th root element 9 | */ 10 | className: PropTypes.string, 11 | 12 | /** 13 | * Format function. Called with an array of copyrights from visible layers 14 | * and returns the copyright. 15 | */ 16 | format: PropTypes.func, 17 | 18 | /** 19 | * A map. 20 | */ 21 | map: PropTypes.instanceOf(Map).isRequired, 22 | }; 23 | 24 | const defaultProps = { 25 | className: "rs-copyright", 26 | format: (copyrights) => { 27 | return copyrights.join(" | "); 28 | }, 29 | }; 30 | 31 | /** 32 | * The Copyright component uses the 33 | * [mobility-toolbox-js CopyrightControl](https://mobility-toolbox-js.geops.io/api/class/src/mapbox/controls/CopyrightControl%20js~CopyrightControl%20html-offset-anchor) 34 | * to render the layer copyrights. 35 | */ 36 | function Copyright({ 37 | className = defaultProps.className, 38 | format = defaultProps.format, 39 | map, 40 | ...other 41 | }) { 42 | const [copyrights, setCopyrights] = useState([]); 43 | 44 | const control = useMemo( 45 | () => { 46 | return new CopyrightControl({ 47 | element: document.createElement("div"), 48 | render() { 49 | // eslint-disable-next-line react/no-this-in-sfc 50 | const newCopyrights = this.getCopyrights(); 51 | if (copyrights.toString() !== newCopyrights.toString()) { 52 | setCopyrights(newCopyrights); 53 | } 54 | }, 55 | target: document.createElement("div"), 56 | }); 57 | }, 58 | // eslint-disable-next-line react-hooks/exhaustive-deps 59 | [], 60 | ); 61 | 62 | // Ensure the control is not associated to the wrong map 63 | useEffect(() => { 64 | if (!control) { 65 | return () => {}; 66 | } 67 | 68 | control.map = map; 69 | 70 | return () => { 71 | control.map = null; 72 | }; 73 | }, [map, control]); 74 | 75 | if (!control || !control.getCopyrights().length) { 76 | return null; 77 | } 78 | 79 | return ( 80 |
89 | ); 90 | } 91 | 92 | Copyright.propTypes = propTypes; 93 | 94 | export default React.memo(Copyright); 95 | -------------------------------------------------------------------------------- /src/components/Copyright/Copyright.md.scss: -------------------------------------------------------------------------------- 1 | .rs-copyright-example { 2 | .rs-copyright { 3 | font-size: 11px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Copyright/Copyright.test.js: -------------------------------------------------------------------------------- 1 | import { act, render } from "@testing-library/react"; 2 | import "jest-canvas-mock"; 3 | import { Layer } from "mobility-toolbox-js/ol"; 4 | import { Map, View } from "ol"; 5 | import TileLayer from "ol/layer/Tile"; 6 | import TileSource from "ol/source/Tile"; 7 | import Tile from "ol/Tile"; 8 | import { createXYZ } from "ol/tilegrid"; 9 | import React from "react"; 10 | 11 | import Copyright from "./Copyright"; 12 | 13 | const image = new Image(); 14 | image.width = 256; 15 | image.height = 256; 16 | 17 | const tileLoadFunction = () => { 18 | const tile = new Tile([0, 0, -1], 2 /* LOADED */); 19 | tile.getImage = () => { 20 | return image; 21 | }; 22 | return tile; 23 | }; 24 | 25 | const getOLTileLayer = () => { 26 | const layer = new TileLayer({ 27 | source: new TileSource({ 28 | projection: "EPSG:3857", 29 | tileGrid: createXYZ(), 30 | }), 31 | }); 32 | layer.getSource().getTile = tileLoadFunction; 33 | return layer; 34 | }; 35 | 36 | const getLayer = (copyrights, visible = true) => { 37 | return new Layer({ 38 | copyrights, 39 | olLayer: getOLTileLayer(), 40 | visible, 41 | }); 42 | }; 43 | 44 | let layers; 45 | let map; 46 | 47 | describe("Copyright", () => { 48 | beforeEach(() => { 49 | const target = document.createElement("div"); 50 | document.body.appendChild(target); 51 | layers = [getLayer("bar"), getLayer("foo", false)]; 52 | map = new Map({ 53 | controls: [], 54 | layers: layers.map((layer) => { 55 | return layer.olLayer; 56 | }), 57 | target, 58 | view: new View({ 59 | center: [0, 0], 60 | zoom: 0, 61 | }), 62 | }); 63 | map.setSize([200, 200]); 64 | layers.forEach((layer) => { 65 | layer.attachToMap(map); 66 | }); 67 | act(() => { 68 | map.renderSync(); 69 | }); 70 | }); 71 | 72 | afterEach(() => { 73 | layers.forEach((layer) => { 74 | layer.detachFromMap(map); 75 | }); 76 | map.setTarget(null); 77 | map = null; 78 | }); 79 | 80 | test("is empty if no layers are visible", () => { 81 | const { container } = render(); 82 | expect(container.innerHTML).toBe(""); 83 | }); 84 | 85 | test("displays one copyright", () => { 86 | const { container } = render(); 87 | act(() => { 88 | map.renderSync(); 89 | }); 90 | expect(container.textContent).toBe("bar"); 91 | }); 92 | 93 | test("displays 2 copyrights", () => { 94 | const { container } = render(); 95 | layers[0].visible = true; 96 | layers[1].visible = true; 97 | act(() => { 98 | map.renderSync(); 99 | }); 100 | act(() => { 101 | map.renderSync(); 102 | }); 103 | 104 | expect(container.textContent).toBe("bar | foo"); 105 | }); 106 | 107 | test("displays a copyright using a custom format", () => { 108 | const { container } = render( 109 | { 111 | return `Number of copyrights: ${copyrights.length}`; 112 | }} 113 | map={map} 114 | />, 115 | ); 116 | 117 | act(() => { 118 | map.renderSync(); 119 | }); 120 | 121 | expect(container.textContent).toBe("Number of copyrights: 1"); 122 | }); 123 | 124 | test("set a custom className", () => { 125 | const { container } = render(); 126 | 127 | act(() => { 128 | map.renderSync(); 129 | }); 130 | 131 | expect(container.querySelectorAll(".foo").length).toBe(1); 132 | }); 133 | 134 | test("set a custom attribute to the root element", () => { 135 | const { container } = render( 136 | , 137 | ); 138 | 139 | act(() => { 140 | map.renderSync(); 141 | }); 142 | 143 | expect(container.querySelectorAll("[foo]").length).toBe(1); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /src/components/Copyright/README.md: -------------------------------------------------------------------------------- 1 | The following example demonstrates the use of Copyright. 2 | 3 | ```js 4 | import React from 'react'; 5 | import Map from 'ol/Map'; 6 | import Tile from 'ol/layer/Tile'; 7 | import OSM from 'ol/source/OSM'; 8 | import { defaults } from 'ol/control'; 9 | import { Layer, MapboxLayer } from 'mobility-toolbox-js/ol'; 10 | import BasicMap from 'react-spatial/components/BasicMap'; 11 | import Copyright from 'react-spatial/components/Copyright'; 12 | 13 | const map = new Map({ 14 | controls: defaults({ 15 | attribution: false, 16 | }), 17 | }); 18 | 19 | const layers = [ 20 | new MapboxLayer({ 21 | url: `https://maps.geops.io/styles/base_bright_v2/style.json?key=${window.apiKey}`, 22 | }), 23 | new Layer({ 24 | copyrights: '© My custom copyright for OSM Contributors', 25 | olLayer: new Tile({ 26 | source: new OSM(), 27 | }), 28 | }), 29 | ]; 30 | window.layers = layers; 31 | 32 |
33 | 34 | 35 |
; 36 | ``` 37 | -------------------------------------------------------------------------------- /src/components/Copyright/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Copyright"; 2 | -------------------------------------------------------------------------------- /src/components/FeatureExportButton/FeatureExportButton.js: -------------------------------------------------------------------------------- 1 | import { Layer } from "mobility-toolbox-js/ol"; 2 | import KMLFormat from "ol/format/KML"; 3 | import PropTypes from "prop-types"; 4 | import React, { PureComponent } from "react"; 5 | 6 | import KML from "../../utils/KML"; 7 | 8 | const propTypes = { 9 | /** 10 | * Children content of the Feature export button. 11 | */ 12 | children: PropTypes.node, 13 | 14 | /** 15 | * Format to export features (function). 16 | * Supported formats: https://openlayers.org/en/latest/apidoc/module-ol_format_Feature-FeatureFormat.html 17 | */ 18 | format: PropTypes.func, 19 | 20 | /** 21 | * An existing [mobility-toolbox-js Layer](https://mobility-toolbox-js.geops.io/api/identifiers%20html#ol-layers), 22 | * using a valid [ol/source/Vector](https://openlayers.org/en/latest/apidoc/module-ol_source_Vector.html) 23 | */ 24 | layer: PropTypes.instanceOf(Layer).isRequired, 25 | 26 | /** 27 | * Map projection. 28 | */ 29 | projection: PropTypes.string, 30 | }; 31 | 32 | const defaultProps = { 33 | children: null, 34 | format: KMLFormat, 35 | projection: "EPSG:3857", 36 | }; 37 | 38 | /** 39 | * The FeatureExportButton component creates a button that exports feature geometries 40 | * from a [[mobility-toolbox-js Layer](https://mobility-toolbox-js.geops.io/api/identifiers%20html#ol-layers)] 41 | * containing an [ol/layer/Vector](https://openlayers.org/en/latest/apidoc/module-ol_layer_Vector-VectorLayer.html) 42 | * with a [ol/source/Vector](https://openlayers.org/en/latest/apidoc/module-ol_source_Vector.html) on click.
43 | * The default export format is KML, which supports the features' style export.
44 | * Other formats do not always support style export (See specific format specs). 45 | */ 46 | class FeatureExportButton extends PureComponent { 47 | static createFeatureString(layer, projection, format) { 48 | if (format === KMLFormat) { 49 | return KML.writeFeatures(layer, projection); 50 | } 51 | 52 | // eslint-disable-next-line new-cap 53 | return new format().writeFeatures(layer.olLayer.getSource().getFeatures(), { 54 | featureProjection: projection, 55 | }); 56 | } 57 | 58 | static exportFeatures(layer, projection, format) { 59 | const now = new Date() 60 | .toJSON() 61 | .slice(0, 20) 62 | .replace(/[.:T-]+/g, ""); 63 | const featString = this.createFeatureString(layer, projection, format); 64 | 65 | const formatString = featString 66 | ? featString.match(/<(\w+)\s+\w+.*?>/)[1] 67 | : "xml"; 68 | 69 | const fileName = `exported_features_${now}.${formatString}`; 70 | const charset = document.characterSet || "UTF-8"; 71 | const type = `${ 72 | formatString === "kml" 73 | ? "data:application/vnd.google-earth.kml+xml" 74 | : "data:text/xml" 75 | };charset=${charset}`; 76 | 77 | if (featString) { 78 | if (window.navigator.msSaveBlob) { 79 | // ie 11 and higher 80 | window.navigator.msSaveBlob(new Blob([featString], { type }), fileName); 81 | } else { 82 | const link = document.createElement("a"); 83 | link.download = fileName; 84 | link.href = `${type},${encodeURIComponent(featString)}`; 85 | link.click(); 86 | } 87 | } 88 | } 89 | 90 | render() { 91 | const { children, format, layer, projection, ...other } = this.props; 92 | 93 | return ( 94 |
{ 101 | return FeatureExportButton.exportFeatures(layer, projection, format); 102 | }} 103 | onKeyPress={(evt) => { 104 | return ( 105 | evt.which === 13 && 106 | FeatureExportButton.exportFeatures(layer, projection, format) 107 | ); 108 | }} 109 | > 110 | {children} 111 |
112 | ); 113 | } 114 | } 115 | 116 | FeatureExportButton.propTypes = propTypes; 117 | FeatureExportButton.defaultProps = defaultProps; 118 | 119 | export default FeatureExportButton; 120 | -------------------------------------------------------------------------------- /src/components/FeatureExportButton/FeatureExportButton.md.scss: -------------------------------------------------------------------------------- 1 | .rs-feature-export-example { 2 | .rs-feature-export-example-btns { 3 | display: flex; 4 | justify-content: space-evenly; 5 | 6 | .rs-feature-export-button { 7 | margin: 15px; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/FeatureExportButton/README.md: -------------------------------------------------------------------------------- 1 | 2 | The following example demonstrates the use of FeatureExportButton. 3 | 4 | ```jsx 5 | import React from 'react'; 6 | import { Layer, MapboxLayer } from 'mobility-toolbox-js/ol'; 7 | import VectorLayer from 'ol/layer/Vector'; 8 | import VectorSource from 'ol/source/Vector'; 9 | import Feature from 'ol/Feature'; 10 | import Point from 'ol/geom/Point'; 11 | import Circle from 'ol/geom/Circle'; 12 | import { Icon, Style,Stroke,Fill,Circle as CircleStyle } from 'ol/style'; 13 | import GPX from 'ol/format/GPX'; 14 | import { geopsTheme, Header, Footer } from '@geops/geops-ui'; 15 | import { ThemeProvider } from '@mui/material'; 16 | import Button from '@mui/material/Button'; 17 | import BasicMap from 'react-spatial/components/BasicMap'; 18 | import FeatureExportButton from 'react-spatial/components/FeatureExportButton'; 19 | 20 | 21 | const vectorLayer = new Layer({ 22 | olLayer: new VectorLayer({ 23 | style: new Style({ 24 | image: new Icon({ 25 | anchor: [0.5, 46], 26 | anchorXUnits: 'fraction', 27 | anchorYUnits: 'pixels', 28 | src: 'https://openlayers.org/en/latest/examples/data/icon.png', 29 | size: [32, 48] 30 | }), 31 | }), 32 | source: new VectorSource({ 33 | features: [ 34 | new Feature({ 35 | geometry: new Point([819103.972418, 6120013.078324]), 36 | }), 37 | ], 38 | }), 39 | }), 40 | }); 41 | 42 | const layers = [ 43 | new MapboxLayer({ 44 | url: `https://maps.geops.io/styles/travic_v2/style.json?key=${apiKey}`, 45 | }), 46 | vectorLayer, 47 | ]; 48 | 49 | 50 |
51 | 57 |
58 | 59 | 62 | 63 | 67 | 70 | 71 |
72 |
73 |
74 | ``` 75 | -------------------------------------------------------------------------------- /src/components/FeatureExportButton/__snapshots__/FeatureExportButton.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FeatureExportButton should match snapshot should match snapshot with children passed. 1`] = ` 4 |
11 |
12 | Foo 13 |
14 |
15 | `; 16 | 17 | exports[`FeatureExportButton should match snapshot should match snapshot with cutom attributes. 1`] = ` 18 |
26 | `; 27 | 28 | exports[`FeatureExportButton should match snapshot with default attributes. 1`] = ` 29 |
36 | `; 37 | 38 | exports[`FeatureExportButton triggers onClick #createFeatureString() using KMLFormat should export kml by default. 1`] = ` 39 | 44 | 45 | 46 | ExportLayer 47 | 48 | 49 | 59 | 60 | 61 | 62 | 1 63 | 64 | 65 | 66 | 67 | 68 | 7.358136177061042,48.07903229472336 69 | 70 | 71 | 72 | 73 | 74 | `; 75 | -------------------------------------------------------------------------------- /src/components/FeatureExportButton/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./FeatureExportButton"; 2 | -------------------------------------------------------------------------------- /src/components/FitExtent/FitExtent.js: -------------------------------------------------------------------------------- 1 | import OLMap from "ol/Map"; 2 | import PropTypes from "prop-types"; 3 | import React from "react"; 4 | 5 | const propTypes = { 6 | /** 7 | * Button content. 8 | */ 9 | children: PropTypes.node.isRequired, 10 | 11 | /** 12 | * CSS class for the fitExtent button. 13 | */ 14 | className: PropTypes.string, 15 | 16 | /** 17 | * The extent to be zoomed. 18 | */ 19 | extent: PropTypes.arrayOf(PropTypes.number).isRequired, 20 | 21 | /** 22 | * An [ol/map](https://openlayers.org/en/latest/apidoc/module-ol_Map-Map.html). 23 | */ 24 | map: PropTypes.instanceOf(OLMap).isRequired, 25 | }; 26 | 27 | /** 28 | * The FitExtent component creates a button that updates the current extent of 29 | * an [ol/map](https://openlayers.org/en/latest/apidoc/module-ol_Map-Map.html). 30 | */ 31 | function FitExtent({ 32 | children, 33 | className = "rs-fit-extent", 34 | extent, 35 | map, 36 | ...other 37 | }) { 38 | const fit = (evt) => { 39 | if (evt.which && evt.which !== 13) { 40 | return; 41 | } 42 | map.getView().cancelAnimations(); 43 | map.getView().fit(extent, map.getSize()); 44 | }; 45 | 46 | return ( 47 |
56 | {children} 57 |
58 | ); 59 | } 60 | 61 | FitExtent.propTypes = propTypes; 62 | 63 | export default FitExtent; 64 | -------------------------------------------------------------------------------- /src/components/FitExtent/FitExtent.md.scss: -------------------------------------------------------------------------------- 1 | .rs-fit-extent { 2 | width: 120px; 3 | margin: 10px auto; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/FitExtent/FitExtent.test.js: -------------------------------------------------------------------------------- 1 | import Adapter from "@cfaester/enzyme-adapter-react-18"; 2 | import { configure, shallow } from "enzyme"; 3 | import OLMap from "ol/Map"; 4 | import OLView from "ol/View"; 5 | import React from "react"; 6 | import renderer from "react-test-renderer"; 7 | 8 | import FitExtent from "./FitExtent"; 9 | 10 | configure({ adapter: new Adapter() }); 11 | 12 | const extent = [1, 2, 3, 4]; 13 | 14 | test("Button should match snapshot.", () => { 15 | const map = new OLMap({}); 16 | const component = renderer.create( 17 | 18 | FitExtent 19 | , 20 | ); 21 | const tree = component.toJSON(); 22 | expect(tree).toMatchSnapshot(); 23 | }); 24 | 25 | test("Should fit the extent.", () => { 26 | const map = new OLMap({ view: new OLView({ center: [0, 0], zoom: 7 }) }); 27 | const wrapper = shallow( 28 | 29 | FitExtent 30 | , 31 | ); 32 | wrapper.find(".fit-ext").first().simulate("click", {}); 33 | const calculatedExtent = map.getView().calculateExtent(map.getSize()); 34 | 35 | expect(calculatedExtent).toStrictEqual([1, 2, 3, 4]); 36 | }); 37 | 38 | test("Should fit the extent on return.", () => { 39 | const map = new OLMap({ view: new OLView({ center: [0, 0], zoom: 7 }) }); 40 | const wrapper = shallow( 41 | 42 | FitExtent 43 | , 44 | ); 45 | wrapper.find(".fit-ext").first().simulate("click", { which: 13 }); 46 | const calculatedExtent = map.getView().calculateExtent(map.getSize()); 47 | 48 | expect(calculatedExtent).toStrictEqual([1, 2, 3, 4]); 49 | }); 50 | -------------------------------------------------------------------------------- /src/components/FitExtent/README.md: -------------------------------------------------------------------------------- 1 | 2 | The following example demonstrates the use of FitExtent. 3 | 4 | ```jsx 5 | import React from 'react'; 6 | import { MapboxLayer } from 'mobility-toolbox-js/ol'; 7 | import Tile from 'ol/layer/Tile'; 8 | import OSM from 'ol/source/OSM'; 9 | import Map from 'ol/Map'; 10 | import { geopsTheme } from '@geops/geops-ui'; 11 | import { ThemeProvider } from '@mui/material'; 12 | import Button from '@mui/material/Button'; 13 | import FitExtent from 'react-spatial/components/FitExtent'; 14 | import BasicMap from 'react-spatial/components/BasicMap'; 15 | 16 | const extent = [-15380353.1391, 2230738.2886, -6496535.908, 6927029.2369]; 17 | 18 | const map = new Map({ controls: [] }); 19 | 20 | const layers = [ 21 | new MapboxLayer({ 22 | url: `https://maps.geops.io/styles/travic_v2/style.json?key=${apiKey}`, 23 | }), 24 | ]; 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | ``` 35 | -------------------------------------------------------------------------------- /src/components/FitExtent/__snapshots__/FitExtent.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Button should match snapshot. 1`] = ` 4 |
11 | FitExtent 12 |
13 | `; 14 | -------------------------------------------------------------------------------- /src/components/FitExtent/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./FitExtent"; 2 | -------------------------------------------------------------------------------- /src/components/Geolocation/Geolocation.md.scss: -------------------------------------------------------------------------------- 1 | .rs-geolocation-example { 2 | position: relative; 3 | 4 | .rs-geolocation { 5 | top: 10px; 6 | right: 10px; 7 | position: absolute; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Geolocation/Geolocation.scss: -------------------------------------------------------------------------------- 1 | @use "../../themes/default/variables"; 2 | 3 | .rs-geolocation { 4 | cursor: pointer; 5 | height: variables.$btn-size-base; 6 | width: variables.$btn-size-base; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | border-radius: 50%; 11 | text-align: center; 12 | font-size: 20px; 13 | color: white; 14 | background-color: variables.$btn-primary-color; 15 | transition: background-color 0.5s ease; 16 | 17 | &:hover { 18 | background-color: variables.$btn-primary-color-hover; 19 | } 20 | 21 | &.rs-active { 22 | animation-name: blinking; 23 | animation-duration: 3s; 24 | animation-timing-function: linear; 25 | animation-iteration-count: infinite; 26 | } 27 | 28 | @keyframes blinking { 29 | 0% { 30 | color: white; 31 | } 32 | 33 | 50% { 34 | color: variables.$btn-primary-color; 35 | } 36 | 37 | 100% { 38 | color: white; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Geolocation/README.md: -------------------------------------------------------------------------------- 1 | 2 | The following example demonstrates the use of Geolocation. 3 | 4 | ```jsx 5 | import React from 'react'; 6 | import { MapboxLayer } from 'mobility-toolbox-js/ol'; 7 | import Tile from 'ol/layer/Tile'; 8 | import OSM from 'ol/source/OSM'; 9 | import Map from 'ol/Map'; 10 | import Geolocation from 'react-spatial/components/Geolocation'; 11 | import BasicMap from 'react-spatial/components/BasicMap'; 12 | 13 | const map = new Map({ controls: [] }); 14 | 15 | const layers = [ 16 | new MapboxLayer({ 17 | url: `https://maps.geops.io/styles/travic_v2/style.json?key=${apiKey}`, 18 | }), 19 | ]; 20 | 21 |
22 | 23 | 24 |
25 | ``` 26 | -------------------------------------------------------------------------------- /src/components/Geolocation/__snapshots__/Geolocation.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Geolocation should match snapshot minimum props 1`] = ` 4 |
11 | 26 | 29 | 30 |
31 | `; 32 | 33 | exports[`Geolocation should match snapshot with class name 1`] = ` 34 |
41 | 56 | 59 | 60 |
61 | `; 62 | 63 | exports[`Geolocation should match snapshot with title 1`] = ` 64 |
72 | 87 | 90 | 91 |
92 | `; 93 | -------------------------------------------------------------------------------- /src/components/Geolocation/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Geolocation"; 2 | -------------------------------------------------------------------------------- /src/components/LayerTree/LayerTree.md.scss: -------------------------------------------------------------------------------- 1 | .rs-layer-tree-example { 2 | position: relative; 3 | height: 300px; 4 | 5 | .rs-map { 6 | width: 100%; 7 | height: 100%; 8 | flex-grow: 1; 9 | } 10 | 11 | .rs-layer-tree { 12 | background: white; 13 | padding: 10px 5px; 14 | width: 250px; 15 | top: 10px; 16 | left: 10px; 17 | bottom: 10px; 18 | position: absolute; 19 | margin: auto; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/LayerTree/LayerTree.scss: -------------------------------------------------------------------------------- 1 | @use "../../themes/default/variables"; 2 | 3 | .rs-layer-tree { 4 | width: 100%; 5 | overflow-y: auto; 6 | 7 | .rs-layer-tree-arrow { 8 | border: solid black; 9 | border-width: 0 1px 1px 0; 10 | display: inline-block; 11 | padding: 0; 12 | background: transparent; 13 | width: 5px; 14 | height: 5px; 15 | } 16 | 17 | .rs-layer-tree-arrow-collapsed { 18 | transform: rotate(45deg); 19 | margin-top: -3px; 20 | } 21 | 22 | .rs-layer-tree-arrow-expanded { 23 | transform: rotate(-135deg); 24 | margin-top: 3px; 25 | } 26 | 27 | .rs-layer-tree-item { 28 | display: flex; 29 | align-items: center; 30 | padding: 10px; 31 | position: relative; 32 | 33 | & > * { 34 | margin-right: 10px; 35 | align-items: center; 36 | } 37 | 38 | div { 39 | /* HACK: Without this it breaks the click on the label (Chrome) */ 40 | position: relative; 41 | } 42 | 43 | label + div { 44 | display: inline-block; 45 | } 46 | 47 | /* CSS for the toggle div */ 48 | .rs-layer-tree-toggle { 49 | width: auto; 50 | height: auto; 51 | flex-grow: 1; 52 | display: flex; 53 | align-items: center; 54 | justify-content: left; 55 | 56 | div:first-child { 57 | margin-right: 10px; 58 | } 59 | } 60 | 61 | /* Customize the label (the container) */ 62 | label { 63 | display: inline-block; 64 | position: relative; 65 | cursor: pointer; 66 | } 67 | 68 | label, 69 | label input { 70 | height: 18px; 71 | min-width: 18px; 72 | } 73 | 74 | label input, 75 | label span { 76 | position: absolute; 77 | top: 0; 78 | left: 0; 79 | margin: 0; 80 | } 81 | 82 | label input { 83 | display: none; 84 | } 85 | 86 | /* Create a custom checkbox */ 87 | label span { 88 | background-color: white; 89 | border: 1px solid lightgray; 90 | width: calc(100% - 2px); 91 | height: calc(100% - 2px); 92 | } 93 | 94 | .rs-layer-tree-input-radio span { 95 | border-radius: 50%; 96 | } 97 | 98 | /* On mouse-over, add a grey background color */ 99 | label:hover input ~ span { 100 | background-color: #ccc; 101 | } 102 | 103 | /* Create the checkmark/indicator (hidden when not checked) */ 104 | label span::after { 105 | content: ''; 106 | position: absolute; 107 | display: none; 108 | } 109 | 110 | /* Style the checkmark/indicator */ 111 | .rs-layer-tree-input-checkbox span::after { 112 | left: 5px; 113 | top: 1px; 114 | width: 5px; 115 | height: 10px; 116 | border: solid variables.$brand-primary; 117 | border-width: 0 1px 1px 0; 118 | transform: rotate(45deg); 119 | } 120 | 121 | /* Style the (dot/circle) */ 122 | .rs-layer-tree-input-radio span::after { 123 | top: 2px; 124 | left: 2px; 125 | width: 12px; 126 | height: 12px; 127 | border-radius: 50%; 128 | background: variables.$brand-primary; 129 | } 130 | 131 | label input:checked ~ span::after { 132 | display: block; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/components/LayerTree/README.md: -------------------------------------------------------------------------------- 1 | The following example demonstrates the use of LayerTree. 2 | 3 | ```jsx 4 | import React, { useEffect } from 'react'; 5 | import { MapboxLayer, MapboxStyleLayer, Layer } from 'mobility-toolbox-js/ol'; 6 | import { Style, Circle, Stroke, Fill } from 'ol/style'; 7 | import VectorLayer from 'ol/layer/Vector'; 8 | import VectorSource from 'ol/source/Vector'; 9 | import GeoJSONFormat from 'ol/format/GeoJSON'; 10 | import LayerTree from 'react-spatial/components/LayerTree'; 11 | import BasicMap from 'react-spatial/components/BasicMap'; 12 | 13 | const baseTravic = new MapboxLayer({ 14 | name: 'Base - Bright', 15 | group: 'baseLayer', 16 | url: `https://maps.geops.io/styles/travic_v2_generalized/style.json?key=${apiKey}`, 17 | }); 18 | 19 | const stations = new MapboxStyleLayer({ 20 | name: 'Stations', 21 | mapboxLayer: baseTravic, 22 | styleLayersFilter: (layer) => { 23 | return layer.metadata && /mapset_stations/.test(layer.metadata['mapset.filter']) 24 | } 25 | }); 26 | 27 | const railLines = new MapboxStyleLayer({ 28 | name: 'Railways routes', 29 | mapboxLayer: baseTravic, 30 | styleLayer: { 31 | id: 'rail', 32 | type: 'line', 33 | source: 'base', 34 | 'source-layer': 'osm_edges', 35 | filter: ['==', 'vehicle_type_prior', 'Zug'], 36 | paint: { 37 | 'line-color': 'rgba(255, 0, 0, 1)', 38 | 'line-width': 2, 39 | }, 40 | }, 41 | }); 42 | 43 | 44 | baseTravic.children = [railLines, stations]; 45 | 46 | const baseDark = new MapboxLayer({ 47 | name: 'Base - Dark', 48 | group: 'baseLayer', 49 | visible: false, 50 | url: `https://maps.geops.io/styles/base_dark_v2/style.json?key=${apiKey}`, 51 | }); 52 | 53 | const layers = [baseDark, baseTravic]; 54 | 55 |
56 | 62 | layer.get('hidden')} 65 | expandChildren 66 | /> 67 |
; 68 | ``` 69 | -------------------------------------------------------------------------------- /src/components/LayerTree/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./LayerTree"; 2 | -------------------------------------------------------------------------------- /src/components/MousePosition/MousePosition.md.scss: -------------------------------------------------------------------------------- 1 | .rs-mouse-position-example { 2 | .rs-mouse-position { 3 | display: flex; 4 | align-items: center; 5 | margin: 10px 0; 6 | 7 | span { 8 | margin-left: 10px; 9 | } 10 | 11 | select { 12 | background: none; 13 | height: 20px; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/MousePosition/MousePosition.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import Adapter from "@cfaester/enzyme-adapter-react-18"; 3 | import { configure, mount } from "enzyme"; 4 | import "jest-canvas-mock"; 5 | import OLMousePosition from "ol/control/MousePosition"; 6 | import OLMap from "ol/Map"; 7 | import React from "react"; 8 | import renderer from "react-test-renderer"; 9 | 10 | import MousePosition from "./MousePosition"; 11 | 12 | configure({ adapter: new Adapter() }); 13 | const expectSnapshot = (props) => { 14 | const map = new OLMap({}); 15 | const component = renderer.create(); 16 | const tree = component.toJSON(); 17 | expect(tree).toMatchSnapshot(); 18 | }; 19 | 20 | describe("MousePosition", () => { 21 | describe("matches snapshots", () => { 22 | test("using default values.", () => { 23 | expectSnapshot({}); 24 | }); 25 | 26 | test("using no projections.", () => { 27 | expectSnapshot({ 28 | projections: [], 29 | }); 30 | }); 31 | 32 | test("using only one projection", () => { 33 | expectSnapshot({ 34 | projections: [{ label: "foo", value: "foo" }], 35 | }); 36 | }); 37 | 38 | test("using multiple projections.", () => { 39 | expectSnapshot({ 40 | projections: [ 41 | { label: "foo", value: "foo" }, 42 | { label: "bar", value: "bar" }, 43 | ], 44 | }); 45 | }); 46 | }); 47 | 48 | test("add MousePosition control to the map.", () => { 49 | const map = new OLMap({}); 50 | const spy = jest.spyOn(map, "removeControl"); 51 | const spy2 = jest.spyOn(map, "addControl"); 52 | const fn = jest.fn(); 53 | mount( 54 | , 64 | ); 65 | const ctrl = spy2.mock.calls[0][0]; 66 | expect(spy).toHaveBeenCalledTimes(0); 67 | expect(spy2).toHaveBeenCalledTimes(1); 68 | expect(ctrl).toBeInstanceOf(OLMousePosition); 69 | expect(ctrl.getProjection().getCode()).toBe("EPSG:4326"); 70 | expect(ctrl.getCoordinateFormat()).toBe(fn); 71 | }); 72 | 73 | test("add/remove MousePosition control on mount/unmount.", () => { 74 | const map = new OLMap({}); 75 | const spy = jest.spyOn(map, "removeControl"); 76 | const spy2 = jest.spyOn(map, "addControl"); 77 | const wrapper = mount(); 78 | expect(spy).toHaveBeenCalledTimes(0); 79 | expect(spy2).toHaveBeenCalledTimes(1); 80 | wrapper.unmount(); 81 | expect(spy).toHaveBeenCalledTimes(1); 82 | expect(spy.mock.calls[0][0]).toBe(spy2.mock.calls[0][0]); 83 | }); 84 | 85 | test("triggers onChange when select projection.", () => { 86 | const map = new OLMap({}); 87 | const onChange = jest.fn(() => {}); 88 | const wrapper = mount( 89 | , 100 | ); 101 | // onChange triggered on instantiation. 102 | expect(onChange).toHaveBeenCalledTimes(0); 103 | wrapper.find("select").simulate("change", {}); 104 | expect(onChange).toHaveBeenCalledTimes(1); 105 | }); 106 | 107 | test("applies new format and value when we select a new projection.", () => { 108 | const map = new OLMap({}); 109 | const spy = jest.spyOn(map, "addControl"); 110 | const projs = [ 111 | { 112 | format: jest.fn(), 113 | label: "EPSG:4326", 114 | value: "EPSG:4326", 115 | }, 116 | { 117 | format: jest.fn(), 118 | label: "EPSG:3857", 119 | value: "EPSG:3857", 120 | }, 121 | ]; 122 | const wrapper = mount(); 123 | 124 | const ctrl = spy.mock.calls[0][0]; 125 | expect(ctrl.getProjection().getCode()).toBe(projs[0].value); 126 | expect(ctrl.getCoordinateFormat()).toBe(projs[0].format); 127 | wrapper 128 | .find("select") 129 | .simulate("change", { target: { value: "EPSG:3857" } }); 130 | expect(ctrl.getProjection().getCode()).toBe(projs[1].value); 131 | expect(ctrl.getCoordinateFormat()).toBe(projs[1].format); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /src/components/MousePosition/README.md: -------------------------------------------------------------------------------- 1 | 2 | The following example demonstrates the use of MousePosition. 3 | 4 | ```jsx 5 | import React from 'react'; 6 | import { MapboxLayer } from 'mobility-toolbox-js/ol'; 7 | import Tile from 'ol/layer/Tile'; 8 | import OSM from 'ol/source/OSM'; 9 | import Map from 'ol/Map'; 10 | import BasicMap from 'react-spatial/components/BasicMap'; 11 | import MousePosition from 'react-spatial/components/MousePosition'; 12 | 13 | const map = new Map({ controls: [] }); 14 | 15 | const layers = [ 16 | new MapboxLayer({ 17 | url: `https://maps.geops.io/styles/travic_v2/style.json?key=${apiKey}`, 18 | }), 19 | ]; 20 | 21 |
22 | 23 | { 34 | const decimals = 4; 35 | const text = []; 36 | coordinates.forEach(input => { 37 | const coord = 38 | Math.round(parseFloat(input) * 10 ** decimals) / 39 | 10 ** decimals; 40 | const parts = coord.toString().split('.'); 41 | parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, "'"); 42 | text.push(parts.join()); 43 | }); 44 | return `Coordinates: ${text.join(' ')}`; 45 | }, 46 | }, 47 | ]} 48 | /> 49 |
50 | ``` 51 | -------------------------------------------------------------------------------- /src/components/MousePosition/__snapshots__/MousePosition.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`MousePosition matches snapshots using default values. 1`] = ` 4 |
7 | 23 | 26 |
27 | `; 28 | 29 | exports[`MousePosition matches snapshots using multiple projections. 1`] = ` 30 |
33 | 49 | 52 |
53 | `; 54 | 55 | exports[`MousePosition matches snapshots using no projections. 1`] = `null`; 56 | 57 | exports[`MousePosition matches snapshots using only one projection 1`] = ` 58 |
61 | 72 | 75 |
76 | `; 77 | -------------------------------------------------------------------------------- /src/components/MousePosition/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./MousePosition"; 2 | -------------------------------------------------------------------------------- /src/components/NorthArrow/NorthArrow.js: -------------------------------------------------------------------------------- 1 | import OLMap from "ol/Map"; 2 | import { toDegrees } from "ol/math"; 3 | import { unByKey } from "ol/Observable"; 4 | import PropTypes from "prop-types"; 5 | import React, { useEffect, useState } from "react"; 6 | 7 | import NorthArrowSimple from "../../images/northArrow.svg"; 8 | import NorthArrowCircle from "../../images/northArrowCircle.svg"; 9 | 10 | const propTypes = { 11 | /** 12 | * Children content of the north arrow. 13 | */ 14 | children: PropTypes.node, 15 | 16 | /** 17 | * Display circle around the north arrow. Not used if pass children. 18 | */ 19 | circled: PropTypes.bool, 20 | 21 | /** 22 | * An [ol/map](https://openlayers.org/en/latest/apidoc/module-ol_Map-Map.html). 23 | */ 24 | map: PropTypes.instanceOf(OLMap).isRequired, 25 | 26 | /** 27 | * Rotation of the north arrow in degrees. 28 | */ 29 | rotationOffset: PropTypes.number, 30 | }; 31 | 32 | const getRotation = (map, rotationOffset) => { 33 | return toDegrees(map.getView().getRotation()) + rotationOffset; 34 | }; 35 | 36 | /** 37 | * This NorthArrow component inserts an arrow pointing north into an 38 | * [ol/map](https://openlayers.org/en/latest/apidoc/module-ol_Map-Map.html). 39 | */ 40 | function NorthArrow({ 41 | children = null, 42 | circled = false, 43 | map, 44 | rotationOffset = 0, 45 | ...other 46 | }) { 47 | const [rotation, setRotation] = useState(rotationOffset); 48 | 49 | useEffect(() => { 50 | if (!map) { 51 | return null; 52 | } 53 | const key = map.on("postrender", () => { 54 | setRotation(getRotation(map, rotationOffset)); 55 | }); 56 | return () => { 57 | unByKey(key); 58 | }; 59 | }, [map, rotationOffset]); 60 | 61 | return ( 62 |
68 | {children || (circled ? : )} 69 |
70 | ); 71 | } 72 | 73 | NorthArrow.propTypes = propTypes; 74 | 75 | export default React.memo(NorthArrow); 76 | -------------------------------------------------------------------------------- /src/components/NorthArrow/NorthArrow.scss: -------------------------------------------------------------------------------- 1 | .rs-north-arrow { 2 | position: absolute; 3 | bottom: 5px; 4 | right: 10px; 5 | height: 80px; 6 | width: 80px; 7 | 8 | svg { 9 | height: 100%; 10 | width: 100%; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/NorthArrow/NorthArrow.test.js: -------------------------------------------------------------------------------- 1 | import Adapter from "@cfaester/enzyme-adapter-react-18"; 2 | import { configure, mount } from "enzyme"; 3 | import "jest-canvas-mock"; 4 | import OLMap from "ol/Map"; 5 | import MapEvent from "ol/MapEvent"; 6 | import OLView from "ol/View"; 7 | import React from "react"; 8 | import { act } from "react-dom/test-utils"; 9 | import { TiImage } from "react-icons/ti"; 10 | import renderer from "react-test-renderer"; 11 | 12 | import NorthArrow from "./NorthArrow"; 13 | 14 | configure({ adapter: new Adapter() }); 15 | let olView; 16 | let olMap; 17 | 18 | describe("NorthArrow", () => { 19 | beforeEach(() => { 20 | olView = new OLView(); 21 | olMap = new OLMap({ view: olView }); 22 | }); 23 | 24 | test("should match snapshot with default value.", () => { 25 | let component; 26 | renderer.act(() => { 27 | component = renderer.create(); 28 | }); 29 | const tree = component.toJSON(); 30 | expect(tree).toMatchSnapshot(); 31 | }); 32 | 33 | test("should match snapshot with custom attributes.", () => { 34 | let component; 35 | renderer.act(() => { 36 | component = renderer.create( 37 | , 38 | ); 39 | }); 40 | const tree = component.toJSON(); 41 | expect(tree).toMatchSnapshot(); 42 | }); 43 | 44 | test("should match snapshot with children.", () => { 45 | let component; 46 | renderer.act(() => { 47 | component = renderer.create( 48 | 49 | 50 | , 51 | ); 52 | }); 53 | const tree = component.toJSON(); 54 | expect(tree).toMatchSnapshot(); 55 | }); 56 | 57 | test("should match snapshot rotated.", () => { 58 | let component; 59 | renderer.act(() => { 60 | component = renderer.create( 61 | , 62 | ); 63 | }); 64 | const tree = component.toJSON(); 65 | expect(tree).toMatchSnapshot(); 66 | }); 67 | 68 | test("should match snapshot with circle.", () => { 69 | let component; 70 | renderer.act(() => { 71 | component = renderer.create(); 72 | }); 73 | const tree = component.toJSON(); 74 | expect(tree).toMatchSnapshot(); 75 | }); 76 | 77 | test("should react on view rotation (transform: `rotate(20deg)`)", () => { 78 | const wrapper = mount(); 79 | // Trigger view rotation 80 | olMap.getView().setRotation(0.3490658503988659); 81 | act(() => { 82 | olMap.dispatchEvent(new MapEvent("postrender", olMap)); 83 | // 20 degrees = 0.3490658503988659 radians 84 | }); 85 | expect(wrapper.html()).toMatchSnapshot(); 86 | }); 87 | 88 | test("should react on view rotation with offset (transform: `rotate(10deg)`)", () => { 89 | const wrapper = mount(); 90 | olMap.getView().setRotation(0.3490658503988659); 91 | act(() => { 92 | olMap.dispatchEvent(new MapEvent("postrender", olMap)); 93 | }); 94 | expect(wrapper.html()).toMatchSnapshot(); 95 | }); 96 | 97 | test("should remove post render event on unmount", () => { 98 | const wrapper = mount(); 99 | // eslint-disable-next-line no-underscore-dangle 100 | expect(olMap.listeners_.postrender.length).toBe(4); 101 | wrapper.unmount(); 102 | // eslint-disable-next-line no-underscore-dangle 103 | expect(olMap.listeners_.postrender.length).toBe(3); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /src/components/NorthArrow/README.md: -------------------------------------------------------------------------------- 1 | 2 | The following example demonstrates the use of NorthArrowExample (Alt + Shift + click to rotate). 3 | 4 | ```jsx 5 | import React from 'react'; 6 | import { Layer } from 'mobility-toolbox-js/ol'; 7 | import Map from 'ol/Map'; 8 | import { DragRotate, defaults } from 'ol/interaction'; 9 | import Tile from 'ol/layer/Tile'; 10 | import TileGrid from 'ol/tilegrid/TileGrid'; 11 | import TileImageSource from 'ol/source/TileImage'; 12 | import { getCenter} from 'ol/extent'; 13 | import BasicMap from 'react-spatial/components/BasicMap'; 14 | import NorthArrow from 'react-spatial/components/NorthArrow'; 15 | 16 | const extent = [599500, 199309, 600714, 200002]; 17 | 18 | const map = new Map({ controls: [] }); 19 | 20 | const layers = [ 21 | new Layer({ 22 | olLayer: new Tile({ 23 | extent, 24 | source: new TileImageSource({ 25 | tileUrlFunction: c => 26 | '//plans.trafimage.ch/static/tiles/' + 27 | `bern_aussenplan/${c[0]}/${c[1]}/${-c[2]-1}.png`, 28 | tileGrid: new TileGrid({ 29 | origin: [extent[0], extent[1]], 30 | resolutions: [ 31 | 6.927661, 32 | 3.4638305, 33 | 1.73191525, 34 | 0.865957625, 35 | 0.4329788125, 36 | 0.21648940625, 37 | 0.108244703125, 38 | ], 39 | }), 40 | }), 41 | }), 42 | }), 43 | ]; 44 | 45 |
46 | 53 | 58 |
; 59 | ``` 60 | -------------------------------------------------------------------------------- /src/components/NorthArrow/__snapshots__/NorthArrow.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`NorthArrow should match snapshot rotated. 1`] = ` 4 |
12 | 15 |
16 | `; 17 | 18 | exports[`NorthArrow should match snapshot with children. 1`] = ` 19 |
27 | 43 | 48 | 51 | 52 |
53 | `; 54 | 55 | exports[`NorthArrow should match snapshot with circle. 1`] = ` 56 |
64 | 67 |
68 | `; 69 | 70 | exports[`NorthArrow should match snapshot with custom attributes. 1`] = ` 71 |
80 | 83 |
84 | `; 85 | 86 | exports[`NorthArrow should match snapshot with default value. 1`] = ` 87 |
95 | 98 |
99 | `; 100 | 101 | exports[`NorthArrow should react on view rotation (transform: \`rotate(20deg)\`) 1`] = ` 102 |
105 | 106 | 107 |
108 | `; 109 | 110 | exports[`NorthArrow should react on view rotation with offset (transform: \`rotate(10deg)\`) 1`] = ` 111 |
114 | 115 | 116 |
117 | `; 118 | -------------------------------------------------------------------------------- /src/components/NorthArrow/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./NorthArrow"; 2 | -------------------------------------------------------------------------------- /src/components/Overlay/Overlay.md.scss: -------------------------------------------------------------------------------- 1 | .tm-overlay-example { 2 | border: 2px solid #838383; 3 | background-color: #eee; 4 | position: relative; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | height: 300px; 9 | width: 100%; 10 | 11 | .tm-overlay-container { 12 | width: 150px; 13 | } 14 | 15 | .tm-clickable-feature { 16 | padding: 10px; 17 | border-radius: 5px; 18 | border: 1px solid #838383; 19 | } 20 | 21 | .tm-overlay-mobile { 22 | .tm-overlay-mobile-content { 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | 27 | div { 28 | width: 80px; 29 | padding: 10px; 30 | border-radius: 5px; 31 | height: unset; 32 | border: 1px solid #838383; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Overlay/Overlay.scss: -------------------------------------------------------------------------------- 1 | .tm-overlay, 2 | .tm-overlay-mobile { 3 | position: absolute; 4 | background-color: white; 5 | left: 0; 6 | overscroll-behavior: contain; 7 | } 8 | 9 | .tm-overlay { 10 | border-right: 1px solid #eee; 11 | height: 100%; 12 | } 13 | 14 | .tm-overlay-mobile { 15 | border-top: 1px solid #eee; 16 | bottom: 0; 17 | 18 | .tm-overlay-mobile-content { 19 | margin-top: 20px; 20 | height: calc(100% - 20px); 21 | } 22 | 23 | .tm-overlay-handler { 24 | font-weight: bold; 25 | font-size: 20px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Overlay/Overlay.test.js: -------------------------------------------------------------------------------- 1 | import Adapter from "@cfaester/enzyme-adapter-react-18"; 2 | import { configure, mount } from "enzyme"; 3 | import PropTypes from "prop-types"; 4 | import { Resizable } from "re-resizable"; 5 | import React, { useState } from "react"; 6 | import { act } from "react-dom/test-utils"; 7 | import renderer from "react-test-renderer"; 8 | import ResizeObserver from "resize-observer-polyfill"; 9 | 10 | import Overlay from "./Overlay"; 11 | 12 | jest.mock("resize-observer-polyfill"); 13 | 14 | configure({ adapter: new Adapter() }); 15 | 16 | const propTypes = { 17 | isMobileResizable: PropTypes.bool, 18 | thresholdWidthForMobile: PropTypes.number, 19 | }; 20 | 21 | const defaultProps = { 22 | isMobileResizable: undefined, 23 | thresholdWidthForMobile: undefined, 24 | }; 25 | 26 | function BasicComponent({ isMobileResizable, thresholdWidthForMobile }) { 27 | const [ref, setRef] = useState(null); 28 | 29 | return ( 30 | <> 31 |
{ 34 | if (node !== ref) { 35 | setRef(node); 36 | } 37 | }} 38 | /> 39 | 44 | Test content 45 | 46 | 47 | ); 48 | } 49 | BasicComponent.propTypes = propTypes; 50 | BasicComponent.defaultProps = defaultProps; 51 | 52 | describe("Overlay", () => { 53 | test("should match snapshot.", () => { 54 | const component = renderer.create(Test content); 55 | const tree = component.toJSON(); 56 | expect(tree).toMatchSnapshot(); 57 | }); 58 | 59 | test("should react on observe resize.", () => { 60 | const wrapper = mount(); 61 | const target = wrapper.find(".observer").getDOMNode(); 62 | 63 | act(() => { 64 | // The mock class set the onResize property, we just have to run it to 65 | // simulate a resize 66 | ResizeObserver.onResize([ 67 | { 68 | contentRect: { 69 | height: 200, 70 | width: 200, 71 | }, 72 | target, 73 | }, 74 | ]); 75 | }); 76 | wrapper.update(); 77 | 78 | expect(wrapper.find(".tm-overlay").length > 0).toBe(false); 79 | expect(wrapper.find(".tm-overlay-mobile").length > 0).toBe(true); 80 | }); 81 | 82 | test("should force mobile overlay display on big screen.", () => { 83 | const wrapper = mount( 84 | , 85 | ); 86 | const target = wrapper.find(".observer").getDOMNode(); 87 | 88 | act(() => { 89 | ResizeObserver.onResize([ 90 | { 91 | contentRect: { 92 | height: 200, 93 | width: 1200, 94 | }, 95 | target, 96 | }, 97 | ]); 98 | }); 99 | wrapper.update(); 100 | 101 | expect(wrapper.find(".tm-overlay").length > 0).toBe(false); 102 | expect(wrapper.find(".tm-overlay-mobile").length > 0).toBe(true); 103 | }); 104 | 105 | test("should allow resizing with top handler on mobile.", () => { 106 | const wrapper = mount(); 107 | const target = wrapper.find(".observer").getDOMNode(); 108 | 109 | // Force resize to make it mobile. 110 | act(() => { 111 | ResizeObserver.onResize([ 112 | { 113 | contentRect: { 114 | height: 200, 115 | width: 200, 116 | }, 117 | target, 118 | }, 119 | ]); 120 | }); 121 | wrapper.update(); 122 | 123 | const resizableProps = wrapper.find(Resizable).props(); 124 | 125 | expect(resizableProps.enable.top).toBe(true); 126 | }); 127 | 128 | test("should not allow resizing with top handler on mobile.", () => { 129 | const wrapper = mount(); 130 | const target = wrapper.find(".observer").getDOMNode(); 131 | 132 | // Force resize to make it mobile. 133 | act(() => { 134 | ResizeObserver.onResize([ 135 | { 136 | contentRect: { 137 | height: 200, 138 | width: 200, 139 | }, 140 | target, 141 | }, 142 | ]); 143 | }); 144 | wrapper.update(); 145 | 146 | const resizableProps = wrapper.find(Resizable).props(); 147 | 148 | expect(resizableProps.enable.top).toBe(false); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /src/components/Overlay/README.md: -------------------------------------------------------------------------------- 1 | 2 | The following example demonstrates the use of Overlay. 3 | 4 | ```jsx 5 | import React, { useState, useEffect, useRef } from 'react'; 6 | import Overlay from 'react-spatial/components/Overlay'; 7 | 8 | function OverlayExample() { 9 | const [open, setOpen] = useState(true); 10 | const [ref, setRef] = useState(null); 11 | const refDiv = useRef(null); 12 | 13 | useEffect(() => { 14 | setRef(refDiv); 15 | }, [refDiv]); 16 | 17 | return ( 18 |
22 |
{ 26 | setOpen(!open); 27 | }} 28 | > 29 | Toggle Overlay 30 |
31 | {open && ref ? ( 32 | 44 |
{ 47 | setOpen(false); 48 | }} 49 | > 50 | Close Overlay 51 |
52 |
53 | ) : null} 54 |
55 | ); 56 | } 57 | 58 | ; 59 | ``` 60 | -------------------------------------------------------------------------------- /src/components/Overlay/__snapshots__/Overlay.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Overlay should match snapshot. 1`] = ` 4 |
7 | Test content 8 |
9 | `; 10 | -------------------------------------------------------------------------------- /src/components/Overlay/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Overlay"; 2 | -------------------------------------------------------------------------------- /src/components/Permalink/Permalink.md.scss: -------------------------------------------------------------------------------- 1 | .rs-permalink-example { 2 | .rs-permalink-example-btns { 3 | display: flex; 4 | justify-content: space-evenly; 5 | margin: 10px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Permalink/README.md: -------------------------------------------------------------------------------- 1 | 2 | The following example demonstrates the use of Permalink. 3 | 4 | ```jsx 5 | import React from 'react'; 6 | import VectorSource from 'ol/source/Vector'; 7 | import VectorLayer from 'ol/layer/Vector'; 8 | import { Style, Circle, Stroke, Fill } from 'ol/style'; 9 | import GeoJSONFormat from 'ol/format/GeoJSON'; 10 | import { geopsTheme } from '@geops/geops-ui'; 11 | import { ThemeProvider } from '@mui/material'; 12 | import { Layer, MapboxLayer } from 'mobility-toolbox-js/ol'; 13 | import Button from '@mui/material/Button'; 14 | import Permalink from 'react-spatial/components/Permalink'; 15 | import BasicMap from 'react-spatial/components/BasicMap'; 16 | import Map from 'ol/Map'; 17 | 18 | const map = new Map({ controls: [] }); 19 | 20 | const swissBoundries = new Layer({ 21 | name: 'Swiss boundaries', 22 | key: 'swiss.boundaries', 23 | visible: true, 24 | olLayer: new VectorLayer({ 25 | source: new VectorSource({ 26 | url: 'https://raw.githubusercontent.com/openlayers/openlayers/' + 27 | '3c64018b3754cf605ea19cbbe4c8813304da2539/examples/data/geojson/' + 28 | 'switzerland.geojson', 29 | format: new GeoJSONFormat(), 30 | }), 31 | style: new Style({ 32 | image: new Circle({ 33 | radius: 5, 34 | fill: new Fill({ 35 | color: '#ff0000', 36 | }), 37 | }), 38 | stroke: new Stroke({ 39 | color: '#ffcc33', 40 | width: 2, 41 | }), 42 | }), 43 | }) 44 | }); 45 | 46 | const baseLayers = [ 47 | new MapboxLayer({ 48 | url: `https://maps.geops.io/styles/base_bright_v2/style.json?key=${apiKey}`, 49 | name: 'Base - Bright', 50 | key: 'basebright.baselayer', 51 | }), 52 | new MapboxLayer({ 53 | url: `https://maps.geops.io/styles/base_dark_v2/style.json?key=${apiKey}`, 54 | name: 'Base - Dark', 55 | key: 'basedark.baselayer', 56 | visible: false, 57 | }), 58 | ]; 59 | 60 | const layers = [...baseLayers, swissBoundries]; 61 | 62 | 63 |
64 | 65 | { 72 | return baseLayers.includes(l); 73 | }} 74 | isLayerHidden={l => { 75 | let hasParentHidden = false; 76 | let { parent } = l; 77 | while (!hasParentHidden && parent) { 78 | hasParentHidden = parent.get('hideInLegend'); 79 | parent = parent.parent; 80 | } 81 | return l.get('hideInLegend') || hasParentHidden; 82 | }} 83 | /> 84 |
85 | 92 | 106 |
107 |
108 |
109 | ``` 110 | -------------------------------------------------------------------------------- /src/components/Permalink/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Permalink"; 2 | -------------------------------------------------------------------------------- /src/components/Popup/Popup.md.scss: -------------------------------------------------------------------------------- 1 | .rs-popup-example { 2 | position: relative; 3 | overflow: hidden; 4 | } 5 | 6 | .rs-w-xs .rs-popup { 7 | left: 0 !important; 8 | right: 0; 9 | bottom: 0; 10 | // stylelint-disable 11 | top: auto !important; 12 | border: 1px solid #e8e8e8; 13 | } 14 | 15 | .rs-w-xs .rs-popup > div { 16 | position: relative; 17 | left: 0; 18 | bottom: 0; 19 | min-width: 0; 20 | 21 | &::after, 22 | &::before { 23 | display: none; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Popup/Popup.scss: -------------------------------------------------------------------------------- 1 | @use "../../themes/default/variables"; 2 | 3 | .rs-popup { 4 | position: absolute; 5 | 6 | .rs-popup-container { 7 | position: absolute; 8 | background-color: white; 9 | filter: drop-shadow(variables.$box-shadow); 10 | bottom: 12px; 11 | left: -50px; 12 | min-width: 220px; 13 | 14 | &::after, 15 | &::before { 16 | top: 100%; 17 | border: solid transparent; 18 | content: ' '; 19 | height: 0; 20 | width: 0; 21 | position: absolute; 22 | pointer-events: none; 23 | } 24 | 25 | &::after { 26 | border-top-color: white; 27 | border-width: 10px; 28 | left: 48px; 29 | margin-left: -10px; 30 | } 31 | 32 | &::before { 33 | border-top-color: #ccc; 34 | border-width: 11px; 35 | left: 48px; 36 | margin-left: -11px; 37 | } 38 | 39 | .rs-popup-close-bt { 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | } 44 | 45 | .rs-popup-header { 46 | display: flex; 47 | align-items: center; 48 | justify-content: space-between; 49 | background-color: #f5f5f5; 50 | font-weight: bold; 51 | padding: 10px; 52 | } 53 | 54 | .rs-popup-body { 55 | padding: 10px; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/Popup/README.md: -------------------------------------------------------------------------------- 1 | 2 | The following example demonstrates the use of Popup. 3 | 4 | ```jsx 5 | import React, { useState, useMemo, useCallback } from 'react'; 6 | import { Layer, MapboxLayer} from 'mobility-toolbox-js/ol'; 7 | import VectorLayer from 'ol/layer/Vector'; 8 | import { Map, Feature } from 'ol'; 9 | import Point from 'ol/geom/Point'; 10 | import OSM from 'ol/source/OSM'; 11 | import VectorSource from 'ol/source/Vector'; 12 | import TileGrid from 'ol/tilegrid/TileGrid'; 13 | import { getCenter } from 'ol/extent'; 14 | import { Style, Circle, Fill, Icon } from 'ol/style'; 15 | import BasicMap from 'react-spatial/components/BasicMap'; 16 | import Popup from 'react-spatial/components/Popup'; 17 | 18 | const map = new Map({ controls: [] }); 19 | 20 | const layers = [ 21 | new MapboxLayer({ 22 | url: `https://maps.geops.io/styles/base_dark_v2/style.json?key=${apiKey}`, 23 | }), 24 | new Layer({ 25 | olLayer: new VectorLayer({ 26 | source: new VectorSource({ 27 | features: [ 28 | new Feature({ 29 | geometry: new Point([874105.13, 6106172.77]), 30 | }), 31 | new Feature({ 32 | geometry: new Point([873105.13, 6106172.77]), 33 | }), 34 | ], 35 | }), 36 | style: new Style({ 37 | image: new Icon({ 38 | anchor: [0.5, 46], 39 | anchorXUnits: 'fraction', 40 | anchorYUnits: 'pixels', 41 | src: 'https://openlayers.org/en/latest/examples/data/icon.png', 42 | size: [32, 48] 43 | }), 44 | }), 45 | }), 46 | }), 47 | ]; 48 | 49 | 50 | function PopupExample() { 51 | const [featureClicked, setFeatureClicked] = useState(); 52 | 53 | const content = useMemo(() => { 54 | return featureClicked && 55 | featureClicked 56 | .getGeometry() 57 | .getCoordinates() 58 | .toString(); 59 | }, [featureClicked]); 60 | 61 | const onFeaturesClick = useCallback((features) => { 62 | setFeatureClicked(features.length ? features[0] : null); 63 | }, []); 64 | 65 | const onCloseClick = useCallback(()=> { 66 | setFeatureClicked(null); 67 | }, []); 68 | 69 | return ( 70 |
71 | 79 | 86 |
{content}
87 |
88 |
89 | ); 90 | } 91 | 92 | ; 93 | ``` 94 | -------------------------------------------------------------------------------- /src/components/Popup/__snapshots__/Popup.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Popup should match snapshot with default values. 1`] = ` 4 |
13 |
17 |
20 |
29 | 44 | 48 | 51 | 52 |
53 |
54 |
57 |
60 |
61 |
62 |
63 | `; 64 | 65 | exports[`Popup should match snapshot with tabIndex defined. 1`] = ` 66 |
75 |
79 |
82 |
91 | 106 | 110 | 113 | 114 |
115 |
116 |
119 |
122 |
123 |
124 |
125 | `; 126 | 127 | exports[`Popup should match snapshot without close button. 1`] = ` 128 |
137 |
141 |
144 |
147 |
150 |
151 |
152 |
153 | `; 154 | 155 | exports[`Popup should match snapshot without feature 1`] = `null`; 156 | 157 | exports[`Popup should match snapshot without header. 1`] = ` 158 |
167 |
171 |
174 |
177 |
178 |
179 |
180 | `; 181 | -------------------------------------------------------------------------------- /src/components/Popup/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Popup"; 2 | -------------------------------------------------------------------------------- /src/components/README.md: -------------------------------------------------------------------------------- 1 | # Components 2 | 3 | This folder contains all the components. 4 | 5 | ## Create a new component 6 | 7 | Each component must have this structure: 8 | 9 | ```bash 10 | src/ 11 | components/ 12 | MyCategory/ 13 | MyComponent/ 14 | index.js // ES module export. 15 | MyComponent.js // The JSX component WITHOUT hardcoded classNames !!!! 16 | MyComponent.test.js // The test file with at least snapshots tests. 17 | MyComponent.scss // A sass file with default CSS when the main html element of MyComponent uses rs-mycomponent CSS class. 18 | README.md // The MyComponentExample component of use to display in the doc. 19 | MyComponent.md.scss // A sass file for the MyComponentExample component used in README.md 20 | ``` 21 | 22 | Some rules must be followed: 23 | 24 | - a component must allow to provide a className to the main html element of the component. 25 | - a component must be controlled by his parent via props. 26 | - a component must use `children` property instead of `renderXXX` functions when possible. 27 | - a component must propagate basic props `` when possible. 28 | - a component must have tests. 29 | - a component can provide a translation function using a `t` property. 30 | - default props must have a value or `undefined` (no `null` otherwise the attribute is created in the snapshot). 31 | - no redux stuff. 32 | - no hardcoded `className`, only in default props. 33 | - no translation library specific stuff. 34 | 35 | ## Create a new component from another 36 | 37 | ```bash 38 | yarn cp 39 | ``` 40 | 41 | then follow the guide. 42 | -------------------------------------------------------------------------------- /src/components/ResizeHandler/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./ResizeHandler"; 2 | -------------------------------------------------------------------------------- /src/components/RouteSchedule/README.md: -------------------------------------------------------------------------------- 1 | 2 | The following example demonstrates the use of RouteSchedule. 3 | 4 | ```jsx 5 | import React, { useState, useEffect } from 'react'; 6 | import { Layer, RealtimeLayer } from 'mobility-toolbox-js/ol'; 7 | import Tile from 'ol/layer/Tile'; 8 | import OSM from 'ol/source/OSM'; 9 | import BasicMap from 'react-spatial/components/BasicMap'; 10 | import RouteSchedule from 'react-spatial/components/RouteSchedule'; 11 | import { ToggleButton } from '@mui/material'; 12 | import { FaFilter } from 'react-icons/fa'; 13 | import { GpsFixed as GpsFixedIcon } from '@mui/icons-material'; 14 | 15 | 16 | // The `apiKey` used here is for demonstration purposes only. 17 | // Please get your own api key at https://developer.geops.io/. 18 | const trackerLayer = new RealtimeLayer({ 19 | url: 'wss://api.geops.io/tracker-ws/v1/ws', 20 | apiKey: window.apiKey, 21 | }); 22 | 23 | const layers = [ 24 | new Layer({ 25 | olLayer: new Tile({ 26 | source: new OSM(), 27 | }), 28 | }), 29 | trackerLayer, 30 | ]; 31 | 32 | let updateInterval; 33 | 34 | 35 | const getVehicleCoord = (routeIdentifier) => { 36 | const [trajectory] = trackerLayer.getVehicle((traj) => { 37 | return traj.properties.route_identifier === routeIdentifier; 38 | }); 39 | return trajectory && trajectory.properties.coordinate; 40 | }; 41 | 42 | function RouteScheduleExample() { 43 | const [lineInfos, setLineInfos] = useState(null); 44 | const [filterActive, setFilterActive] = useState(false); 45 | const [followActive, setFollowActive] = useState(false); 46 | const [center, setCenter] = useState([951560, 6002550]); 47 | const [feature, setFeature] = useState(); 48 | 49 | useEffect(()=> { 50 | let vehicleId = null; 51 | if (feature) { 52 | vehicleId = feature.get('train_id'); 53 | trackerLayer.api.subscribeStopSequence(vehicleId, ({ content: [stopSequence] }) => { 54 | if (stopSequence) { 55 | setLineInfos(stopSequence); 56 | } 57 | }); 58 | } else { 59 | setLineInfos(); 60 | } 61 | return ()=> { 62 | if (vehicleId){ 63 | trackerLayer.api.unsubscribeStopSequence(vehicleId); 64 | } 65 | } 66 | }, [feature]); 67 | 68 | useEffect(()=> { 69 | trackerLayer.onClick(([feature])=> { 70 | setFeature(feature); 71 | }); 72 | }, []); 73 | 74 | useEffect(()=> { 75 | trackerLayer.map.updateSize(); 76 | }, [lineInfos]); 77 | 78 | return ( 79 |
80 | ( 84 | <> 85 | { 89 | if (!filterActive) { 90 | trackerLayer.filter = (trajectory) => { 91 | return trajectory.properties.route_identifier === routeIdentifier; 92 | }; 93 | } else { 94 | trackerLayer.filter = null; 95 | } 96 | setFilterActive(!filterActive); 97 | }}> 98 | 99 | 100 | { 104 | clearInterval(updateInterval); 105 | if (!followActive) { 106 | updateInterval = window.setInterval(() => { 107 | const coord = getVehicleCoord(routeIdentifier); 108 | if (coord) { 109 | setCenter(coord); 110 | } 111 | }, 50); 112 | } 113 | setFollowActive(!followActive); 114 | }}> 115 | 116 | 117 | 118 | )} 119 | /> 120 | 126 |
127 | ); 128 | } 129 | 130 | ; 131 | ``` 132 | -------------------------------------------------------------------------------- /src/components/RouteSchedule/RouteSchedule.md.scss: -------------------------------------------------------------------------------- 1 | @use "../../themes/default/variables"; 2 | 3 | .rt-route-schedule-example { 4 | font-family: variables.$font-family; 5 | position: relative; 6 | display: flex; 7 | 8 | .rt-route-schedule { 9 | display: flex; 10 | flex-direction: column; 11 | height: 300px; 12 | width: 50%; 13 | 14 | .rt-route-header { 15 | padding-top: 0; 16 | } 17 | 18 | .rt-route-footer { 19 | padding-bottom: 0; 20 | } 21 | 22 | .rt-route-header, 23 | .rt-route-footer { 24 | flex: 0 0 auto; /* 'auto' is for IE11 */ 25 | } 26 | 27 | .rt-route-body { 28 | overflow-y: auto; 29 | flex: 2; 30 | } 31 | } 32 | 33 | .rs-map { 34 | flex: 1; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/RouteSchedule/RouteSchedule.scss: -------------------------------------------------------------------------------- 1 | @use "../../themes/default/variables"; 2 | 3 | .rt-route-schedule { 4 | background-color: white; 5 | width: 350px; 6 | overflow: hidden; 7 | 8 | .rt-route-header { 9 | display: flex; 10 | align-items: center; 11 | padding: 15px 10px 0; 12 | 13 | .rt-route-title { 14 | display: flex; 15 | flex-direction: column; 16 | 17 | .rt-route-name { 18 | padding-bottom: 8px; 19 | font-weight: bold; 20 | } 21 | } 22 | 23 | .rt-route-buttons { 24 | margin-left: auto; 25 | display: flex; 26 | 27 | button { 28 | width: 35px; 29 | height: 35px; 30 | margin: 10px 5px 15px; 31 | color: black; 32 | } 33 | } 34 | 35 | .rt-route-icon { 36 | border-radius: 20px; 37 | min-width: 20px; 38 | height: 20px; 39 | line-height: 20px; 40 | border: solid black 2px; 41 | padding: 5px; 42 | display: block; 43 | float: left; 44 | margin: 15px; 45 | margin-top: 10px; 46 | text-align: center; 47 | font-size: 14px; 48 | font-weight: bold; 49 | } 50 | } 51 | 52 | .rt-route-footer { 53 | padding: 20px; 54 | display: flex; 55 | align-items: center; 56 | } 57 | 58 | .rt-route-copyright { 59 | display: flex; 60 | flex-wrap: wrap; 61 | } 62 | 63 | .rt-route-body { 64 | font-size: 14px; 65 | padding: 0 20px; 66 | 67 | .rt-route-station { 68 | display: flex; 69 | align-items: center; 70 | cursor: pointer; 71 | border-radius: 4px; 72 | 73 | &:first-child, 74 | &:last-child { 75 | font-weight: bold; 76 | } 77 | 78 | &:hover { 79 | color: white; 80 | background-color: variables.$brand-secondary; 81 | } 82 | 83 | .rt-route-times, 84 | .rt-route-delay { 85 | display: flex; 86 | flex-direction: column; 87 | width: 40px; 88 | min-width: 40px; 89 | padding: 0 3px; 90 | } 91 | } 92 | 93 | .rt-route-station.rt-passed, 94 | .rt-route-station.rt-no-stop { 95 | .rt-route-delay { 96 | span { 97 | display: none; 98 | } 99 | } 100 | } 101 | 102 | .rt-route-station.rt-passed { 103 | opacity: 0.7; 104 | 105 | .rt-route-icon-mask { 106 | height: 0; 107 | } 108 | } 109 | 110 | .rt-route-station:first-child { 111 | .rt-route-time-arrival { 112 | display: none; 113 | } 114 | } 115 | 116 | .rt-route-station:last-child { 117 | .rt-route-time-departure { 118 | display: none; 119 | } 120 | } 121 | } 122 | 123 | .rt-route-cancelled { 124 | text-decoration: line-through; 125 | color: rgb(236 43 43); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/components/RouteSchedule/RouteSchedule.test.js: -------------------------------------------------------------------------------- 1 | import "jest-date-mock"; 2 | import { RealtimeLayer as TrackerLayer } from "mobility-toolbox-js/ol"; 3 | import React from "react"; 4 | import renderer from "react-test-renderer"; 5 | 6 | import RouteSchedule from "."; 7 | 8 | const RealDate = Date; 9 | 10 | const lineInfos = { 11 | backgroundColor: "ff8a00", 12 | destination: "Station name", 13 | id: 9959310, 14 | longName: "T 3", 15 | operator: "foo", 16 | operatorUrl: "foo.ch", 17 | publisher: "bar", 18 | publisherUrl: "bar.ch", 19 | routeIdentifier: "03634.003849.004:9", 20 | shortName: "3", 21 | stations: [ 22 | { 23 | aimedArrivalTime: 1571729580000, 24 | aimedDepartureTime: 1571729580000, 25 | arrivalDelay: 60000, // +1m 26 | arrivalTime: 1571729580000 + 60000, 27 | coordinates: [8.51772, 47.3586], 28 | departureDelay: 60000, 29 | departureTime: 1571729580000 + 60000, 30 | stationId: "1", 31 | stationName: "first stop", 32 | }, 33 | { 34 | aimedArrivalTime: 1571729903000, 35 | aimedDepartureTime: 1571729903000, 36 | arrivalDelay: 0, // +0 37 | arrivalTime: 1571729903000, 38 | coordinates: [8.54119, 47.36646], 39 | departureDelay: 120000, // +2m 40 | departureTime: 1571729903000 + 120000, 41 | stationId: "2", 42 | stationName: "second stop", 43 | }, 44 | { 45 | aimedArrivalTime: 0, 46 | aimedDepartureTime: 0, 47 | arrivalDelay: null, // no realtime 48 | arrivalTime: 0, 49 | coordinates: [8.54119, 47.36646], 50 | departureDelay: null, // no realtime 51 | departureTime: 0, 52 | stationId: "4", 53 | stationName: "no stop", 54 | }, 55 | { 56 | aimedArrivalTime: 1571730323000, 57 | aimedDepartureTime: 0, 58 | arrivalDelay: 240000, // +4m 59 | arrivalTime: 1571730323000 + 240000, 60 | coordinates: [8.54119, 50], 61 | departureDelay: 0, // +0 62 | departureTime: 0, 63 | stationId: "3", 64 | stationName: "third stop", 65 | }, 66 | ], 67 | vehicleType: 0, 68 | }; 69 | 70 | describe("RouteSchedule", () => { 71 | beforeEach(() => { 72 | global.Date = jest.fn(() => { 73 | return { 74 | getHours: () => { 75 | return 9; 76 | }, 77 | getMinutes: () => { 78 | return 1; 79 | }, 80 | }; 81 | }); 82 | Object.assign(Date, RealDate); 83 | }); 84 | 85 | afterEach(() => { 86 | global.Date = RealDate; 87 | }); 88 | 89 | test("matches snapshots.", () => { 90 | const trackerLayer = new TrackerLayer({}); 91 | const component = renderer.create( 92 | { 95 | return
Button
; 96 | }} 97 | setCenter={() => {}} 98 | trackerLayer={trackerLayer} 99 | />, 100 | ); 101 | const tree = component.toJSON(); 102 | expect(tree).toMatchSnapshot(); 103 | }); 104 | 105 | // to test: on station click 106 | // to test: time formating 107 | // to test: delay formating 108 | // to test: delay color 109 | // to test: no arrival delay on first station 110 | // to test: no arrival date on first station 111 | // to test: no departure delay on last station 112 | // to test: no departure date on last station 113 | // to test: font bold on first and last station 114 | // to test: custom getDelayString prop 115 | }); 116 | -------------------------------------------------------------------------------- /src/components/RouteSchedule/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./RouteSchedule"; 2 | -------------------------------------------------------------------------------- /src/components/ScaleLine/README.md: -------------------------------------------------------------------------------- 1 | 2 | The following example demonstrates the use of ScaleLine. 3 | 4 | ```js 5 | import React, { Component } from 'react'; 6 | import { MapboxLayer } from 'mobility-toolbox-js/ol'; 7 | import Tile from 'ol/layer/Tile'; 8 | import OSM from 'ol/source/OSM'; 9 | import Map from 'ol/Map'; 10 | import BasicMap from 'react-spatial/components/BasicMap'; 11 | import ScaleLine from 'react-spatial/components/ScaleLine'; 12 | 13 | const map = new Map({ controls: [] }); 14 | 15 | const layers = [ 16 | new MapboxLayer({ 17 | url: `https://maps.geops.io/styles/travic_v2/style.json?key=${apiKey}`, 18 | }), 19 | ]; 20 | 21 |
22 | 27 | 28 |
29 | ``` 30 | -------------------------------------------------------------------------------- /src/components/ScaleLine/ScaleLine.js: -------------------------------------------------------------------------------- 1 | import OLScaleLine from "ol/control/ScaleLine"; 2 | import OLMap from "ol/Map"; 3 | import PropTypes from "prop-types"; 4 | import React, { useEffect, useRef } from "react"; 5 | 6 | const propTypes = { 7 | /** 8 | * ol/map. 9 | */ 10 | map: PropTypes.instanceOf(OLMap).isRequired, 11 | 12 | /** 13 | * Options for ol/control/ScaleLine. 14 | * See https://openlayers.org/en/latest/apidoc/module-ol_control_ScaleLine-ScaleLine.html 15 | */ 16 | options: PropTypes.object, 17 | }; 18 | 19 | const defaultProps = { 20 | options: {}, 21 | }; 22 | 23 | /** 24 | * The ScaleLine component creates an 25 | * [ol/control/ScaleLine](https://openlayers.org/en/latest/apidoc/module-ol_control_ScaleLine-ScaleLine.html) 26 | * for an [ol/map](https://openlayers.org/en/latest/apidoc/module-ol_Map-Map.html). 27 | */ 28 | function ScaleLine({ map, options = defaultProps.options, ...other }) { 29 | const ref = useRef(); 30 | 31 | useEffect(() => { 32 | const control = new OLScaleLine({ 33 | ...options, 34 | ...{ target: ref.current }, 35 | }); 36 | 37 | map.addControl(control); 38 | return () => { 39 | map.removeControl(control); 40 | }; 41 | }, [map, options]); 42 | 43 | // eslint-disable-next-line react/jsx-props-no-spreading 44 | return
; 45 | } 46 | 47 | ScaleLine.propTypes = propTypes; 48 | 49 | export default React.memo(ScaleLine); 50 | -------------------------------------------------------------------------------- /src/components/ScaleLine/ScaleLine.scss: -------------------------------------------------------------------------------- 1 | @use "../../themes/default/variables"; 2 | 3 | .rs-scale-line .ol-scale-line-inner { 4 | border-width: 0 1px 1px; 5 | border-color: 1px solid variables.$gray; 6 | border-style: solid; 7 | padding: variables.$padding-base; 8 | font-size: variables.$font-size-small; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/ScaleLine/ScaleLine.test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/react"; 2 | import "jest-canvas-mock"; 3 | import OLMap from "ol/Map"; 4 | import OLView from "ol/View"; 5 | import React from "react"; 6 | 7 | import ScaleLine from "./ScaleLine"; 8 | 9 | describe("ScaleLine", () => { 10 | test("matches snapshot", () => { 11 | const map = new OLMap({ view: new OLView({ center: [0, 0], zoom: 7 }) }); 12 | const component = render(); 13 | expect(component.container.innerHTML).toMatchSnapshot(); 14 | }); 15 | 16 | test("remove control on unmount.", () => { 17 | const map = new OLMap({ controls: [] }); 18 | const spy = jest.spyOn(map, "removeControl"); 19 | const spy2 = jest.spyOn(map, "addControl"); 20 | const { unmount } = render(); 21 | expect(spy).toHaveBeenCalledTimes(0); 22 | unmount(); 23 | expect(spy).toHaveBeenCalledTimes(1); 24 | expect(spy.mock.calls[0][0]).toBe(spy2.mock.calls[0][0]); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/ScaleLine/__snapshots__/ScaleLine.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ScaleLine matches snapshot 1`] = ` 4 |
5 |
8 |
9 |
10 |
11 |
12 | `; 13 | -------------------------------------------------------------------------------- /src/components/ScaleLine/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./ScaleLine"; 2 | -------------------------------------------------------------------------------- /src/components/StopsFinder/README.md: -------------------------------------------------------------------------------- 1 | 2 | This demonstrates the use of the StopsFinder component. 3 | 4 | ```jsx 5 | import React from 'react'; 6 | import { Layer } from 'mobility-toolbox-js/ol'; 7 | import Map from 'ol/Map'; 8 | import Tile from 'ol/layer/Tile'; 9 | import { fromLonLat } from 'ol/proj'; 10 | import OSM from 'ol/source/OSM'; 11 | import BasicMap from 'react-spatial/components/BasicMap'; 12 | import { ThemeProvider } from '@mui/material'; 13 | import { geopsTheme} from '@geops/geops-ui'; 14 | import StopsFinder from 'react-spatial/components/StopsFinder'; 15 | 16 | const map = new Map({ controls: [] }); 17 | 18 | const layers = [ 19 | new Layer({ 20 | olLayer: new Tile({ 21 | source: new OSM(), 22 | }), 23 | }), 24 | ]; 25 | 26 | // The `apiKey` used here is for demonstration purposes only. 27 | // Please get your own api key at https://developer.geops.io/. 28 | const { apiKey } = window; 29 | 30 | 31 | 38 | 39 | { 43 | console.log(geometry); 44 | map.getView().setCenter(fromLonLat(geometry.coordinates)); 45 | }} 46 | /> 47 | 48 | ``` 49 | -------------------------------------------------------------------------------- /src/components/StopsFinder/StopsFinder.test.js: -------------------------------------------------------------------------------- 1 | import { Map } from "ol"; 2 | import React from "react"; 3 | import renderer from "react-test-renderer"; 4 | 5 | import StopsFinder from "."; 6 | 7 | describe("StopsFinder", () => { 8 | let map; 9 | 10 | beforeEach(() => { 11 | map = new Map({}); 12 | }); 13 | 14 | test("matches snapshots.", () => { 15 | const component = renderer.create(); 16 | expect(component.toJSON()).toMatchSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/StopsFinder/StopsFinderOption.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import { CircularProgress, styled } from "@mui/material"; 3 | import PropTypes from "prop-types"; 4 | import React, { lazy, Suspense } from "react"; 5 | 6 | const ext = "_round-blue-01.svg"; 7 | const iconForMot = {}; 8 | [ 9 | "bus", 10 | "ferry", 11 | "gondola", 12 | "tram", 13 | "rail", 14 | "funicular", 15 | "cable_car", 16 | "subway", 17 | ].forEach((mot) => { 18 | iconForMot[mot] = lazy(() => { 19 | return import(`../../images/mots/${mot}${ext}`); 20 | }); 21 | }); 22 | 23 | const StyledFlex = styled("div")(() => ({ 24 | alignItems: "center", 25 | display: "flex", 26 | gap: 5, 27 | })); 28 | 29 | function StopsFinderOption({ option, ...props }) { 30 | return ( 31 | }> 32 | 33 | {Object.entries(option.properties?.mot).map(([key, value]) => { 34 | if (value) { 35 | const MotIcon = iconForMot[key]; 36 | return ( 37 | 38 | 39 | 40 | ); 41 | } 42 | return null; 43 | })} 44 | {option.properties.name} 45 | 46 | 47 | ); 48 | } 49 | 50 | StopsFinderOption.propTypes = { 51 | option: PropTypes.object.isRequired, 52 | }; 53 | 54 | export default React.memo(StopsFinderOption); 55 | -------------------------------------------------------------------------------- /src/components/StopsFinder/__snapshots__/StopsFinder.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`StopsFinder matches snapshots. 1`] = ` 4 |
11 |
14 | 22 |
27 | 49 |
52 | 93 |
94 |
98 | 101 | 102 | Search stops 103 | 104 | 105 |
106 |
107 |
108 |
109 | `; 110 | -------------------------------------------------------------------------------- /src/components/StopsFinder/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./StopsFinder"; 2 | -------------------------------------------------------------------------------- /src/components/Zoom/README.md: -------------------------------------------------------------------------------- 1 | 2 | The following example demonstrates the use of Zoom. 3 | 4 | ```jsx 5 | import React from 'react'; 6 | import { MapboxLayer } from 'mobility-toolbox-js/ol'; 7 | import Tile from 'ol/layer/Tile'; 8 | import OSM from 'ol/source/OSM'; 9 | import Map from 'ol/Map'; 10 | import BasicMap from 'react-spatial/components/BasicMap'; 11 | import Zoom from 'react-spatial/components/Zoom'; 12 | 13 | const map = new Map({ controls: [] }); 14 | 15 | const layers = [ 16 | new MapboxLayer({ 17 | url: `https://maps.geops.io/styles/travic_v2/style.json?key=${apiKey}`, 18 | }) 19 | ]; 20 | 21 |
22 | 23 | 24 |
25 | ``` 26 | -------------------------------------------------------------------------------- /src/components/Zoom/Zoom.md.scss: -------------------------------------------------------------------------------- 1 | @use '../../themes/default/variables'; 2 | 3 | .rs-zoom-example { 4 | position: relative; 5 | 6 | .rs-map { 7 | height: 400px; 8 | } 9 | 10 | .rs-zooms-bar { 11 | top: 0; 12 | right: 10px; 13 | } 14 | 15 | .rs-zoom-in, 16 | .rs-zoom-out { 17 | background: variables.$btn-secondary-color; 18 | border-radius: 50%; 19 | color: white; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Zoom/Zoom.scss: -------------------------------------------------------------------------------- 1 | @use "../../themes/default/variables"; 2 | 3 | .rs-zooms-bar { 4 | position: absolute; 5 | top: 0; 6 | right: 0; 7 | 8 | div { 9 | margin: 10px 0; 10 | } 11 | 12 | .rs-zoom-in, 13 | .rs-zoom-out { 14 | cursor: pointer; 15 | height: variables.$btn-size-base; 16 | width: variables.$btn-size-base; 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | transition: background-color 0.5s ease, color 0.5s ease; 21 | border: none; 22 | 23 | &:disabled { 24 | pointer-events: none; 25 | opacity: 0.4; 26 | } 27 | } 28 | 29 | .rs-zoomslider-wrapper { 30 | display: flex; 31 | justify-content: center; 32 | 33 | .ol-zoomslider { 34 | position: relative; 35 | top: 0; 36 | left: 0; 37 | user-select: none; 38 | background-color: rgb(255 255 255 / 40%); 39 | border: 1px solid variables.$brand-secondary; 40 | border-radius: 3px; 41 | outline: none; 42 | overflow: hidden; 43 | width: 14px; 44 | height: 200px; 45 | margin: 0; 46 | 47 | .ol-zoomslider-thumb { 48 | user-select: none; 49 | position: relative; 50 | display: block; 51 | background: variables.$brand-secondary; 52 | outline: none; 53 | overflow: hidden; 54 | cursor: pointer; 55 | font-size: 1.14em; 56 | height: 20px; 57 | width: 14px; 58 | margin: 0; 59 | padding: 0; 60 | border: none; 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/Zoom/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Zoom"; 2 | -------------------------------------------------------------------------------- /src/images/RouteSchedule/firstStation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/images/RouteSchedule/firstStation.png -------------------------------------------------------------------------------- /src/images/RouteSchedule/lastStation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/images/RouteSchedule/lastStation.png -------------------------------------------------------------------------------- /src/images/RouteSchedule/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/images/RouteSchedule/line.png -------------------------------------------------------------------------------- /src/images/RouteSchedule/station.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/images/RouteSchedule/station.png -------------------------------------------------------------------------------- /src/images/baselayer/baselayer.basebright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/images/baselayer/baselayer.basebright.png -------------------------------------------------------------------------------- /src/images/baselayer/baselayer.osm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/images/baselayer/baselayer.osm.png -------------------------------------------------------------------------------- /src/images/baselayer/baselayer.travic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/images/baselayer/baselayer.travic.png -------------------------------------------------------------------------------- /src/images/baselayer/open.topo.map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/images/baselayer/open.topo.map.png -------------------------------------------------------------------------------- /src/images/baselayer/osm.baselayer.hot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/images/baselayer/osm.baselayer.hot.png -------------------------------------------------------------------------------- /src/images/baselayer/osm.baselayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/images/baselayer/osm.baselayer.png -------------------------------------------------------------------------------- /src/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/images/favicon.png -------------------------------------------------------------------------------- /src/images/geops_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/images/geops_logo.png -------------------------------------------------------------------------------- /src/images/geops_logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/geops_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/images/geops_qr.png -------------------------------------------------------------------------------- /src/images/mots/bus_poi-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/images/mots/bus_poi-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/images/mots/bus_round-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/mots/bus_round-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/mots/bus_square-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/mots/bus_square-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/mots/cable_car_poi-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/mots/cable_car_poi-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/mots/cable_car_round-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/mots/cable_car_round-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/mots/cable_car_square-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/images/mots/cable_car_square-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/images/mots/ferry_poi-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/mots/ferry_poi-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/mots/ferry_round-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/mots/ferry_round-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/mots/ferry_square-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/mots/ferry_square-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/mots/funicular_round-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/mots/funicular_round-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/images/mots/funicular_square-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/images/mots/gondola_round-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/mots/rail_poi-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/images/mots/rail_poi-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/images/mots/rail_round-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/images/mots/rail_round-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/images/mots/rail_square-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/mots/rail_square-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/mots/subway_round blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/images/mots/subway_round-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/images/mots/tram_poi-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/mots/tram_poi-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/mots/tram_round-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/mots/tram_round-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/mots/tram_square-blue-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/mots/tram_square-grey-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/northArrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/northArrow.url.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/northArrowCircle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/northArrowCircle.url.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/propTypes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | const STATE_BOARDING = "BOARDING"; 4 | const STATE_LEAVING = "LEAVING"; 5 | const STATE_PENDING = "PENDING"; 6 | const STATE_TIME_BASED = "TIME_BASED"; 7 | const STATE_STOP_CANCELLED = "STOP_CANCELLED"; 8 | const STATE_JOURNEY_CANCELLED = "JOURNEY_CANCELLED"; 9 | 10 | const station = PropTypes.shape({ 11 | aimedArrivalTime: PropTypes.number, // time in milliseconds. 12 | aimedDepartureTime: PropTypes.number, // time in milliseconds. 13 | arrivalDelay: PropTypes.number, // time in milliseconds. 14 | arrivalTime: PropTypes.number, // time in milliseconds with the delay included. 15 | cancelled: PropTypes.bool, 16 | coordinates: PropTypes.arrayOf(PropTypes.number), 17 | departureDelay: PropTypes.number, // time in milliseconds. 18 | departureTime: PropTypes.number, // time in milliseconds with the delay included 19 | noDropOff: PropTypes.bool, 20 | noPickUp: PropTypes.bool, 21 | state: PropTypes.oneOf([ 22 | null, 23 | STATE_BOARDING, 24 | STATE_LEAVING, 25 | STATE_PENDING, 26 | STATE_TIME_BASED, 27 | STATE_STOP_CANCELLED, 28 | STATE_JOURNEY_CANCELLED, 29 | ]), 30 | stationId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 31 | stationName: PropTypes.string, 32 | wheelchairAccessible: PropTypes.bool, 33 | }); 34 | 35 | const lineInfos = PropTypes.shape({ 36 | backgroundColor: PropTypes.string, 37 | bicyclesAllowed: PropTypes.bool, 38 | color: PropTypes.string, 39 | destination: PropTypes.string, 40 | feedsId: PropTypes.number, 41 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 42 | license: PropTypes.string, 43 | licenseNote: PropTypes.string, 44 | licenseUrl: PropTypes.string, 45 | longName: PropTypes.string, 46 | operatingInformations: PropTypes.object, 47 | operator: PropTypes.string, 48 | operatorTimeZone: PropTypes.string, 49 | operatorUrl: PropTypes.string, 50 | publisher: PropTypes.string, 51 | publisherTimeZone: PropTypes.string, 52 | publisherUrl: PropTypes.string, 53 | realTime: PropTypes.number, 54 | shortName: PropTypes.string, 55 | stations: PropTypes.arrayOf(station), 56 | vehicleType: PropTypes.number, 57 | wheelchairAccessible: PropTypes.bool, 58 | }); 59 | 60 | export default { 61 | lineInfos, 62 | STATE_BOARDING, 63 | STATE_LEAVING, 64 | station, 65 | }; 66 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import ResizeObserver from "resize-observer-polyfill"; 2 | 3 | global.URL.createObjectURL = jest.fn(() => { 4 | return "fooblob"; 5 | }); 6 | 7 | global.ResizeObserver = ResizeObserver; 8 | -------------------------------------------------------------------------------- /src/styleguidist/ComponentsList.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | // Import default implementation from react-styleguidist using the full path 4 | import ComponentsListRenderer from "react-styleguidist/lib/client/rsg-components/ComponentsList/ComponentsListRenderer"; 5 | import getUrl from "react-styleguidist/lib/client/utils/getUrl"; 6 | 7 | const propTypes = { 8 | classes: PropTypes.object, 9 | hashPath: PropTypes.array, 10 | items: PropTypes.array.isRequired, 11 | useHashId: PropTypes.bool, 12 | useRouterLinks: PropTypes.bool, 13 | }; 14 | 15 | const defaultProps = { 16 | hashPath: [], 17 | }; 18 | 19 | function ComponentsList({ 20 | classes, 21 | hashPath = defaultProps.hashPath, 22 | items, 23 | useHashId = true, 24 | useRouterLinks = false, 25 | }) { 26 | const mappedItems = items.map((item) => { 27 | return { 28 | ...item, 29 | href: item.href 30 | ? item.href 31 | : // Conflict with Permalink Component: Remove the first '/' to avoid page reload on click 32 | getUrl({ 33 | anchor: !useRouterLinks, 34 | hashPath: useRouterLinks ? hashPath : false, 35 | id: useRouterLinks ? useHashId : false, 36 | name: item.name, 37 | slug: item.slug, 38 | }) 39 | .replace(/^\/index.html+/g, "") 40 | .replace(/^\/+/g, ""), 41 | }; 42 | }); 43 | return ; 44 | } 45 | 46 | ComponentsList.propTypes = propTypes; 47 | 48 | export default ComponentsList; 49 | -------------------------------------------------------------------------------- /src/styleguidist/styleguidist.css: -------------------------------------------------------------------------------- 1 | /* Overwrtie styleguidist styles */ 2 | 3 | .link-active { 4 | font-weight: bold !important; 5 | } 6 | 7 | header a:hover { 8 | text-decoration: none; 9 | color: inherit; 10 | } 11 | 12 | #promo { 13 | position: fixed; 14 | bottom: 55px; 15 | background-color: #e90; 16 | right: -65px; 17 | transform: rotate(-45deg); 18 | z-index: 1; 19 | line-height: 1.2; 20 | } 21 | 22 | #promo-text { 23 | color: white; 24 | font-size: 14px; 25 | font-family: sans-serif; 26 | font-weight: bold; 27 | margin: 0; 28 | padding: 3px 70px; 29 | } 30 | 31 | #promo a, 32 | #promo a:hover { 33 | text-decoration: none; 34 | } 35 | 36 | footer { 37 | position: relative !important; 38 | } 39 | -------------------------------------------------------------------------------- /src/themes/README.md: -------------------------------------------------------------------------------- 1 | # Themes 2 | 3 | We are NOT a CSS library so we provide some default CSS helpers to help you start but feel free to use your own technology bootstrap or JSS or styled-compoennts. 4 | 5 | ## How to use 6 | 7 | We provide a set of CSS variables and classes to help you start using `react-spatial` . 8 | To use it just import the `index.scss` file of the `default` theme in your application: 9 | 10 | ```js 11 | import 'react-spatial/themes/default/index.scss'; 12 | ``` 13 | 14 | If you want to override variables just import the `default/variables.css` and the default CSS files you want, there is one CSS file by component. 15 | 16 | ## Create a new theme 17 | 18 | Just add a folder with an `index.scss` file. 19 | 20 | Some rules must be followed: 21 | 22 | - no positionning css for container. 23 | - no size information without using CSS variable. 24 | - use `display: flex` for container when possible. 25 | 26 | Of course those rules must be adapted depending on the component. 27 | -------------------------------------------------------------------------------- /src/themes/default/components.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * This file load the css of components. 3 | */ 4 | @use '../../components/BaseLayerSwitcher/BaseLayerSwitcher'; 5 | @use '../../components/Geolocation/Geolocation'; 6 | @use '../../components/LayerTree/LayerTree'; 7 | @use '../../components/NorthArrow/NorthArrow'; 8 | @use '../../components/Popup/Popup'; 9 | @use '../../components/ScaleLine/ScaleLine'; 10 | @use '../../components/Zoom/Zoom'; 11 | @use '../../components/RouteSchedule/RouteSchedule'; 12 | @use '../../components/Overlay/Overlay'; 13 | -------------------------------------------------------------------------------- /src/themes/default/examples.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is loaded by the styleguide and provide additionnal css for all examples. 3 | */ 4 | @use 'index'; 5 | @use 'variables'; 6 | @use 'mixins'; 7 | @use '../../components/BaseLayerSwitcher/BaseLayerSwitcher.md'; 8 | @use '../../components/BasicMap/BasicMap.md'; 9 | @use '../../components/CanvasSaveButton/CanvasSaveButton.md'; 10 | @use '../../components/FeatureExportButton/FeatureExportButton.md'; 11 | @use '../../components/FitExtent/FitExtent.md'; 12 | @use '../../components/Geolocation/Geolocation.md'; 13 | @use '../../components/LayerTree/LayerTree.md'; 14 | @use '../../components/MousePosition/MousePosition.md'; 15 | @use '../../components/Permalink/Permalink.md'; 16 | @use '../../components/Popup/Popup.md'; 17 | @use '../../components/Zoom/Zoom.md'; 18 | @use '../../components/RouteSchedule/RouteSchedule.md'; 19 | @use '../../components/Copyright/Copyright.md'; 20 | @use '../../components/Overlay/Overlay.md'; 21 | 22 | $link-color: #000; 23 | $link-color-hover: #000; 24 | 25 | /* Load 'a' mixin */ 26 | @include mixins.a(); 27 | 28 | a { 29 | font-size: 11px; 30 | } 31 | 32 | body { 33 | font-family: variables.$font-family; 34 | } 35 | -------------------------------------------------------------------------------- /src/themes/default/index.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * This file load the default theme, for all the components. 3 | */ 4 | @use 'variables'; 5 | @use 'mixins'; 6 | @use 'components'; 7 | 8 | [role='button']:not([disabled]), 9 | button:not([disabled]), 10 | a:not([disabled]) { 11 | cursor: pointer; 12 | } 13 | -------------------------------------------------------------------------------- /src/themes/default/mixins.scss: -------------------------------------------------------------------------------- 1 | @use "variables"; 2 | 3 | /** 4 | * This file defines mixins. 5 | */ 6 | 7 | /** 8 | * Define basic style for tag, using variables. 9 | */ 10 | @mixin a() { 11 | a { 12 | color: variables.$link-color; 13 | text-decoration: variables.$link-decoration; 14 | 15 | &:hover { 16 | color: variables.$link-color-hover; 17 | text-decoration: variables.$link-decoration-hover; 18 | } 19 | 20 | &:active { 21 | color: variables.$link-color-active; 22 | } 23 | 24 | &.rs-selected { 25 | font-weight: bold; 26 | } 27 | } 28 | } 29 | 30 | /** 31 | * Load fonts. 32 | */ 33 | @mixin loadFonts($paths...) { 34 | @each $family in $paths { 35 | @font-face { 36 | font-family: #{$family}; 37 | src: url('#{$family}.eot'); 38 | src: 39 | url('#{$family}.woff2') format('woff2'), 40 | url('#{$family}.woff') format('woff'); 41 | font-weight: normal; 42 | font-style: normal; 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * Create a simple @keyframes animation. 49 | */ 50 | @mixin keyframes($name, $propName, $start, $end) { 51 | @keyframes #{$name} { 52 | 0% { 53 | #{$propName}: $start; 54 | } 55 | 56 | 100% { 57 | #{$propName}: $end; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/themes/default/variables.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * This file defines variables. 3 | */ 4 | @use "sass:color"; 5 | @use "sass:math"; 6 | 7 | /* Color */ 8 | $brand-primary: #eb0000; 9 | $brand-secondary: #003d85; 10 | $gray-base: #000; 11 | $gray-lighter: color.adjust($gray-base, $lightness: 93.5%); 12 | $gray-light: color.adjust($gray-base, $lightness: 46.7%); 13 | $gray: color.adjust($gray-base,$lightness: 33.5%); 14 | $gray-dark: color.adjust($gray-base, $lightness: 20%); 15 | $gray-darker: color.adjust($gray-base, $lightness: 13.5%); 16 | 17 | /* Text */ 18 | $font-family: arial, sans-serif; 19 | $font-family-bold: arial, sans-serif; 20 | $font-size-base: 15px; 21 | $font-size-small: math.ceil($font-size-base * 0.85); 22 | $font-size-large: math.ceil($font-size-base * 1.25); 23 | 24 | /* Shadow */ 25 | $box-shadow: 0 1px 4px rgb(0 0 0 / 20%); 26 | 27 | /* Padding */ 28 | $padding-base: 5px; 29 | $padding-base-vertical: 5px; 30 | $padding-base-horizontal: 5px; 31 | 32 | /* Link */ 33 | $link-color: $brand-primary; 34 | $link-color-hover: color.adjust($brand-primary,$lightness: -10%); 35 | $link-color-active: color.adjust($brand-primary, $lightness: -5%); 36 | $link-decoration: none; 37 | $link-decoration-hover: underline; 38 | 39 | /* Buttons */ 40 | $btn-primary-color: $brand-primary; 41 | $btn-primary-color-hover: color.adjust($brand-primary, $lightness: -10%); 42 | $btn-primary-color-active: color.adjust($brand-primary, $lightness: -5%); 43 | $btn-secondary-color: $brand-secondary; 44 | $btn-secondary-color-hover: color.adjust($btn-secondary-color, $lightness: -10%); 45 | $btn-secondary-color-active: color.adjust($btn-secondary-color, $lightness: -5%); 46 | $btn-size-base: 50px; 47 | $btn-size-small: math.ceil($btn-size-base * 0.85); 48 | $btn-size-large: math.ceil($btn-size-base * 1.25); 49 | 50 | /* Z-index */ 51 | $zindex-base: 0; 52 | $zindex-lower: 200; 53 | $zindex-low: 400; 54 | $zindex-high: 600; 55 | $zindex-higher: 800; 56 | 57 | /* Others */ 58 | $header-height: 55px; 59 | $footer-height: 25px; 60 | $sidebar-open-width: 200px; 61 | -------------------------------------------------------------------------------- /src/utils/GlobalsForOle.js: -------------------------------------------------------------------------------- 1 | import { OL3Parser } from "jsts/org/locationtech/jts/io"; 2 | import { BufferOp } from "jsts/org/locationtech/jts/operation/buffer"; 3 | import { OverlayOp } from "jsts/org/locationtech/jts/operation/overlay"; 4 | import Collection from "ol/Collection"; 5 | import Control from "ol/control/Control"; 6 | import * as events from "ol/events"; 7 | import * as condition from "ol/events/condition"; 8 | import { getCenter } from "ol/extent"; 9 | import Feature from "ol/Feature"; 10 | import { 11 | LineString, 12 | MultiLineString, 13 | MultiPoint, 14 | MultiPolygon, 15 | Point, 16 | Polygon, 17 | } from "ol/geom"; 18 | import LinearRing from "ol/geom/LinearRing"; 19 | import { fromExtent } from "ol/geom/Polygon"; 20 | import Draw from "ol/interaction/Draw"; 21 | import Modify from "ol/interaction/Modify"; 22 | import Pointer from "ol/interaction/Pointer"; 23 | import Select from "ol/interaction/Select"; 24 | import Snap from "ol/interaction/Snap"; 25 | import OLVectorLayer from "ol/layer/Vector"; 26 | import Observable, { unByKey } from "ol/Observable"; 27 | import VectorSource from "ol/source/Vector"; 28 | import Circle from "ol/style/Circle"; 29 | import Fill from "ol/style/Fill"; 30 | import Icon from "ol/style/Icon"; 31 | import RegularShape from "ol/style/RegularShape"; 32 | import Stroke from "ol/style/Stroke"; 33 | import Style from "ol/style/Style"; 34 | 35 | /** 36 | * This module create window.ol and window.jsts variables for ole editor. 37 | */ 38 | if (!window.ol) { 39 | window.ol = { 40 | Collection, 41 | control: { 42 | Control, 43 | }, 44 | events: { 45 | ...events, 46 | condition: { 47 | ...condition, 48 | }, 49 | }, 50 | extent: { 51 | getCenter, 52 | }, 53 | Feature, 54 | geom: { 55 | LinearRing, 56 | LineString, 57 | MultiLineString, 58 | MultiPoint, 59 | MultiPolygon, 60 | Point, 61 | Polygon, 62 | }, 63 | interaction: { 64 | Draw, 65 | Modify, 66 | Pointer, 67 | Select, 68 | Snap, 69 | }, 70 | layer: { 71 | Vector: OLVectorLayer, 72 | }, 73 | Observable: { 74 | ...Observable, 75 | unByKey, 76 | }, 77 | source: { 78 | Vector: VectorSource, 79 | }, 80 | style: { 81 | Circle, 82 | Fill, 83 | Icon, 84 | RegularShape, 85 | Stroke, 86 | Style, 87 | }, 88 | }; 89 | window.ol.geom.Polygon.fromExtent = fromExtent; 90 | } 91 | 92 | if (!window.jsts) { 93 | window.jsts = { 94 | io: { 95 | OL3Parser, 96 | }, 97 | operation: { buffer: { BufferOp }, overlay: { OverlayOp } }, 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /src/utils/Styles.js: -------------------------------------------------------------------------------- 1 | import { Circle, Fill, Stroke, Style, Text } from "ol/style"; 2 | 3 | // Default style for Ol 4 | const fill = new Fill({ 5 | color: "rgba(255,255,255,0.4)", 6 | }); 7 | const stroke = new Stroke({ 8 | color: "#3399CC", 9 | width: 1.25, 10 | }); 11 | const dfltOlStyle = new Style({ 12 | fill, 13 | image: new Circle({ 14 | fill, 15 | radius: 5, 16 | stroke, 17 | }), 18 | stroke, 19 | }); 20 | 21 | // Default style for KML layer 22 | const kmlFill = new Fill({ 23 | color: [255, 0, 0, 0.7], 24 | }); 25 | const kmlStroke = new Stroke({ 26 | color: [255, 0, 0, 1], 27 | width: 1.5, 28 | }); 29 | const kmlcircle = new Circle({ 30 | fill: kmlFill, 31 | radius: 7, 32 | stroke: kmlStroke, 33 | }); 34 | const kmlStyle = new Style({ 35 | fill: kmlFill, 36 | image: kmlcircle, 37 | stroke: kmlStroke, 38 | text: new Text({ 39 | fill: kmlFill, 40 | font: "normal 16px Helvetica", 41 | stroke: new Stroke({ 42 | color: [255, 255, 255, 1], 43 | width: 3, 44 | }), 45 | }), 46 | }); 47 | 48 | export { kmlStyle }; 49 | 50 | export default { 51 | default: dfltOlStyle, 52 | }; 53 | -------------------------------------------------------------------------------- /src/utils/__snapshots__/KML.test.js.snap.KML-readFeatures()-and-writeFeatures()-should-read-and-write-lineDash-and-fillPattern-style-for-polygon.canvas-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/utils/__snapshots__/KML.test.js.snap.KML-readFeatures()-and-writeFeatures()-should-read-and-write-lineDash-and-fillPattern-style-for-polygon.canvas-image.png -------------------------------------------------------------------------------- /src/utils/__snapshots__/getPolygonPattern.test.js.snap.getPolygonPattern()-render-pattern-2-(cross)-color-and-(light-blue)-opacity.canvas-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/utils/__snapshots__/getPolygonPattern.test.js.snap.getPolygonPattern()-render-pattern-2-(cross)-color-and-(light-blue)-opacity.canvas-image.png -------------------------------------------------------------------------------- /src/utils/__snapshots__/getPolygonPattern.test.js.snap.getPolygonPattern()-render-pattern-3-(diagonal-line-from-bottom-left-tot-top-right)-with-color-(light-blue)-and-opacity.canvas-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/utils/__snapshots__/getPolygonPattern.test.js.snap.getPolygonPattern()-render-pattern-3-(diagonal-line-from-bottom-left-tot-top-right)-with-color-(light-blue)-and-opacity.canvas-image.png -------------------------------------------------------------------------------- /src/utils/__snapshots__/getPolygonPattern.test.js.snap.getPolygonPattern()-render-pattern-4-(diagonal-line-from-top-left-to-bottom-right)-with-color-(light-blue)-and-opacity.canvas-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geops/react-spatial/9390e38ff578186cdd0a86e5263d516c2eefba9d/src/utils/__snapshots__/getPolygonPattern.test.js.snap.getPolygonPattern()-render-pattern-4-(diagonal-line-from-top-left-to-bottom-right)-with-color-(light-blue)-and-opacity.canvas-image.png -------------------------------------------------------------------------------- /src/utils/getPolygonPattern.js: -------------------------------------------------------------------------------- 1 | import { DEVICE_PIXEL_RATIO } from "ol/has"; 2 | 3 | const getPolygonPattern = (patternId = 1, color = [235, 0, 0, 1]) => { 4 | if (patternId === 1) { 5 | return color; 6 | } 7 | 8 | const canvasElement = document.createElement("canvas"); 9 | const pixelRatio = DEVICE_PIXEL_RATIO; 10 | 11 | canvasElement.width = 20 * pixelRatio; 12 | canvasElement.height = 20 * pixelRatio; 13 | 14 | let pattern = {}; 15 | const ctx = canvasElement.getContext("2d"); 16 | ctx.strokeStyle = `rgba(${color.toString()})`; 17 | ctx.fillStyle = `rgba(${color.toString()})`; 18 | ctx.lineWidth = 3; 19 | 20 | switch (patternId) { 21 | case 2: 22 | /* Hatched pattern */ 23 | /* Ascending line */ 24 | ctx.beginPath(); 25 | ctx.moveTo(0, canvasElement.height); 26 | ctx.lineTo(0, canvasElement.height - ctx.lineWidth / 2); 27 | ctx.lineTo(canvasElement.width - ctx.lineWidth / 2, 0); 28 | ctx.lineTo(canvasElement.width, 0); 29 | ctx.lineTo(canvasElement.width, ctx.lineWidth / 2); 30 | ctx.lineTo(ctx.lineWidth / 2, canvasElement.height); 31 | ctx.lineTo(ctx.lineWidth / 2, canvasElement.height); 32 | ctx.fill(); 33 | ctx.closePath(); 34 | 35 | /* Descending line */ 36 | ctx.beginPath(); 37 | ctx.moveTo(0, 0); 38 | ctx.lineTo(0, ctx.lineWidth / 2); 39 | ctx.lineTo(canvasElement.width - ctx.lineWidth / 2, canvasElement.height); 40 | ctx.lineTo(canvasElement.width, canvasElement.height); 41 | ctx.lineTo(canvasElement.width, canvasElement.height - ctx.lineWidth / 2); 42 | ctx.lineTo(ctx.lineWidth / 2, 0); 43 | ctx.lineTo(0, 0); 44 | ctx.fill(); 45 | ctx.closePath(); 46 | 47 | pattern = ctx.createPattern(canvasElement, "repeat"); 48 | pattern.canvas = canvasElement; 49 | break; 50 | case 3: 51 | /* Shade ascending pattern */ 52 | /* Corner triangle */ 53 | ctx.beginPath(); 54 | ctx.moveTo(0, 0); 55 | ctx.lineTo(0, ctx.lineWidth / 2); 56 | ctx.lineTo(ctx.lineWidth / 2, 0); 57 | ctx.fill(); 58 | ctx.closePath(); 59 | 60 | /* Ascending line */ 61 | ctx.beginPath(); 62 | ctx.moveTo(0, canvasElement.height); 63 | ctx.lineTo(0, canvasElement.height - ctx.lineWidth / 2); 64 | ctx.lineTo(canvasElement.width - ctx.lineWidth / 2, 0); 65 | ctx.lineTo(canvasElement.width, 0); 66 | ctx.lineTo(canvasElement.width, ctx.lineWidth / 2); 67 | ctx.lineTo(ctx.lineWidth / 2, canvasElement.height); 68 | ctx.lineTo(ctx.lineWidth / 2, canvasElement.height); 69 | ctx.fill(); 70 | ctx.closePath(); 71 | 72 | /* Corner triangle */ 73 | ctx.beginPath(); 74 | ctx.moveTo(canvasElement.width, canvasElement.height); 75 | ctx.lineTo(canvasElement.width, canvasElement.height - ctx.lineWidth / 2); 76 | ctx.lineTo(canvasElement.width - ctx.lineWidth / 2, canvasElement.height); 77 | ctx.fill(); 78 | ctx.closePath(); 79 | 80 | pattern = ctx.createPattern(canvasElement, "repeat"); 81 | pattern.canvas = canvasElement; 82 | break; 83 | case 4: 84 | /* Shade descending pattern */ 85 | /* Corner triangle */ 86 | ctx.beginPath(); 87 | ctx.moveTo(canvasElement.width, 0); 88 | ctx.lineTo(canvasElement.width, ctx.lineWidth / 2); 89 | ctx.lineTo(canvasElement.width - ctx.lineWidth / 2, 0); 90 | ctx.fill(); 91 | ctx.closePath(); 92 | 93 | /* Descending line */ 94 | ctx.beginPath(); 95 | ctx.moveTo(0, 0); 96 | ctx.lineTo(0, ctx.lineWidth / 2); 97 | ctx.lineTo(canvasElement.width - ctx.lineWidth / 2, canvasElement.height); 98 | ctx.lineTo(canvasElement.width, canvasElement.height); 99 | ctx.lineTo(canvasElement.width, canvasElement.height - ctx.lineWidth / 2); 100 | ctx.lineTo(ctx.lineWidth / 2, 0); 101 | ctx.lineTo(0, 0); 102 | ctx.fill(); 103 | ctx.closePath(); 104 | 105 | /* Corner triangle */ 106 | ctx.beginPath(); 107 | ctx.moveTo(0, canvasElement.height); 108 | ctx.lineTo(0, canvasElement.height - ctx.lineWidth / 2); 109 | ctx.lineTo(ctx.lineWidth / 2, canvasElement.height); 110 | ctx.fill(); 111 | ctx.closePath(); 112 | 113 | pattern = ctx.createPattern(canvasElement, "repeat"); 114 | pattern.canvas = canvasElement; 115 | break; 116 | default: 117 | } 118 | 119 | if (patternId === 0) { 120 | pattern.empty = true; 121 | } 122 | 123 | pattern.id = patternId; 124 | pattern.color = color; 125 | 126 | return pattern; 127 | }; 128 | 129 | export default getPolygonPattern; 130 | -------------------------------------------------------------------------------- /src/utils/getPolygonPattern.test.js: -------------------------------------------------------------------------------- 1 | import getPolygonPattern from "./getPolygonPattern"; 2 | 3 | describe("getPolygonPattern()", () => { 4 | test("render pattern with default properties (id=1, color = [235, 0, 0, 1])", () => { 5 | const color = [235, 0, 0, 1]; 6 | const pattern = getPolygonPattern(); 7 | expect(pattern).toEqual(color); 8 | expect(pattern.id).toBe(); 9 | expect(pattern.color).toBe(); 10 | expect(pattern.empty).toBe(); 11 | expect(pattern.canvas).toBe(); 12 | }); 13 | 14 | test("render pattern 0 (no fill) color and (light blue) opacity", () => { 15 | const id = 0; 16 | const color = [0, 60, 80, 0.41000000000000003]; 17 | const pattern = getPolygonPattern(id, [0, 60, 80, 0.41000000000000003]); 18 | expect(pattern.id).toBe(id); 19 | expect(pattern.color).toEqual(color); 20 | expect(pattern.empty).toBe(true); 21 | expect(pattern.canvas).toBe(); 22 | }); 23 | 24 | test("render pattern 1 (full by color) color and (light blue) opacity", () => { 25 | const id = 1; 26 | const color = [0, 60, 80, 0.41000000000000003]; 27 | const pattern = getPolygonPattern(id, [0, 60, 80, 0.41000000000000003]); 28 | expect(pattern).toEqual(color); 29 | expect(pattern.id).toBe(); 30 | expect(pattern.color).toBe(); 31 | expect(pattern.empty).toBe(); 32 | expect(pattern.canvas).toBe(); 33 | }); 34 | 35 | test("render pattern 2 (cross) color and (light blue) opacity", () => { 36 | const id = 2; 37 | const color = [0, 60, 80, 0.41000000000000003]; 38 | const pattern = getPolygonPattern(id, [0, 60, 80, 0.41000000000000003]); 39 | expect(pattern.id).toBe(id); 40 | expect(pattern.color).toEqual(color); 41 | expect(pattern.empty).toBe(); 42 | }); 43 | 44 | test("render pattern 3 (diagonal line from bottom-left tot top-right) with color (light blue) and opacity", () => { 45 | const id = 3; 46 | const color = [0, 60, 80, 0.41000000000000003]; 47 | const pattern = getPolygonPattern(id, [0, 60, 80, 0.41000000000000003]); 48 | expect(pattern.id).toBe(id); 49 | expect(pattern.color).toEqual(color); 50 | expect(pattern.empty).toBe(); 51 | }); 52 | 53 | test("render pattern 4 (diagonal line from top-left to bottom-right) with color (light blue) and opacity", () => { 54 | const id = 4; 55 | const color = [0, 60, 80, 0.41000000000000003]; 56 | const pattern = getPolygonPattern(id, [0, 60, 80, 0.41000000000000003]); 57 | expect(pattern.id).toBe(id); 58 | expect(pattern.color).toEqual(color); 59 | expect(pattern.empty).toBe(); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/utils/timeUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a string representation of a number, with a zero if the number is lower than 10. 3 | * @ignore 4 | */ 5 | export const pad = (integer) => { 6 | return integer < 10 ? `0${integer}` : integer; 7 | }; 8 | 9 | /** 10 | * Returns a 'hh:mm' string from a time in ms. 11 | * @param {Number} timeInMs Time in milliseconds. 12 | * @ignore 13 | */ 14 | export const getHoursAndMinutes = (timeInMs) => { 15 | if (!timeInMs || timeInMs <= 0) { 16 | return ""; 17 | } 18 | const date = new Date(timeInMs); 19 | return `${pad(date.getHours())}:${pad(date.getMinutes())}`; 20 | }; 21 | 22 | /** 23 | * Returns a string representing a delay. 24 | * @param {Number} timeInMs Delay time in milliseconds. 25 | * @ignore 26 | */ 27 | export const getDelayString = (delayInMs) => { 28 | let timeInMs = delayInMs; 29 | if (timeInMs < 0) { 30 | timeInMs = 0; 31 | } 32 | const h = Math.floor(timeInMs / 3600000); 33 | const m = Math.floor((timeInMs % 3600000) / 60000); 34 | const s = Math.floor(((timeInMs % 3600000) % 60000) / 1000); 35 | 36 | if (s === 0 && h === 0 && m === 0) { 37 | return "+0"; 38 | } 39 | if (s === 0 && h === 0) { 40 | return `+${m}m`; 41 | } 42 | if (s === 0) { 43 | return `+${h}h${m}m`; 44 | } 45 | if (m === 0 && h === 0) { 46 | return `+${s}s`; 47 | } 48 | if (h === 0) { 49 | return `+${m}m${s}s`; 50 | } 51 | return `+${h}h${m}m${s}s`; 52 | }; 53 | -------------------------------------------------------------------------------- /src/utils/timeUtils.test.js: -------------------------------------------------------------------------------- 1 | import { getDelayString, getHoursAndMinutes } from "./timeUtils"; 2 | 3 | const RealDate = Date; 4 | describe("timeUtils", () => { 5 | beforeEach(() => { 6 | global.Date = jest.fn(() => { 7 | return { 8 | getHours: () => { 9 | return 0; 10 | }, 11 | getMinutes: () => { 12 | return 2; 13 | }, 14 | }; 15 | }); 16 | Object.assign(Date, RealDate); 17 | }); 18 | 19 | afterEach(() => { 20 | global.Date = RealDate; 21 | }); 22 | 23 | test("getHoursAndMinutes should be correct.", () => { 24 | expect(getHoursAndMinutes(123456)).toBe("00:02"); 25 | }); 26 | 27 | test("getDelayString should be correct.", () => { 28 | expect(getDelayString(123456)).toBe("+2m3s"); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["stylelint-scss"], 3 | extends: ["stylelint-config-standard", "stylelint-config-recommended-scss"], 4 | rules: { 5 | "import-notation": "string", 6 | }, 7 | }; 8 | --------------------------------------------------------------------------------