@things-factory/weather 9.0.0-9.0.0-beta.59.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/assets/images/wind.png +0 -0
  3. package/assets/images/wind2.png +0 -0
  4. package/client/bootstrap.ts +1 -0
  5. package/client/index.ts +0 -0
  6. package/client/ol-components/webgl-canvas-layer.ts +413 -0
  7. package/client/ol-components/wind-data-tile-layer.ts +485 -0
  8. package/client/ol-components/wind-rasterized-layer.ts +437 -0
  9. package/client/pages/weather-page.ts +353 -0
  10. package/client/route.ts +7 -0
  11. package/client/tsconfig.json +11 -0
  12. package/config/config.development.js +28 -0
  13. package/config/config.production.js +10 -0
  14. package/data/geo-json/countries-max-geo.json +1 -0
  15. package/dist-client/bootstrap.d.ts +1 -0
  16. package/dist-client/bootstrap.js +2 -0
  17. package/dist-client/bootstrap.js.map +1 -0
  18. package/dist-client/index.d.ts +0 -0
  19. package/dist-client/index.js +2 -0
  20. package/dist-client/index.js.map +1 -0
  21. package/dist-client/ol-components/webgl-canvas-layer.d.ts +61 -0
  22. package/dist-client/ol-components/webgl-canvas-layer.js +364 -0
  23. package/dist-client/ol-components/webgl-canvas-layer.js.map +1 -0
  24. package/dist-client/ol-components/wind-data-tile-layer.d.ts +59 -0
  25. package/dist-client/ol-components/wind-data-tile-layer.js +414 -0
  26. package/dist-client/ol-components/wind-data-tile-layer.js.map +1 -0
  27. package/dist-client/ol-components/wind-rasterized-layer.d.ts +59 -0
  28. package/dist-client/ol-components/wind-rasterized-layer.js +363 -0
  29. package/dist-client/ol-components/wind-rasterized-layer.js.map +1 -0
  30. package/dist-client/pages/weather-page.d.ts +23 -0
  31. package/dist-client/pages/weather-page.js +348 -0
  32. package/dist-client/pages/weather-page.js.map +1 -0
  33. package/dist-client/route.d.ts +1 -0
  34. package/dist-client/route.js +8 -0
  35. package/dist-client/route.js.map +1 -0
  36. package/dist-client/tsconfig.tsbuildinfo +1 -0
  37. package/dist-server/controllers/open-meteo-adapter.d.ts +4 -0
  38. package/dist-server/controllers/open-meteo-adapter.js +87 -0
  39. package/dist-server/controllers/open-meteo-adapter.js.map +1 -0
  40. package/dist-server/controllers/types.d.ts +55 -0
  41. package/dist-server/controllers/types.js +3 -0
  42. package/dist-server/controllers/types.js.map +1 -0
  43. package/dist-server/controllers/weather-controller.d.ts +12 -0
  44. package/dist-server/controllers/weather-controller.js +179 -0
  45. package/dist-server/controllers/weather-controller.js.map +1 -0
  46. package/dist-server/index.d.ts +1 -0
  47. package/dist-server/index.js +4 -0
  48. package/dist-server/index.js.map +1 -0
  49. package/dist-server/routers/geo-router.d.ts +59 -0
  50. package/dist-server/routers/geo-router.js +277 -0
  51. package/dist-server/routers/geo-router.js.map +1 -0
  52. package/dist-server/routers/weather-router.d.ts +1 -0
  53. package/dist-server/routers/weather-router.js +10 -0
  54. package/dist-server/routers/weather-router.js.map +1 -0
  55. package/dist-server/routes.d.ts +1 -0
  56. package/dist-server/routes.js +11 -0
  57. package/dist-server/routes.js.map +1 -0
  58. package/dist-server/services/chrome-renderer.d.ts +13 -0
  59. package/dist-server/services/chrome-renderer.js +162 -0
  60. package/dist-server/services/chrome-renderer.js.map +1 -0
  61. package/dist-server/services/headless-pool.d.ts +4 -0
  62. package/dist-server/services/headless-pool.js +89 -0
  63. package/dist-server/services/headless-pool.js.map +1 -0
  64. package/dist-server/tsconfig.tsbuildinfo +1 -0
  65. package/dist-server/utils/svg-utils.d.ts +35 -0
  66. package/dist-server/utils/svg-utils.js +89 -0
  67. package/dist-server/utils/svg-utils.js.map +1 -0
  68. package/dist-server/utils/tile-utils.d.ts +38 -0
  69. package/dist-server/utils/tile-utils.js +64 -0
  70. package/dist-server/utils/tile-utils.js.map +1 -0
  71. package/package.json +46 -0
  72. package/server/controllers/open-meteo-adapter.ts +96 -0
  73. package/server/controllers/types.ts +63 -0
  74. package/server/controllers/weather-controller.ts +192 -0
  75. package/server/index.ts +1 -0
  76. package/server/routers/geo-router.ts +297 -0
  77. package/server/routers/weather-router.ts +8 -0
  78. package/server/routes.ts +10 -0
  79. package/server/services/chrome-renderer.ts +206 -0
  80. package/server/services/data.json +8438 -0
  81. package/server/services/headless-pool.ts +97 -0
  82. package/server/tsconfig.json +10 -0
  83. package/server/utils/svg-utils.ts +108 -0
  84. package/server/utils/tile-utils.ts +77 -0
  85. package/things-factory.config.js +8 -0
  86. package/translations/en.json +1 -0
  87. package/translations/ja.json +1 -0
  88. package/translations/ko.json +1 -0
  89. package/translations/ms.json +1 -0
  90. package/translations/zh.json +1 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
+ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
+
8
+ <!-- ## [Unreleased] -->
Binary file
Binary file
@@ -0,0 +1 @@
1
+ export default function bootstrap() {}
File without changes
@@ -0,0 +1,413 @@
1
+ import ImageLayer from 'ol/layer/Image.js'
2
+ import ImageCanvasSource from 'ol/source/ImageCanvas.js'
3
+ import { transformExtent } from 'ol/proj.js'
4
+
5
+ /**
6
+ * WebGL을 사용하여 날씨 데이터 이미지를 지도에 오버레이하는 레이어
7
+ * 이미지의 RGBA 채널에는 온도, 습도, 바람, 강수량 데이터가 인코딩되어 있음
8
+ */
9
+ class WebGLCanvasLayer extends ImageLayer<ImageCanvasSource> {
10
+ private canvas: HTMLCanvasElement | null = null
11
+ private gl: WebGLRenderingContext | null = null
12
+ private program: WebGLProgram | null = null
13
+ private texture: WebGLTexture | null = null
14
+ private buffer: WebGLBuffer | null = null
15
+ private imageLoaded: boolean = false
16
+ private pendingRender: boolean = false
17
+ private imageUrl: string
18
+ private dataType: string
19
+
20
+ /**
21
+ * WebGL 캔버스 레이어 생성자
22
+ * @param imageUrl 날씨 데이터가 인코딩된 이미지 URL
23
+ * @param dataType 시각화할 데이터 유형 (temperature, humidity, wind, precipitation)
24
+ */
25
+ constructor(imageUrl: string, dataType: string = 'temperature') {
26
+ super({
27
+ source: new ImageCanvasSource({
28
+ canvasFunction: (extent, resolution, pixelRatio, size, projection) => {
29
+ this._initializeWebGL(size, pixelRatio)
30
+
31
+ if (this.gl && this.program) {
32
+ // 현재 지도 뷰의 extent를 지리 좌표(EPSG:4326)로 변환
33
+ const worldExtent = transformExtent(extent, projection.getCode(), 'EPSG:4326')
34
+
35
+ // 이미지가 로드된 경우 렌더링
36
+ if (this.imageLoaded && this.texture) {
37
+ this._updateWebGLUniforms(worldExtent, resolution, size, pixelRatio)
38
+ this._renderTexture(worldExtent, size, resolution, pixelRatio)
39
+ } else if (!this.pendingRender) {
40
+ this.pendingRender = true
41
+ this._loadImage(this.imageUrl, worldExtent, size, resolution, pixelRatio)
42
+ }
43
+ }
44
+
45
+ return this.canvas!
46
+ },
47
+ projection: 'EPSG:3857'
48
+ })
49
+ })
50
+
51
+ this.imageUrl = imageUrl
52
+ this.dataType = dataType
53
+ this.on('change:visible', () => this.changed())
54
+ }
55
+
56
+ /**
57
+ * 데이터 시각화 유형 변경
58
+ */
59
+ updateDataType(type: string) {
60
+ this.dataType = type
61
+ this.changed()
62
+ }
63
+
64
+ /**
65
+ * 리소스 해제
66
+ */
67
+ dispose() {
68
+ if (this.gl) {
69
+ if (this.buffer) this.gl.deleteBuffer(this.buffer)
70
+ if (this.texture) this.gl.deleteTexture(this.texture)
71
+ if (this.program) this.gl.deleteProgram(this.program)
72
+ }
73
+ super.dispose()
74
+ }
75
+
76
+ /**
77
+ * 이미지 URL 업데이트
78
+ */
79
+ updateImageUrl(newUrl: string) {
80
+ if (this.imageUrl === newUrl) return
81
+
82
+ this.imageUrl = newUrl
83
+ this.imageLoaded = false
84
+ this.pendingRender = false
85
+
86
+ if (this.texture && this.gl) {
87
+ this.gl.deleteTexture(this.texture)
88
+ this.texture = null
89
+ }
90
+
91
+ this.changed()
92
+ }
93
+
94
+ /**
95
+ * WebGL 컨텍스트 초기화
96
+ */
97
+ private _initializeWebGL(size: number[], pixelRatio: number = 1) {
98
+ const width = Math.round(size[0] * pixelRatio)
99
+ const height = Math.round(size[1] * pixelRatio)
100
+
101
+ if (!this.canvas) {
102
+ // 캔버스 생성 및 WebGL 초기화
103
+ this.canvas = document.createElement('canvas')
104
+ this.canvas.width = width
105
+ this.canvas.height = height
106
+
107
+ this.gl = this.canvas.getContext('webgl', {
108
+ alpha: true,
109
+ antialias: false,
110
+ depth: false
111
+ })
112
+
113
+ if (!this.gl) return
114
+
115
+ this.program = this._createShaderProgram()
116
+ if (this.program) {
117
+ this._initializeBuffers()
118
+ }
119
+ } else if (this.canvas.width !== width || this.canvas.height !== height) {
120
+ // 캔버스 크기 조정
121
+ this.canvas.width = width
122
+ this.canvas.height = height
123
+ if (this.gl) this.gl.viewport(0, 0, width, height)
124
+ }
125
+ }
126
+
127
+ /**
128
+ * 버텍스 버퍼 초기화
129
+ */
130
+ private _initializeBuffers() {
131
+ if (!this.gl) return
132
+
133
+ // 사각형 버텍스 (위치(x,y) + 텍스처 좌표(s,t))
134
+ const vertices = new Float32Array([
135
+ -1,
136
+ -1,
137
+ 0,
138
+ 1, // 좌하단
139
+ 1,
140
+ -1,
141
+ 1,
142
+ 1, // 우하단
143
+ -1,
144
+ 1,
145
+ 0,
146
+ 0, // 좌상단
147
+ 1,
148
+ 1,
149
+ 1,
150
+ 0 // 우상단
151
+ ])
152
+
153
+ this.buffer = this.gl.createBuffer()
154
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer)
155
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, vertices, this.gl.STATIC_DRAW)
156
+ }
157
+
158
+ /**
159
+ * 이미지 로드 및 텍스처 생성
160
+ */
161
+ private _loadImage(url: string, worldExtent: number[], size: number[], resolution: number, pixelRatio: number) {
162
+ const img = new Image()
163
+ img.crossOrigin = 'anonymous'
164
+
165
+ img.onload = () => {
166
+ if (!this.gl) return
167
+
168
+ try {
169
+ // 텍스처 생성 및 설정
170
+ this.texture = this.gl.createTexture()
171
+ this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture)
172
+
173
+ // 경도 방향으로 래핑 (날짜 변경선 처리)
174
+ // this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.REPEAT)
175
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE)
176
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE)
177
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR)
178
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR)
179
+
180
+ // 텍스처 데이터 업로드
181
+ this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false) // 알파값 손실 방지
182
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, img)
183
+
184
+ this.imageLoaded = true
185
+ this.pendingRender = false
186
+
187
+ // 이미지 로드 후 렌더링
188
+ this._renderTexture(worldExtent, size, resolution, pixelRatio)
189
+ this.changed()
190
+ } catch (err) {
191
+ console.error('텍스처 생성 실패:', err)
192
+ this.pendingRender = false
193
+ }
194
+ }
195
+
196
+ img.onerror = () => {
197
+ console.error('이미지 로드 실패:', url)
198
+ this.pendingRender = false
199
+ }
200
+
201
+ img.src = url
202
+ }
203
+
204
+ /**
205
+ * 텍스처 렌더링
206
+ */
207
+ private _renderTexture(worldExtent: number[], size: number[], resolution: number, pixelRatio: number) {
208
+ if (!this.gl || !this.program || !this.texture || !this.buffer) return
209
+
210
+ const gl = this.gl
211
+ const width = Math.round(size[0] * pixelRatio)
212
+ const height = Math.round(size[1] * pixelRatio)
213
+
214
+ this._updateWebGLUniforms(worldExtent, resolution, size, pixelRatio)
215
+
216
+ // 기본 WebGL 상태 설정
217
+ gl.viewport(0, 0, width, height)
218
+ gl.clearColor(0, 0, 0, 0)
219
+ gl.enable(gl.BLEND)
220
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
221
+ gl.clear(gl.COLOR_BUFFER_BIT)
222
+
223
+ // 셰이더 프로그램 활성화
224
+ gl.useProgram(this.program)
225
+
226
+ // 텍스처 바인딩
227
+ gl.activeTexture(gl.TEXTURE0)
228
+ gl.bindTexture(gl.TEXTURE_2D, this.texture)
229
+
230
+ // 유니폼 설정
231
+ const dataTypeVal = ['temperature', 'humidity', 'wind', 'precipitation'].indexOf(this.dataType)
232
+
233
+ gl.uniform1i(gl.getUniformLocation(this.program, 'u_texture'), 0)
234
+ gl.uniform1f(gl.getUniformLocation(this.program, 'u_opacity'), 0.8 * pixelRatio)
235
+ gl.uniform1i(gl.getUniformLocation(this.program, 'u_dataType'), dataTypeVal >= 0 ? dataTypeVal : 0)
236
+ gl.uniform4fv(
237
+ gl.getUniformLocation(this.program, 'u_worldExtent'),
238
+ new Float32Array([worldExtent[0], worldExtent[1], worldExtent[2], worldExtent[3]])
239
+ )
240
+
241
+ // 버퍼 바인딩 및 속성 설정
242
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer)
243
+
244
+ const a_position = gl.getAttribLocation(this.program, 'a_position')
245
+ const a_texCoord = gl.getAttribLocation(this.program, 'a_texCoord')
246
+
247
+ gl.enableVertexAttribArray(a_position)
248
+ gl.enableVertexAttribArray(a_texCoord)
249
+ gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 16, 0)
250
+ gl.vertexAttribPointer(a_texCoord, 2, gl.FLOAT, false, 16, 8)
251
+
252
+ // 사각형 렌더링
253
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
254
+ }
255
+
256
+ /**
257
+ * 셰이더 프로그램 생성
258
+ */
259
+ private _createShaderProgram(): WebGLProgram | null {
260
+ if (!this.gl) return null
261
+
262
+ // 버텍스 셰이더: 지리 좌표와 텍스처 좌표 간 변환
263
+ const vertexShaderSource = `
264
+ precision highp float;
265
+
266
+ attribute vec4 a_position; // Rectangle coordinates
267
+ attribute vec2 a_texCoord; // Texture coordinates
268
+ varying vec2 v_texCoord;
269
+
270
+ uniform vec2 u_resolution; // Resolution (meters per pixel)
271
+ uniform vec2 u_offset; // Offset in map coordinates
272
+ uniform float u_zoomResolution; // 🚀 This will store the resolution passed from JS
273
+
274
+ void main() {
275
+ float x = ((a_position.x - u_offset.x * u_zoomResolution) / u_resolution.x) * 2.0 - 1.0;
276
+ float y = ((a_position.y - u_offset.y * u_zoomResolution) / u_resolution.y) * 2.0 - 1.0;
277
+
278
+ gl_Position = vec4(x, y, 0.0, 1.0);
279
+ v_texCoord = a_texCoord;
280
+ }
281
+ `
282
+
283
+ // 프래그먼트 셰이더: 데이터 시각화
284
+ const fragmentShaderSource = `
285
+ precision highp float;
286
+
287
+ uniform sampler2D u_texture;
288
+ uniform float u_opacity;
289
+ uniform int u_dataType;
290
+ varying vec2 v_texCoord;
291
+
292
+ // 색상 그라데이션 생성
293
+ vec3 getColorForData(float value) {
294
+ // 값 범위 확장
295
+ value = clamp(value * 1.2, 0.0, 1.0);
296
+
297
+ if (u_dataType == 0) { // 온도
298
+ // 파랑(저온) -> 빨강(고온)
299
+ return mix(vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0), value);
300
+ }
301
+ else if (u_dataType == 1) { // 습도
302
+ // 하늘색(건조) -> 진한 파랑(습함)
303
+ return mix(vec3(0.5, 0.5, 1.0), vec3(0.0, 0.0, 0.8), value);
304
+ }
305
+ else if (u_dataType == 2) { // 바람
306
+ // 녹색(약함) -> 노랑(강함)
307
+ return mix(vec3(0.0, 0.5, 0.0), vec3(1.0, 1.0, 0.0), value);
308
+ }
309
+ else { // 강수량
310
+ // 투명(없음) -> 보라(많음)
311
+ float intensity = value * 1.5;
312
+ return vec3(0.5, 0.0, 0.5) * intensity;
313
+ }
314
+ }
315
+
316
+ void main() {
317
+ // 텍스처에서 데이터 가져오기
318
+ vec4 texColor = texture2D(u_texture, v_texCoord);
319
+
320
+ // 데이터가 없는 경우 무시
321
+ if (texColor.a < 0.01) {
322
+ discard;
323
+ }
324
+
325
+ // 데이터 채널 선택
326
+ float value = 0.0;
327
+ if (u_dataType == 0) value = texColor.r; // 온도
328
+ else if (u_dataType == 1) value = texColor.g; // 습도
329
+ else if (u_dataType == 2) value = texColor.b; // 바람
330
+ else value = texColor.a; // 강수량
331
+
332
+ // 데이터 시각화
333
+ vec3 color = getColorForData(value);
334
+ float alpha = u_dataType == 3 ? value * u_opacity : u_opacity;
335
+
336
+ gl_FragColor = vec4(color, alpha);
337
+ }
338
+ `
339
+
340
+ // 셰이더 컴파일
341
+ const vertexShader = this._compileShader(this.gl.VERTEX_SHADER, vertexShaderSource)
342
+ const fragmentShader = this._compileShader(this.gl.FRAGMENT_SHADER, fragmentShaderSource)
343
+
344
+ if (!vertexShader || !fragmentShader) return null
345
+
346
+ // 프로그램 생성 및 링크
347
+ const program = this.gl.createProgram()!
348
+ this.gl.attachShader(program, vertexShader)
349
+ this.gl.attachShader(program, fragmentShader)
350
+ this.gl.linkProgram(program)
351
+
352
+ if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
353
+ console.error('WebGL 프로그램 링크 실패:', this.gl.getProgramInfoLog(program))
354
+ return null
355
+ }
356
+
357
+ return program
358
+ }
359
+
360
+ /**
361
+ * 셰이더 컴파일
362
+ */
363
+ private _compileShader(type: number, source: string): WebGLShader | null {
364
+ if (!this.gl) return null
365
+
366
+ const shader = this.gl.createShader(type)!
367
+ this.gl.shaderSource(shader, source)
368
+ this.gl.compileShader(shader)
369
+
370
+ if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
371
+ console.error('셰이더 컴파일 오류:', this.gl.getShaderInfoLog(shader))
372
+ this.gl.deleteShader(shader)
373
+ return null
374
+ }
375
+
376
+ return shader
377
+ }
378
+
379
+ private _updateWebGLUniforms(worldExtent: number[], resolution: number, size: number[], pixelRatio: number) {
380
+ if (!this.gl || !this.program) return
381
+
382
+ const gl = this.gl
383
+ gl.useProgram(this.program)
384
+
385
+ const textureWidth = size[0] * pixelRatio
386
+ const textureHeight = size[1] * pixelRatio
387
+
388
+ // ✅ Apply resolution correctly (fixing zoom scaling issue)
389
+ const u_resolution = [
390
+ (worldExtent[2] - worldExtent[0]) / textureWidth,
391
+ (worldExtent[3] - worldExtent[1]) / textureHeight
392
+ ]
393
+
394
+ // ✅ Convert u_offset to OpenGL coordinates
395
+ const u_offset = [
396
+ (worldExtent[0] - (worldExtent[2] + worldExtent[0]) / 2) / (worldExtent[2] - worldExtent[0]),
397
+ (worldExtent[1] - (worldExtent[3] + worldExtent[1]) / 2) / (worldExtent[3] - worldExtent[1])
398
+ ]
399
+
400
+ console.log('📌 _updateWebGLUniforms called')
401
+ console.log('🟢 resolution:', resolution)
402
+ console.log('🟢 pixelRatio:', pixelRatio)
403
+ console.log('🟢 textureWidth:', textureWidth, 'textureHeight:', textureHeight)
404
+ console.log('🟢 u_resolution:', u_resolution)
405
+ console.log('🟢 u_offset:', u_offset)
406
+
407
+ gl.uniform2fv(gl.getUniformLocation(this.program, 'u_resolution'), u_resolution)
408
+ gl.uniform2fv(gl.getUniformLocation(this.program, 'u_offset'), u_offset)
409
+ gl.uniform1f(gl.getUniformLocation(this.program, 'u_zoomResolution'), resolution) // 🚀 Pass resolution!
410
+ }
411
+ }
412
+
413
+ export default WebGLCanvasLayer