@things-factory/weather 9.0.0-beta.55 → 9.0.0-beta.56
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/client/ol-components/wind-data-tile-layer.ts +485 -0
- package/client/ol-components/wind-rasterized-layer.ts +437 -0
- package/client/pages/weather-page.ts +149 -144
- package/dist-client/ol-components/wind-data-tile-layer.d.ts +59 -0
- package/dist-client/ol-components/wind-data-tile-layer.js +414 -0
- package/dist-client/ol-components/wind-data-tile-layer.js.map +1 -0
- package/dist-client/ol-components/wind-rasterized-layer.d.ts +59 -0
- package/dist-client/ol-components/wind-rasterized-layer.js +363 -0
- package/dist-client/ol-components/wind-rasterized-layer.js.map +1 -0
- package/dist-client/pages/weather-page.d.ts +7 -2
- package/dist-client/pages/weather-page.js +145 -125
- package/dist-client/pages/weather-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/client/ol-components/webgl-image-static.ts +0 -244
- package/client/ol-components/wind.ts +0 -120
|
@@ -1,244 +0,0 @@
|
|
|
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
|
-
private opacity: number = 0.8
|
|
14
|
-
private blendMode: string = 'normal'
|
|
15
|
-
|
|
16
|
-
constructor(option: {
|
|
17
|
-
url: string
|
|
18
|
-
projection: ProjectionLike
|
|
19
|
-
interpolation?: boolean
|
|
20
|
-
imageExtent: Extent
|
|
21
|
-
crossOrigin?: string
|
|
22
|
-
attributions?: AttributionLike
|
|
23
|
-
opacity?: number
|
|
24
|
-
blendMode?: string
|
|
25
|
-
}) {
|
|
26
|
-
super({
|
|
27
|
-
...option,
|
|
28
|
-
imageLoadFunction: (image, src) => this._loadImageWithWebGL(image, src)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
// 옵션에서 값 설정 (기본값 제공)
|
|
32
|
-
this.opacity = option.opacity !== undefined ? option.opacity : 0.8
|
|
33
|
-
this.blendMode = option.blendMode || 'normal'
|
|
34
|
-
|
|
35
|
-
// WebGL 컨텍스트 설정
|
|
36
|
-
this.canvas = document.createElement('canvas')
|
|
37
|
-
this.gl = this.canvas.getContext('webgl', {
|
|
38
|
-
alpha: true,
|
|
39
|
-
antialias: false,
|
|
40
|
-
depth: false
|
|
41
|
-
}) as WebGLRenderingContext
|
|
42
|
-
|
|
43
|
-
if (!this.gl) {
|
|
44
|
-
throw new Error('WebGL을 초기화할 수 없습니다.')
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
this._initializeWebGL()
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* 투명도 설정
|
|
52
|
-
*/
|
|
53
|
-
public setOpacity(opacity: number): void {
|
|
54
|
-
if (opacity !== this.opacity) {
|
|
55
|
-
this.opacity = opacity
|
|
56
|
-
if (this.texture) {
|
|
57
|
-
this._renderTexture()
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 블렌드 모드 설정
|
|
64
|
-
*/
|
|
65
|
-
public setBlendMode(blendMode: string): void {
|
|
66
|
-
if (blendMode !== this.blendMode) {
|
|
67
|
-
this.blendMode = blendMode
|
|
68
|
-
if (this.texture) {
|
|
69
|
-
this._renderTexture()
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* WebGL 초기화
|
|
76
|
-
*/
|
|
77
|
-
private _initializeWebGL() {
|
|
78
|
-
this.program = this._createShaderProgram()
|
|
79
|
-
|
|
80
|
-
if (!this.program) return
|
|
81
|
-
|
|
82
|
-
this._initializeBuffers()
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* WebGL에서 사용하는 버퍼 설정
|
|
87
|
-
*/
|
|
88
|
-
private _initializeBuffers() {
|
|
89
|
-
const vertices = new Float32Array([-1, -1, 0, 1, 1, -1, 1, 1, -1, 1, 0, 0, 1, 1, 1, 0])
|
|
90
|
-
const gl = this.gl!
|
|
91
|
-
|
|
92
|
-
this.buffer = gl.createBuffer()
|
|
93
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer)
|
|
94
|
-
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* WebGL을 사용하여 렌더링
|
|
99
|
-
*/
|
|
100
|
-
private _renderTexture() {
|
|
101
|
-
if (!this.gl || !this.program || !this.texture || !this.buffer) return
|
|
102
|
-
|
|
103
|
-
this.gl.viewport(0, 0, this.canvas!.width, this.canvas!.height)
|
|
104
|
-
this.gl.clearColor(0, 0, 0, 0)
|
|
105
|
-
this.gl.clear(this.gl.COLOR_BUFFER_BIT)
|
|
106
|
-
this.gl.useProgram(this.program)
|
|
107
|
-
|
|
108
|
-
this.gl.activeTexture(this.gl.TEXTURE0)
|
|
109
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture)
|
|
110
|
-
this.gl.uniform1i(this.gl.getUniformLocation(this.program, 'u_texture'), 0)
|
|
111
|
-
|
|
112
|
-
// 사용자 지정 투명도 사용
|
|
113
|
-
this.gl.uniform1f(this.gl.getUniformLocation(this.program, 'u_opacity'), this.opacity)
|
|
114
|
-
|
|
115
|
-
// 블렌드 모드 설정
|
|
116
|
-
let blendModeValue = 0 // normal
|
|
117
|
-
switch (this.blendMode) {
|
|
118
|
-
case 'multiply':
|
|
119
|
-
blendModeValue = 1
|
|
120
|
-
break
|
|
121
|
-
case 'screen':
|
|
122
|
-
blendModeValue = 2
|
|
123
|
-
break
|
|
124
|
-
case 'color-dodge':
|
|
125
|
-
blendModeValue = 3
|
|
126
|
-
break
|
|
127
|
-
case 'invert':
|
|
128
|
-
blendModeValue = 4
|
|
129
|
-
break
|
|
130
|
-
}
|
|
131
|
-
this.gl.uniform1i(this.gl.getUniformLocation(this.program, 'u_blendMode'), blendModeValue)
|
|
132
|
-
|
|
133
|
-
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer)
|
|
134
|
-
const a_position = this.gl.getAttribLocation(this.program, 'a_position')
|
|
135
|
-
const a_texCoord = this.gl.getAttribLocation(this.program, 'a_texCoord')
|
|
136
|
-
|
|
137
|
-
this.gl.enableVertexAttribArray(a_position)
|
|
138
|
-
this.gl.enableVertexAttribArray(a_texCoord)
|
|
139
|
-
this.gl.vertexAttribPointer(a_position, 2, this.gl.FLOAT, false, 16, 0)
|
|
140
|
-
this.gl.vertexAttribPointer(a_texCoord, 2, this.gl.FLOAT, false, 16, 8)
|
|
141
|
-
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* WebGL에서 기후 데이터 이미지를 로드
|
|
146
|
-
*/
|
|
147
|
-
private _loadImageWithWebGL(image: ImageWrapper, src: string) {
|
|
148
|
-
const img = new Image()
|
|
149
|
-
img.crossOrigin = 'anonymous'
|
|
150
|
-
|
|
151
|
-
img.onload = () => {
|
|
152
|
-
if (!this.gl) return
|
|
153
|
-
|
|
154
|
-
// 캔버스 크기 설정
|
|
155
|
-
this.canvas!.width = img.width
|
|
156
|
-
this.canvas!.height = img.height
|
|
157
|
-
|
|
158
|
-
this.texture = this.gl.createTexture()
|
|
159
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture)
|
|
160
|
-
|
|
161
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE)
|
|
162
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE)
|
|
163
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR)
|
|
164
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR)
|
|
165
|
-
|
|
166
|
-
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, img)
|
|
167
|
-
|
|
168
|
-
this._renderTexture()
|
|
169
|
-
;(image.getImage() as HTMLImageElement | HTMLVideoElement).src = this.canvas!.toDataURL('image/png')
|
|
170
|
-
|
|
171
|
-
this.gl = null
|
|
172
|
-
this.canvas = null
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
img.src = src
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* 셰이더 프로그램 생성
|
|
180
|
-
*/
|
|
181
|
-
private _createShaderProgram(): WebGLProgram | null {
|
|
182
|
-
const vertexShaderSource = `
|
|
183
|
-
precision highp float;
|
|
184
|
-
attribute vec4 a_position;
|
|
185
|
-
attribute vec2 a_texCoord;
|
|
186
|
-
varying vec2 v_texCoord;
|
|
187
|
-
void main() {
|
|
188
|
-
gl_Position = a_position;
|
|
189
|
-
v_texCoord = a_texCoord;
|
|
190
|
-
}`
|
|
191
|
-
|
|
192
|
-
const fragmentShaderSource = `
|
|
193
|
-
precision highp float;
|
|
194
|
-
uniform sampler2D u_texture;
|
|
195
|
-
uniform float u_opacity;
|
|
196
|
-
uniform int u_blendMode;
|
|
197
|
-
varying vec2 v_texCoord;
|
|
198
|
-
|
|
199
|
-
void main() {
|
|
200
|
-
vec4 texColor = texture2D(u_texture, v_texCoord);
|
|
201
|
-
|
|
202
|
-
// 완전 투명한 픽셀은 렌더링하지 않음
|
|
203
|
-
if (texColor.a < 0.01) {
|
|
204
|
-
discard;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
float temperature = texColor.r; // 예: 빨간 채널에 온도 정보가 있다고 가정
|
|
208
|
-
vec3 color;
|
|
209
|
-
|
|
210
|
-
if (temperature < 0.3) {
|
|
211
|
-
color = mix(vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 1.0), temperature / 0.3); // 파란색에서 청록색으로
|
|
212
|
-
} else if (temperature < 0.6) {
|
|
213
|
-
color = mix(vec3(0.0, 1.0, 1.0), vec3(0.0, 1.0, 0.0), (temperature - 0.3) / 0.3); // 청록색에서 녹색으로
|
|
214
|
-
} else {
|
|
215
|
-
color = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), (temperature - 0.6) / 0.4); // 녹색에서 빨간색으로
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
gl_FragColor = vec4(color, texColor.a * u_opacity);
|
|
219
|
-
}`
|
|
220
|
-
|
|
221
|
-
const gl = this.gl!
|
|
222
|
-
const vertexShader = this._compileShader(gl.VERTEX_SHADER, vertexShaderSource)
|
|
223
|
-
const fragmentShader = this._compileShader(gl.FRAGMENT_SHADER, fragmentShaderSource)
|
|
224
|
-
if (!vertexShader || !fragmentShader) return null
|
|
225
|
-
|
|
226
|
-
const program = gl.createProgram()!
|
|
227
|
-
gl.attachShader(program, vertexShader)
|
|
228
|
-
gl.attachShader(program, fragmentShader)
|
|
229
|
-
gl.linkProgram(program)
|
|
230
|
-
|
|
231
|
-
return gl.getProgramParameter(program, gl.LINK_STATUS) ? program : null
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
private _compileShader(type: number, source: string): WebGLShader | null {
|
|
235
|
-
const gl = this.gl!
|
|
236
|
-
|
|
237
|
-
const shader = gl.createShader(type)!
|
|
238
|
-
gl.shaderSource(shader, source)
|
|
239
|
-
gl.compileShader(shader)
|
|
240
|
-
return gl.getShaderParameter(shader, gl.COMPILE_STATUS) ? shader : null
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export default WebGLImageStatic
|
|
@@ -1,120 +0,0 @@
|
|
|
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
|