@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.
- package/CHANGELOG.md +8 -0
- package/assets/images/wind.png +0 -0
- package/client/bootstrap.ts +1 -0
- package/client/index.ts +0 -0
- package/client/ol-components/webgl-canvas-layer.ts +413 -0
- package/client/ol-components/webgl-image-static.ts +174 -0
- package/client/ol-components/wind.ts +120 -0
- package/client/pages/weather-page.ts +318 -0
- package/client/route.ts +7 -0
- package/client/tsconfig.json +11 -0
- package/config/config.development.js +25 -0
- package/config/config.production.js +7 -0
- package/data/geo-json/countries-max-geo.json +1 -0
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +2 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/index.d.ts +0 -0
- package/dist-client/index.js +2 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/ol-components/webgl-canvas-layer.d.ts +61 -0
- package/dist-client/ol-components/webgl-canvas-layer.js +364 -0
- package/dist-client/ol-components/webgl-canvas-layer.js.map +1 -0
- package/dist-client/ol-components/webgl-image-static.d.ts +41 -0
- package/dist-client/ol-components/webgl-image-static.js +135 -0
- package/dist-client/ol-components/webgl-image-static.js.map +1 -0
- package/dist-client/ol-components/wind.d.ts +3 -0
- package/dist-client/ol-components/wind.js +104 -0
- package/dist-client/ol-components/wind.js.map +1 -0
- package/dist-client/pages/weather-page.d.ts +18 -0
- package/dist-client/pages/weather-page.js +300 -0
- package/dist-client/pages/weather-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +8 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/controllers/open-meteo-adapter.d.ts +4 -0
- package/dist-server/controllers/open-meteo-adapter.js +83 -0
- package/dist-server/controllers/open-meteo-adapter.js.map +1 -0
- package/dist-server/controllers/types.d.ts +55 -0
- package/dist-server/controllers/types.js +3 -0
- package/dist-server/controllers/types.js.map +1 -0
- package/dist-server/controllers/weather-controller.d.ts +12 -0
- package/dist-server/controllers/weather-controller.js +164 -0
- package/dist-server/controllers/weather-controller.js.map +1 -0
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +4 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/routers/geo-router.d.ts +59 -0
- package/dist-server/routers/geo-router.js +277 -0
- package/dist-server/routers/geo-router.js.map +1 -0
- package/dist-server/routers/weather-router.d.ts +1 -0
- package/dist-server/routers/weather-router.js +10 -0
- package/dist-server/routers/weather-router.js.map +1 -0
- package/dist-server/routes.d.ts +1 -0
- package/dist-server/routes.js +11 -0
- package/dist-server/routes.js.map +1 -0
- package/dist-server/services/chrome-renderer.d.ts +13 -0
- package/dist-server/services/chrome-renderer.js +137 -0
- package/dist-server/services/chrome-renderer.js.map +1 -0
- package/dist-server/services/headless-pool.d.ts +4 -0
- package/dist-server/services/headless-pool.js +89 -0
- package/dist-server/services/headless-pool.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/dist-server/utils/svg-utils.d.ts +35 -0
- package/dist-server/utils/svg-utils.js +89 -0
- package/dist-server/utils/svg-utils.js.map +1 -0
- package/dist-server/utils/tile-utils.d.ts +38 -0
- package/dist-server/utils/tile-utils.js +64 -0
- package/dist-server/utils/tile-utils.js.map +1 -0
- package/package.json +45 -0
- package/server/controllers/open-meteo-adapter.ts +91 -0
- package/server/controllers/types.ts +63 -0
- package/server/controllers/weather-controller.ts +176 -0
- package/server/index.ts +1 -0
- package/server/routers/geo-router.ts +297 -0
- package/server/routers/weather-router.ts +8 -0
- package/server/routes.ts +10 -0
- package/server/services/chrome-renderer.ts +175 -0
- package/server/services/data.json +8438 -0
- package/server/services/headless-pool.ts +97 -0
- package/server/tsconfig.json +10 -0
- package/server/utils/svg-utils.ts +108 -0
- package/server/utils/tile-utils.ts +77 -0
- package/things-factory.config.js +8 -0
- package/translations/en.json +1 -0
- package/translations/ja.json +1 -0
- package/translations/ko.json +1 -0
- package/translations/ms.json +1 -0
- 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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function bootstrap() {}
|
package/client/index.ts
ADDED
|
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
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import ImageStatic from 'ol/source/ImageStatic'
|
|
2
|
+
import ImageWrapper from 'ol/Image'
|
|
3
|
+
import { Extent } from 'ol/extent'
|
|
4
|
+
import { ProjectionLike } from 'ol/proj'
|
|
5
|
+
import { AttributionLike } from 'ol/source/Source'
|
|
6
|
+
|
|
7
|
+
class WebGLImageStatic extends ImageStatic {
|
|
8
|
+
private canvas: HTMLCanvasElement | null = null
|
|
9
|
+
private gl: WebGLRenderingContext | null = null
|
|
10
|
+
private program: WebGLProgram | null = null
|
|
11
|
+
private texture: WebGLTexture | null = null
|
|
12
|
+
private buffer: WebGLBuffer | null = null
|
|
13
|
+
|
|
14
|
+
constructor(option: {
|
|
15
|
+
url: string
|
|
16
|
+
projection: ProjectionLike
|
|
17
|
+
interpolation?: boolean
|
|
18
|
+
imageExtent: Extent
|
|
19
|
+
crossOrigin?: string
|
|
20
|
+
attributions?: AttributionLike
|
|
21
|
+
}) {
|
|
22
|
+
super({
|
|
23
|
+
...option,
|
|
24
|
+
imageLoadFunction: (image, src) => this._loadImageWithWebGL(image, src)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// WebGL 컨텍스트 설정
|
|
28
|
+
this.canvas = document.createElement('canvas')
|
|
29
|
+
this.gl = this.canvas.getContext('webgl', {
|
|
30
|
+
alpha: true,
|
|
31
|
+
antialias: false,
|
|
32
|
+
depth: false
|
|
33
|
+
}) as WebGLRenderingContext
|
|
34
|
+
|
|
35
|
+
if (!this.gl) {
|
|
36
|
+
throw new Error('WebGL을 초기화할 수 없습니다.')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this._initializeWebGL()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* WebGL 초기화
|
|
44
|
+
*/
|
|
45
|
+
private _initializeWebGL() {
|
|
46
|
+
this.program = this._createShaderProgram()
|
|
47
|
+
|
|
48
|
+
if (!this.program) return
|
|
49
|
+
|
|
50
|
+
this._initializeBuffers()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* WebGL에서 사용하는 버퍼 설정
|
|
55
|
+
*/
|
|
56
|
+
private _initializeBuffers() {
|
|
57
|
+
const vertices = new Float32Array([-1, -1, 0, 1, 1, -1, 1, 1, -1, 1, 0, 0, 1, 1, 1, 0])
|
|
58
|
+
const gl = this.gl!
|
|
59
|
+
|
|
60
|
+
this.buffer = gl.createBuffer()
|
|
61
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer)
|
|
62
|
+
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* WebGL을 사용하여 렌더링
|
|
67
|
+
*/
|
|
68
|
+
private _renderTexture() {
|
|
69
|
+
if (!this.gl || !this.program || !this.texture || !this.buffer) return
|
|
70
|
+
|
|
71
|
+
this.gl.viewport(0, 0, this.canvas!.width, this.canvas!.height)
|
|
72
|
+
this.gl.clearColor(0, 0, 0, 0)
|
|
73
|
+
this.gl.clear(this.gl.COLOR_BUFFER_BIT)
|
|
74
|
+
this.gl.useProgram(this.program)
|
|
75
|
+
|
|
76
|
+
this.gl.activeTexture(this.gl.TEXTURE0)
|
|
77
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture)
|
|
78
|
+
this.gl.uniform1i(this.gl.getUniformLocation(this.program, 'u_texture'), 0)
|
|
79
|
+
this.gl.uniform1f(this.gl.getUniformLocation(this.program, 'u_opacity'), 0.8)
|
|
80
|
+
|
|
81
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer)
|
|
82
|
+
const a_position = this.gl.getAttribLocation(this.program, 'a_position')
|
|
83
|
+
const a_texCoord = this.gl.getAttribLocation(this.program, 'a_texCoord')
|
|
84
|
+
|
|
85
|
+
this.gl.enableVertexAttribArray(a_position)
|
|
86
|
+
this.gl.enableVertexAttribArray(a_texCoord)
|
|
87
|
+
this.gl.vertexAttribPointer(a_position, 2, this.gl.FLOAT, false, 16, 0)
|
|
88
|
+
this.gl.vertexAttribPointer(a_texCoord, 2, this.gl.FLOAT, false, 16, 8)
|
|
89
|
+
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* WebGL에서 기후 데이터 이미지를 로드
|
|
94
|
+
*/
|
|
95
|
+
private _loadImageWithWebGL(image: ImageWrapper, src: string) {
|
|
96
|
+
const img = new Image()
|
|
97
|
+
img.crossOrigin = 'anonymous'
|
|
98
|
+
|
|
99
|
+
img.onload = () => {
|
|
100
|
+
if (!this.gl) return
|
|
101
|
+
this.texture = this.gl.createTexture()
|
|
102
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture)
|
|
103
|
+
|
|
104
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE)
|
|
105
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE)
|
|
106
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR)
|
|
107
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR)
|
|
108
|
+
|
|
109
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, img)
|
|
110
|
+
|
|
111
|
+
this._renderTexture()
|
|
112
|
+
;(image.getImage() as HTMLImageElement | HTMLVideoElement).src = this.canvas!.toDataURL('image/png')
|
|
113
|
+
|
|
114
|
+
this.gl = null
|
|
115
|
+
this.canvas = null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
img.src = src
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 셰이더 프로그램 생성
|
|
123
|
+
*/
|
|
124
|
+
private _createShaderProgram(): WebGLProgram | null {
|
|
125
|
+
const vertexShaderSource = `
|
|
126
|
+
precision highp float;
|
|
127
|
+
attribute vec4 a_position;
|
|
128
|
+
attribute vec2 a_texCoord;
|
|
129
|
+
varying vec2 v_texCoord;
|
|
130
|
+
void main() {
|
|
131
|
+
gl_Position = a_position;
|
|
132
|
+
v_texCoord = a_texCoord;
|
|
133
|
+
}`
|
|
134
|
+
|
|
135
|
+
const fragmentShaderSource = `
|
|
136
|
+
precision highp float;
|
|
137
|
+
uniform sampler2D u_texture;
|
|
138
|
+
uniform float u_opacity;
|
|
139
|
+
varying vec2 v_texCoord;
|
|
140
|
+
void main() {
|
|
141
|
+
vec4 texColor = texture2D(u_texture, v_texCoord);
|
|
142
|
+
if (texColor.a < 0.01) {
|
|
143
|
+
discard;
|
|
144
|
+
}
|
|
145
|
+
// gl_FragColor = vec4(texColor.rgb, texColor.a * u_opacity);
|
|
146
|
+
|
|
147
|
+
vec3 invertedColor = vec3(1.0) - texColor.rgb;
|
|
148
|
+
gl_FragColor = vec4(invertedColor, texColor.a * u_opacity);
|
|
149
|
+
}`
|
|
150
|
+
|
|
151
|
+
const gl = this.gl!
|
|
152
|
+
const vertexShader = this._compileShader(gl.VERTEX_SHADER, vertexShaderSource)
|
|
153
|
+
const fragmentShader = this._compileShader(gl.FRAGMENT_SHADER, fragmentShaderSource)
|
|
154
|
+
if (!vertexShader || !fragmentShader) return null
|
|
155
|
+
|
|
156
|
+
const program = gl.createProgram()!
|
|
157
|
+
gl.attachShader(program, vertexShader)
|
|
158
|
+
gl.attachShader(program, fragmentShader)
|
|
159
|
+
gl.linkProgram(program)
|
|
160
|
+
|
|
161
|
+
return gl.getProgramParameter(program, gl.LINK_STATUS) ? program : null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private _compileShader(type: number, source: string): WebGLShader | null {
|
|
165
|
+
const gl = this.gl!
|
|
166
|
+
|
|
167
|
+
const shader = gl.createShader(type)!
|
|
168
|
+
gl.shaderSource(shader, source)
|
|
169
|
+
gl.compileShader(shader)
|
|
170
|
+
return gl.getShaderParameter(shader, gl.COMPILE_STATUS) ? shader : null
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export default WebGLImageStatic
|