@operato/weather 9.0.1 → 9.0.34
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 +17 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/layers/base-weather-layer.d.ts +56 -0
- package/dist/src/layers/base-weather-layer.js +83 -0
- package/dist/src/layers/base-weather-layer.js.map +1 -0
- package/dist/src/layers/cloud-layer.d.ts +17 -0
- package/dist/src/layers/cloud-layer.js +203 -0
- package/dist/src/layers/cloud-layer.js.map +1 -0
- package/dist/src/layers/precipitation-layer.d.ts +27 -0
- package/dist/src/layers/precipitation-layer.js +344 -0
- package/dist/src/layers/precipitation-layer.js.map +1 -0
- package/dist/src/layers/solar-layer.d.ts +17 -0
- package/dist/src/layers/solar-layer.js +253 -0
- package/dist/src/layers/solar-layer.js.map +1 -0
- package/dist/src/layers/temperature-layer.d.ts +13 -0
- package/dist/src/layers/temperature-layer.js +230 -0
- package/dist/src/layers/temperature-layer.js.map +1 -0
- package/dist/src/layers/wind-layer.d.ts +60 -0
- package/dist/src/layers/wind-layer.js +311 -0
- package/dist/src/layers/wind-layer.js.map +1 -0
- package/dist/src/ox-weather.d.ts +112 -0
- package/dist/src/ox-weather.js +645 -0
- package/dist/src/ox-weather.js.map +1 -0
- package/dist/src/ox-wind.d.ts +91 -0
- package/dist/src/ox-wind.js +425 -0
- package/dist/src/ox-wind.js.map +1 -0
- package/dist/src/services/weather-service.d.ts +79 -0
- package/dist/src/services/weather-service.js +560 -0
- package/dist/src/services/weather-service.js.map +1 -0
- package/dist/src/types.d.ts +71 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
### [9.0.34](https://github.com/hatiolab/operato/compare/v9.0.33...v9.0.34) (2025-09-15)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @operato/weather
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### [9.0.2](https://github.com/hatiolab/operato/compare/v9.0.1...v9.0.2) (2025-07-01)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### :bug: Bug Fix
|
|
18
|
+
|
|
19
|
+
* invalid package configuration ([ccde20b](https://github.com/hatiolab/operato/commit/ccde20b75e6cbd1f881e5bac631e361aebfd21c3))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
6
23
|
### [9.0.1](https://github.com/hatiolab/operato/compare/v9.0.0...v9.0.1) (2025-07-01)
|
|
7
24
|
|
|
8
25
|
**Note:** Version bump only for package @operato/weather
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ox-weather.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA","sourcesContent":["export * from './ox-weather.js'\n"]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import BaseLayer from 'ol/layer/Base.js';
|
|
2
|
+
import { Map } from 'ol';
|
|
3
|
+
import { WeatherData, WeatherLayerType } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* 날씨 레이어의 기본 클래스
|
|
6
|
+
* 모든 레이어 타입은 이 클래스를 상속합니다.
|
|
7
|
+
*/
|
|
8
|
+
export declare abstract class BaseWeatherLayer {
|
|
9
|
+
protected map?: Map;
|
|
10
|
+
protected layer?: BaseLayer;
|
|
11
|
+
protected weatherData: WeatherData[];
|
|
12
|
+
/**
|
|
13
|
+
* 레이어의 타입을 반환합니다.
|
|
14
|
+
*/
|
|
15
|
+
abstract getType(): WeatherLayerType;
|
|
16
|
+
/**
|
|
17
|
+
* 레이어를 생성합니다. 이 메서드는 각 서브클래스에서 구현해야 합니다.
|
|
18
|
+
*/
|
|
19
|
+
abstract createLayer(): BaseLayer;
|
|
20
|
+
/**
|
|
21
|
+
* 맵에 레이어를 추가합니다.
|
|
22
|
+
*/
|
|
23
|
+
addToMap(map: Map): BaseLayer;
|
|
24
|
+
/**
|
|
25
|
+
* 맵에서 레이어를 제거합니다.
|
|
26
|
+
*/
|
|
27
|
+
removeFromMap(): void;
|
|
28
|
+
/**
|
|
29
|
+
* 레이어에 날씨 데이터를 설정합니다.
|
|
30
|
+
*/
|
|
31
|
+
setWeatherData(data: WeatherData[]): void;
|
|
32
|
+
/**
|
|
33
|
+
* 레이어를 업데이트합니다. 데이터가 변경된 경우 호출됩니다.
|
|
34
|
+
*/
|
|
35
|
+
protected updateLayer(): void;
|
|
36
|
+
/**
|
|
37
|
+
* 레이어 인스턴스를 반환합니다.
|
|
38
|
+
*/
|
|
39
|
+
getLayer(): BaseLayer | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* 레이어의 가시성을 설정합니다.
|
|
42
|
+
*/
|
|
43
|
+
setVisible(visible: boolean): void;
|
|
44
|
+
/**
|
|
45
|
+
* 레이어의 가시성 상태를 반환합니다.
|
|
46
|
+
*/
|
|
47
|
+
isVisible(): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* 애니메이션을 시작합니다. 애니메이션이 필요한 레이어에서 오버라이드합니다.
|
|
50
|
+
*/
|
|
51
|
+
startAnimation(): void;
|
|
52
|
+
/**
|
|
53
|
+
* 애니메이션을 중지합니다. 애니메이션이 필요한 레이어에서 오버라이드합니다.
|
|
54
|
+
*/
|
|
55
|
+
stopAnimation(): void;
|
|
56
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 날씨 레이어의 기본 클래스
|
|
3
|
+
* 모든 레이어 타입은 이 클래스를 상속합니다.
|
|
4
|
+
*/
|
|
5
|
+
export class BaseWeatherLayer {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.weatherData = [];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 맵에 레이어를 추가합니다.
|
|
11
|
+
*/
|
|
12
|
+
addToMap(map) {
|
|
13
|
+
this.map = map;
|
|
14
|
+
if (!this.layer) {
|
|
15
|
+
this.layer = this.createLayer();
|
|
16
|
+
}
|
|
17
|
+
this.map.addLayer(this.layer);
|
|
18
|
+
return this.layer;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 맵에서 레이어를 제거합니다.
|
|
22
|
+
*/
|
|
23
|
+
removeFromMap() {
|
|
24
|
+
if (this.map && this.layer) {
|
|
25
|
+
this.map.removeLayer(this.layer);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 레이어에 날씨 데이터를 설정합니다.
|
|
30
|
+
*/
|
|
31
|
+
setWeatherData(data) {
|
|
32
|
+
this.weatherData = data;
|
|
33
|
+
this.updateLayer();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 레이어를 업데이트합니다. 데이터가 변경된 경우 호출됩니다.
|
|
37
|
+
*/
|
|
38
|
+
updateLayer() {
|
|
39
|
+
if (this.layer) {
|
|
40
|
+
this.removeFromMap();
|
|
41
|
+
this.layer = this.createLayer();
|
|
42
|
+
if (this.map) {
|
|
43
|
+
this.map.addLayer(this.layer);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 레이어 인스턴스를 반환합니다.
|
|
49
|
+
*/
|
|
50
|
+
getLayer() {
|
|
51
|
+
return this.layer;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 레이어의 가시성을 설정합니다.
|
|
55
|
+
*/
|
|
56
|
+
setVisible(visible) {
|
|
57
|
+
if (this.layer) {
|
|
58
|
+
this.layer.setVisible(visible);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 레이어의 가시성 상태를 반환합니다.
|
|
63
|
+
*/
|
|
64
|
+
isVisible() {
|
|
65
|
+
if (this.layer) {
|
|
66
|
+
return this.layer.getVisible();
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 애니메이션을 시작합니다. 애니메이션이 필요한 레이어에서 오버라이드합니다.
|
|
72
|
+
*/
|
|
73
|
+
startAnimation() {
|
|
74
|
+
// 기본 구현은 비어있음
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 애니메이션을 중지합니다. 애니메이션이 필요한 레이어에서 오버라이드합니다.
|
|
78
|
+
*/
|
|
79
|
+
stopAnimation() {
|
|
80
|
+
// 기본 구현은 비어있음
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=base-weather-layer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-weather-layer.js","sourceRoot":"","sources":["../../../src/layers/base-weather-layer.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,MAAM,OAAgB,gBAAgB;IAAtC;QAGY,gBAAW,GAAkB,EAAE,CAAA;IA8F3C,CAAC;IAlFC;;OAEG;IACH,QAAQ,CAAC,GAAQ;QACf,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QACd,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAA;QACjC,CAAC;QAED,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC7B,OAAO,IAAI,CAAC,KAAK,CAAA;IACnB,CAAC;IAED;;OAEG;IACH,aAAa;QACX,IAAI,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC3B,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAClC,CAAC;IACH,CAAC;IAED;;OAEG;IACH,cAAc,CAAC,IAAmB;QAChC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;QACvB,IAAI,CAAC,WAAW,EAAE,CAAA;IACpB,CAAC;IAED;;OAEG;IACO,WAAW;QACnB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,aAAa,EAAE,CAAA;YACpB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAA;YAC/B,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;gBACb,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YAC/B,CAAC;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACH,QAAQ;QACN,OAAO,IAAI,CAAC,KAAK,CAAA;IACnB,CAAC;IAED;;OAEG;IACH,UAAU,CAAC,OAAgB;QACzB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;QAChC,CAAC;IACH,CAAC;IAED;;OAEG;IACH,SAAS;QACP,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAA;QAChC,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IAED;;OAEG;IACH,cAAc;QACZ,cAAc;IAChB,CAAC;IAED;;OAEG;IACH,aAAa;QACX,cAAc;IAChB,CAAC;CACF","sourcesContent":["import BaseLayer from 'ol/layer/Base.js'\nimport { Map } from 'ol'\nimport { WeatherData, WeatherLayerType } from '../types.js'\n\n/**\n * 날씨 레이어의 기본 클래스\n * 모든 레이어 타입은 이 클래스를 상속합니다.\n */\nexport abstract class BaseWeatherLayer {\n protected map?: Map\n protected layer?: BaseLayer\n protected weatherData: WeatherData[] = []\n\n /**\n * 레이어의 타입을 반환합니다.\n */\n abstract getType(): WeatherLayerType\n\n /**\n * 레이어를 생성합니다. 이 메서드는 각 서브클래스에서 구현해야 합니다.\n */\n abstract createLayer(): BaseLayer\n\n /**\n * 맵에 레이어를 추가합니다.\n */\n addToMap(map: Map) {\n this.map = map\n if (!this.layer) {\n this.layer = this.createLayer()\n }\n\n this.map.addLayer(this.layer)\n return this.layer\n }\n\n /**\n * 맵에서 레이어를 제거합니다.\n */\n removeFromMap() {\n if (this.map && this.layer) {\n this.map.removeLayer(this.layer)\n }\n }\n\n /**\n * 레이어에 날씨 데이터를 설정합니다.\n */\n setWeatherData(data: WeatherData[]) {\n this.weatherData = data\n this.updateLayer()\n }\n\n /**\n * 레이어를 업데이트합니다. 데이터가 변경된 경우 호출됩니다.\n */\n protected updateLayer() {\n if (this.layer) {\n this.removeFromMap()\n this.layer = this.createLayer()\n if (this.map) {\n this.map.addLayer(this.layer)\n }\n }\n }\n\n /**\n * 레이어 인스턴스를 반환합니다.\n */\n getLayer(): BaseLayer | undefined {\n return this.layer\n }\n\n /**\n * 레이어의 가시성을 설정합니다.\n */\n setVisible(visible: boolean) {\n if (this.layer) {\n this.layer.setVisible(visible)\n }\n }\n\n /**\n * 레이어의 가시성 상태를 반환합니다.\n */\n isVisible(): boolean {\n if (this.layer) {\n return this.layer.getVisible()\n }\n return false\n }\n\n /**\n * 애니메이션을 시작합니다. 애니메이션이 필요한 레이어에서 오버라이드합니다.\n */\n startAnimation() {\n // 기본 구현은 비어있음\n }\n\n /**\n * 애니메이션을 중지합니다. 애니메이션이 필요한 레이어에서 오버라이드합니다.\n */\n stopAnimation() {\n // 기본 구현은 비어있음\n }\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import BaseLayer from 'ol/layer/Base';
|
|
2
|
+
import { BaseWeatherLayer } from './base-weather-layer.js';
|
|
3
|
+
import { WeatherData, WeatherLayerType } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* 구름 레이어 구현
|
|
6
|
+
*/
|
|
7
|
+
export declare class CloudLayer extends BaseWeatherLayer {
|
|
8
|
+
getType(): WeatherLayerType;
|
|
9
|
+
/**
|
|
10
|
+
* 벡터 레이어를 생성하여 구름을 표현합니다.
|
|
11
|
+
*/
|
|
12
|
+
createLayer(): BaseLayer;
|
|
13
|
+
/**
|
|
14
|
+
* 레이어에 날씨 데이터를 설정합니다.
|
|
15
|
+
*/
|
|
16
|
+
setWeatherData(data: WeatherData[]): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import VectorSource from 'ol/source/Vector';
|
|
2
|
+
import VectorLayer from 'ol/layer/Vector';
|
|
3
|
+
import Feature from 'ol/Feature';
|
|
4
|
+
import { fromLonLat } from 'ol/proj';
|
|
5
|
+
import { Style, Fill } from 'ol/style';
|
|
6
|
+
import Polygon from 'ol/geom/Polygon';
|
|
7
|
+
import { BaseWeatherLayer } from './base-weather-layer.js';
|
|
8
|
+
/**
|
|
9
|
+
* 구름 레이어 구현
|
|
10
|
+
*/
|
|
11
|
+
export class CloudLayer extends BaseWeatherLayer {
|
|
12
|
+
getType() {
|
|
13
|
+
return 'cloud';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 벡터 레이어를 생성하여 구름을 표현합니다.
|
|
17
|
+
*/
|
|
18
|
+
createLayer() {
|
|
19
|
+
const source = new VectorSource();
|
|
20
|
+
if (!this.weatherData || this.weatherData.length === 0) {
|
|
21
|
+
return new VectorLayer({
|
|
22
|
+
source,
|
|
23
|
+
zIndex: 7
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
// 각 데이터 포인트의 위치와 구름 커버리지 추출
|
|
27
|
+
const points = this.weatherData.map(data => ({
|
|
28
|
+
lon: data.location.longitude,
|
|
29
|
+
lat: data.location.latitude,
|
|
30
|
+
coverage: data.cloud.coverage / 100
|
|
31
|
+
}));
|
|
32
|
+
console.log('구름 데이터 포인트:', points.length, '첫 번째 포인트:', points[0]);
|
|
33
|
+
// 개선된 구름 커버리지 보간 함수 - IDW(Inverse Distance Weighting) 기반 보간법
|
|
34
|
+
const interpolateCoverage = (lon, lat) => {
|
|
35
|
+
// 가장 가까운 이웃 개수 제한 - 모든 포인트 대신 가까운 포인트만 사용하여 국지적 패턴 유지
|
|
36
|
+
const MAX_NEIGHBORS = 5;
|
|
37
|
+
// 현재 위치에서 모든 포인트까지의 거리 계산
|
|
38
|
+
const distances = points.map(point => {
|
|
39
|
+
const dx = lon - point.lon;
|
|
40
|
+
const dy = lat - point.lat;
|
|
41
|
+
return {
|
|
42
|
+
point,
|
|
43
|
+
dist: Math.sqrt(dx * dx + dy * dy)
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
// 거리에 따라 정렬하고 가장 가까운 이웃만 선택
|
|
47
|
+
const nearestNeighbors = distances.sort((a, b) => a.dist - b.dist).slice(0, MAX_NEIGHBORS);
|
|
48
|
+
// 포인트가 정확히 일치하는 경우
|
|
49
|
+
if (nearestNeighbors.length > 0 && nearestNeighbors[0].dist < 0.0001) {
|
|
50
|
+
return nearestNeighbors[0].point.coverage;
|
|
51
|
+
}
|
|
52
|
+
// 역거리 가중치 보간법 적용 - 거리의 제곱에 반비례하는 가중치 사용
|
|
53
|
+
let totalWeight = 0;
|
|
54
|
+
let weightedCoverage = 0;
|
|
55
|
+
// 가중치 지수 - 낮을수록 더 부드러운 보간, 높을수록 가까운 값에 더 큰 영향
|
|
56
|
+
const power = 1.5;
|
|
57
|
+
nearestNeighbors.forEach(({ point, dist }) => {
|
|
58
|
+
// 작은 값으로 나누기 방지
|
|
59
|
+
const adjustedDist = Math.max(dist, 0.0001);
|
|
60
|
+
const weight = 1 / Math.pow(adjustedDist, power);
|
|
61
|
+
totalWeight += weight;
|
|
62
|
+
weightedCoverage += point.coverage * weight;
|
|
63
|
+
});
|
|
64
|
+
return weightedCoverage / totalWeight;
|
|
65
|
+
};
|
|
66
|
+
// 구름 커버리지에 따른 색상 계산 함수
|
|
67
|
+
const getColorFromCoverage = (coverage) => {
|
|
68
|
+
// 흰색에서 짙은 회색으로 그라데이션 (구름 농도가 짙을수록 더 어두워짐)
|
|
69
|
+
const intensity = Math.min(1, Math.max(0, coverage));
|
|
70
|
+
// 더 어두운 회색으로 조정
|
|
71
|
+
const brightness = Math.round(255 - intensity * 130);
|
|
72
|
+
// 구름 농도에 따라 투명도 조정 - 옅은 구름은 더 투명하게
|
|
73
|
+
const alpha = 0.2 + intensity * 0.6;
|
|
74
|
+
return `rgba(${brightness}, ${brightness}, ${brightness}, ${alpha})`;
|
|
75
|
+
};
|
|
76
|
+
// 그라데이션을 적용할 그리드 생성
|
|
77
|
+
const bounds = {
|
|
78
|
+
minLon: Infinity,
|
|
79
|
+
maxLon: -Infinity,
|
|
80
|
+
minLat: Infinity,
|
|
81
|
+
maxLat: -Infinity
|
|
82
|
+
};
|
|
83
|
+
// 데이터 포인트로부터 영역 경계 계산
|
|
84
|
+
points.forEach(point => {
|
|
85
|
+
bounds.minLon = Math.min(bounds.minLon, point.lon);
|
|
86
|
+
bounds.maxLon = Math.max(bounds.maxLon, point.lon);
|
|
87
|
+
bounds.minLat = Math.min(bounds.minLat, point.lat);
|
|
88
|
+
bounds.maxLat = Math.max(bounds.maxLat, point.lat);
|
|
89
|
+
});
|
|
90
|
+
// 경계에 마진 추가
|
|
91
|
+
const margin = 0.1;
|
|
92
|
+
bounds.minLon -= margin;
|
|
93
|
+
bounds.maxLon += margin;
|
|
94
|
+
bounds.minLat -= margin;
|
|
95
|
+
bounds.maxLat += margin;
|
|
96
|
+
// 고해상도 그리드 생성 (더 매끄러운 그라데이션을 위해)
|
|
97
|
+
const gridResolution = 80; // 적절한 해상도로 조정
|
|
98
|
+
const lonStep = (bounds.maxLon - bounds.minLon) / gridResolution;
|
|
99
|
+
const latStep = (bounds.maxLat - bounds.minLat) / gridResolution;
|
|
100
|
+
// 인접 셀 간의 값 차이 최소화를 위한 스무딩 함수
|
|
101
|
+
const smoothGrid = (grid, passes = 2) => {
|
|
102
|
+
const rows = grid.length;
|
|
103
|
+
if (rows === 0)
|
|
104
|
+
return grid;
|
|
105
|
+
const cols = grid[0].length;
|
|
106
|
+
const result = Array(rows)
|
|
107
|
+
.fill(0)
|
|
108
|
+
.map(() => Array(cols).fill(0));
|
|
109
|
+
// 여러 번 스무딩 패스 적용
|
|
110
|
+
for (let pass = 0; pass < passes; pass++) {
|
|
111
|
+
const currentGrid = pass === 0 ? grid : result;
|
|
112
|
+
for (let i = 0; i < rows; i++) {
|
|
113
|
+
for (let j = 0; j < cols; j++) {
|
|
114
|
+
// 현재 셀과 인접 셀의 평균 계산
|
|
115
|
+
let sum = currentGrid[i][j];
|
|
116
|
+
let count = 1;
|
|
117
|
+
// 상하좌우 셀 확인
|
|
118
|
+
const neighbors = [
|
|
119
|
+
[i - 1, j],
|
|
120
|
+
[i + 1, j],
|
|
121
|
+
[i, j - 1],
|
|
122
|
+
[i, j + 1]
|
|
123
|
+
];
|
|
124
|
+
neighbors.forEach(([ni, nj]) => {
|
|
125
|
+
if (ni >= 0 && ni < rows && nj >= 0 && nj < cols) {
|
|
126
|
+
sum += currentGrid[ni][nj];
|
|
127
|
+
count++;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
// 평균값으로 스무딩 (약간의 원본 가중치 부여)
|
|
131
|
+
result[i][j] = currentGrid[i][j] * 0.6 + ((sum - currentGrid[i][j]) / (count - 1)) * 0.4;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
};
|
|
137
|
+
// 구름 데이터 그리드 생성
|
|
138
|
+
const coverageGrid = [];
|
|
139
|
+
for (let i = 0; i < gridResolution; i++) {
|
|
140
|
+
coverageGrid[i] = [];
|
|
141
|
+
for (let j = 0; j < gridResolution; j++) {
|
|
142
|
+
const centerLon = bounds.minLon + (j + 0.5) * lonStep;
|
|
143
|
+
const centerLat = bounds.minLat + (i + 0.5) * latStep;
|
|
144
|
+
coverageGrid[i][j] = interpolateCoverage(centerLon, centerLat);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// 그리드 스무딩 적용
|
|
148
|
+
const smoothedGrid = smoothGrid(coverageGrid, 2);
|
|
149
|
+
// 셀 생성 및 피처 추가
|
|
150
|
+
for (let i = 0; i < gridResolution; i++) {
|
|
151
|
+
for (let j = 0; j < gridResolution; j++) {
|
|
152
|
+
const lonMin = bounds.minLon + j * lonStep;
|
|
153
|
+
const lonMax = bounds.minLon + (j + 1) * lonStep;
|
|
154
|
+
const latMin = bounds.minLat + i * latStep;
|
|
155
|
+
const latMax = bounds.minLat + (i + 1) * latStep;
|
|
156
|
+
// 스무딩된 값 사용
|
|
157
|
+
const interpolatedCoverage = smoothedGrid[i][j];
|
|
158
|
+
// 너무 작은 커버리지는 제외
|
|
159
|
+
if (interpolatedCoverage < 0.01)
|
|
160
|
+
continue;
|
|
161
|
+
// 셀 좌표 계산 및 변환
|
|
162
|
+
const coordinates = [
|
|
163
|
+
[lonMin, latMin],
|
|
164
|
+
[lonMax, latMin],
|
|
165
|
+
[lonMax, latMax],
|
|
166
|
+
[lonMin, latMax],
|
|
167
|
+
[lonMin, latMin]
|
|
168
|
+
].map(coord => fromLonLat(coord));
|
|
169
|
+
// 다각형 피처 생성
|
|
170
|
+
const feature = new Feature({
|
|
171
|
+
geometry: new Polygon([coordinates]),
|
|
172
|
+
coverage: interpolatedCoverage
|
|
173
|
+
});
|
|
174
|
+
source.addFeature(feature);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// 벡터 레이어 생성
|
|
178
|
+
return new VectorLayer({
|
|
179
|
+
source,
|
|
180
|
+
zIndex: 7,
|
|
181
|
+
className: 'cloud-layer',
|
|
182
|
+
opacity: 0.9,
|
|
183
|
+
style: feature => {
|
|
184
|
+
const coverage = feature.get('coverage');
|
|
185
|
+
return new Style({
|
|
186
|
+
fill: new Fill({
|
|
187
|
+
color: getColorFromCoverage(coverage)
|
|
188
|
+
}),
|
|
189
|
+
// 경계선 완전히 제거
|
|
190
|
+
stroke: undefined
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 레이어에 날씨 데이터를 설정합니다.
|
|
197
|
+
*/
|
|
198
|
+
setWeatherData(data) {
|
|
199
|
+
this.weatherData = data;
|
|
200
|
+
this.updateLayer();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=cloud-layer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud-layer.js","sourceRoot":"","sources":["../../../src/layers/cloud-layer.ts"],"names":[],"mappings":"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","sourcesContent":["import BaseLayer from 'ol/layer/Base'\nimport VectorSource from 'ol/source/Vector'\nimport VectorLayer from 'ol/layer/Vector'\nimport Feature from 'ol/Feature'\nimport Point from 'ol/geom/Point'\nimport { fromLonLat } from 'ol/proj'\nimport { Style, Fill, Stroke } from 'ol/style'\nimport Polygon from 'ol/geom/Polygon'\n\nimport { BaseWeatherLayer } from './base-weather-layer.js'\nimport { WeatherData, WeatherLayerType } from '../types.js'\n\n/**\n * 구름 레이어 구현\n */\nexport class CloudLayer extends BaseWeatherLayer {\n getType(): WeatherLayerType {\n return 'cloud'\n }\n\n /**\n * 벡터 레이어를 생성하여 구름을 표현합니다.\n */\n createLayer(): BaseLayer {\n const source = new VectorSource()\n\n if (!this.weatherData || this.weatherData.length === 0) {\n return new VectorLayer({\n source,\n zIndex: 7\n })\n }\n\n // 각 데이터 포인트의 위치와 구름 커버리지 추출\n const points = this.weatherData.map(data => ({\n lon: data.location.longitude,\n lat: data.location.latitude,\n coverage: data.cloud.coverage / 100\n }))\n\n console.log('구름 데이터 포인트:', points.length, '첫 번째 포인트:', points[0])\n\n // 개선된 구름 커버리지 보간 함수 - IDW(Inverse Distance Weighting) 기반 보간법\n const interpolateCoverage = (lon: number, lat: number) => {\n // 가장 가까운 이웃 개수 제한 - 모든 포인트 대신 가까운 포인트만 사용하여 국지적 패턴 유지\n const MAX_NEIGHBORS = 5\n\n // 현재 위치에서 모든 포인트까지의 거리 계산\n const distances = points.map(point => {\n const dx = lon - point.lon\n const dy = lat - point.lat\n return {\n point,\n dist: Math.sqrt(dx * dx + dy * dy)\n }\n })\n\n // 거리에 따라 정렬하고 가장 가까운 이웃만 선택\n const nearestNeighbors = distances.sort((a, b) => a.dist - b.dist).slice(0, MAX_NEIGHBORS)\n\n // 포인트가 정확히 일치하는 경우\n if (nearestNeighbors.length > 0 && nearestNeighbors[0].dist < 0.0001) {\n return nearestNeighbors[0].point.coverage\n }\n\n // 역거리 가중치 보간법 적용 - 거리의 제곱에 반비례하는 가중치 사용\n let totalWeight = 0\n let weightedCoverage = 0\n\n // 가중치 지수 - 낮을수록 더 부드러운 보간, 높을수록 가까운 값에 더 큰 영향\n const power = 1.5\n\n nearestNeighbors.forEach(({ point, dist }) => {\n // 작은 값으로 나누기 방지\n const adjustedDist = Math.max(dist, 0.0001)\n const weight = 1 / Math.pow(adjustedDist, power)\n\n totalWeight += weight\n weightedCoverage += point.coverage * weight\n })\n\n return weightedCoverage / totalWeight\n }\n\n // 구름 커버리지에 따른 색상 계산 함수\n const getColorFromCoverage = (coverage: number) => {\n // 흰색에서 짙은 회색으로 그라데이션 (구름 농도가 짙을수록 더 어두워짐)\n const intensity = Math.min(1, Math.max(0, coverage))\n\n // 더 어두운 회색으로 조정\n const brightness = Math.round(255 - intensity * 130)\n\n // 구름 농도에 따라 투명도 조정 - 옅은 구름은 더 투명하게\n const alpha = 0.2 + intensity * 0.6\n\n return `rgba(${brightness}, ${brightness}, ${brightness}, ${alpha})`\n }\n\n // 그라데이션을 적용할 그리드 생성\n const bounds = {\n minLon: Infinity,\n maxLon: -Infinity,\n minLat: Infinity,\n maxLat: -Infinity\n }\n\n // 데이터 포인트로부터 영역 경계 계산\n points.forEach(point => {\n bounds.minLon = Math.min(bounds.minLon, point.lon)\n bounds.maxLon = Math.max(bounds.maxLon, point.lon)\n bounds.minLat = Math.min(bounds.minLat, point.lat)\n bounds.maxLat = Math.max(bounds.maxLat, point.lat)\n })\n\n // 경계에 마진 추가\n const margin = 0.1\n bounds.minLon -= margin\n bounds.maxLon += margin\n bounds.minLat -= margin\n bounds.maxLat += margin\n\n // 고해상도 그리드 생성 (더 매끄러운 그라데이션을 위해)\n const gridResolution = 80 // 적절한 해상도로 조정\n const lonStep = (bounds.maxLon - bounds.minLon) / gridResolution\n const latStep = (bounds.maxLat - bounds.minLat) / gridResolution\n\n // 인접 셀 간의 값 차이 최소화를 위한 스무딩 함수\n const smoothGrid = (grid: number[][], passes: number = 2): number[][] => {\n const rows = grid.length\n if (rows === 0) return grid\n\n const cols = grid[0].length\n const result = Array(rows)\n .fill(0)\n .map(() => Array(cols).fill(0))\n\n // 여러 번 스무딩 패스 적용\n for (let pass = 0; pass < passes; pass++) {\n const currentGrid = pass === 0 ? grid : result\n\n for (let i = 0; i < rows; i++) {\n for (let j = 0; j < cols; j++) {\n // 현재 셀과 인접 셀의 평균 계산\n let sum = currentGrid[i][j]\n let count = 1\n\n // 상하좌우 셀 확인\n const neighbors = [\n [i - 1, j],\n [i + 1, j],\n [i, j - 1],\n [i, j + 1]\n ]\n\n neighbors.forEach(([ni, nj]) => {\n if (ni >= 0 && ni < rows && nj >= 0 && nj < cols) {\n sum += currentGrid[ni][nj]\n count++\n }\n })\n\n // 평균값으로 스무딩 (약간의 원본 가중치 부여)\n result[i][j] = currentGrid[i][j] * 0.6 + ((sum - currentGrid[i][j]) / (count - 1)) * 0.4\n }\n }\n }\n\n return result\n }\n\n // 구름 데이터 그리드 생성\n const coverageGrid: number[][] = []\n\n for (let i = 0; i < gridResolution; i++) {\n coverageGrid[i] = []\n for (let j = 0; j < gridResolution; j++) {\n const centerLon = bounds.minLon + (j + 0.5) * lonStep\n const centerLat = bounds.minLat + (i + 0.5) * latStep\n coverageGrid[i][j] = interpolateCoverage(centerLon, centerLat)\n }\n }\n\n // 그리드 스무딩 적용\n const smoothedGrid = smoothGrid(coverageGrid, 2)\n\n // 셀 생성 및 피처 추가\n for (let i = 0; i < gridResolution; i++) {\n for (let j = 0; j < gridResolution; j++) {\n const lonMin = bounds.minLon + j * lonStep\n const lonMax = bounds.minLon + (j + 1) * lonStep\n const latMin = bounds.minLat + i * latStep\n const latMax = bounds.minLat + (i + 1) * latStep\n\n // 스무딩된 값 사용\n const interpolatedCoverage = smoothedGrid[i][j]\n\n // 너무 작은 커버리지는 제외\n if (interpolatedCoverage < 0.01) continue\n\n // 셀 좌표 계산 및 변환\n const coordinates = [\n [lonMin, latMin],\n [lonMax, latMin],\n [lonMax, latMax],\n [lonMin, latMax],\n [lonMin, latMin]\n ].map(coord => fromLonLat(coord))\n\n // 다각형 피처 생성\n const feature = new Feature({\n geometry: new Polygon([coordinates]),\n coverage: interpolatedCoverage\n })\n\n source.addFeature(feature)\n }\n }\n\n // 벡터 레이어 생성\n return new VectorLayer({\n source,\n zIndex: 7,\n className: 'cloud-layer',\n opacity: 0.9,\n style: feature => {\n const coverage = feature.get('coverage') as number\n return new Style({\n fill: new Fill({\n color: getColorFromCoverage(coverage)\n }),\n // 경계선 완전히 제거\n stroke: undefined\n })\n }\n })\n }\n\n /**\n * 레이어에 날씨 데이터를 설정합니다.\n */\n setWeatherData(data: WeatherData[]) {\n this.weatherData = data\n this.updateLayer()\n }\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BaseWeatherLayer } from './base-weather-layer';
|
|
2
|
+
import { WeatherData, WeatherLayerType } from '../types';
|
|
3
|
+
import BaseLayer from 'ol/layer/Base';
|
|
4
|
+
/**
|
|
5
|
+
* 강수량 레이어 구현
|
|
6
|
+
*/
|
|
7
|
+
export declare class PrecipitationLayer extends BaseWeatherLayer {
|
|
8
|
+
private animationFrameId?;
|
|
9
|
+
private animationTime;
|
|
10
|
+
getType(): WeatherLayerType;
|
|
11
|
+
/**
|
|
12
|
+
* 벡터 레이어를 생성하여 강수량을 표현합니다.
|
|
13
|
+
*/
|
|
14
|
+
createLayer(): BaseLayer;
|
|
15
|
+
/**
|
|
16
|
+
* 애니메이션을 시작합니다.
|
|
17
|
+
*/
|
|
18
|
+
startAnimation(): void;
|
|
19
|
+
/**
|
|
20
|
+
* 애니메이션을 중지합니다.
|
|
21
|
+
*/
|
|
22
|
+
stopAnimation(): void;
|
|
23
|
+
/**
|
|
24
|
+
* 레이어에 날씨 데이터를 설정합니다.
|
|
25
|
+
*/
|
|
26
|
+
setWeatherData(data: WeatherData[]): void;
|
|
27
|
+
}
|