signalk-tides 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -33
- package/app/App.css +103 -0
- package/app/App.tsx +15 -0
- package/app/components/Footer.tsx +8 -0
- package/app/components/Position.tsx +18 -0
- package/app/components/TideChart.tsx +149 -0
- package/app/coordinate.ts +62 -0
- package/app/hooks/useContainerDimensions.ts +13 -0
- package/app/hooks/useTideData.ts +21 -0
- package/app/main.tsx +9 -0
- package/app/public/icon.svg +17 -0
- package/app/views/TidesView.tsx +44 -0
- package/app/vite-env.d.ts +1 -0
- package/docs/icon.sketch +0 -0
- package/eslint.config.js +28 -0
- package/index.html +16 -0
- package/package.json +39 -5
- package/src/{index.js → index.ts} +40 -17
- package/src/sources/noaa.ts +111 -0
- package/src/sources/stormglass.ts +64 -0
- package/src/sources/worldtides.ts +66 -0
- package/src/types/noaa.d.ts +40 -0
- package/src/types/stormglass.d.ts +29 -0
- package/src/types/worldtides.d.ts +19 -0
- package/src/types.ts +48 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.mts +16 -0
- package/.vscode/settings.json +0 -4
- package/extremes.json +0 -1
- package/heights.json +0 -1
- package/src/sources/noaa.js +0 -131
- package/src/sources/stormglass.js +0 -70
- package/src/sources/worldtides.js +0 -72
- package/test/index.js +0 -2
- package/test/responses/stations.json +0 -114
package/README.md
CHANGED
|
@@ -35,79 +35,173 @@ $ curl http://localhost:3000/signalk/v2/api/resources/tides
|
|
|
35
35
|
|
|
36
36
|
```json
|
|
37
37
|
{
|
|
38
|
-
"id" : "9710441",
|
|
39
|
-
"name" : "Nurse Channel",
|
|
40
|
-
"position" : {
|
|
41
|
-
"latitude" : 22.516666666667,
|
|
42
|
-
"longitude" : -75.85
|
|
43
|
-
},
|
|
44
38
|
"extremes" : [
|
|
45
39
|
{
|
|
46
|
-
"time" : "2025-03-
|
|
40
|
+
"time" : "2025-03-29T00:45:00.000Z",
|
|
41
|
+
"type" : "Low",
|
|
42
|
+
"value" : 0.025
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"time" : "2025-03-29T07:20:00.000Z",
|
|
46
|
+
"type" : "High",
|
|
47
|
+
"value" : 1.928
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"time" : "2025-03-29T13:18:00.000Z",
|
|
51
|
+
"type" : "Low",
|
|
52
|
+
"value" : 0.044
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"time" : "2025-03-29T19:45:00.000Z",
|
|
56
|
+
"type" : "High",
|
|
57
|
+
"value" : 1.815
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"time" : "2025-03-30T01:24:00.000Z",
|
|
61
|
+
"type" : "Low",
|
|
62
|
+
"value" : 0.164
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"time" : "2025-03-30T07:52:00.000Z",
|
|
66
|
+
"type" : "High",
|
|
67
|
+
"value" : 2.024
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"time" : "2025-03-30T14:05:00.000Z",
|
|
71
|
+
"type" : "Low",
|
|
72
|
+
"value" : -0.131
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"time" : "2025-03-30T20:43:00.000Z",
|
|
76
|
+
"type" : "High",
|
|
77
|
+
"value" : 1.733
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"time" : "2025-03-31T02:05:00.000Z",
|
|
81
|
+
"type" : "Low",
|
|
82
|
+
"value" : 0.334
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"time" : "2025-03-31T08:27:00.000Z",
|
|
86
|
+
"type" : "High",
|
|
87
|
+
"value" : 2.084
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"time" : "2025-03-31T14:53:00.000Z",
|
|
91
|
+
"type" : "Low",
|
|
92
|
+
"value" : -0.247
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"time" : "2025-03-31T21:44:00.000Z",
|
|
96
|
+
"type" : "High",
|
|
97
|
+
"value" : 1.632
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"time" : "2025-04-01T02:48:00.000Z",
|
|
101
|
+
"type" : "Low",
|
|
102
|
+
"value" : 0.517
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"time" : "2025-04-01T09:06:00.000Z",
|
|
106
|
+
"type" : "High",
|
|
107
|
+
"value" : 2.094
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"time" : "2025-04-01T15:46:00.000Z",
|
|
111
|
+
"type" : "Low",
|
|
112
|
+
"value" : -0.29
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"time" : "2025-04-01T22:51:00.000Z",
|
|
47
116
|
"type" : "High",
|
|
48
|
-
"value" :
|
|
117
|
+
"value" : 1.529
|
|
49
118
|
},
|
|
50
119
|
{
|
|
51
|
-
"time" : "2025-
|
|
120
|
+
"time" : "2025-04-02T03:37:00.000Z",
|
|
52
121
|
"type" : "Low",
|
|
53
|
-
"value" :
|
|
122
|
+
"value" : 0.695
|
|
54
123
|
},
|
|
55
124
|
{
|
|
56
|
-
"time" : "2025-
|
|
125
|
+
"time" : "2025-04-02T09:49:00.000Z",
|
|
57
126
|
"type" : "High",
|
|
58
|
-
"value" :
|
|
127
|
+
"value" : 2.05
|
|
59
128
|
},
|
|
60
129
|
{
|
|
61
|
-
"time" : "2025-
|
|
130
|
+
"time" : "2025-04-02T16:43:00.000Z",
|
|
62
131
|
"type" : "Low",
|
|
63
|
-
"value" : -0.
|
|
132
|
+
"value" : -0.263
|
|
64
133
|
},
|
|
65
134
|
{
|
|
66
|
-
"time" : "2025-
|
|
135
|
+
"time" : "2025-04-03T00:04:00.000Z",
|
|
67
136
|
"type" : "High",
|
|
68
|
-
"value" :
|
|
137
|
+
"value" : 1.448
|
|
69
138
|
},
|
|
70
139
|
{
|
|
71
|
-
"time" : "2025-
|
|
140
|
+
"time" : "2025-04-03T04:35:00.000Z",
|
|
72
141
|
"type" : "Low",
|
|
73
|
-
"value" : 0.
|
|
142
|
+
"value" : 0.847
|
|
74
143
|
},
|
|
75
144
|
{
|
|
76
|
-
"time" : "2025-
|
|
145
|
+
"time" : "2025-04-03T10:39:00.000Z",
|
|
77
146
|
"type" : "High",
|
|
78
|
-
"value" :
|
|
147
|
+
"value" : 1.957
|
|
79
148
|
},
|
|
80
149
|
{
|
|
81
|
-
"time" : "2025-
|
|
150
|
+
"time" : "2025-04-03T17:50:00.000Z",
|
|
82
151
|
"type" : "Low",
|
|
83
|
-
"value" : -0.
|
|
152
|
+
"value" : -0.189
|
|
84
153
|
},
|
|
85
154
|
{
|
|
86
|
-
"time" : "2025-
|
|
155
|
+
"time" : "2025-04-04T01:24:00.000Z",
|
|
87
156
|
"type" : "High",
|
|
88
|
-
"value" :
|
|
157
|
+
"value" : 1.421
|
|
89
158
|
},
|
|
90
159
|
{
|
|
91
|
-
"time" : "2025-
|
|
160
|
+
"time" : "2025-04-04T05:49:00.000Z",
|
|
92
161
|
"type" : "Low",
|
|
93
|
-
"value" : 0.
|
|
162
|
+
"value" : 0.946
|
|
94
163
|
},
|
|
95
164
|
{
|
|
96
|
-
"time" : "2025-
|
|
165
|
+
"time" : "2025-04-04T11:40:00.000Z",
|
|
97
166
|
"type" : "High",
|
|
98
|
-
"value" :
|
|
167
|
+
"value" : 1.837
|
|
99
168
|
},
|
|
100
169
|
{
|
|
101
|
-
"time" : "2025-
|
|
170
|
+
"time" : "2025-04-04T19:07:00.000Z",
|
|
102
171
|
"type" : "Low",
|
|
103
|
-
"value" : 0.
|
|
172
|
+
"value" : -0.11
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"time" : "2025-04-05T02:40:00.000Z",
|
|
176
|
+
"type" : "High",
|
|
177
|
+
"value" : 1.455
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"time" : "2025-04-05T07:19:00.000Z",
|
|
181
|
+
"type" : "Low",
|
|
182
|
+
"value" : 0.958
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"time" : "2025-04-05T12:53:00.000Z",
|
|
186
|
+
"type" : "High",
|
|
187
|
+
"value" : 1.728
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"time" : "2025-04-05T20:25:00.000Z",
|
|
191
|
+
"type" : "Low",
|
|
192
|
+
"value" : -0.061
|
|
104
193
|
}
|
|
105
|
-
]
|
|
194
|
+
],
|
|
195
|
+
"station" : {
|
|
196
|
+
"name" : "Rincon Point, Pier 22 1/2",
|
|
197
|
+
"position" : {
|
|
198
|
+
"latitude" : 37.79,
|
|
199
|
+
"longitude" : -122.387
|
|
200
|
+
}
|
|
201
|
+
}
|
|
106
202
|
}
|
|
107
203
|
```
|
|
108
204
|
|
|
109
|
-
|
|
110
|
-
|
|
111
205
|
## Sources
|
|
112
206
|
|
|
113
207
|
- [NOAA](https://tidesandcurrents.noaa.gov/web_services_info.html) (US only)
|
package/app/App.css
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
color-scheme: dark;
|
|
5
|
+
font-weight: 400;
|
|
6
|
+
text-rendering: optimizeLegibility;
|
|
7
|
+
-webkit-font-smoothing: antialiased;
|
|
8
|
+
-moz-osx-font-smoothing: grayscale;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
html {
|
|
12
|
+
background: linear-gradient(rgb(2, 75, 102), rgb(0, 20, 30));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
margin: 0;
|
|
17
|
+
min-height: 100vh;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.TideChart {
|
|
21
|
+
width: 100vw;
|
|
22
|
+
overflow-x: auto;
|
|
23
|
+
overflow-y: hidden;
|
|
24
|
+
flex: 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.TideChart__Gradient--stop1 {
|
|
28
|
+
stop-color: rgba(56, 143, 202, 0.5);
|
|
29
|
+
}
|
|
30
|
+
.TideChart__Gradient--stop2 {
|
|
31
|
+
stop-color: rgba(56, 143, 202, 0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.TideChart__Line {
|
|
35
|
+
fill: none;
|
|
36
|
+
stroke: rgba(80, 190, 255, 1);
|
|
37
|
+
stroke-width: 3;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.TideChart__DataPoint {
|
|
41
|
+
fill: rgb(2, 75, 102);
|
|
42
|
+
stroke: white;
|
|
43
|
+
stroke-width: 3;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.TideChart__Text {
|
|
47
|
+
fill: rgba(255, 255, 255, 0.9);
|
|
48
|
+
font-weight: bold;
|
|
49
|
+
text-anchor: middle;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.TideChart__Text--High {
|
|
53
|
+
alignment-baseline: hanging;
|
|
54
|
+
}
|
|
55
|
+
.TideChart__Text--Low {
|
|
56
|
+
alignment-baseline: bottom;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.TideChart__Depth {
|
|
60
|
+
opacity: 0.8;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.TideChart__XAxis {
|
|
64
|
+
opacity: 0.3;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.TideChart__XAxis .tick {
|
|
68
|
+
font-size: 1rem;
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.TideChart__LowWater {
|
|
73
|
+
stroke: rgba(255, 255, 255, 0.25);
|
|
74
|
+
stroke-width: 3;
|
|
75
|
+
stroke-dasharray: 1, 6;
|
|
76
|
+
stroke-linecap: round;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.TideChart__Now {
|
|
80
|
+
stroke: rgba(180, 50, 50, 0.8);
|
|
81
|
+
stroke-linecap: round;
|
|
82
|
+
stroke-width: 4;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.Position {
|
|
86
|
+
font-size: 0.9rem;
|
|
87
|
+
text-align: right;
|
|
88
|
+
font-variant-numeric: tabular-nums;
|
|
89
|
+
display: inline-flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.Position__Coordinate {
|
|
94
|
+
display: inline-flex;
|
|
95
|
+
flex-direction: row;
|
|
96
|
+
justify-content: end;
|
|
97
|
+
gap: 0.2rem;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.Position__Direction {
|
|
101
|
+
width: 1.5rem;
|
|
102
|
+
text-align: left;
|
|
103
|
+
}
|
package/app/App.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import './App.css'
|
|
3
|
+
import { TidesView } from './views/TidesView'
|
|
4
|
+
import { Footer } from './components/Footer'
|
|
5
|
+
|
|
6
|
+
function App() {
|
|
7
|
+
return (
|
|
8
|
+
<StrictMode>
|
|
9
|
+
<TidesView />
|
|
10
|
+
<Footer />
|
|
11
|
+
</StrictMode>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default App
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Coordinate } from "../coordinate";
|
|
2
|
+
|
|
3
|
+
export function Position({ latitude, longitude }: { latitude: number, longitude: number }) {
|
|
4
|
+
const lat = Coordinate.lat(latitude);
|
|
5
|
+
const lon = Coordinate.lon(longitude);
|
|
6
|
+
return (
|
|
7
|
+
<div className="Position">
|
|
8
|
+
<div className="Position__Coordinate">
|
|
9
|
+
<span>{lat.format('DD mm')}</span>
|
|
10
|
+
<span className="Position__Direction">{lat.format('X')}</span>
|
|
11
|
+
</div>
|
|
12
|
+
<div className="Position__Coordinate">
|
|
13
|
+
<span>{lon.format('DD mm')}</span>
|
|
14
|
+
<span className="Position__Direction">{lon.format('X')}</span>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import * as d3 from "d3";
|
|
3
|
+
import { TideExtreme } from "../hooks/useTideData";
|
|
4
|
+
import moment from "moment";
|
|
5
|
+
import { useContainerDimensions } from "../hooks/useContainerDimensions";
|
|
6
|
+
|
|
7
|
+
type TideChartProps = {
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
marginTop?: number;
|
|
11
|
+
marginRight?: number;
|
|
12
|
+
marginBottom?: number;
|
|
13
|
+
marginLeft?: number;
|
|
14
|
+
units?: "m" | "ft";
|
|
15
|
+
timeFormat?: string;
|
|
16
|
+
data: TideExtreme[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function TideChart({
|
|
20
|
+
data,
|
|
21
|
+
marginTop = 20,
|
|
22
|
+
marginRight = 6,
|
|
23
|
+
marginBottom = 30,
|
|
24
|
+
marginLeft = 6,
|
|
25
|
+
units = "m",
|
|
26
|
+
timeFormat = "HH:mm",
|
|
27
|
+
}: TideChartProps) {
|
|
28
|
+
const container = useRef<HTMLDivElement>(null);
|
|
29
|
+
const now = useRef<SVGLineElement>(null);
|
|
30
|
+
const { height = 0 } = useContainerDimensions(container) || {};
|
|
31
|
+
const [width, setWidth] = useState(0)
|
|
32
|
+
const gx = useRef<SVGGElement>(null);
|
|
33
|
+
const x = useRef<d3.ScaleTime<number, number, never>>(null);
|
|
34
|
+
const y = useRef<d3.ScaleLinear<number, number, never>>(null);
|
|
35
|
+
const area = useRef<d3.Area<TideExtreme>>(null);
|
|
36
|
+
const line = useRef<d3.Line<TideExtreme>>(null);
|
|
37
|
+
const textPadding = 25;
|
|
38
|
+
const yPadding = 0.3; // meters
|
|
39
|
+
|
|
40
|
+
// Adjust width relative to height and number of forecasts
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setWidth(height / 4 * data.length)
|
|
43
|
+
}, [height, data])
|
|
44
|
+
|
|
45
|
+
function displayDepth(value: number) {
|
|
46
|
+
return units === "m" ? `${value.toFixed(2)} m` : `${(value * 3.28084).toFixed(1)} ft`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function displayTime(value: string) {
|
|
50
|
+
return moment(value).format(timeFormat);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const [min = 0, max = 0] = d3.extent(data, d => new Date(d.time));
|
|
55
|
+
|
|
56
|
+
x.current = d3.scaleTime()
|
|
57
|
+
.domain([min, max])
|
|
58
|
+
.range([marginLeft, width - marginRight]);
|
|
59
|
+
|
|
60
|
+
const [yMin = 0, yMax = 0] = d3.extent(data, d => d.value)
|
|
61
|
+
const yPad = (yMax - yMin) * .3;
|
|
62
|
+
|
|
63
|
+
// Declare the y (vertical position) scale.
|
|
64
|
+
y.current = d3.scaleLinear()
|
|
65
|
+
.domain([yMin - yPad, yMax + yPad])
|
|
66
|
+
.range([height - marginBottom, marginTop]);
|
|
67
|
+
|
|
68
|
+
// Declare the area generator.
|
|
69
|
+
area.current = d3.area<TideExtreme>()
|
|
70
|
+
.curve(d3.curveMonotoneX)
|
|
71
|
+
.x(d => x.current!(new Date(d.time)))
|
|
72
|
+
.y0(y.current!(d3.min(data, d => d.value - yPadding) ?? 0))
|
|
73
|
+
.y1(d => y.current!(d.value));
|
|
74
|
+
|
|
75
|
+
line.current = d3.line<TideExtreme>()
|
|
76
|
+
.curve(d3.curveMonotoneX)
|
|
77
|
+
.x(d => x.current!(new Date(d.time)))
|
|
78
|
+
.y(d => y.current!(d.value))
|
|
79
|
+
|
|
80
|
+
if (gx.current) d3.select(gx.current).call(d3.axisBottom(x.current));
|
|
81
|
+
}, [data, height, width, marginBottom, marginLeft, marginRight, marginTop])
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
now.current?.scrollIntoView({ block: 'center', inline: 'center' })
|
|
85
|
+
}, [data, now, width])
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="TideChart" ref={container}>
|
|
89
|
+
<svg width={width} height={height}>
|
|
90
|
+
<defs>
|
|
91
|
+
<linearGradient id="gradient" gradientTransform="rotate(90)">
|
|
92
|
+
<stop className="TideChart__Gradient--stop1" offset="0" />
|
|
93
|
+
<stop className="TideChart__Gradient--stop2" offset="100%" />
|
|
94
|
+
</linearGradient>
|
|
95
|
+
</defs>
|
|
96
|
+
<g className="TideChart__XAxis" ref={gx} transform={`translate(0,${height - marginBottom})`} />
|
|
97
|
+
|
|
98
|
+
<line
|
|
99
|
+
className="TideChart__LowWater"
|
|
100
|
+
x1={marginLeft}
|
|
101
|
+
x2={width - marginRight}
|
|
102
|
+
y1={y.current?.(0)}
|
|
103
|
+
y2={y.current?.(0)}
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
<path fill="url(#gradient)" d={area.current?.(data) || ''} />
|
|
107
|
+
<path className="TideChart__Line" d={line.current?.(data) || ''} />
|
|
108
|
+
|
|
109
|
+
<line
|
|
110
|
+
ref={now}
|
|
111
|
+
className="TideChart__Now"
|
|
112
|
+
x1={x.current?.(new Date())}
|
|
113
|
+
x2={x.current?.(new Date())}
|
|
114
|
+
y1={marginTop}
|
|
115
|
+
y2={height - marginTop}
|
|
116
|
+
/>
|
|
117
|
+
|
|
118
|
+
<g>
|
|
119
|
+
{
|
|
120
|
+
data.map((d, i) => (
|
|
121
|
+
<g key={i}>
|
|
122
|
+
<circle
|
|
123
|
+
className="TideChart__DataPoint"
|
|
124
|
+
cx={x.current?.(new Date(d.time))}
|
|
125
|
+
cy={y.current?.(d.value)}
|
|
126
|
+
r={5}
|
|
127
|
+
/>
|
|
128
|
+
{
|
|
129
|
+
(i !== 0 && i !== data.length - 1) &&
|
|
130
|
+
<text
|
|
131
|
+
className={["TideChart__Text", `TideChart__Text--${d.type}`].join(" ")}
|
|
132
|
+
y={d.type === "High" ? marginTop + textPadding : height - marginBottom - textPadding}
|
|
133
|
+
>
|
|
134
|
+
<tspan className="TideChart__Depth" x={x.current?.(new Date(d.time))} dy={d.type === "High" ? "1.5em" : "-1.5em"}>
|
|
135
|
+
{displayDepth(d.value)}
|
|
136
|
+
</tspan>
|
|
137
|
+
<tspan className="TideChart__Time" x={x.current?.(new Date(d.time))} dy={d.type === "High" ? "-1.5em" : "1.5em"}>
|
|
138
|
+
{displayTime(d.time)}
|
|
139
|
+
</tspan>
|
|
140
|
+
</text>
|
|
141
|
+
}
|
|
142
|
+
</g>
|
|
143
|
+
))
|
|
144
|
+
}
|
|
145
|
+
</g>
|
|
146
|
+
</svg>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type CoordinateFormatOptions = {
|
|
2
|
+
precision: number,
|
|
3
|
+
units: { degrees: string, minutes: string, seconds: string }
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Adapted from https://github.com/nerik/formatcoords
|
|
7
|
+
export class Coordinate {
|
|
8
|
+
static lat(value: number) {
|
|
9
|
+
return new Coordinate(value, ['N', 'S']);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static lon(value: number) {
|
|
13
|
+
return new Coordinate(value, ['E', 'W']);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
constructor(public value: number, public directions: [string, string]) {
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get degrees() {
|
|
20
|
+
return Math.abs(this.value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get minutes() {
|
|
24
|
+
return (this.degrees % 1) * 60;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get seconds() {
|
|
28
|
+
return (this.minutes % 1) * 60;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get direction() {
|
|
32
|
+
return this.directions[this.value > 0 ? 0 : 1];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
format(format: string, options?: CoordinateFormatOptions) {
|
|
36
|
+
if (isNaN(this.value)) return;
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
precision = 3,
|
|
40
|
+
units = { degrees: '°', minutes: '′', seconds: '″' }
|
|
41
|
+
} = options ?? {};
|
|
42
|
+
|
|
43
|
+
const formats: [RegExp, () => string][] = [
|
|
44
|
+
[/DD/g, () => [Math.floor(this.degrees), units.degrees].join('')],
|
|
45
|
+
[/dd/g, () => [this.degrees.toFixed(precision), units.degrees].join('')],
|
|
46
|
+
[/D/g, () => Math.floor(this.degrees).toString()],
|
|
47
|
+
[/d/g, () => this.degrees.toFixed(precision)],
|
|
48
|
+
[/MM/g, () => [Math.floor(this.minutes), units.minutes].join('')],
|
|
49
|
+
[/mm/g, () => [this.minutes.toFixed(precision), units.minutes].join('')],
|
|
50
|
+
[/M/g, () => Math.floor(this.minutes).toString()],
|
|
51
|
+
[/m/g, () => this.minutes.toFixed(precision)],
|
|
52
|
+
[/ss/g, () => [this.seconds.toFixed(precision), units.seconds].join('')],
|
|
53
|
+
[/s/g, () => this.seconds.toFixed(precision)],
|
|
54
|
+
[/-/g, () => this.value < 0 ? '-' : ''],
|
|
55
|
+
[/X/g, () => this.direction],
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
return formats.reduce((result, [regex, replacer]) => {
|
|
59
|
+
return result.replace(regex, replacer);
|
|
60
|
+
}, format);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RefObject, useLayoutEffect, useState } from "react";
|
|
2
|
+
import useResizeObserver from '@react-hook/resize-observer'
|
|
3
|
+
|
|
4
|
+
export function useContainerDimensions<T extends HTMLElement | null>(ref: RefObject<T>) {
|
|
5
|
+
const [size, setSize] = useState<DOMRect>()
|
|
6
|
+
|
|
7
|
+
useLayoutEffect(() => {
|
|
8
|
+
setSize(ref.current?.getBoundingClientRect())
|
|
9
|
+
}, [ref])
|
|
10
|
+
|
|
11
|
+
useResizeObserver(ref, (entry) => setSize(entry.contentRect))
|
|
12
|
+
return size
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { TideForecastResult, TideExtreme } from "../../src/types";
|
|
3
|
+
|
|
4
|
+
const { VITE_SIGNALK_URL = window.location.toString() } = import.meta.env;
|
|
5
|
+
export const API_URL = new URL("/signalk/v2/api/resources/tides", VITE_SIGNALK_URL).toString();
|
|
6
|
+
export const SETTINGS_URL = new URL("/#/serverConfiguration/plugins/tides", VITE_SIGNALK_URL).toString();
|
|
7
|
+
|
|
8
|
+
export type { TideForecastResult, TideExtreme }
|
|
9
|
+
|
|
10
|
+
export function useTideData() {
|
|
11
|
+
const [data, setData] = useState<TideForecastResult>()
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
(async () => {
|
|
15
|
+
const res = await fetch(API_URL)
|
|
16
|
+
setData(await res.json())
|
|
17
|
+
})()
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
return data;
|
|
21
|
+
}
|
package/app/main.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg width="200px" height="200px" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
3
|
+
<title>icon</title>
|
|
4
|
+
<defs>
|
|
5
|
+
<rect id="path-1" x="0" y="0" width="200" height="200"></rect>
|
|
6
|
+
</defs>
|
|
7
|
+
<g id="icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
8
|
+
<rect fill="#FFFFFF" x="0" y="0" width="200" height="200"></rect>
|
|
9
|
+
<mask id="mask-2" fill="white">
|
|
10
|
+
<use xlink:href="#path-1"></use>
|
|
11
|
+
</mask>
|
|
12
|
+
<use id="Rectangle" fill="#6BB4B8" xlink:href="#path-1"></use>
|
|
13
|
+
<rect id="Rectangle" fill="#6BB4B8" mask="url(#mask-2)" x="0" y="0" width="200" height="200" rx="30"></rect>
|
|
14
|
+
<polyline id="Path" fill="#60A9AD" fill-rule="nonzero" mask="url(#mask-2)" points="1.42108547e-14 83.43 1 84.03 2 84.64 3 85.26 4 85.91 5 86.56 6 87.23 7 87.91 8 88.61 9 89.31 10 90.02 11 90.75 12 91.48 13 92.23 14 92.98 15 93.73 16 94.5 17 95.27 18 96.04 19 96.82 20 97.6 21 98.38 22 99.16 23 99.95 24 100.73 25 101.52 26 102.3 27 103.08 28 103.86 29 104.63 30 105.4 31 106.17 32 106.93 33 107.68 34 108.42 35 109.16 36 109.88 37 110.6 38 111.3 39 112 40 112.68 41 113.35 42 114.01 43 114.65 44 115.28 45 115.9 46 116.49 47 117.08 48 117.64 49 118.19 50 118.72 51 119.23 52 119.72 53 120.2 54 120.65 55 121.08 56 121.49 57 121.88 58 122.25 59 122.6 60 122.92 61 123.23 62 123.5 63 123.76 64 123.99 65 124.2 66 124.39 67 124.55 68 124.68 69 124.8 70 124.88 71 124.95 72 124.99 73 125 74 124.99 75 124.95 76 124.89 77 124.81 78 124.7 79 124.57 80 124.41 81 124.23 82 124.02 83 123.79 84 123.54 85 123.26 86 122.96 87 122.64 88 122.3 89 121.93 90 121.54 91 121.14 92 120.71 93 120.26 94 119.78 95 119.3 96 118.79 97 118.26 98 117.71 99 117.15 100 116.57 101 115.97 102 115.36 103 114.74 104 114.09 105 113.44 106 112.77 107 112.09 108 111.39 109 110.69 110 109.98 111 109.25 112 108.52 113 107.77 114 107.02 115 106.27 116 105.5 117 104.73 118 103.96 119 103.18 120 102.4 121 101.62 122 100.84 123 100.05 124 99.27 125 98.48 126 97.7 127 96.92 128 96.14 129 95.37 130 94.6 131 93.83 132 93.07 133 92.32 134 91.58 135 90.84 136 90.12 137 89.4 138 88.7 139 88 140 87.32 141 86.65 142 85.99 143 85.35 144 84.72 145 84.1 146 83.51 147 82.92 148 82.36 149 81.81 150 81.28 151 80.77 152 80.28 153 79.8 154 79.35 155 78.92 156 78.51 157 78.12 158 77.75 159 77.4 160 77.08 161 76.77 162 76.5 163 76.24 164 76.01 165 75.8 166 75.61 167 75.45 168 75.32 169 75.2 170 75.12 171 75.05 172 75.01 173 75 174 75.01 175 75.05 176 75.11 177 75.19 178 75.3 179 75.43 180 75.59 181 75.77 182 75.98 183 76.21 184 76.46 185 76.74 186 77.04 187 77.36 188 77.7 189 78.07 190 78.46 191 78.86 192 79.29 193 79.74 194 80.22 195 80.7 196 81.21 197 81.74 198 82.29 199 82.85 200 83.43 200 250 1.42108547e-14 250"></polyline>
|
|
15
|
+
<polyline id="Path" fill="#98CBCE" fill-rule="nonzero" mask="url(#mask-2)" points="1.42108547e-14 100 1 100.79 2 101.57 3 102.35 4 103.13 5 103.91 6 104.68 7 105.45 8 106.22 9 106.97 10 107.73 11 108.47 12 109.2 13 109.93 14 110.64 15 111.35 16 112.04 17 112.73 18 113.4 19 114.05 20 114.69 21 115.32 22 115.94 23 116.53 24 117.11 25 117.68 26 118.22 27 118.75 28 119.26 29 119.75 30 120.23 31 120.68 32 121.11 33 121.52 34 121.91 35 122.28 36 122.62 37 122.94 38 123.24 39 123.52 40 123.78 41 124.01 42 124.21 43 124.4 44 124.56 45 124.69 46 124.8 47 124.89 48 124.95 49 124.99 50 125 51 124.99 52 124.95 53 124.89 54 124.8 55 124.69 56 124.56 57 124.4 58 124.21 59 124.01 60 123.78 61 123.52 62 123.24 63 122.94 64 122.62 65 122.28 66 121.91 67 121.52 68 121.11 69 120.68 70 120.23 71 119.75 72 119.26 73 118.75 74 118.22 75 117.68 76 117.11 77 116.53 78 115.94 79 115.32 80 114.69 81 114.05 82 113.4 83 112.73 84 112.04 85 111.35 86 110.64 87 109.93 88 109.2 89 108.47 90 107.73 91 106.97 92 106.22 93 105.45 94 104.68 95 103.91 96 103.13 97 102.35 98 101.57 99 100.79 100 100 101 99.21 102 98.43 103 97.65 104 96.87 105 96.09 106 95.32 107 94.55 108 93.78 109 93.03 110 92.27 111 91.53 112 90.8 113 90.07 114 89.36 115 88.65 116 87.96 117 87.27 118 86.6 119 85.95 120 85.31 121 84.68 122 84.06 123 83.47 124 82.89 125 82.32 126 81.78 127 81.25 128 80.74 129 80.25 130 79.77 131 79.32 132 78.89 133 78.48 134 78.09 135 77.72 136 77.38 137 77.06 138 76.76 139 76.48 140 76.22 141 75.99 142 75.79 143 75.6 144 75.44 145 75.31 146 75.2 147 75.11 148 75.05 149 75.01 150 75 151 75.01 152 75.05 153 75.11 154 75.2 155 75.31 156 75.44 157 75.6 158 75.79 159 75.99 160 76.22 161 76.48 162 76.76 163 77.06 164 77.38 165 77.72 166 78.09 167 78.48 168 78.89 169 79.32 170 79.77 171 80.25 172 80.74 173 81.25 174 81.78 175 82.32 176 82.89 177 83.47 178 84.06 179 84.68 180 85.31 181 85.95 182 86.6 183 87.27 184 87.96 185 88.65 186 89.36 187 90.07 188 90.8 189 91.53 190 92.27 191 93.03 192 93.78 193 94.55 194 95.32 195 96.09 196 96.87 197 97.65 198 98.43 199 99.21 200 100 200 250 1.42108547e-14 250"></polyline>
|
|
16
|
+
</g>
|
|
17
|
+
</svg>
|