@things-factory/weather 9.0.0-beta.41

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/assets/images/wind.png +0 -0
  3. package/client/bootstrap.ts +1 -0
  4. package/client/index.ts +0 -0
  5. package/client/ol-components/webgl-canvas-layer.ts +413 -0
  6. package/client/ol-components/webgl-image-static.ts +174 -0
  7. package/client/ol-components/wind.ts +120 -0
  8. package/client/pages/weather-page.ts +318 -0
  9. package/client/route.ts +7 -0
  10. package/client/tsconfig.json +11 -0
  11. package/config/config.development.js +25 -0
  12. package/config/config.production.js +7 -0
  13. package/data/geo-json/countries-max-geo.json +1 -0
  14. package/dist-client/bootstrap.d.ts +1 -0
  15. package/dist-client/bootstrap.js +2 -0
  16. package/dist-client/bootstrap.js.map +1 -0
  17. package/dist-client/index.d.ts +0 -0
  18. package/dist-client/index.js +2 -0
  19. package/dist-client/index.js.map +1 -0
  20. package/dist-client/ol-components/webgl-canvas-layer.d.ts +61 -0
  21. package/dist-client/ol-components/webgl-canvas-layer.js +364 -0
  22. package/dist-client/ol-components/webgl-canvas-layer.js.map +1 -0
  23. package/dist-client/ol-components/webgl-image-static.d.ts +41 -0
  24. package/dist-client/ol-components/webgl-image-static.js +135 -0
  25. package/dist-client/ol-components/webgl-image-static.js.map +1 -0
  26. package/dist-client/ol-components/wind.d.ts +3 -0
  27. package/dist-client/ol-components/wind.js +104 -0
  28. package/dist-client/ol-components/wind.js.map +1 -0
  29. package/dist-client/pages/weather-page.d.ts +18 -0
  30. package/dist-client/pages/weather-page.js +300 -0
  31. package/dist-client/pages/weather-page.js.map +1 -0
  32. package/dist-client/route.d.ts +1 -0
  33. package/dist-client/route.js +8 -0
  34. package/dist-client/route.js.map +1 -0
  35. package/dist-client/tsconfig.tsbuildinfo +1 -0
  36. package/dist-server/controllers/open-meteo-adapter.d.ts +4 -0
  37. package/dist-server/controllers/open-meteo-adapter.js +83 -0
  38. package/dist-server/controllers/open-meteo-adapter.js.map +1 -0
  39. package/dist-server/controllers/types.d.ts +55 -0
  40. package/dist-server/controllers/types.js +3 -0
  41. package/dist-server/controllers/types.js.map +1 -0
  42. package/dist-server/controllers/weather-controller.d.ts +12 -0
  43. package/dist-server/controllers/weather-controller.js +164 -0
  44. package/dist-server/controllers/weather-controller.js.map +1 -0
  45. package/dist-server/index.d.ts +1 -0
  46. package/dist-server/index.js +4 -0
  47. package/dist-server/index.js.map +1 -0
  48. package/dist-server/routers/geo-router.d.ts +59 -0
  49. package/dist-server/routers/geo-router.js +277 -0
  50. package/dist-server/routers/geo-router.js.map +1 -0
  51. package/dist-server/routers/weather-router.d.ts +1 -0
  52. package/dist-server/routers/weather-router.js +10 -0
  53. package/dist-server/routers/weather-router.js.map +1 -0
  54. package/dist-server/routes.d.ts +1 -0
  55. package/dist-server/routes.js +11 -0
  56. package/dist-server/routes.js.map +1 -0
  57. package/dist-server/services/chrome-renderer.d.ts +13 -0
  58. package/dist-server/services/chrome-renderer.js +137 -0
  59. package/dist-server/services/chrome-renderer.js.map +1 -0
  60. package/dist-server/services/headless-pool.d.ts +4 -0
  61. package/dist-server/services/headless-pool.js +89 -0
  62. package/dist-server/services/headless-pool.js.map +1 -0
  63. package/dist-server/tsconfig.tsbuildinfo +1 -0
  64. package/dist-server/utils/svg-utils.d.ts +35 -0
  65. package/dist-server/utils/svg-utils.js +89 -0
  66. package/dist-server/utils/svg-utils.js.map +1 -0
  67. package/dist-server/utils/tile-utils.d.ts +38 -0
  68. package/dist-server/utils/tile-utils.js +64 -0
  69. package/dist-server/utils/tile-utils.js.map +1 -0
  70. package/package.json +45 -0
  71. package/server/controllers/open-meteo-adapter.ts +91 -0
  72. package/server/controllers/types.ts +63 -0
  73. package/server/controllers/weather-controller.ts +176 -0
  74. package/server/index.ts +1 -0
  75. package/server/routers/geo-router.ts +297 -0
  76. package/server/routers/weather-router.ts +8 -0
  77. package/server/routes.ts +10 -0
  78. package/server/services/chrome-renderer.ts +175 -0
  79. package/server/services/data.json +8438 -0
  80. package/server/services/headless-pool.ts +97 -0
  81. package/server/tsconfig.json +10 -0
  82. package/server/utils/svg-utils.ts +108 -0
  83. package/server/utils/tile-utils.ts +77 -0
  84. package/things-factory.config.js +8 -0
  85. package/translations/en.json +1 -0
  86. package/translations/ja.json +1 -0
  87. package/translations/ko.json +1 -0
  88. package/translations/ms.json +1 -0
  89. package/translations/zh.json +1 -0
@@ -0,0 +1,120 @@
1
+ import { get as getProjection, transform } from 'ol/proj.js'
2
+ import DataTileSource from 'ol/source/DataTile.js'
3
+ import { createXYZ, wrapX } from 'ol/tilegrid.js'
4
+
5
+ const windData: Promise<{ data: ArrayBuffer; width: number; height: number }> = new Promise((resolve, reject) => {
6
+ const image = new Image()
7
+ image.onload = () => {
8
+ const canvas = document.createElement('canvas')
9
+ const width = image.width
10
+ const height = image.height
11
+ canvas.width = width
12
+ canvas.height = height
13
+
14
+ const context = canvas.getContext('2d')!
15
+ context.drawImage(image, 0, 0)
16
+ const data = context.getImageData(0, 0, width, height).data
17
+ resolve({ data, width, height })
18
+ }
19
+ image.onerror = () => {
20
+ reject(new Error('failed to load'))
21
+ }
22
+ image.src = '/assets/images/wind.png'
23
+ })
24
+
25
+ function bilinearInterpolation(xAlong, yAlong, v11, v21, v12, v22) {
26
+ const q11 = (1 - xAlong) * (1 - yAlong) * v11
27
+ const q21 = xAlong * (1 - yAlong) * v21
28
+ const q12 = (1 - xAlong) * yAlong * v12
29
+ const q22 = xAlong * yAlong * v22
30
+ return q11 + q21 + q12 + q22
31
+ }
32
+
33
+ function interpolatePixels(xAlong, yAlong, p11, p21, p12, p22) {
34
+ return p11.map((_, i) => bilinearInterpolation(xAlong, yAlong, p11[i], p21[i], p12[i], p22[i]))
35
+ }
36
+
37
+ const dataTileGrid = createXYZ()
38
+ const dataTileSize = 256
39
+
40
+ const inputImageProjection = getProjection('EPSG:4326')!
41
+ const dataTileProjection = getProjection('EPSG:3857')!
42
+
43
+ const inputBands = 4
44
+ const dataBands = 3
45
+
46
+ // range of wind velocities
47
+ // these values are stretched between 0 and 255 in the png
48
+ const minU = -21.32
49
+ const maxU = 26.8
50
+ const deltaU = maxU - minU
51
+ const minV = -21.57
52
+ const maxV = 21.42
53
+ const deltaV = maxV - minV
54
+
55
+ const wind = new DataTileSource({
56
+ // transition must be 0, see https://github.com/openlayers/openlayers/issues/16119
57
+ transition: 0,
58
+ wrapX: true,
59
+ async loader(z, x, y) {
60
+ const { data: inputData, width: inputWidth, height: inputHeight } = await windData
61
+
62
+ const tileCoord = wrapX(dataTileGrid, [z, x, y], dataTileProjection)
63
+ const extent = dataTileGrid.getTileCoordExtent(tileCoord)
64
+ const resolution = dataTileGrid.getResolution(z)
65
+ const data = new Float32Array(dataTileSize * dataTileSize * dataBands)
66
+ for (let row = 0; row < dataTileSize; ++row) {
67
+ let offset = row * dataTileSize * dataBands
68
+ const mapY = extent[3] - row * resolution
69
+ for (let col = 0; col < dataTileSize; ++col) {
70
+ const mapX = extent[0] + col * resolution
71
+ const [lon, lat] = transform([mapX, mapY], dataTileProjection, inputImageProjection)
72
+
73
+ const x = (inputWidth * (lon + 180)) / 360
74
+ let x1 = Math.floor(x)
75
+ let x2 = Math.ceil(x)
76
+ const xAlong = x - x1
77
+ if (x1 < 0) {
78
+ x1 += inputWidth
79
+ }
80
+ if (x2 >= inputWidth) {
81
+ x2 -= inputWidth
82
+ }
83
+
84
+ const y = (inputHeight * (90 - lat)) / 180
85
+ let y1 = Math.floor(y)
86
+ let y2 = Math.ceil(y)
87
+ const yAlong = y - y1
88
+ if (y1 < 0) {
89
+ y1 = 0
90
+ }
91
+ if (y2 >= inputHeight) {
92
+ y2 = inputHeight - 1
93
+ }
94
+
95
+ const corners = [
96
+ [x1, y1],
97
+ [x2, y1],
98
+ [x1, y2],
99
+ [x2, y2]
100
+ ]
101
+
102
+ const pixels = corners.map(([cx, cy]) => {
103
+ const inputOffset = (cy * 360 + cx) * inputBands
104
+ return [inputData[inputOffset], inputData[inputOffset + 1]]
105
+ })
106
+
107
+ const interpolated = interpolatePixels(xAlong, yAlong, ...(pixels as [any, any, any, any]))
108
+ const u = minU + (deltaU * interpolated[0]) / 255
109
+ const v = minV + (deltaV * interpolated[1]) / 255
110
+
111
+ data[offset] = u
112
+ data[offset + 1] = v
113
+ offset += dataBands
114
+ }
115
+ }
116
+ return data
117
+ }
118
+ })
119
+
120
+ export default wind
@@ -0,0 +1,318 @@
1
+ import { css, html } from 'lit'
2
+ import { customElement, property, state } from 'lit/decorators.js'
3
+ import { PageView } from '@operato/shell'
4
+
5
+ import Map from 'ol/Map.js'
6
+ import View from 'ol/View.js'
7
+ import TileLayer from 'ol/layer/Tile.js'
8
+ import { fromLonLat } from 'ol/proj.js'
9
+ import 'ol/ol.css'
10
+ import { MVT } from 'ol/format'
11
+ import { DEVICE_PIXEL_RATIO } from 'ol/has.js'
12
+ import Flow from 'ol/layer/Flow.js'
13
+
14
+ import ImageLayer from 'ol/layer/Image'
15
+ import WebGLImageStatic from '../ol-components/webgl-image-static'
16
+ import { VectorTile, XYZ } from 'ol/source'
17
+ import VectorSource from 'ol/source/Vector'
18
+ import colormap from 'colormap'
19
+
20
+ import Heatmap from 'ol/layer/Heatmap.js'
21
+ import Feature from 'ol/Feature.js'
22
+ import Point from 'ol/geom/Point.js'
23
+ import { Stroke, Style } from 'ol/style.js'
24
+
25
+ import wind from '../ol-components/wind.js'
26
+ import VectorTileLayer from 'ol/layer/VectorTile'
27
+
28
+ const colors = colormap({
29
+ colormap: 'jet',
30
+ nshades: 10,
31
+ alpha: 0.3,
32
+ format: 'rgba'
33
+ })
34
+
35
+ @customElement('weather-page')
36
+ export class WeatherPage extends PageView {
37
+ static get styles() {
38
+ return css`
39
+ :host {
40
+ display: flex;
41
+ flex-direction: column;
42
+ height: 100%;
43
+ }
44
+
45
+ .map-container {
46
+ flex: 1;
47
+ display: flex;
48
+ position: relative;
49
+ background-color: #f9f9f9;
50
+ overflow: hidden;
51
+ }
52
+
53
+ #map {
54
+ flex: 1;
55
+ }
56
+
57
+ .controls {
58
+ position: absolute;
59
+ top: 10px;
60
+ right: 10px;
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 10px;
64
+ background: rgba(255, 255, 255, 0.8);
65
+ padding: 15px;
66
+ border-radius: 4px;
67
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
68
+ z-index: 10;
69
+ }
70
+
71
+ .weather-btn {
72
+ cursor: pointer;
73
+ padding: 8px 12px;
74
+ background-color: #f0f0f0;
75
+ border: 1px solid #ddd;
76
+ border-radius: 4px;
77
+ font-weight: bold;
78
+ transition: all 0.3s;
79
+ }
80
+
81
+ .weather-btn.active {
82
+ background-color: #4281f5;
83
+ color: white;
84
+ border-color: #2d5ed9;
85
+ }
86
+
87
+ .refresh-btn {
88
+ margin-top: 10px;
89
+ cursor: pointer;
90
+ padding: 8px 12px;
91
+ background-color: #00c851;
92
+ color: white;
93
+ border: none;
94
+ border-radius: 4px;
95
+ font-weight: bold;
96
+ transition: all 0.3s;
97
+ }
98
+
99
+ .refresh-btn:hover {
100
+ background-color: #00a651;
101
+ }
102
+ `
103
+ }
104
+
105
+ @property({ type: String }) private activeType = 'temperature'
106
+ @state() private map: any
107
+ @state() private weatherLayer: any
108
+ @state() private windLayer: any
109
+
110
+ constructor() {
111
+ super()
112
+ }
113
+
114
+ render() {
115
+ return html`
116
+ <div class="map-container">
117
+ <div id="map"></div>
118
+
119
+ <div class="controls">
120
+ <button
121
+ class="weather-btn ${this.activeType === 'temperature' ? 'active' : ''}"
122
+ @click=${() => this._changeWeatherType('temperature')}
123
+ >
124
+ 온도
125
+ </button>
126
+ <button
127
+ class="weather-btn ${this.activeType === 'humidity' ? 'active' : ''}"
128
+ @click=${() => this._changeWeatherType('humidity')}
129
+ >
130
+ 습도
131
+ </button>
132
+ <button
133
+ class="weather-btn ${this.activeType === 'wind' ? 'active' : ''}"
134
+ @click=${() => this._changeWeatherType('wind')}
135
+ >
136
+ 바람
137
+ </button>
138
+ <button
139
+ class="weather-btn ${this.activeType === 'precipitation' ? 'active' : ''}"
140
+ @click=${() => this._changeWeatherType('precipitation')}
141
+ >
142
+ 강수량
143
+ </button>
144
+ <button class="refresh-btn" @click=${this._refreshWeatherMap}>새로고침</button>
145
+ </div>
146
+ </div>
147
+ `
148
+ }
149
+
150
+ connectedCallback(): void {
151
+ super.connectedCallback()
152
+
153
+ requestAnimationFrame(() => {
154
+ this._initMap()
155
+ })
156
+ }
157
+
158
+ disconnectedCallback(): void {
159
+ super.disconnectedCallback()
160
+
161
+ this.map.removeLayer(this.weatherLayer)
162
+ this.weatherLayer.dispose()
163
+ this.map.dispose()
164
+
165
+ this.map = null
166
+ this.weatherLayer = null
167
+ }
168
+
169
+ private _initMap() {
170
+ const lightOSM = new TileLayer({
171
+ source: new XYZ({
172
+ // url: 'https://{a-c}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png',
173
+ // url: 'https://{a-c}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png',
174
+ url: 'https://{a-c}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png',
175
+ // url: 'https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
176
+ // url: 'https://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
177
+ // url: 'https://api.maptiler.com/maps/toner-lines/{z}/{x}/{y}.png?key=uHikv5CybbRm7cDLjRca'
178
+ // attributions: '© MapTiler'
179
+ attributions: '© OpenStreetMap contributors, © CartoDB'
180
+ }),
181
+ background: 'tranparent',
182
+ opacity: 1
183
+ })
184
+
185
+ const labelOSM = new TileLayer({
186
+ source: new XYZ({
187
+ url: 'https://{a-c}.basemaps.cartocdn.com/dark_only_labels/{z}/{x}/{y}{r}.png'
188
+ // url: 'https://{a-c}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png'
189
+ }),
190
+ background: 'tranparent',
191
+ opacity: 1
192
+ })
193
+
194
+ const vectorTile = new VectorTile({
195
+ format: new MVT({
196
+ layerName: 'countries',
197
+ layers: ['countries']
198
+ }),
199
+ url: `/geo/tiles/{z}/{x}/{y}.mvt`,
200
+ maxZoom: 10,
201
+ projection: 'EPSG:3857'
202
+ })
203
+
204
+ const outlineLayer = new VectorTileLayer({
205
+ source: vectorTile,
206
+ style: new Style({
207
+ stroke: new Stroke({
208
+ color: '#858585',
209
+ width: 1
210
+ })
211
+ })
212
+ })
213
+
214
+ console.log('🛠 벡터 타일 레이어:', outlineLayer, outlineLayer.getSource())
215
+
216
+ /* heatmap */
217
+
218
+ const generateHeatmapData = () => {
219
+ const features: Feature[] = []
220
+ for (let i = 0; i < 20000; i++) {
221
+ // 더 많은 데이터 포인트 생성
222
+ const lon = Math.random() * 360 - 180 // 전 세계 경도 (-180 ~ 180)
223
+ const lat = Math.random() * 170 - 85 // 전 세계 위도 (-85 ~ 85)
224
+
225
+ const weight = Math.random() * 1 // 가중치 (0 ~ 2)
226
+
227
+ const feature = new Feature({
228
+ geometry: new Point(fromLonLat([lon, lat])),
229
+ weight: weight
230
+ })
231
+ features.push(feature)
232
+ }
233
+ return features
234
+ }
235
+
236
+ const vectorSource = new VectorSource({
237
+ features: generateHeatmapData()
238
+ })
239
+
240
+ const heatmapLayer = new Heatmap({
241
+ source: vectorSource,
242
+ blur: 60, // 흐림 정도 증가
243
+ radius: 60, // 점의 반경 증가
244
+ weight: feature => feature.get('weight'), // 가중치 적용
245
+ gradient: colors.map(c => `rgba(${c.join(',')})`), // 색상 적용
246
+ opacity: 0.8
247
+ })
248
+
249
+ this.map = new Map({
250
+ target: this.renderRoot.querySelector('#map') as HTMLElement,
251
+ pixelRatio: Math.min(DEVICE_PIXEL_RATIO, 2),
252
+ layers: [lightOSM, heatmapLayer, outlineLayer, labelOSM],
253
+ view: new View({
254
+ center: fromLonLat([127.5, 36.5]),
255
+ zoom: 7.5
256
+ })
257
+ })
258
+
259
+ // this._addWeatherLayer()
260
+ this._addWindLayer()
261
+ }
262
+
263
+ private _addWindLayer() {
264
+ if (this.windLayer) {
265
+ this.map.removeLayer(this.windLayer)
266
+ console.log('🚨 기존 날씨 레이어 제거')
267
+ }
268
+
269
+ const maxSpeed = 20
270
+
271
+ const colorStops: (number | [number, number, number, number])[] = []
272
+ for (let i = 0; i < colors.length; ++i) {
273
+ colorStops.push((i * maxSpeed) / (colors.length - 1))
274
+ colorStops.push(colors[i])
275
+ }
276
+
277
+ this.windLayer = new Flow({
278
+ source: wind,
279
+ maxSpeed,
280
+ particles: 20000,
281
+ style: {
282
+ color: ['interpolate', ['linear'], ['get', 'speed'], ...colorStops]
283
+ }
284
+ })
285
+
286
+ this.map.addLayer(this.windLayer)
287
+ }
288
+
289
+ private _addWeatherLayer() {
290
+ if (this.weatherLayer) {
291
+ this.map.removeLayer(this.weatherLayer)
292
+ console.log('🚨 기존 날씨 레이어 제거')
293
+ }
294
+
295
+ this.weatherLayer = new ImageLayer({
296
+ source: new WebGLImageStatic({
297
+ url: `/weather/map/${this.activeType}.png`,
298
+ imageExtent: [-20037508.3427892, -20037508.3427892, 20037508.3427892, 20037508.3427892],
299
+ interpolation: true,
300
+ projection: 'EPSG:3857' // 좌표 시스템 설정
301
+ })
302
+ })
303
+
304
+ this.map.addLayer(this.weatherLayer)
305
+
306
+ console.log('🚨 날씨 레이어 추가', this.map.getLayers())
307
+ }
308
+
309
+ private _changeWeatherType(type: string) {
310
+ if (this.activeType === type) return
311
+ this.activeType = type
312
+ this._addWeatherLayer()
313
+ }
314
+
315
+ private _refreshWeatherMap() {
316
+ this._addWeatherLayer()
317
+ }
318
+ }
@@ -0,0 +1,7 @@
1
+ export default function route(page: string) {
2
+ switch (page) {
3
+ case 'weather-page':
4
+ import('./pages/weather-page.js')
5
+ return page
6
+ }
7
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig-base.json",
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "declaration": true,
6
+ "module": "esnext",
7
+ "outDir": "../dist-client",
8
+ "baseUrl": "./"
9
+ },
10
+ "include": ["./**/*"]
11
+ }
@@ -0,0 +1,25 @@
1
+ const DEFAULT_CACHE_TTL = 3600 // 1시간 (초 단위)
2
+ const DEFAULT_TILE_SIZE = 256
3
+ const DEFAULT_MAX_ZOOM = 14
4
+
5
+ module.exports = {
6
+ weather: {
7
+ cacheTtl: DEFAULT_CACHE_TTL * 1000, // 밀리초 단위로 변환
8
+
9
+ // 타일 설정
10
+ tileSize: DEFAULT_TILE_SIZE,
11
+ maxZoom: DEFAULT_MAX_ZOOM,
12
+
13
+ // Mock 데이터 사용 여부
14
+ useMockData: true,
15
+
16
+ // 타일 생성 설정
17
+ defaultResolution: 8, // 기본 그리드 해상도
18
+ minResolution: 4, // 최소 그리드 해상도 (낮은 줌 레벨에서 사용)
19
+ maxResolution: 16, // 최대 그리드 해상도 (높은 줌 레벨에서 사용)
20
+
21
+ openWeatherMap: {
22
+ ApiKey: '41937147fdd666b7184c85f8388b845b'
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ weather: {
3
+ openWeatherMap: {
4
+ ApiKey: '1234567890'
5
+ }
6
+ }
7
+ }