@operato/weather 9.0.1 → 9.0.2

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 (35) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/src/index.d.ts +1 -0
  3. package/dist/src/index.js +2 -0
  4. package/dist/src/index.js.map +1 -0
  5. package/dist/src/layers/base-weather-layer.d.ts +56 -0
  6. package/dist/src/layers/base-weather-layer.js +83 -0
  7. package/dist/src/layers/base-weather-layer.js.map +1 -0
  8. package/dist/src/layers/cloud-layer.d.ts +17 -0
  9. package/dist/src/layers/cloud-layer.js +203 -0
  10. package/dist/src/layers/cloud-layer.js.map +1 -0
  11. package/dist/src/layers/precipitation-layer.d.ts +27 -0
  12. package/dist/src/layers/precipitation-layer.js +344 -0
  13. package/dist/src/layers/precipitation-layer.js.map +1 -0
  14. package/dist/src/layers/solar-layer.d.ts +17 -0
  15. package/dist/src/layers/solar-layer.js +253 -0
  16. package/dist/src/layers/solar-layer.js.map +1 -0
  17. package/dist/src/layers/temperature-layer.d.ts +13 -0
  18. package/dist/src/layers/temperature-layer.js +230 -0
  19. package/dist/src/layers/temperature-layer.js.map +1 -0
  20. package/dist/src/layers/wind-layer.d.ts +60 -0
  21. package/dist/src/layers/wind-layer.js +311 -0
  22. package/dist/src/layers/wind-layer.js.map +1 -0
  23. package/dist/src/ox-weather.d.ts +112 -0
  24. package/dist/src/ox-weather.js +645 -0
  25. package/dist/src/ox-weather.js.map +1 -0
  26. package/dist/src/ox-wind.d.ts +91 -0
  27. package/dist/src/ox-wind.js +425 -0
  28. package/dist/src/ox-wind.js.map +1 -0
  29. package/dist/src/services/weather-service.d.ts +79 -0
  30. package/dist/src/services/weather-service.js +560 -0
  31. package/dist/src/services/weather-service.js.map +1 -0
  32. package/dist/src/types.d.ts +71 -0
  33. package/dist/src/types.js +2 -0
  34. package/dist/src/types.js.map +1 -0
  35. package/package.json +4 -4
@@ -0,0 +1,253 @@
1
+ import { BaseWeatherLayer } from './base-weather-layer';
2
+ import VectorSource from 'ol/source/Vector';
3
+ import VectorLayer from 'ol/layer/Vector';
4
+ import Feature from 'ol/Feature';
5
+ import { fromLonLat } from 'ol/proj';
6
+ import { Style, Fill } from 'ol/style';
7
+ import Polygon from 'ol/geom/Polygon';
8
+ /**
9
+ * 태양광 레이어 구현
10
+ */
11
+ export class SolarLayer extends BaseWeatherLayer {
12
+ getType() {
13
+ return 'solar';
14
+ }
15
+ /**
16
+ * 벡터 레이어를 생성하여 태양광을 표현합니다.
17
+ */
18
+ createLayer() {
19
+ const source = new VectorSource();
20
+ console.log('태양광 레이어 생성 - 데이터 존재 여부:', !!this.weatherData, '데이터 개수:', this.weatherData?.length);
21
+ if (!this.weatherData || this.weatherData.length === 0) {
22
+ console.log('태양광 레이어: 날씨 데이터가 없음');
23
+ return new VectorLayer({
24
+ source,
25
+ zIndex: 8
26
+ });
27
+ }
28
+ // 태양광 데이터 유효성 확인
29
+ const validData = this.weatherData.filter(data => {
30
+ const hasSolar = data.solar && typeof data.solar.radiation === 'number' && data.cloud && typeof data.cloud.coverage === 'number';
31
+ if (!hasSolar) {
32
+ console.log('태양광 데이터 없음:', data);
33
+ }
34
+ return hasSolar;
35
+ });
36
+ console.log('유효한 태양광 데이터 수:', validData.length);
37
+ if (validData.length === 0) {
38
+ console.log('태양광 레이어: 유효한 태양광 데이터가 없음');
39
+ return new VectorLayer({
40
+ source,
41
+ zIndex: 8
42
+ });
43
+ }
44
+ // 각 데이터 포인트의 위치와 태양광 값 추출
45
+ const points = validData.map(data => ({
46
+ lon: data.location.longitude,
47
+ lat: data.location.latitude,
48
+ radiation: data.solar.radiation,
49
+ // 구름 영향 고려
50
+ cloudImpact: Math.max(0, 1 - data.cloud.coverage / 100),
51
+ // 구름 영향을 고려한 유효 태양광
52
+ effectiveRadiation: data.solar.radiation * Math.max(0, 1 - data.cloud.coverage / 100)
53
+ }));
54
+ console.log('태양광 데이터 포인트:', points.length, '첫 번째 포인트:', points[0]);
55
+ // 태양광 최대/최소값 계산
56
+ let minRadiation = Infinity;
57
+ let maxRadiation = -Infinity;
58
+ points.forEach(point => {
59
+ minRadiation = Math.min(minRadiation, point.effectiveRadiation);
60
+ maxRadiation = Math.max(maxRadiation, point.effectiveRadiation);
61
+ });
62
+ console.log('태양광 최소/최대값:', minRadiation, maxRadiation);
63
+ // 태양광 정규화 함수 - 0~1 범위로 변환
64
+ const normalizeRadiation = (radiation) => {
65
+ if (maxRadiation === minRadiation)
66
+ return 0.5;
67
+ return (radiation - minRadiation) / (maxRadiation - minRadiation);
68
+ };
69
+ // 개선된 태양광 보간 함수 - IDW(Inverse Distance Weighting) 기반 보간법
70
+ const interpolateRadiation = (lon, lat) => {
71
+ // 가장 가까운 이웃 개수 제한 - 모든 포인트 대신 가까운 포인트만 사용하여 국지적 패턴 유지
72
+ const MAX_NEIGHBORS = 5;
73
+ // 현재 위치에서 모든 포인트까지의 거리 계산
74
+ const distances = points.map(point => {
75
+ const dx = lon - point.lon;
76
+ const dy = lat - point.lat;
77
+ return {
78
+ point,
79
+ dist: Math.sqrt(dx * dx + dy * dy)
80
+ };
81
+ });
82
+ // 거리에 따라 정렬하고 가장 가까운 이웃만 선택
83
+ const nearestNeighbors = distances.sort((a, b) => a.dist - b.dist).slice(0, MAX_NEIGHBORS);
84
+ // 포인트가 정확히 일치하는 경우
85
+ if (nearestNeighbors.length > 0 && nearestNeighbors[0].dist < 0.0001) {
86
+ return nearestNeighbors[0].point.effectiveRadiation;
87
+ }
88
+ // 역거리 가중치 보간법 적용 - 거리의 제곱에 반비례하는 가중치 사용
89
+ let totalWeight = 0;
90
+ let weightedRadiation = 0;
91
+ // 가중치 지수 - 낮을수록 더 부드러운 보간, 높을수록 가까운 값에 더 큰 영향
92
+ const power = 1.5;
93
+ nearestNeighbors.forEach(({ point, dist }) => {
94
+ // 작은 값으로 나누기 방지
95
+ const adjustedDist = Math.max(dist, 0.0001);
96
+ const weight = 1 / Math.pow(adjustedDist, power);
97
+ totalWeight += weight;
98
+ weightedRadiation += point.effectiveRadiation * weight;
99
+ });
100
+ return weightedRadiation / totalWeight;
101
+ };
102
+ // 태양광에 따른 색상 계산 함수
103
+ const getColorFromRadiation = (radiation) => {
104
+ const normalized = normalizeRadiation(radiation);
105
+ // 태양광 표현에 적합한 색상 - 낮은 값은 더 어두운 노랑, 높은 값은 밝고 강렬한 노랑
106
+ const hue = 50 - normalized * 10; // 50~40 범위의 노란색
107
+ const saturation = 60 + normalized * 40; // 60~100% 채도
108
+ const lightness = 30 + normalized * 60; // 30~90% 밝기
109
+ // 태양광 강도에 따라 투명도 조정 - 강한 태양광은 더 선명하게
110
+ const alpha = 0.3 + normalized * 0.5;
111
+ return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;
112
+ };
113
+ // 그라데이션을 적용할 그리드 생성
114
+ const bounds = {
115
+ minLon: Infinity,
116
+ maxLon: -Infinity,
117
+ minLat: Infinity,
118
+ maxLat: -Infinity
119
+ };
120
+ // 데이터 포인트로부터 영역 경계 계산
121
+ points.forEach(point => {
122
+ bounds.minLon = Math.min(bounds.minLon, point.lon);
123
+ bounds.maxLon = Math.max(bounds.maxLon, point.lon);
124
+ bounds.minLat = Math.min(bounds.minLat, point.lat);
125
+ bounds.maxLat = Math.max(bounds.maxLat, point.lat);
126
+ });
127
+ // 경계에 마진 추가
128
+ const margin = 0.1;
129
+ bounds.minLon -= margin;
130
+ bounds.maxLon += margin;
131
+ bounds.minLat -= margin;
132
+ bounds.maxLat += margin;
133
+ // 고해상도 그리드 생성 (더 매끄러운 그라데이션을 위해)
134
+ const gridResolution = 100; // 적절한 해상도로 조정
135
+ const lonStep = (bounds.maxLon - bounds.minLon) / gridResolution;
136
+ const latStep = (bounds.maxLat - bounds.minLat) / gridResolution;
137
+ // 인접 셀 간의 값 차이 최소화를 위한 스무딩 함수
138
+ const smoothGrid = (grid, passes = 2) => {
139
+ const rows = grid.length;
140
+ if (rows === 0)
141
+ return grid;
142
+ const cols = grid[0].length;
143
+ const result = Array(rows)
144
+ .fill(0)
145
+ .map(() => Array(cols).fill(0));
146
+ // 여러 번 스무딩 패스 적용
147
+ for (let pass = 0; pass < passes; pass++) {
148
+ const currentGrid = pass === 0 ? grid : result;
149
+ for (let i = 0; i < rows; i++) {
150
+ for (let j = 0; j < cols; j++) {
151
+ // 현재 셀과 인접 셀의 평균 계산
152
+ let sum = currentGrid[i][j];
153
+ let count = 1;
154
+ // 상하좌우 셀 확인
155
+ const neighbors = [
156
+ [i - 1, j],
157
+ [i + 1, j],
158
+ [i, j - 1],
159
+ [i, j + 1]
160
+ ];
161
+ neighbors.forEach(([ni, nj]) => {
162
+ if (ni >= 0 && ni < rows && nj >= 0 && nj < cols) {
163
+ sum += currentGrid[ni][nj];
164
+ count++;
165
+ }
166
+ });
167
+ // 평균값으로 스무딩 (약간의 원본 가중치 부여)
168
+ result[i][j] = currentGrid[i][j] * 0.6 + ((sum - currentGrid[i][j]) / (count - 1)) * 0.4;
169
+ }
170
+ }
171
+ }
172
+ return result;
173
+ };
174
+ // 태양광 데이터 그리드 생성
175
+ const radiationGrid = [];
176
+ for (let i = 0; i < gridResolution; i++) {
177
+ radiationGrid[i] = [];
178
+ for (let j = 0; j < gridResolution; j++) {
179
+ const centerLon = bounds.minLon + (j + 0.5) * lonStep;
180
+ const centerLat = bounds.minLat + (i + 0.5) * latStep;
181
+ radiationGrid[i][j] = interpolateRadiation(centerLon, centerLat);
182
+ }
183
+ }
184
+ // 그리드 스무딩 적용
185
+ const smoothedGrid = smoothGrid(radiationGrid, 2);
186
+ // 셀 생성 및 피처 추가
187
+ for (let i = 0; i < gridResolution; i++) {
188
+ for (let j = 0; j < gridResolution; j++) {
189
+ const lonMin = bounds.minLon + j * lonStep;
190
+ const lonMax = bounds.minLon + (j + 1) * lonStep;
191
+ const latMin = bounds.minLat + i * latStep;
192
+ const latMax = bounds.minLat + (i + 1) * latStep;
193
+ // 스무딩된 값 사용
194
+ const interpolatedRadiation = smoothedGrid[i][j];
195
+ // 너무 작은 태양광은 제외 (최소값의 5% 미만)
196
+ if (interpolatedRadiation < minRadiation + (maxRadiation - minRadiation) * 0.05)
197
+ continue;
198
+ // 셀 좌표 계산 및 변환
199
+ const coordinates = [
200
+ [lonMin, latMin],
201
+ [lonMax, latMin],
202
+ [lonMax, latMax],
203
+ [lonMin, latMax],
204
+ [lonMin, latMin]
205
+ ].map(coord => fromLonLat(coord));
206
+ // 다각형 피처 생성
207
+ const feature = new Feature({
208
+ geometry: new Polygon([coordinates]),
209
+ radiation: interpolatedRadiation
210
+ });
211
+ source.addFeature(feature);
212
+ }
213
+ }
214
+ // CSS 블렌딩 모드 정의를 위한 클래스 이름 추가
215
+ const layerClass = 'solar-layer';
216
+ // 블렌딩 모드 스타일 추가
217
+ if (!document.getElementById('solar-layer-style')) {
218
+ const style = document.createElement('style');
219
+ style.id = 'solar-layer-style';
220
+ style.textContent = `
221
+ .${layerClass} canvas {
222
+ mix-blend-mode: overlay;
223
+ }
224
+ `;
225
+ document.head.appendChild(style);
226
+ }
227
+ // 벡터 레이어 생성
228
+ return new VectorLayer({
229
+ source,
230
+ zIndex: 8,
231
+ className: layerClass,
232
+ opacity: 0.8,
233
+ style: feature => {
234
+ const radiation = feature.get('radiation');
235
+ return new Style({
236
+ fill: new Fill({
237
+ color: getColorFromRadiation(radiation)
238
+ }),
239
+ // 경계선 완전히 제거
240
+ stroke: undefined
241
+ });
242
+ }
243
+ });
244
+ }
245
+ /**
246
+ * 레이어에 날씨 데이터를 설정합니다.
247
+ */
248
+ setWeatherData(data) {
249
+ this.weatherData = data;
250
+ this.updateLayer();
251
+ }
252
+ }
253
+ //# sourceMappingURL=solar-layer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solar-layer.js","sourceRoot":"","sources":["../../../src/layers/solar-layer.ts"],"names":[],"mappings":"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","sourcesContent":["import { BaseWeatherLayer } from './base-weather-layer'\nimport { WeatherData, WeatherLayerType } from '../types'\nimport BaseLayer from 'ol/layer/Base'\nimport VectorSource from 'ol/source/Vector'\nimport VectorLayer from 'ol/layer/Vector'\nimport Feature from 'ol/Feature'\nimport { fromLonLat } from 'ol/proj'\nimport { Style, Fill } from 'ol/style'\nimport Polygon from 'ol/geom/Polygon'\n\n/**\n * 태양광 레이어 구현\n */\nexport class SolarLayer extends BaseWeatherLayer {\n getType(): WeatherLayerType {\n return 'solar'\n }\n\n /**\n * 벡터 레이어를 생성하여 태양광을 표현합니다.\n */\n createLayer(): BaseLayer {\n const source = new VectorSource()\n\n console.log('태양광 레이어 생성 - 데이터 존재 여부:', !!this.weatherData, '데이터 개수:', this.weatherData?.length)\n\n if (!this.weatherData || this.weatherData.length === 0) {\n console.log('태양광 레이어: 날씨 데이터가 없음')\n return new VectorLayer({\n source,\n zIndex: 8\n })\n }\n\n // 태양광 데이터 유효성 확인\n const validData = this.weatherData.filter(data => {\n const hasSolar =\n data.solar && typeof data.solar.radiation === 'number' && data.cloud && typeof data.cloud.coverage === 'number'\n if (!hasSolar) {\n console.log('태양광 데이터 없음:', data)\n }\n return hasSolar\n })\n\n console.log('유효한 태양광 데이터 수:', validData.length)\n\n if (validData.length === 0) {\n console.log('태양광 레이어: 유효한 태양광 데이터가 없음')\n return new VectorLayer({\n source,\n zIndex: 8\n })\n }\n\n // 각 데이터 포인트의 위치와 태양광 값 추출\n const points = validData.map(data => ({\n lon: data.location.longitude,\n lat: data.location.latitude,\n radiation: data.solar.radiation,\n // 구름 영향 고려\n cloudImpact: Math.max(0, 1 - data.cloud.coverage / 100),\n // 구름 영향을 고려한 유효 태양광\n effectiveRadiation: data.solar.radiation * Math.max(0, 1 - data.cloud.coverage / 100)\n }))\n\n console.log('태양광 데이터 포인트:', points.length, '첫 번째 포인트:', points[0])\n\n // 태양광 최대/최소값 계산\n let minRadiation = Infinity\n let maxRadiation = -Infinity\n\n points.forEach(point => {\n minRadiation = Math.min(minRadiation, point.effectiveRadiation)\n maxRadiation = Math.max(maxRadiation, point.effectiveRadiation)\n })\n\n console.log('태양광 최소/최대값:', minRadiation, maxRadiation)\n\n // 태양광 정규화 함수 - 0~1 범위로 변환\n const normalizeRadiation = (radiation: number) => {\n if (maxRadiation === minRadiation) return 0.5\n return (radiation - minRadiation) / (maxRadiation - minRadiation)\n }\n\n // 개선된 태양광 보간 함수 - IDW(Inverse Distance Weighting) 기반 보간법\n const interpolateRadiation = (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.effectiveRadiation\n }\n\n // 역거리 가중치 보간법 적용 - 거리의 제곱에 반비례하는 가중치 사용\n let totalWeight = 0\n let weightedRadiation = 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 weightedRadiation += point.effectiveRadiation * weight\n })\n\n return weightedRadiation / totalWeight\n }\n\n // 태양광에 따른 색상 계산 함수\n const getColorFromRadiation = (radiation: number) => {\n const normalized = normalizeRadiation(radiation)\n\n // 태양광 표현에 적합한 색상 - 낮은 값은 더 어두운 노랑, 높은 값은 밝고 강렬한 노랑\n const hue = 50 - normalized * 10 // 50~40 범위의 노란색\n const saturation = 60 + normalized * 40 // 60~100% 채도\n const lightness = 30 + normalized * 60 // 30~90% 밝기\n\n // 태양광 강도에 따라 투명도 조정 - 강한 태양광은 더 선명하게\n const alpha = 0.3 + normalized * 0.5\n\n return `hsla(${hue}, ${saturation}%, ${lightness}%, ${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 = 100 // 적절한 해상도로 조정\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 radiationGrid: number[][] = []\n\n for (let i = 0; i < gridResolution; i++) {\n radiationGrid[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 radiationGrid[i][j] = interpolateRadiation(centerLon, centerLat)\n }\n }\n\n // 그리드 스무딩 적용\n const smoothedGrid = smoothGrid(radiationGrid, 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 interpolatedRadiation = smoothedGrid[i][j]\n\n // 너무 작은 태양광은 제외 (최소값의 5% 미만)\n if (interpolatedRadiation < minRadiation + (maxRadiation - minRadiation) * 0.05) 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 radiation: interpolatedRadiation\n })\n\n source.addFeature(feature)\n }\n }\n\n // CSS 블렌딩 모드 정의를 위한 클래스 이름 추가\n const layerClass = 'solar-layer'\n\n // 블렌딩 모드 스타일 추가\n if (!document.getElementById('solar-layer-style')) {\n const style = document.createElement('style')\n style.id = 'solar-layer-style'\n style.textContent = `\n .${layerClass} canvas {\n mix-blend-mode: overlay;\n }\n `\n document.head.appendChild(style)\n }\n\n // 벡터 레이어 생성\n return new VectorLayer({\n source,\n zIndex: 8,\n className: layerClass,\n opacity: 0.8,\n style: feature => {\n const radiation = feature.get('radiation') as number\n return new Style({\n fill: new Fill({\n color: getColorFromRadiation(radiation)\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,13 @@
1
+ import BaseLayer from 'ol/layer/Base';
2
+ import { BaseWeatherLayer } from './base-weather-layer.js';
3
+ import { WeatherLayerType } from '../types.js';
4
+ /**
5
+ * 온도 레이어 구현
6
+ */
7
+ export declare class TemperatureLayer extends BaseWeatherLayer {
8
+ getType(): WeatherLayerType;
9
+ /**
10
+ * 히트맵 레이어를 생성하여 온도를 표현합니다.
11
+ */
12
+ createLayer(): BaseLayer;
13
+ }
@@ -0,0 +1,230 @@
1
+ import VectorSource from 'ol/source/Vector';
2
+ import Feature from 'ol/Feature';
3
+ import { fromLonLat } from 'ol/proj';
4
+ import Polygon from 'ol/geom/Polygon';
5
+ import { Fill } from 'ol/style';
6
+ import VectorLayer from 'ol/layer/Vector';
7
+ import { Style } from 'ol/style';
8
+ import { BaseWeatherLayer } from './base-weather-layer.js';
9
+ /**
10
+ * 온도 레이어 구현
11
+ */
12
+ export class TemperatureLayer extends BaseWeatherLayer {
13
+ getType() {
14
+ return 'temperature';
15
+ }
16
+ /**
17
+ * 히트맵 레이어를 생성하여 온도를 표현합니다.
18
+ */
19
+ createLayer() {
20
+ const source = new VectorSource();
21
+ console.log('온도 레이어 생성 - 데이터 존재 여부:', !!this.weatherData, '데이터 개수:', this.weatherData?.length);
22
+ if (!this.weatherData || this.weatherData.length === 0) {
23
+ console.log('온도 레이어: 날씨 데이터가 없음');
24
+ return new VectorLayer({
25
+ source,
26
+ zIndex: 5
27
+ });
28
+ }
29
+ // 온도 값이 있는지 확인
30
+ const validData = this.weatherData.filter(data => {
31
+ const hasTemp = data.temperature && typeof data.temperature.current === 'number';
32
+ if (!hasTemp) {
33
+ console.log('온도 데이터 없음:', data);
34
+ }
35
+ return hasTemp;
36
+ });
37
+ console.log('유효한 온도 데이터 수:', validData.length);
38
+ if (validData.length === 0) {
39
+ console.log('온도 레이어: 유효한 온도 데이터가 없음');
40
+ return new VectorLayer({
41
+ source,
42
+ zIndex: 5
43
+ });
44
+ }
45
+ // 온도 범위 계산
46
+ let minTemp = Infinity;
47
+ let maxTemp = -Infinity;
48
+ validData.forEach(data => {
49
+ const temp = data.temperature.current;
50
+ minTemp = Math.min(minTemp, temp);
51
+ maxTemp = Math.max(maxTemp, temp);
52
+ });
53
+ // 각 데이터 포인트의 위치와 온도 추출
54
+ const points = validData.map(data => ({
55
+ lon: data.location.longitude,
56
+ lat: data.location.latitude,
57
+ temp: data.temperature.current
58
+ }));
59
+ console.log('온도 데이터 포인트:', points.length, '첫 번째 포인트:', points[0]);
60
+ // 온도 정규화 함수 - 0~1 범위로 변환
61
+ const normalizeTemp = (temp) => {
62
+ if (maxTemp === minTemp)
63
+ return 0.5;
64
+ return (temp - minTemp) / (maxTemp - minTemp);
65
+ };
66
+ // 개선된 온도 보간 함수 - IDW(Inverse Distance Weighting) 기반 보간법
67
+ const interpolateTemp = (lon, lat) => {
68
+ // 가장 가까운 이웃 개수 제한 - 모든 포인트 대신 가까운 포인트만 사용하여 국지적 패턴 유지
69
+ const MAX_NEIGHBORS = 5;
70
+ // 현재 위치에서 모든 포인트까지의 거리 계산
71
+ const distances = points.map(point => {
72
+ const dx = lon - point.lon;
73
+ const dy = lat - point.lat;
74
+ return {
75
+ point,
76
+ dist: Math.sqrt(dx * dx + dy * dy)
77
+ };
78
+ });
79
+ // 거리에 따라 정렬하고 가장 가까운 이웃만 선택
80
+ const nearestNeighbors = distances.sort((a, b) => a.dist - b.dist).slice(0, MAX_NEIGHBORS);
81
+ // 포인트가 정확히 일치하는 경우
82
+ if (nearestNeighbors.length > 0 && nearestNeighbors[0].dist < 0.0001) {
83
+ return nearestNeighbors[0].point.temp;
84
+ }
85
+ // 역거리 가중치 보간법 적용 - 거리의 제곱에 반비례하는 가중치 사용
86
+ let totalWeight = 0;
87
+ let weightedTemp = 0;
88
+ // 가중치 지수 - 낮을수록 더 부드러운 보간, 높을수록 가까운 값에 더 큰 영향
89
+ const power = 1.5;
90
+ nearestNeighbors.forEach(({ point, dist }) => {
91
+ // 작은 값으로 나누기 방지
92
+ const adjustedDist = Math.max(dist, 0.0001);
93
+ const weight = 1 / Math.pow(adjustedDist, power);
94
+ totalWeight += weight;
95
+ weightedTemp += point.temp * weight;
96
+ });
97
+ return weightedTemp / totalWeight;
98
+ };
99
+ // 온도에 따른 색상 계산 함수
100
+ const getColorFromTemp = (temp) => {
101
+ const normalized = normalizeTemp(temp);
102
+ // HSL 색상 모델 사용 - 보다 자연스러운 온도 색상 맵핑
103
+ // 파랑(추움, 240) -> 하양(중간, 180) -> 빨강(더움, 0)
104
+ const hue = Math.max(0, Math.min(240, 240 - normalized * 240));
105
+ // 온도에 따라 채도 변화 - 극단적 온도에서 더 선명하게
106
+ const saturation = 70 + Math.abs(normalized - 0.5) * 60;
107
+ // 온도에 따라 밝기 조정 - 중간 온도는 더 밝게
108
+ const lightness = 50 + Math.abs(normalized - 0.5) * 20;
109
+ // 투명도 설정 - 중간 온도는 더 투명하게, 극단적 온도는 더 선명하게
110
+ const alpha = 0.4 + Math.abs(normalized - 0.5) * 0.4;
111
+ return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;
112
+ };
113
+ // 그라데이션을 적용할 그리드 생성
114
+ const bounds = {
115
+ minLon: Infinity,
116
+ maxLon: -Infinity,
117
+ minLat: Infinity,
118
+ maxLat: -Infinity
119
+ };
120
+ // 데이터 포인트로부터 영역 경계 계산
121
+ points.forEach(point => {
122
+ bounds.minLon = Math.min(bounds.minLon, point.lon);
123
+ bounds.maxLon = Math.max(bounds.maxLon, point.lon);
124
+ bounds.minLat = Math.min(bounds.minLat, point.lat);
125
+ bounds.maxLat = Math.max(bounds.maxLat, point.lat);
126
+ });
127
+ // 경계에 마진 추가
128
+ const margin = 0.1;
129
+ bounds.minLon -= margin;
130
+ bounds.maxLon += margin;
131
+ bounds.minLat -= margin;
132
+ bounds.maxLat += margin;
133
+ // 고해상도 그리드 생성 (더 매끄러운 그라데이션을 위해)
134
+ const gridResolution = 120; // 적절한 해상도로 조정
135
+ const lonStep = (bounds.maxLon - bounds.minLon) / gridResolution;
136
+ const latStep = (bounds.maxLat - bounds.minLat) / gridResolution;
137
+ // 인접 셀 간의 온도 차이 최소화를 위한 스무딩 함수
138
+ const smoothGrid = (grid, passes = 2) => {
139
+ const rows = grid.length;
140
+ if (rows === 0)
141
+ return grid;
142
+ const cols = grid[0].length;
143
+ const result = Array(rows)
144
+ .fill(0)
145
+ .map(() => Array(cols).fill(0));
146
+ // 여러 번 스무딩 패스 적용
147
+ for (let pass = 0; pass < passes; pass++) {
148
+ const currentGrid = pass === 0 ? grid : result;
149
+ for (let i = 0; i < rows; i++) {
150
+ for (let j = 0; j < cols; j++) {
151
+ // 현재 셀과 인접 셀의 평균 계산
152
+ let sum = currentGrid[i][j];
153
+ let count = 1;
154
+ // 상하좌우 셀 확인
155
+ const neighbors = [
156
+ [i - 1, j],
157
+ [i + 1, j],
158
+ [i, j - 1],
159
+ [i, j + 1]
160
+ ];
161
+ neighbors.forEach(([ni, nj]) => {
162
+ if (ni >= 0 && ni < rows && nj >= 0 && nj < cols) {
163
+ sum += currentGrid[ni][nj];
164
+ count++;
165
+ }
166
+ });
167
+ // 평균값으로 스무딩 (약간의 원본 가중치 부여)
168
+ result[i][j] = currentGrid[i][j] * 0.6 + ((sum - currentGrid[i][j]) / (count - 1)) * 0.4;
169
+ }
170
+ }
171
+ }
172
+ return result;
173
+ };
174
+ // 온도 데이터 그리드 생성
175
+ const tempGrid = [];
176
+ for (let i = 0; i < gridResolution; i++) {
177
+ tempGrid[i] = [];
178
+ for (let j = 0; j < gridResolution; j++) {
179
+ const centerLon = bounds.minLon + (j + 0.5) * lonStep;
180
+ const centerLat = bounds.minLat + (i + 0.5) * latStep;
181
+ tempGrid[i][j] = interpolateTemp(centerLon, centerLat);
182
+ }
183
+ }
184
+ // 그리드 스무딩 적용
185
+ const smoothedGrid = smoothGrid(tempGrid, 2);
186
+ // 셀 생성 및 피처 추가
187
+ for (let i = 0; i < gridResolution; i++) {
188
+ for (let j = 0; j < gridResolution; j++) {
189
+ const lonMin = bounds.minLon + j * lonStep;
190
+ const lonMax = bounds.minLon + (j + 1) * lonStep;
191
+ const latMin = bounds.minLat + i * latStep;
192
+ const latMax = bounds.minLat + (i + 1) * latStep;
193
+ // 스무딩된 온도값 사용
194
+ const interpolatedTemp = smoothedGrid[i][j];
195
+ // 셀 좌표 계산 및 변환
196
+ const coordinates = [
197
+ [lonMin, latMin],
198
+ [lonMax, latMin],
199
+ [lonMax, latMax],
200
+ [lonMin, latMax],
201
+ [lonMin, latMin]
202
+ ].map(coord => fromLonLat(coord));
203
+ // 다각형 피처 생성
204
+ const feature = new Feature({
205
+ geometry: new Polygon([coordinates]),
206
+ temperature: interpolatedTemp
207
+ });
208
+ source.addFeature(feature);
209
+ }
210
+ }
211
+ // 벡터 레이어 생성 - 경계선 제거
212
+ return new VectorLayer({
213
+ source,
214
+ zIndex: 6,
215
+ opacity: 0.8, // 전체 레이어 투명도 설정
216
+ className: 'temperature-layer', // 사용자 정의 클래스 이름 추가
217
+ style: feature => {
218
+ const temp = feature.get('temperature');
219
+ return new Style({
220
+ fill: new Fill({
221
+ color: getColorFromTemp(temp)
222
+ }),
223
+ // 경계선 완전히 제거
224
+ stroke: undefined
225
+ });
226
+ }
227
+ });
228
+ }
229
+ }
230
+ //# sourceMappingURL=temperature-layer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"temperature-layer.js","sourceRoot":"","sources":["../../../src/layers/temperature-layer.ts"],"names":[],"mappings":"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","sourcesContent":["import BaseLayer from 'ol/layer/Base'\nimport VectorSource from 'ol/source/Vector'\nimport { Heatmap as HeatmapLayer } from 'ol/layer'\nimport Feature from 'ol/Feature'\nimport Point from 'ol/geom/Point'\nimport { fromLonLat } from 'ol/proj'\nimport Polygon from 'ol/geom/Polygon'\nimport { Fill } from 'ol/style'\nimport VectorLayer from 'ol/layer/Vector'\nimport { Style, Stroke } from 'ol/style'\n\nimport { BaseWeatherLayer } from './base-weather-layer.js'\nimport { WeatherData, WeatherLayerType } from '../types.js'\n\n/**\n * 온도 레이어 구현\n */\nexport class TemperatureLayer extends BaseWeatherLayer {\n getType(): WeatherLayerType {\n return 'temperature'\n }\n\n /**\n * 히트맵 레이어를 생성하여 온도를 표현합니다.\n */\n createLayer(): BaseLayer {\n const source = new VectorSource()\n\n console.log('온도 레이어 생성 - 데이터 존재 여부:', !!this.weatherData, '데이터 개수:', this.weatherData?.length)\n\n if (!this.weatherData || this.weatherData.length === 0) {\n console.log('온도 레이어: 날씨 데이터가 없음')\n return new VectorLayer({\n source,\n zIndex: 5\n })\n }\n\n // 온도 값이 있는지 확인\n const validData = this.weatherData.filter(data => {\n const hasTemp = data.temperature && typeof data.temperature.current === 'number'\n if (!hasTemp) {\n console.log('온도 데이터 없음:', data)\n }\n return hasTemp\n })\n\n console.log('유효한 온도 데이터 수:', validData.length)\n\n if (validData.length === 0) {\n console.log('온도 레이어: 유효한 온도 데이터가 없음')\n return new VectorLayer({\n source,\n zIndex: 5\n })\n }\n\n // 온도 범위 계산\n let minTemp = Infinity\n let maxTemp = -Infinity\n\n validData.forEach(data => {\n const temp = data.temperature.current\n minTemp = Math.min(minTemp, temp)\n maxTemp = Math.max(maxTemp, temp)\n })\n\n // 각 데이터 포인트의 위치와 온도 추출\n const points = validData.map(data => ({\n lon: data.location.longitude,\n lat: data.location.latitude,\n temp: data.temperature.current\n }))\n\n console.log('온도 데이터 포인트:', points.length, '첫 번째 포인트:', points[0])\n\n // 온도 정규화 함수 - 0~1 범위로 변환\n const normalizeTemp = (temp: number) => {\n if (maxTemp === minTemp) return 0.5\n return (temp - minTemp) / (maxTemp - minTemp)\n }\n\n // 개선된 온도 보간 함수 - IDW(Inverse Distance Weighting) 기반 보간법\n const interpolateTemp = (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.temp\n }\n\n // 역거리 가중치 보간법 적용 - 거리의 제곱에 반비례하는 가중치 사용\n let totalWeight = 0\n let weightedTemp = 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 weightedTemp += point.temp * weight\n })\n\n return weightedTemp / totalWeight\n }\n\n // 온도에 따른 색상 계산 함수\n const getColorFromTemp = (temp: number) => {\n const normalized = normalizeTemp(temp)\n\n // HSL 색상 모델 사용 - 보다 자연스러운 온도 색상 맵핑\n // 파랑(추움, 240) -> 하양(중간, 180) -> 빨강(더움, 0)\n const hue = Math.max(0, Math.min(240, 240 - normalized * 240))\n\n // 온도에 따라 채도 변화 - 극단적 온도에서 더 선명하게\n const saturation = 70 + Math.abs(normalized - 0.5) * 60\n\n // 온도에 따라 밝기 조정 - 중간 온도는 더 밝게\n const lightness = 50 + Math.abs(normalized - 0.5) * 20\n\n // 투명도 설정 - 중간 온도는 더 투명하게, 극단적 온도는 더 선명하게\n const alpha = 0.4 + Math.abs(normalized - 0.5) * 0.4\n\n return `hsla(${hue}, ${saturation}%, ${lightness}%, ${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 = 120 // 적절한 해상도로 조정\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 tempGrid: number[][] = []\n\n for (let i = 0; i < gridResolution; i++) {\n tempGrid[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 tempGrid[i][j] = interpolateTemp(centerLon, centerLat)\n }\n }\n\n // 그리드 스무딩 적용\n const smoothedGrid = smoothGrid(tempGrid, 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 interpolatedTemp = smoothedGrid[i][j]\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 temperature: interpolatedTemp\n })\n\n source.addFeature(feature)\n }\n }\n\n // 벡터 레이어 생성 - 경계선 제거\n return new VectorLayer({\n source,\n zIndex: 6,\n opacity: 0.8, // 전체 레이어 투명도 설정\n className: 'temperature-layer', // 사용자 정의 클래스 이름 추가\n style: feature => {\n const temp = feature.get('temperature') as number\n return new Style({\n fill: new Fill({\n color: getColorFromTemp(temp)\n }),\n // 경계선 완전히 제거\n stroke: undefined\n })\n }\n })\n }\n}\n"]}
@@ -0,0 +1,60 @@
1
+ import BaseLayer from 'ol/layer/Base';
2
+ import { WeatherLayerType } from '../types';
3
+ import { BaseWeatherLayer } from './base-weather-layer';
4
+ /**
5
+ * 바람 레이어 클래스
6
+ * OpenLayers Flow 레이어를 사용하여 바람 흐름을 표현합니다.
7
+ * https://github.com/openlayers/openlayers/blob/v10.4.0/examples/wind.js 참고
8
+ */
9
+ export declare class WindLayer extends BaseWeatherLayer {
10
+ private windLayer?;
11
+ private windSource?;
12
+ protected layerType: string;
13
+ private maxSpeed;
14
+ private minSpeed;
15
+ private tileGrid;
16
+ private tileSize;
17
+ private weatherPoints;
18
+ /**
19
+ * 생성자
20
+ */
21
+ constructor(options?: any);
22
+ /**
23
+ * 레이어 타입 반환
24
+ */
25
+ getType(): WeatherLayerType;
26
+ /**
27
+ * 레이어 생성
28
+ * OpenLayers Flow 레이어를 사용하여 바람 흐름을 표현합니다.
29
+ * OpenLayers 공식 예제 참고: https://github.com/openlayers/openlayers/blob/v10.4.0/examples/wind.js
30
+ */
31
+ createLayer(): BaseLayer;
32
+ /**
33
+ * 날씨 데이터에서 바람 정보 추출
34
+ */
35
+ private prepareWindData;
36
+ /**
37
+ * 목업 바람 데이터 생성
38
+ */
39
+ private createMockWindData;
40
+ /**
41
+ * 타일에 바람 데이터 생성
42
+ */
43
+ private createWindTile;
44
+ /**
45
+ * 주어진 위치의 바람 벡터를 보간하여 반환
46
+ */
47
+ private interpolateWindVector;
48
+ /**
49
+ * 레이어 가시성 설정
50
+ */
51
+ setVisible(visible: boolean): void;
52
+ /**
53
+ * 레이어 제거
54
+ */
55
+ removeLayer(): void;
56
+ /**
57
+ * 레이어 업데이트
58
+ */
59
+ protected updateLayer(): void;
60
+ }