{
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 | {
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 |
63 | `;
64 |
65 | exports[`Popup should match snapshot with tabIndex defined. 1`] = `
66 |
75 |
79 |
82 |
91 |
114 |
115 |
116 |
123 |
124 |
125 | `;
126 |
127 | exports[`Popup should match snapshot without close button. 1`] = `
128 |
153 | `;
154 |
155 | exports[`Popup should match snapshot without feature 1`] = `null`;
156 |
157 | exports[`Popup should match snapshot without header. 1`] = `
158 |
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 |
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 |
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 |
9 |
--------------------------------------------------------------------------------
/src/images/mots/bus_poi-grey-01.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/images/mots/bus_round-blue-01.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/images/mots/bus_round-grey-01.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/images/mots/bus_square-blue-01.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/images/mots/bus_square-grey-01.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/images/mots/cable_car_poi-blue-01.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/images/mots/cable_car_poi-grey-01.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/images/mots/cable_car_round-blue-01.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/images/mots/cable_car_round-grey-01.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/images/mots/cable_car_square-blue-01.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/images/mots/cable_car_square-grey-01.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/images/mots/ferry_poi-blue-01.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/images/mots/ferry_poi-grey-01.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/images/mots/ferry_round-blue-01.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/images/mots/ferry_round-grey-01.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/images/mots/ferry_square-blue-01.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/images/mots/ferry_square-grey-01.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/images/mots/funicular_round-blue-01.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/images/mots/funicular_round-grey-01.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/images/mots/funicular_square-blue-01.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/images/mots/gondola_round-blue-01.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/images/mots/rail_poi-blue-01.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/images/mots/rail_poi-grey-01.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/images/mots/rail_round-blue-01.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/images/mots/rail_round-grey-01.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/images/mots/rail_square-blue-01.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/images/mots/rail_square-grey-01.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/images/mots/subway_round blue-01.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/images/mots/subway_round-blue-01.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/src/images/mots/tram_poi-blue-01.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/images/mots/tram_poi-grey-01.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/images/mots/tram_round-blue-01.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/images/mots/tram_round-grey-01.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/images/mots/tram_square-blue-01.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/images/mots/tram_square-grey-01.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------