@things-factory/weather 9.0.0-beta.52 → 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/config/config.development.js +3 -0
- package/config/config.production.js +3 -0
- 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/dist-server/controllers/open-meteo-adapter.js +3 -1
- package/dist-server/controllers/open-meteo-adapter.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/server/controllers/open-meteo-adapter.ts +3 -1
- package/client/ol-components/webgl-image-static.ts +0 -244
- package/client/ol-components/wind.ts +0 -120
|
@@ -0,0 +1,485 @@
|
|
|
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
|
+
import Layer from 'ol/layer/Layer.js'
|
|
5
|
+
import BaseLayer from 'ol/layer/Base.js'
|
|
6
|
+
import Map from 'ol/Map.js'
|
|
7
|
+
import { WeatherLayerType } from '@hyperviz/weather' // BaseWeatherLayer 인터페이스 호환을 위해 타입 임포트
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 바람 데이터를 표시하기 위한 타일 기반 레이어
|
|
11
|
+
* PNG 이미지 데이터를 DataTileSource로 변환하여 효율적인 렌더링 제공
|
|
12
|
+
*
|
|
13
|
+
* @class WindDataTileLayer
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// --- 데이터 로딩 및 변환 로직 ---
|
|
17
|
+
|
|
18
|
+
/** 바람 데이터 이미지 로딩 Promise */
|
|
19
|
+
const windData: Promise<{ data: ArrayBuffer; width: number; height: number }> = new Promise((resolve, reject) => {
|
|
20
|
+
const image = new Image()
|
|
21
|
+
image.onload = () => {
|
|
22
|
+
// 이미지 데이터를 캔버스로 변환하여 픽셀 데이터 접근
|
|
23
|
+
const canvas = document.createElement('canvas')
|
|
24
|
+
const width = image.width
|
|
25
|
+
const height = image.height
|
|
26
|
+
canvas.width = width
|
|
27
|
+
canvas.height = height
|
|
28
|
+
|
|
29
|
+
const context = canvas.getContext('2d')!
|
|
30
|
+
context.drawImage(image, 0, 0)
|
|
31
|
+
const data = context.getImageData(0, 0, width, height).data
|
|
32
|
+
resolve({ data, width, height })
|
|
33
|
+
}
|
|
34
|
+
image.onerror = () => {
|
|
35
|
+
reject(new Error('바람 데이터 이미지 로드 실패'))
|
|
36
|
+
}
|
|
37
|
+
image.src = '/assets/images/wind.png' // 데이터 소스 경로
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 바이리니어 보간법을 사용하여 픽셀 값 계산
|
|
42
|
+
*/
|
|
43
|
+
function bilinearInterpolation(
|
|
44
|
+
xAlong: number,
|
|
45
|
+
yAlong: number,
|
|
46
|
+
v11: number,
|
|
47
|
+
v21: number,
|
|
48
|
+
v12: number,
|
|
49
|
+
v22: number
|
|
50
|
+
): number {
|
|
51
|
+
const q11 = (1 - xAlong) * (1 - yAlong) * v11
|
|
52
|
+
const q21 = xAlong * (1 - yAlong) * v21
|
|
53
|
+
const q12 = (1 - xAlong) * yAlong * v12
|
|
54
|
+
const q22 = xAlong * yAlong * v22
|
|
55
|
+
return q11 + q21 + q12 + q22
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 픽셀 배열에 대한 보간법 적용
|
|
60
|
+
*/
|
|
61
|
+
function interpolatePixels(
|
|
62
|
+
xAlong: number,
|
|
63
|
+
yAlong: number,
|
|
64
|
+
p11: number[],
|
|
65
|
+
p21: number[],
|
|
66
|
+
p12: number[],
|
|
67
|
+
p22: number[]
|
|
68
|
+
): number[] {
|
|
69
|
+
return p11.map((_, i) => bilinearInterpolation(xAlong, yAlong, p11[i], p21[i], p12[i], p22[i]))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 타일 그리드 및 투영 설정
|
|
73
|
+
const dataTileGrid = createXYZ()
|
|
74
|
+
const dataTileSize = 256
|
|
75
|
+
|
|
76
|
+
const inputImageProjection = getProjection('EPSG:4326')!
|
|
77
|
+
const dataTileProjection = getProjection('EPSG:3857')!
|
|
78
|
+
|
|
79
|
+
const inputBands = 4 // RGBA
|
|
80
|
+
const dataBands = 3 // u, v, magnitude
|
|
81
|
+
|
|
82
|
+
// 바람 속도 범위 (원본 이미지 값 기준)
|
|
83
|
+
const minU = -21.32
|
|
84
|
+
const maxU = 26.8
|
|
85
|
+
const deltaU = maxU - minU
|
|
86
|
+
const minV = -21.57
|
|
87
|
+
const maxV = 21.42
|
|
88
|
+
const deltaV = maxV - minV
|
|
89
|
+
|
|
90
|
+
// 바람 속도에 따른 색상 매핑 (속도, [R, G, B, A])
|
|
91
|
+
const windColorMap: Array<[number, [number, number, number, number]]> = [
|
|
92
|
+
[0, [0, 50, 170, 100]], // 약한 바람 (파란색)
|
|
93
|
+
[5, [0, 100, 220, 150]], // 중약한 바람
|
|
94
|
+
[10, [0, 150, 200, 180]], // 중간 바람 (청록색)
|
|
95
|
+
[15, [0, 200, 150, 200]], // 중강한 바람 (녹색)
|
|
96
|
+
[20, [150, 250, 0, 220]], // 강한 바람 (노란색)
|
|
97
|
+
[25, [200, 150, 0, 230]], // 매우 강한 바람 (주황색)
|
|
98
|
+
[30, [250, 50, 0, 250]] // 극심한 바람 (빨간색)
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
// --- 레이어 클래스 구현 ---
|
|
102
|
+
|
|
103
|
+
export class WindDataTileLayer {
|
|
104
|
+
private source: DataTileSource
|
|
105
|
+
private layer: Layer | null = null // 실제 OpenLayers 레이어
|
|
106
|
+
private map: Map | null = null
|
|
107
|
+
private useEnhancedColors: boolean = true // 향상된 색상 매핑 사용 여부
|
|
108
|
+
private __disposed: boolean = false // 레이어가 폐기되었는지 여부를 추적하기 위한 플래그
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 바람 데이터 타일 레이어 생성
|
|
112
|
+
* @param options 옵션 객체
|
|
113
|
+
*/
|
|
114
|
+
constructor(options: { useEnhancedColors?: boolean } = {}) {
|
|
115
|
+
// 향상된 색상 매핑 옵션 설정
|
|
116
|
+
if (options.useEnhancedColors !== undefined) {
|
|
117
|
+
this.useEnhancedColors = options.useEnhancedColors
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// DataTileSource 생성
|
|
121
|
+
this.source = new DataTileSource({
|
|
122
|
+
// transition must be 0 for DataTileSource, see https://github.com/openlayers/openlayers/issues/16119
|
|
123
|
+
transition: 0,
|
|
124
|
+
wrapX: true,
|
|
125
|
+
loader: this._tileLoader.bind(this)
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 타일 데이터 로딩 함수
|
|
131
|
+
* @private
|
|
132
|
+
*/
|
|
133
|
+
private async _tileLoader(z: number, x: number, y: number): Promise<Float32Array> {
|
|
134
|
+
// 타일 경계 체크: 요청된 타일이 유효한 범위를 벗어나면 빈 데이터 반환
|
|
135
|
+
// z=0일 때 x와 y는 모두 0이어야 함
|
|
136
|
+
// z=1일 때 x와 y는 0 또는 1
|
|
137
|
+
// z=2일 때 x와 y는 0, 1, 2, 3 중 하나
|
|
138
|
+
// 즉, z에 대해 x와 y는 0부터 2^z-1 사이의 값이어야 함
|
|
139
|
+
const maxTileCoord = Math.pow(2, z) - 1
|
|
140
|
+
if (x < 0 || y < 0 || x > maxTileCoord || y > maxTileCoord) {
|
|
141
|
+
console.warn(`🚨 타일 범위 초과: z=${z}, x=${x}, y=${y}, 최대값=${maxTileCoord}`)
|
|
142
|
+
return new Float32Array(dataTileSize * dataTileSize * dataBands)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
// 폐기된 레이어의 경우 빈 데이터 반환
|
|
147
|
+
if (this.__disposed) {
|
|
148
|
+
return new Float32Array(dataTileSize * dataTileSize * dataBands)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { data: inputData, width: inputWidth, height: inputHeight } = await windData
|
|
152
|
+
|
|
153
|
+
// wrapX를 통해 날짜변경선 처리
|
|
154
|
+
const tileCoord = wrapX(dataTileGrid, [z, x, y], dataTileProjection)
|
|
155
|
+
const extent = dataTileGrid.getTileCoordExtent(tileCoord)
|
|
156
|
+
const resolution = dataTileGrid.getResolution(z)
|
|
157
|
+
|
|
158
|
+
// 타일 데이터 배열 (Float32Array 사용)
|
|
159
|
+
const data = new Float32Array(dataTileSize * dataTileSize * dataBands)
|
|
160
|
+
|
|
161
|
+
for (let row = 0; row < dataTileSize; ++row) {
|
|
162
|
+
let offset = row * dataTileSize * dataBands
|
|
163
|
+
const mapY = extent[3] - row * resolution
|
|
164
|
+
|
|
165
|
+
for (let col = 0; col < dataTileSize; ++col) {
|
|
166
|
+
const mapX = extent[0] + col * resolution
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
// 현재 픽셀의 좌표를 EPSG:4326으로 변환
|
|
170
|
+
const [lon, lat] = transform([mapX, mapY], dataTileProjection, inputImageProjection)
|
|
171
|
+
|
|
172
|
+
// 유효한 좌표 범위 확인 (-180 ~ 180, -90 ~ 90)
|
|
173
|
+
if (lon < -180 || lon > 180 || lat < -90 || lat > 90) {
|
|
174
|
+
// 범위를 벗어나는 좌표는 기본값으로 처리
|
|
175
|
+
data[offset] = 0 // u (동서 방향 바람)
|
|
176
|
+
data[offset + 1] = 0 // v (남북 방향 바람)
|
|
177
|
+
data[offset + 2] = 0 // magnitude (바람 세기)
|
|
178
|
+
offset += dataBands
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 원본 이미지에서의 상대적 위치 계산
|
|
183
|
+
const x = (inputWidth * (lon + 180)) / 360
|
|
184
|
+
let x1 = Math.floor(x)
|
|
185
|
+
let x2 = Math.ceil(x)
|
|
186
|
+
const xAlong = x - x1
|
|
187
|
+
|
|
188
|
+
// 경도 랩핑 처리 (날짜변경선)
|
|
189
|
+
if (x1 < 0) {
|
|
190
|
+
x1 += inputWidth
|
|
191
|
+
}
|
|
192
|
+
if (x2 >= inputWidth) {
|
|
193
|
+
x2 -= inputWidth
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const y = (inputHeight * (90 - lat)) / 180
|
|
197
|
+
let y1 = Math.floor(y)
|
|
198
|
+
let y2 = Math.ceil(y)
|
|
199
|
+
const yAlong = y - y1
|
|
200
|
+
|
|
201
|
+
// 위도 범위 제한
|
|
202
|
+
if (y1 < 0) {
|
|
203
|
+
y1 = 0
|
|
204
|
+
}
|
|
205
|
+
if (y2 >= inputHeight) {
|
|
206
|
+
y2 = inputHeight - 1
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const corners = [
|
|
210
|
+
[x1, y1],
|
|
211
|
+
[x2, y1],
|
|
212
|
+
[x1, y2],
|
|
213
|
+
[x2, y2]
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
// 주변 4개 픽셀의 u, v 값 읽기
|
|
217
|
+
const pixels = corners.map(([cx, cy]) => {
|
|
218
|
+
// 유효한 인덱스 확인
|
|
219
|
+
if (cx < 0 || cx >= inputWidth || cy < 0 || cy >= inputHeight) {
|
|
220
|
+
return [0, 0] // 잘못된 좌표는 기본값 사용
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const inputOffset = (cy * inputWidth + cx) * inputBands
|
|
224
|
+
// 원본 PNG 데이터에서 u, v 값 추출 (R 채널=u, G 채널=v)
|
|
225
|
+
return [inputData[inputOffset], inputData[inputOffset + 1]]
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// u, v 값 보간
|
|
229
|
+
const interpolated = interpolatePixels(xAlong, yAlong, ...(pixels as [any, any, any, any]))
|
|
230
|
+
|
|
231
|
+
// 실제 u, v 값으로 변환
|
|
232
|
+
const u = minU + (deltaU * interpolated[0]) / 255
|
|
233
|
+
const v = minV + (deltaV * interpolated[1]) / 255
|
|
234
|
+
|
|
235
|
+
// 바람 속도 계산 (옵션)
|
|
236
|
+
const speed = Math.sqrt(u * u + v * v)
|
|
237
|
+
|
|
238
|
+
// 타일 데이터에 저장
|
|
239
|
+
data[offset] = u
|
|
240
|
+
data[offset + 1] = v
|
|
241
|
+
data[offset + 2] = speed // 속도 값 저장 (셰이더에서 활용 가능)
|
|
242
|
+
} catch (pixelError) {
|
|
243
|
+
// 개별 픽셀 처리 중 오류 발생 시 해당 픽셀만 기본값으로 처리
|
|
244
|
+
data[offset] = 0 // u
|
|
245
|
+
data[offset + 1] = 0 // v
|
|
246
|
+
data[offset + 2] = 0 // magnitude
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
offset += dataBands
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return data // Float32Array 반환
|
|
254
|
+
} catch (error) {
|
|
255
|
+
// 타일 로드 실패 로그
|
|
256
|
+
console.warn(`🚨 타일 없음: z=${z}, x=${x}, y=${y}`, error)
|
|
257
|
+
|
|
258
|
+
// 오류 발생 시 빈 타일 데이터 반환
|
|
259
|
+
return new Float32Array(dataTileSize * dataTileSize * dataBands)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 레이어 타입 반환 (호환성용)
|
|
265
|
+
*/
|
|
266
|
+
getType(): WeatherLayerType {
|
|
267
|
+
return 'wind'
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 향상된 색상 매핑 사용 여부 설정
|
|
272
|
+
*/
|
|
273
|
+
setUseEnhancedColors(use: boolean): void {
|
|
274
|
+
this.useEnhancedColors = use
|
|
275
|
+
this.updateLayer() // 변경사항 적용을 위해 레이어 업데이트
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* OpenLayers 레이어 객체 생성 또는 반환
|
|
280
|
+
*/
|
|
281
|
+
createLayer(): BaseLayer {
|
|
282
|
+
if (this.__disposed) {
|
|
283
|
+
console.warn('폐기된 레이어에서 createLayer가 호출되었습니다')
|
|
284
|
+
return new Layer({}) // 빈 레이어 반환
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!this.layer) {
|
|
288
|
+
// 렌더링 파이프라인과의 충돌을 최소화하기 위해 간단한 설정으로 레이어 생성
|
|
289
|
+
this.layer = new Layer({
|
|
290
|
+
source: this.source,
|
|
291
|
+
properties: {
|
|
292
|
+
useEnhancedColors: this.useEnhancedColors,
|
|
293
|
+
// 렌더링 시 이 속성을 확인하도록 구현
|
|
294
|
+
__windLayerDisposed: false
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
return this.layer
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 요청 중인 데이터 로드 중단 및 소스 무효화
|
|
303
|
+
* @private
|
|
304
|
+
*/
|
|
305
|
+
private _clearSourceData(): void {
|
|
306
|
+
if (!this.source) return
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
// 소스에서 로드된 타일 데이터 초기화
|
|
310
|
+
if (typeof this.source.clear === 'function') {
|
|
311
|
+
this.source.clear() // 타일 캐시 정리
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 더 이상 데이터를 제공하지 않도록 소스 무효화
|
|
315
|
+
// 소스의 내부 속성에 접근 (타입 어설션 사용)
|
|
316
|
+
const sourceAny = this.source as any
|
|
317
|
+
if (sourceAny.loader) {
|
|
318
|
+
// 로더 함수를 빈 데이터를 반환하는 함수로 재정의
|
|
319
|
+
sourceAny.loader = (_z: number, _x: number, _y: number) => {
|
|
320
|
+
// 빈 데이터 반환
|
|
321
|
+
return Promise.resolve(new Float32Array(dataTileSize * dataTileSize * dataBands))
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch (e) {
|
|
325
|
+
console.warn('소스 정리 중 오류:', e)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 레이어를 맵에 추가
|
|
331
|
+
*/
|
|
332
|
+
addToMap(map: Map): void {
|
|
333
|
+
if (this.__disposed) {
|
|
334
|
+
console.warn('폐기된 레이어는 맵에 추가할 수 없습니다')
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!this.map) {
|
|
339
|
+
this.map = map
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!this.layer) {
|
|
343
|
+
this.createLayer()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (this.layer && this.map) {
|
|
347
|
+
this.map.addLayer(this.layer)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* 레이어를 맵에서 제거
|
|
353
|
+
*/
|
|
354
|
+
removeFromMap(): void {
|
|
355
|
+
if (!this.layer || !this.map || this.__disposed) {
|
|
356
|
+
return // 이미 제거되었거나 맵에 추가되지 않은 경우
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
// OpenLayers 렌더링 문제 방지를 위한 사전 처리
|
|
361
|
+
this.__disposed = true // 먼저 플래그 설정
|
|
362
|
+
|
|
363
|
+
// 1. 레이어가 렌더링 파이프라인에서 무시되도록 설정
|
|
364
|
+
this.layer.setVisible(false) // 시각적 숨김
|
|
365
|
+
this.layer.set('__windLayerDisposed', true) // 내부 플래그 설정
|
|
366
|
+
this.layer.changed() // 렌더링 파이프라인에 변경 알림
|
|
367
|
+
|
|
368
|
+
// 2. 요청 중인 모든 데이터 로드 중단
|
|
369
|
+
this._clearSourceData()
|
|
370
|
+
|
|
371
|
+
// 3. 연결된 맵과 소스에서 레이어 분리
|
|
372
|
+
try {
|
|
373
|
+
if (this.map && this.layer) {
|
|
374
|
+
// 맵에서 레이어 제거
|
|
375
|
+
this.map.removeLayer(this.layer)
|
|
376
|
+
}
|
|
377
|
+
} catch (e) {
|
|
378
|
+
console.error('맵에서 레이어 제거 중 오류:', e)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 4. 레이어 참조 정리
|
|
382
|
+
this.map = null
|
|
383
|
+
} catch (e) {
|
|
384
|
+
console.error('레이어 제거 중 오류:', e)
|
|
385
|
+
|
|
386
|
+
// 오류 발생 시에도 맵에서 레이어 제거 시도
|
|
387
|
+
try {
|
|
388
|
+
if (this.map && this.layer) {
|
|
389
|
+
this.map.removeLayer(this.layer)
|
|
390
|
+
}
|
|
391
|
+
} catch (e2) {
|
|
392
|
+
// 무시
|
|
393
|
+
}
|
|
394
|
+
this.map = null
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* 레이어 관련 리소스 해제
|
|
400
|
+
*/
|
|
401
|
+
dispose(): void {
|
|
402
|
+
if (this.__disposed) {
|
|
403
|
+
return // 이미 폐기된 경우
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
this.__disposed = true // 폐기 플래그 설정
|
|
407
|
+
|
|
408
|
+
// 맵에서 제거 (아직 제거되지 않은 경우)
|
|
409
|
+
if (this.map && this.layer) {
|
|
410
|
+
try {
|
|
411
|
+
// 렌더링 에러 방지를 위해 먼저 레이어 숨김 및 상태 설정
|
|
412
|
+
this.layer.setVisible(false)
|
|
413
|
+
this.layer.set('__windLayerDisposed', true)
|
|
414
|
+
this.layer.changed()
|
|
415
|
+
|
|
416
|
+
// 맵에서 레이어 제거
|
|
417
|
+
this.map.removeLayer(this.layer)
|
|
418
|
+
} catch (e) {
|
|
419
|
+
console.error('맵에서 레이어 제거 중 오류:', e)
|
|
420
|
+
} finally {
|
|
421
|
+
this.map = null // 맵 참조 해제
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 나머지 리소스 정리
|
|
426
|
+
try {
|
|
427
|
+
// 레이어 정리
|
|
428
|
+
if (this.layer) {
|
|
429
|
+
try {
|
|
430
|
+
// 레이어에서 소스 분리
|
|
431
|
+
if (typeof this.layer.setSource === 'function') {
|
|
432
|
+
this.layer.setSource(null)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// 레이어 객체 정리
|
|
436
|
+
if (typeof this.layer.dispose === 'function') {
|
|
437
|
+
this.layer.dispose()
|
|
438
|
+
}
|
|
439
|
+
} catch (e) {
|
|
440
|
+
console.warn('레이어 정리 중 오류:', e)
|
|
441
|
+
}
|
|
442
|
+
this.layer = null
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 소스 정리
|
|
446
|
+
if (this.source) {
|
|
447
|
+
try {
|
|
448
|
+
// 소스 정리
|
|
449
|
+
if (typeof this.source.dispose === 'function') {
|
|
450
|
+
this.source.dispose()
|
|
451
|
+
}
|
|
452
|
+
} catch (e) {
|
|
453
|
+
console.warn('소스 정리 중 오류:', e)
|
|
454
|
+
}
|
|
455
|
+
this.source = null as any
|
|
456
|
+
}
|
|
457
|
+
} catch (e) {
|
|
458
|
+
console.error('리소스 정리 중 오류:', e)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* 레이어 업데이트 요청 (데이터 변경 시)
|
|
464
|
+
*/
|
|
465
|
+
updateLayer(): void {
|
|
466
|
+
if (this.source) {
|
|
467
|
+
this.source.changed() // 타일 다시 로드 트리거
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 속성 업데이트 (향상된 색상 매핑 옵션)
|
|
471
|
+
if (this.layer) {
|
|
472
|
+
this.layer.set('useEnhancedColors', this.useEnhancedColors)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* 데이터 설정 메서드 (호환성용, 실제 데이터는 PNG 이미지에 고정됨)
|
|
478
|
+
*/
|
|
479
|
+
setWeatherData(data: any): void {
|
|
480
|
+
// 이 구현에서는 외부 데이터를 직접 설정하지 않음
|
|
481
|
+
// 필요하다면 updateLayer 호출 또는 다른 로직 구현 가능
|
|
482
|
+
console.warn('setWeatherData called on WindDataTileLayer, but data is static.')
|
|
483
|
+
this.updateLayer() // 데이터가 변경된 것처럼 타일 갱신 시도
|
|
484
|
+
}
|
|
485
|
+
}
|