@operato/weather 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/CHANGELOG.md +21 -0
- package/README.md +337 -0
- package/assets/images/no-image.png +0 -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/stories/data-weather.stories.d.ts +47 -0
- package/dist/stories/data-weather.stories.js +93 -0
- package/dist/stories/data-weather.stories.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +86 -0
- package/themes/app-theme.css +138 -0
- package/themes/calendar-theme.css +61 -0
- package/themes/dark.css +51 -0
- package/themes/form-theme.css +70 -0
- package/themes/grist-theme.css +175 -0
- package/themes/layout-theme.css +94 -0
- package/themes/light.css +51 -0
- package/themes/material-theme.css +23 -0
- package/themes/md-typescale-styles.css +100 -0
- package/themes/oops-theme.css +22 -0
- package/themes/report-theme.css +47 -0
- package/themes/spacing.css +23 -0
- package/themes/state-color.css +6 -0
- package/themes/tooltip-theme.css +11 -0
- package/translations/en.json +1 -0
- package/translations/ja.json +1 -0
- package/translations/ko.json +1 -0
- package/translations/ms.json +1 -0
- package/translations/zh.json +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ox-weather.js","sourceRoot":"","sources":["../../src/ox-weather.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, css } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { LitElement } from 'lit'\nimport Map from 'ol/Map'\nimport View from 'ol/View'\nimport TileLayer from 'ol/layer/Tile'\nimport OSM from 'ol/source/OSM'\nimport XYZ from 'ol/source/XYZ'\nimport GeoJSON from 'ol/format/GeoJSON'\nimport { defaults as defaultControls, ScaleLine } from 'ol/control'\nimport { fromLonLat, transformExtent } from 'ol/proj'\nimport BaseLayer from 'ol/layer/Base'\nimport VectorLayer from 'ol/layer/Vector'\nimport Flow from 'ol/layer/Flow'\nimport colormap from 'colormap'\n\nimport { WeatherData, WeatherLayerType } from './types.js'\nimport { WeatherService } from './services/weather-service.js'\nimport { BaseWeatherLayer } from './layers/base-weather-layer.js'\nimport { WindLayer } from './layers/wind-layer.js'\nimport { PrecipitationLayer } from './layers/precipitation-layer.js'\nimport { TemperatureLayer } from './layers/temperature-layer.js'\nimport { SolarLayer } from './layers/solar-layer.js'\nimport WebGLVectorLayer from 'ol/layer/WebGLVector.js'\nimport VectorSource from 'ol/source/Vector.js'\n\n/**\n * 날씨 시각화 웹 컴포넌트\n */\n@customElement('ox-weather')\nexport class OxWeather extends LitElement {\n /**\n * 지도 객체\n */\n private map?: Map\n\n /**\n * 지도가 초기화되었는지 여부\n */\n private mapInitialized: boolean = false\n\n /**\n * 날씨 데이터 서비스\n */\n private weatherService: WeatherService = new WeatherService()\n\n /**\n * 날씨 데이터\n */\n private weatherData: WeatherData[] = []\n\n /**\n * 날씨 레이어 목록\n */\n private weatherLayers: BaseWeatherLayer[] = []\n\n /**\n * 위도 (지도 중심점)\n */\n @property({ type: Number })\n latitude: number = 36.0\n\n /**\n * 경도 (지도 중심점)\n */\n @property({ type: Number })\n longitude: number = 128.0\n\n /**\n * 줌 레벨\n */\n @property({ type: Number })\n zoom: number = 6\n\n /**\n * 활성 레이어 목록\n * - 'wind': 바람 레이어\n * - 'precipitation': 강수량 레이어\n * - 'temperature': 온도 레이어\n * - 'solar': 일사량 레이어\n */\n @property({ type: Array })\n activeLayers: WeatherLayerType[] = ['wind' /*, 'precipitation', 'temperature', 'solar' */]\n\n /**\n * 날씨 데이터 업데이트 쓰로틀링 관련 변수\n */\n private lastUpdateTime: number = 0\n private updateThrottleDelay: number = 500 // ms\n private updatePending: boolean = false\n\n // 로딩 상태 속성 추가\n @property({ type: Boolean })\n isLoading: boolean = false\n\n /**\n * 컴포넌트 스타일\n */\n static styles = css`\n :host {\n display: flex;\n flex-direction: column;\n height: 100%;\n }\n\n #map {\n width: 100%;\n height: 100%;\n background-color: #0f1621;\n position: relative;\n }\n\n .map-container {\n flex: 1;\n position: relative;\n width: 100%;\n background-color: #0f1621;\n }\n\n .layer-controls {\n position: absolute;\n top: 10px;\n right: 10px;\n background-color: rgba(0, 0, 0, 0.5);\n border-radius: 5px;\n padding: 10px;\n color: white;\n z-index: 1000;\n font-size: 14px;\n }\n\n .layer-control {\n display: flex;\n align-items: center;\n margin-bottom: 8px;\n }\n\n .layer-control:last-child {\n margin-bottom: 0;\n }\n\n .layer-switch {\n position: relative;\n display: inline-block;\n width: 40px;\n height: 20px;\n margin-right: 10px;\n }\n\n .layer-switch input {\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .slider {\n position: absolute;\n cursor: pointer;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: #333;\n transition: 0.4s;\n border-radius: 20px;\n }\n\n .slider:before {\n position: absolute;\n content: '';\n height: 16px;\n width: 16px;\n left: 2px;\n bottom: 2px;\n background-color: white;\n transition: 0.4s;\n border-radius: 50%;\n }\n\n input:checked + .slider {\n background-color: #2196f3;\n }\n\n input:checked + .slider:before {\n transform: translateX(20px);\n }\n\n .layer-info {\n font-weight: 300;\n color: #eee;\n user-select: none;\n }\n\n /* 로딩 인디케이터 */\n .loading-indicator {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n background-color: rgba(0, 0, 0, 0.7);\n color: white;\n padding: 10px 15px;\n border-radius: 5px;\n font-size: 14px;\n z-index: 1000;\n display: flex;\n align-items: center;\n gap: 10px;\n }\n\n .spinner {\n width: 20px;\n height: 20px;\n border: 3px solid rgba(255, 255, 255, 0.3);\n border-radius: 50%;\n border-top-color: white;\n animation: spin 1s ease-in-out infinite;\n }\n\n @keyframes spin {\n to {\n transform: rotate(360deg);\n }\n }\n\n /* 바람 레이어 스타일 */\n .wind-layer {\n pointer-events: none; /* 마우스 이벤트 통과 */\n }\n\n .wind-layer canvas {\n image-rendering: optimizeSpeed; /* 성능 최적화 */\n opacity: 0; /* 초기에는 숨겨두고 로딩 후 보여줌 */\n transition: opacity 0.5s ease-in-out;\n background-color: transparent !important;\n mix-blend-mode: multiply; /* 배경과 자연스럽게 혼합 */\n animation: fadeIn 0.8s forwards;\n }\n\n @keyframes fadeIn {\n 0% {\n opacity: 0;\n }\n 100% {\n opacity: 0.9;\n }\n }\n `\n\n /**\n * 컴포넌트 초기화\n */\n connectedCallback() {\n super.connectedCallback()\n\n // 날씨 레이어 초기화\n this.weatherLayers = [new WindLayer()] //, new PrecipitationLayer(), new TemperatureLayer(), new SolarLayer()]\n\n // API 서비스 초기화\n this.weatherService = new WeatherService()\n }\n\n /**\n * 첫 번째 업데이트 후 호출 (DOM 접근 가능)\n */\n firstUpdated() {\n this.initMap()\n this.initWeatherData()\n }\n\n /**\n * 지도를 초기화합니다.\n */\n private initMap(): void {\n const mapElement = this.shadowRoot?.querySelector('#map') as HTMLElement\n if (!mapElement) {\n console.error('지도 컨테이너 요소를 찾을 수 없습니다')\n return\n }\n\n this.map = new Map({\n target: mapElement,\n pixelRatio: 2,\n layers: [\n new WebGLVectorLayer({\n source: new VectorSource({\n url: 'https://openlayers.org/data/vector/ocean.json',\n format: new GeoJSON()\n }),\n style: {\n 'fill-color': '#555555'\n }\n })\n ],\n view: new View({\n center: fromLonLat([this.longitude, this.latitude]),\n zoom: this.zoom,\n projection: 'EPSG:3857',\n maxZoom: 19,\n minZoom: 2,\n enableRotation: false\n })\n })\n\n for (const layer of this.weatherLayers) {\n layer.addToMap(this.map)\n }\n\n // 지도 이벤트 리스너 (스케일 변화나 지도 패닝에 따른 데이터 업데이트 비활성화)\n /* \n this.map.on('moveend', () => {\n console.log('지도 이동 감지')\n this.updateWeatherData()\n })\n */\n }\n\n /**\n * 초기 날씨 데이터를 가져옵니다.\n */\n private async initWeatherData() {\n if (!this.map) return\n\n try {\n this.isLoading = true\n console.log('초기 날씨 데이터 로딩 시작')\n\n const view = this.map.getView()\n const extent = view.calculateExtent(this.map.getSize() || [100, 100])\n const projectedExtent = transformExtent(extent, 'EPSG:3857', 'EPSG:4326')\n\n await this.fetchWeatherData(projectedExtent)\n console.log('초기 날씨 데이터 로딩 완료')\n\n // 데이터와 맵이 준비된 후 레이어에 설정\n this.weatherLayers.forEach(layer => {\n layer.setWeatherData(this.weatherData)\n })\n } catch (error) {\n console.error('초기 날씨 데이터 로딩 실패:', error)\n } finally {\n this.isLoading = false\n }\n }\n\n /**\n * 날씨 데이터를 가져옵니다.\n */\n private async fetchWeatherData(bounds?: number[]): Promise<void> {\n try {\n this.isLoading = true\n\n // 날씨 데이터 가져오기 (bounds가 없으면 기본 영역 사용)\n if (bounds) {\n const boundingBox: [[number, number], [number, number]] = [\n [bounds[0], bounds[1]],\n [bounds[2], bounds[3]]\n ]\n this.weatherData = await this.weatherService.fetchWeatherData(boundingBox)\n } else {\n // 기본 한국 영역 설정\n const defaultBounds: [[number, number], [number, number]] = [\n [124.0, 33.0],\n [132.0, 39.0]\n ]\n this.weatherData = await this.weatherService.fetchWeatherData(defaultBounds)\n }\n\n // 각 레이어 업데이트를 분리하여 비동기적으로 처리 OK\n this.updateLayersAsync()\n\n this.isLoading = false\n } catch (error) {\n console.error('날씨 데이터 가져오기 실패:', error)\n this.isLoading = false\n }\n }\n\n /**\n * 날씨 데이터를 업데이트합니다\n */\n private updateWeatherData() {\n if (!this.map) return\n\n // 이미 로딩 중이면 중복 요청 방지 P\n if (this.isLoading) {\n console.log('이미 날씨 데이터 업데이트가 진행 중입니다')\n return\n }\n\n const now = Date.now()\n // 너무 빈번한 업데이트 방지 (마지막 업데이트로부터 500ms 이내면 무시)\n if (now - this.lastUpdateTime < this.updateThrottleDelay) {\n if (!this.updatePending) {\n this.updatePending = true\n setTimeout(() => {\n this.updatePending = false\n this.updateWeatherData()\n }, this.updateThrottleDelay)\n }\n return\n }\n\n this.lastUpdateTime = now\n\n const view = this.map.getView()\n const extent = view.calculateExtent(this.map.getSize() || [100, 100])\n const projectedExtent = transformExtent(extent, 'EPSG:3857', 'EPSG:4326')\n\n this.isLoading = true\n console.log('날씨 데이터 업데이트 요청')\n\n this.fetchWeatherData(projectedExtent)\n .then(() => {\n console.log('날씨 데이터 업데이트 완료')\n })\n .catch(error => {\n console.error('날씨 데이터 업데이트 실패:', error)\n })\n }\n\n /**\n * 날씨 데이터와 활성 레이어가 변경될 때 레이어 업데이트\n */\n updated(changedProperties: any) {\n super.updated(changedProperties)\n\n if (changedProperties.has('weatherData') || changedProperties.has('activeLayers')) {\n this.updateLayers()\n }\n }\n\n /**\n * 날씨 레이어를 비동기적으로 업데이트합니다.\n */\n private updateLayersAsync() {\n if (!this.map) return\n\n // 비동기 처리를 위해 setTimeout 사용\n setTimeout(() => {\n this.updateLayers()\n }, 0)\n }\n\n /**\n * 날씨 레이어를 업데이트합니다.\n */\n private updateLayers() {\n if (!this.map) return\n\n // 각 레이어 업데이트\n for (const layer of this.weatherLayers) {\n const isActive = this.activeLayers.includes(layer.getType())\n const layerInstance = layer.getLayer()\n\n if (!layerInstance) continue\n\n // 레이어 활성화/비활성화\n if (isActive) {\n // 중복 추가 방지를 위해 이미 맵에 있는지 확인\n if (!this.isLayerInMap(layerInstance)) {\n this.map.addLayer(layerInstance)\n }\n\n // 레이어에 날씨 데이터 설정 (비동기적으로 처리)\n setTimeout(() => {\n layer.setWeatherData(this.weatherData)\n }, 0)\n } else {\n // 맵에서 레이어 제거\n try {\n this.map.removeLayer(layerInstance)\n } catch (e) {\n // 이미 제거된 레이어에 대한 예외 무시\n console.log(`레이어 제거 중 오류 무시: ${layer.getType()}`)\n }\n }\n }\n }\n\n /**\n * 레이어가 이미 맵에 있는지 확인합니다.\n * 심층 비교를 통해 정확하게 체크합니다.\n */\n private isLayerInMap(layer: BaseLayer): boolean {\n if (!this.map) return false\n\n try {\n const mapLayers = this.map.getLayers().getArray()\n\n // 레이어 ID 또는 고유 식별자를 통한 비교\n const layerId = layer.get('id') || layer.get('className') || layer.get('zIndex')\n\n // 레이어 식별자가 있는 경우 이를 이용해 비교\n if (layerId) {\n return mapLayers.some(\n mapLayer =>\n mapLayer.get('id') === layerId ||\n mapLayer.get('className') === layerId ||\n mapLayer.get('zIndex') === layerId\n )\n }\n\n // 식별자가 없으면 직접 참조 비교\n return mapLayers.includes(layer)\n } catch (e) {\n console.error('레이어 체크 실패:', e)\n return false\n }\n }\n\n /**\n * 바람 레이어 토글 처리 (특별 관리)\n */\n private handleWindLayerToggle(e: Event) {\n const checkbox = e.target as HTMLInputElement\n const isActive = checkbox.checked\n\n // 바람 레이어 찾기\n const windLayer = this.weatherLayers.find(layer => layer.getType() === 'wind')\n if (!windLayer) return\n\n // 활성화 상태 업데이트\n if (isActive) {\n // 활성화 목록에 추가\n if (!this.activeLayers.includes('wind')) {\n this.activeLayers = [...this.activeLayers, 'wind']\n }\n } else {\n // 활성화 목록에서 제거\n this.activeLayers = this.activeLayers.filter(type => type !== 'wind')\n }\n\n // 비동기적으로 바람 레이어 처리\n setTimeout(() => {\n this.updateWindLayer(windLayer, isActive)\n }, 100)\n }\n\n /**\n * 바람 레이어 업데이트 (최적화)\n */\n private updateWindLayer(windLayer: BaseWeatherLayer, isActive: boolean) {\n if (!this.map) return\n\n const layerInstance = windLayer.getLayer()\n if (!layerInstance) return\n\n console.log(`바람 레이어 ${isActive ? '활성화' : '비활성화'} 처리`)\n\n if (isActive) {\n // 이미 맵에 있는지 확인 후 추가\n if (!this.isLayerInMap(layerInstance)) {\n this.map.addLayer(layerInstance)\n console.log('바람 레이어를 지도에 추가했습니다')\n }\n\n // 데이터 설정 및 애니메이션 시작\n windLayer.setWeatherData(this.weatherData)\n\n // 애니메이션 강제 시작 (안전장치)\n setTimeout(() => {\n try {\n // @ts-ignore - 타입 오류 무시\n const windLayerInstance = windLayer as unknown as WindLayer\n windLayerInstance.startAnimation()\n console.log('바람 애니메이션 강제 시작 요청 완료')\n\n // CSS 스타일 설정 (깜박임 방지)\n const mapElement = this.map?.getTargetElement()\n if (mapElement) {\n const windCanvas = mapElement.querySelector('.wind-layer canvas')\n if (windCanvas) {\n // @ts-ignore: HTMLElement 속성 접근\n windCanvas.style.backgroundColor = 'transparent'\n // @ts-ignore: HTMLElement 속성 접근\n windCanvas.style.transition = 'opacity 0.5s ease-in-out'\n console.log('바람 레이어 캔버스 스타일 적용 완료')\n } else {\n console.log('바람 레이어 캔버스를 찾을 수 없습니다')\n }\n }\n } catch (e) {\n console.error('바람 애니메이션 시작 실패:', e)\n }\n }, 500)\n } else {\n // 맵에서 레이어 제거\n try {\n // @ts-ignore - 타입 오류 무시\n const windLayerInstance = windLayer as unknown as WindLayer\n windLayerInstance.stopAnimation()\n console.log('바람 애니메이션 중지 요청 완료')\n\n this.map.removeLayer(layerInstance)\n } catch (e) {\n console.log('바람 레이어 제거 중 오류 무시:', e)\n }\n }\n }\n\n /**\n * 레이어 토글 처리\n */\n private handleLayerToggle(e: Event, type: WeatherLayerType): void {\n // 바람 레이어는 특별 처리\n if (type === 'wind') {\n this.handleWindLayerToggle(e)\n return\n }\n\n const checkbox = e.target as HTMLInputElement\n const isActive = checkbox.checked\n\n if (isActive) {\n // 활성화 목록에 추가\n if (!this.activeLayers.includes(type)) {\n this.activeLayers = [...this.activeLayers, type]\n }\n } else {\n // 활성화 목록에서 제거\n this.activeLayers = this.activeLayers.filter(t => t !== type)\n }\n }\n\n render() {\n return html`\n <div class=\"map-container\">\n <div id=\"map\"></div>\n\n <div class=\"layer-controls\">\n <div class=\"layer-control\">\n <label class=\"layer-switch\">\n <input\n type=\"checkbox\"\n ?checked=${this.activeLayers.includes('wind')}\n @change=${(e: Event) => this.handleLayerToggle(e, 'wind')}\n />\n <span class=\"slider\"></span>\n </label>\n <div class=\"layer-info\">바람</div>\n </div>\n\n <div class=\"layer-control\">\n <label class=\"layer-switch\">\n <input\n type=\"checkbox\"\n ?checked=${this.activeLayers.includes('precipitation')}\n @change=${(e: Event) => this.handleLayerToggle(e, 'precipitation')}\n />\n <span class=\"slider\"></span>\n </label>\n <div class=\"layer-info\">강수량</div>\n </div>\n\n <div class=\"layer-control\">\n <label class=\"layer-switch\">\n <input\n type=\"checkbox\"\n ?checked=${this.activeLayers.includes('temperature')}\n @change=${(e: Event) => this.handleLayerToggle(e, 'temperature')}\n />\n <span class=\"slider\"></span>\n </label>\n <div class=\"layer-info\">온도</div>\n </div>\n\n <div class=\"layer-control\">\n <label class=\"layer-switch\">\n <input\n type=\"checkbox\"\n ?checked=${this.activeLayers.includes('solar')}\n @change=${(e: Event) => this.handleLayerToggle(e, 'solar')}\n />\n <span class=\"slider\"></span>\n </label>\n <div class=\"layer-info\">일사량</div>\n </div>\n </div>\n\n ${this.isLoading\n ? html`\n <div class=\"loading-indicator\">\n <div class=\"spinner\"></div>\n <div>날씨 데이터 로딩 중...</div>\n </div>\n `\n : ''}\n </div>\n `\n }\n}\n"]}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import { WeatherLayerType } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* 날씨 시각화 웹 컴포넌트
|
|
5
|
+
*/
|
|
6
|
+
export declare class OxWind extends LitElement {
|
|
7
|
+
/**
|
|
8
|
+
* 지도 객체
|
|
9
|
+
*/
|
|
10
|
+
private map?;
|
|
11
|
+
/**
|
|
12
|
+
* 지도가 초기화되었는지 여부
|
|
13
|
+
*/
|
|
14
|
+
private mapInitialized;
|
|
15
|
+
/**
|
|
16
|
+
* 날씨 데이터 서비스
|
|
17
|
+
*/
|
|
18
|
+
private weatherService;
|
|
19
|
+
/**
|
|
20
|
+
* 날씨 데이터
|
|
21
|
+
*/
|
|
22
|
+
private weatherData;
|
|
23
|
+
/**
|
|
24
|
+
* 날씨 레이어 목록
|
|
25
|
+
*/
|
|
26
|
+
private weatherLayers;
|
|
27
|
+
/**
|
|
28
|
+
* 위도 (지도 중심점)
|
|
29
|
+
*/
|
|
30
|
+
latitude: number;
|
|
31
|
+
/**
|
|
32
|
+
* 경도 (지도 중심점)
|
|
33
|
+
*/
|
|
34
|
+
longitude: number;
|
|
35
|
+
/**
|
|
36
|
+
* 줌 레벨
|
|
37
|
+
*/
|
|
38
|
+
zoom: number;
|
|
39
|
+
/**
|
|
40
|
+
* 활성 레이어 목록
|
|
41
|
+
* - 'wind': 바람 레이어
|
|
42
|
+
* - 'precipitation': 강수량 레이어
|
|
43
|
+
* - 'temperature': 온도 레이어
|
|
44
|
+
* - 'solar': 일사량 레이어
|
|
45
|
+
*/
|
|
46
|
+
activeLayers: WeatherLayerType[];
|
|
47
|
+
/**
|
|
48
|
+
* 날씨 데이터 업데이트 쓰로틀링 관련 변수
|
|
49
|
+
*/
|
|
50
|
+
private lastUpdateTime;
|
|
51
|
+
private updateThrottleDelay;
|
|
52
|
+
private updatePending;
|
|
53
|
+
isLoading: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* 컴포넌트 스타일
|
|
56
|
+
*/
|
|
57
|
+
static styles: import("lit").CSSResult;
|
|
58
|
+
/**
|
|
59
|
+
* 컴포넌트 초기화
|
|
60
|
+
*/
|
|
61
|
+
connectedCallback(): void;
|
|
62
|
+
/**
|
|
63
|
+
* 첫 번째 업데이트 후 호출 (DOM 접근 가능)
|
|
64
|
+
*/
|
|
65
|
+
firstUpdated(): void;
|
|
66
|
+
/**
|
|
67
|
+
* 지도를 초기화합니다.
|
|
68
|
+
*/
|
|
69
|
+
private initMap;
|
|
70
|
+
/**
|
|
71
|
+
* 초기 날씨 데이터를 가져옵니다.
|
|
72
|
+
*/
|
|
73
|
+
private initWeatherData;
|
|
74
|
+
/**
|
|
75
|
+
* 날씨 데이터를 가져옵니다.
|
|
76
|
+
*/
|
|
77
|
+
private fetchWeatherData;
|
|
78
|
+
/**
|
|
79
|
+
* 날씨 데이터를 업데이트합니다
|
|
80
|
+
*/
|
|
81
|
+
private updateWeatherData;
|
|
82
|
+
/**
|
|
83
|
+
* 날씨 데이터와 활성 레이어가 변경될 때 레이어 업데이트
|
|
84
|
+
*/
|
|
85
|
+
updated(changedProperties: any): void;
|
|
86
|
+
/**
|
|
87
|
+
* 날씨 레이어를 비동기적으로 업데이트합니다.
|
|
88
|
+
*/
|
|
89
|
+
private updateLayersAsync;
|
|
90
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
91
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
import { html, css } from 'lit';
|
|
3
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
4
|
+
import { LitElement } from 'lit';
|
|
5
|
+
import Map from 'ol/Map';
|
|
6
|
+
import View from 'ol/View';
|
|
7
|
+
import GeoJSON from 'ol/format/GeoJSON';
|
|
8
|
+
import { fromLonLat, transformExtent } from 'ol/proj';
|
|
9
|
+
import { WeatherService } from './services/weather-service.js';
|
|
10
|
+
import { WindLayer } from './layers/wind-layer.js';
|
|
11
|
+
import WebGLVectorLayer from 'ol/layer/WebGLVector.js';
|
|
12
|
+
import VectorSource from 'ol/source/Vector.js';
|
|
13
|
+
/**
|
|
14
|
+
* 날씨 시각화 웹 컴포넌트
|
|
15
|
+
*/
|
|
16
|
+
let OxWind = class OxWind extends LitElement {
|
|
17
|
+
constructor() {
|
|
18
|
+
super(...arguments);
|
|
19
|
+
/**
|
|
20
|
+
* 지도가 초기화되었는지 여부
|
|
21
|
+
*/
|
|
22
|
+
this.mapInitialized = false;
|
|
23
|
+
/**
|
|
24
|
+
* 날씨 데이터 서비스
|
|
25
|
+
*/
|
|
26
|
+
this.weatherService = new WeatherService();
|
|
27
|
+
/**
|
|
28
|
+
* 날씨 데이터
|
|
29
|
+
*/
|
|
30
|
+
this.weatherData = [];
|
|
31
|
+
/**
|
|
32
|
+
* 날씨 레이어 목록
|
|
33
|
+
*/
|
|
34
|
+
this.weatherLayers = [];
|
|
35
|
+
/**
|
|
36
|
+
* 위도 (지도 중심점)
|
|
37
|
+
*/
|
|
38
|
+
this.latitude = 36.0;
|
|
39
|
+
/**
|
|
40
|
+
* 경도 (지도 중심점)
|
|
41
|
+
*/
|
|
42
|
+
this.longitude = 128.0;
|
|
43
|
+
/**
|
|
44
|
+
* 줌 레벨
|
|
45
|
+
*/
|
|
46
|
+
this.zoom = 6;
|
|
47
|
+
/**
|
|
48
|
+
* 활성 레이어 목록
|
|
49
|
+
* - 'wind': 바람 레이어
|
|
50
|
+
* - 'precipitation': 강수량 레이어
|
|
51
|
+
* - 'temperature': 온도 레이어
|
|
52
|
+
* - 'solar': 일사량 레이어
|
|
53
|
+
*/
|
|
54
|
+
this.activeLayers = ['wind' /*, 'precipitation', 'temperature', 'solar' */];
|
|
55
|
+
/**
|
|
56
|
+
* 날씨 데이터 업데이트 쓰로틀링 관련 변수
|
|
57
|
+
*/
|
|
58
|
+
this.lastUpdateTime = 0;
|
|
59
|
+
this.updateThrottleDelay = 500; // ms
|
|
60
|
+
this.updatePending = false;
|
|
61
|
+
// 로딩 상태 속성 추가
|
|
62
|
+
this.isLoading = false;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 컴포넌트 스타일
|
|
66
|
+
*/
|
|
67
|
+
static { this.styles = css `
|
|
68
|
+
:host {
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
height: 100%;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#map {
|
|
75
|
+
width: 100%;
|
|
76
|
+
height: 100%;
|
|
77
|
+
background-color: #0f1621;
|
|
78
|
+
position: relative;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.map-container {
|
|
82
|
+
flex: 1;
|
|
83
|
+
position: relative;
|
|
84
|
+
width: 100%;
|
|
85
|
+
background-color: #0f1621;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.layer-controls {
|
|
89
|
+
position: absolute;
|
|
90
|
+
top: 10px;
|
|
91
|
+
right: 10px;
|
|
92
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
93
|
+
border-radius: 5px;
|
|
94
|
+
padding: 10px;
|
|
95
|
+
color: white;
|
|
96
|
+
z-index: 1000;
|
|
97
|
+
font-size: 14px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.layer-control {
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
margin-bottom: 8px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.layer-control:last-child {
|
|
107
|
+
margin-bottom: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.layer-switch {
|
|
111
|
+
position: relative;
|
|
112
|
+
display: inline-block;
|
|
113
|
+
width: 40px;
|
|
114
|
+
height: 20px;
|
|
115
|
+
margin-right: 10px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.layer-switch input {
|
|
119
|
+
opacity: 0;
|
|
120
|
+
width: 0;
|
|
121
|
+
height: 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.slider {
|
|
125
|
+
position: absolute;
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
top: 0;
|
|
128
|
+
left: 0;
|
|
129
|
+
right: 0;
|
|
130
|
+
bottom: 0;
|
|
131
|
+
background-color: #333;
|
|
132
|
+
transition: 0.4s;
|
|
133
|
+
border-radius: 20px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.slider:before {
|
|
137
|
+
position: absolute;
|
|
138
|
+
content: '';
|
|
139
|
+
height: 16px;
|
|
140
|
+
width: 16px;
|
|
141
|
+
left: 2px;
|
|
142
|
+
bottom: 2px;
|
|
143
|
+
background-color: white;
|
|
144
|
+
transition: 0.4s;
|
|
145
|
+
border-radius: 50%;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
input:checked + .slider {
|
|
149
|
+
background-color: #2196f3;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
input:checked + .slider:before {
|
|
153
|
+
transform: translateX(20px);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.layer-info {
|
|
157
|
+
font-weight: 300;
|
|
158
|
+
color: #eee;
|
|
159
|
+
user-select: none;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* 로딩 인디케이터 */
|
|
163
|
+
.loading-indicator {
|
|
164
|
+
position: absolute;
|
|
165
|
+
top: 50%;
|
|
166
|
+
left: 50%;
|
|
167
|
+
transform: translate(-50%, -50%);
|
|
168
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
169
|
+
color: white;
|
|
170
|
+
padding: 10px 15px;
|
|
171
|
+
border-radius: 5px;
|
|
172
|
+
font-size: 14px;
|
|
173
|
+
z-index: 1000;
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 10px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.spinner {
|
|
180
|
+
width: 20px;
|
|
181
|
+
height: 20px;
|
|
182
|
+
border: 3px solid rgba(255, 255, 255, 0.3);
|
|
183
|
+
border-radius: 50%;
|
|
184
|
+
border-top-color: white;
|
|
185
|
+
animation: spin 1s ease-in-out infinite;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@keyframes spin {
|
|
189
|
+
to {
|
|
190
|
+
transform: rotate(360deg);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* 바람 레이어 스타일 */
|
|
195
|
+
.wind-layer {
|
|
196
|
+
pointer-events: none; /* 마우스 이벤트 통과 */
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.wind-layer canvas {
|
|
200
|
+
image-rendering: optimizeSpeed; /* 성능 최적화 */
|
|
201
|
+
opacity: 0; /* 초기에는 숨겨두고 로딩 후 보여줌 */
|
|
202
|
+
transition: opacity 0.5s ease-in-out;
|
|
203
|
+
background-color: transparent !important;
|
|
204
|
+
mix-blend-mode: multiply; /* 배경과 자연스럽게 혼합 */
|
|
205
|
+
animation: fadeIn 0.8s forwards;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@keyframes fadeIn {
|
|
209
|
+
0% {
|
|
210
|
+
opacity: 0;
|
|
211
|
+
}
|
|
212
|
+
100% {
|
|
213
|
+
opacity: 0.9;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
`; }
|
|
217
|
+
/**
|
|
218
|
+
* 컴포넌트 초기화
|
|
219
|
+
*/
|
|
220
|
+
connectedCallback() {
|
|
221
|
+
super.connectedCallback();
|
|
222
|
+
// 날씨 레이어 초기화
|
|
223
|
+
this.weatherLayers = [new WindLayer()]; //, new PrecipitationLayer(), new TemperatureLayer(), new SolarLayer()]
|
|
224
|
+
// API 서비스 초기화
|
|
225
|
+
this.weatherService = new WeatherService();
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* 첫 번째 업데이트 후 호출 (DOM 접근 가능)
|
|
229
|
+
*/
|
|
230
|
+
firstUpdated() {
|
|
231
|
+
this.initMap();
|
|
232
|
+
this.initWeatherData();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* 지도를 초기화합니다.
|
|
236
|
+
*/
|
|
237
|
+
initMap() {
|
|
238
|
+
const mapElement = this.shadowRoot?.querySelector('#map');
|
|
239
|
+
if (!mapElement) {
|
|
240
|
+
console.error('지도 컨테이너 요소를 찾을 수 없습니다');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
this.map = new Map({
|
|
244
|
+
target: mapElement,
|
|
245
|
+
pixelRatio: 2,
|
|
246
|
+
layers: [
|
|
247
|
+
new WebGLVectorLayer({
|
|
248
|
+
source: new VectorSource({
|
|
249
|
+
url: 'https://openlayers.org/data/vector/ocean.json',
|
|
250
|
+
format: new GeoJSON()
|
|
251
|
+
}),
|
|
252
|
+
style: {
|
|
253
|
+
'fill-color': '#555555'
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
],
|
|
257
|
+
view: new View({
|
|
258
|
+
center: fromLonLat([this.longitude, this.latitude]),
|
|
259
|
+
zoom: this.zoom,
|
|
260
|
+
projection: 'EPSG:3857',
|
|
261
|
+
maxZoom: 19,
|
|
262
|
+
minZoom: 2,
|
|
263
|
+
enableRotation: false
|
|
264
|
+
})
|
|
265
|
+
});
|
|
266
|
+
for (const layer of this.weatherLayers) {
|
|
267
|
+
layer.addToMap(this.map);
|
|
268
|
+
}
|
|
269
|
+
// 지도 이벤트 리스너 (스케일 변화나 지도 패닝에 따른 데이터 업데이트 비활성화)
|
|
270
|
+
/*
|
|
271
|
+
this.map.on('moveend', () => {
|
|
272
|
+
console.log('지도 이동 감지')
|
|
273
|
+
this.updateWeatherData()
|
|
274
|
+
})
|
|
275
|
+
*/
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 초기 날씨 데이터를 가져옵니다.
|
|
279
|
+
*/
|
|
280
|
+
async initWeatherData() {
|
|
281
|
+
if (!this.map)
|
|
282
|
+
return;
|
|
283
|
+
try {
|
|
284
|
+
this.isLoading = true;
|
|
285
|
+
console.log('초기 날씨 데이터 로딩 시작');
|
|
286
|
+
const view = this.map.getView();
|
|
287
|
+
const extent = view.calculateExtent(this.map.getSize() || [100, 100]);
|
|
288
|
+
const projectedExtent = transformExtent(extent, 'EPSG:3857', 'EPSG:4326');
|
|
289
|
+
await this.fetchWeatherData(projectedExtent);
|
|
290
|
+
console.log('초기 날씨 데이터 로딩 완료');
|
|
291
|
+
// 데이터와 맵이 준비된 후 레이어에 설정
|
|
292
|
+
this.weatherLayers.forEach(layer => {
|
|
293
|
+
layer.setWeatherData(this.weatherData);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
console.error('초기 날씨 데이터 로딩 실패:', error);
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
this.isLoading = false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* 날씨 데이터를 가져옵니다.
|
|
305
|
+
*/
|
|
306
|
+
async fetchWeatherData(bounds) {
|
|
307
|
+
try {
|
|
308
|
+
this.isLoading = true;
|
|
309
|
+
// 날씨 데이터 가져오기 (bounds가 없으면 기본 영역 사용)
|
|
310
|
+
if (bounds) {
|
|
311
|
+
const boundingBox = [
|
|
312
|
+
[bounds[0], bounds[1]],
|
|
313
|
+
[bounds[2], bounds[3]]
|
|
314
|
+
];
|
|
315
|
+
this.weatherData = await this.weatherService.fetchWeatherData(boundingBox);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
// 기본 한국 영역 설정
|
|
319
|
+
const defaultBounds = [
|
|
320
|
+
[124.0, 33.0],
|
|
321
|
+
[132.0, 39.0]
|
|
322
|
+
];
|
|
323
|
+
this.weatherData = await this.weatherService.fetchWeatherData(defaultBounds);
|
|
324
|
+
}
|
|
325
|
+
// 각 레이어 업데이트를 분리하여 비동기적으로 처리 OK
|
|
326
|
+
this.updateLayersAsync();
|
|
327
|
+
this.isLoading = false;
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
console.error('날씨 데이터 가져오기 실패:', error);
|
|
331
|
+
this.isLoading = false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* 날씨 데이터를 업데이트합니다
|
|
336
|
+
*/
|
|
337
|
+
updateWeatherData() {
|
|
338
|
+
if (!this.map)
|
|
339
|
+
return;
|
|
340
|
+
// 이미 로딩 중이면 중복 요청 방지 P
|
|
341
|
+
if (this.isLoading) {
|
|
342
|
+
console.log('이미 날씨 데이터 업데이트가 진행 중입니다');
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const now = Date.now();
|
|
346
|
+
// 너무 빈번한 업데이트 방지 (마지막 업데이트로부터 500ms 이내면 무시)
|
|
347
|
+
if (now - this.lastUpdateTime < this.updateThrottleDelay) {
|
|
348
|
+
if (!this.updatePending) {
|
|
349
|
+
this.updatePending = true;
|
|
350
|
+
setTimeout(() => {
|
|
351
|
+
this.updatePending = false;
|
|
352
|
+
this.updateWeatherData();
|
|
353
|
+
}, this.updateThrottleDelay);
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this.lastUpdateTime = now;
|
|
358
|
+
const view = this.map.getView();
|
|
359
|
+
const extent = view.calculateExtent(this.map.getSize() || [100, 100]);
|
|
360
|
+
const projectedExtent = transformExtent(extent, 'EPSG:3857', 'EPSG:4326');
|
|
361
|
+
this.isLoading = true;
|
|
362
|
+
console.log('날씨 데이터 업데이트 요청');
|
|
363
|
+
this.fetchWeatherData(projectedExtent)
|
|
364
|
+
.then(() => {
|
|
365
|
+
console.log('날씨 데이터 업데이트 완료');
|
|
366
|
+
})
|
|
367
|
+
.catch(error => {
|
|
368
|
+
console.error('날씨 데이터 업데이트 실패:', error);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* 날씨 데이터와 활성 레이어가 변경될 때 레이어 업데이트
|
|
373
|
+
*/
|
|
374
|
+
updated(changedProperties) {
|
|
375
|
+
super.updated(changedProperties);
|
|
376
|
+
if (changedProperties.has('weatherData') || changedProperties.has('activeLayers')) {
|
|
377
|
+
// this.updateLayers()
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* 날씨 레이어를 비동기적으로 업데이트합니다.
|
|
382
|
+
*/
|
|
383
|
+
updateLayersAsync() {
|
|
384
|
+
if (!this.map)
|
|
385
|
+
return;
|
|
386
|
+
// 비동기 처리를 위해 setTimeout 사용
|
|
387
|
+
setTimeout(() => {
|
|
388
|
+
// this.updateLayers()
|
|
389
|
+
}, 0);
|
|
390
|
+
}
|
|
391
|
+
render() {
|
|
392
|
+
return html `
|
|
393
|
+
|
|
394
|
+
${this.isLoading
|
|
395
|
+
? html `
|
|
396
|
+
<div class="loading-indicator">
|
|
397
|
+
<div class="spinner"></div>
|
|
398
|
+
<div>날씨 데이터 로딩 중...</div>
|
|
399
|
+
</div>
|
|
400
|
+
`
|
|
401
|
+
: ''}
|
|
402
|
+
</div>
|
|
403
|
+
`;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
__decorate([
|
|
407
|
+
property({ type: Number })
|
|
408
|
+
], OxWind.prototype, "latitude", void 0);
|
|
409
|
+
__decorate([
|
|
410
|
+
property({ type: Number })
|
|
411
|
+
], OxWind.prototype, "longitude", void 0);
|
|
412
|
+
__decorate([
|
|
413
|
+
property({ type: Number })
|
|
414
|
+
], OxWind.prototype, "zoom", void 0);
|
|
415
|
+
__decorate([
|
|
416
|
+
property({ type: Array })
|
|
417
|
+
], OxWind.prototype, "activeLayers", void 0);
|
|
418
|
+
__decorate([
|
|
419
|
+
property({ type: Boolean })
|
|
420
|
+
], OxWind.prototype, "isLoading", void 0);
|
|
421
|
+
OxWind = __decorate([
|
|
422
|
+
customElement('ox-wind')
|
|
423
|
+
], OxWind);
|
|
424
|
+
export { OxWind };
|
|
425
|
+
//# sourceMappingURL=ox-wind.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ox-wind.js","sourceRoot":"","sources":["../../src/ox-wind.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, css } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { LitElement } from 'lit'\nimport Map from 'ol/Map'\nimport View from 'ol/View'\nimport TileLayer from 'ol/layer/Tile'\nimport OSM from 'ol/source/OSM'\nimport XYZ from 'ol/source/XYZ'\nimport GeoJSON from 'ol/format/GeoJSON'\nimport { defaults as defaultControls, ScaleLine } from 'ol/control'\nimport { fromLonLat, transformExtent } from 'ol/proj'\nimport BaseLayer from 'ol/layer/Base'\nimport VectorLayer from 'ol/layer/Vector'\nimport Flow from 'ol/layer/Flow'\nimport colormap from 'colormap'\n\nimport { WeatherData, WeatherLayerType } from './types.js'\nimport { WeatherService } from './services/weather-service.js'\nimport { BaseWeatherLayer } from './layers/base-weather-layer.js'\nimport { WindLayer } from './layers/wind-layer.js'\nimport { PrecipitationLayer } from './layers/precipitation-layer.js'\nimport { TemperatureLayer } from './layers/temperature-layer.js'\nimport { SolarLayer } from './layers/solar-layer.js'\nimport WebGLVectorLayer from 'ol/layer/WebGLVector.js'\nimport VectorSource from 'ol/source/Vector.js'\n\n/**\n * 날씨 시각화 웹 컴포넌트\n */\n@customElement('ox-wind')\nexport class OxWind extends LitElement {\n /**\n * 지도 객체\n */\n private map?: Map\n\n /**\n * 지도가 초기화되었는지 여부\n */\n private mapInitialized: boolean = false\n\n /**\n * 날씨 데이터 서비스\n */\n private weatherService: WeatherService = new WeatherService()\n\n /**\n * 날씨 데이터\n */\n private weatherData: WeatherData[] = []\n\n /**\n * 날씨 레이어 목록\n */\n private weatherLayers: BaseWeatherLayer[] = []\n\n /**\n * 위도 (지도 중심점)\n */\n @property({ type: Number })\n latitude: number = 36.0\n\n /**\n * 경도 (지도 중심점)\n */\n @property({ type: Number })\n longitude: number = 128.0\n\n /**\n * 줌 레벨\n */\n @property({ type: Number })\n zoom: number = 6\n\n /**\n * 활성 레이어 목록\n * - 'wind': 바람 레이어\n * - 'precipitation': 강수량 레이어\n * - 'temperature': 온도 레이어\n * - 'solar': 일사량 레이어\n */\n @property({ type: Array })\n activeLayers: WeatherLayerType[] = ['wind' /*, 'precipitation', 'temperature', 'solar' */]\n\n /**\n * 날씨 데이터 업데이트 쓰로틀링 관련 변수\n */\n private lastUpdateTime: number = 0\n private updateThrottleDelay: number = 500 // ms\n private updatePending: boolean = false\n\n // 로딩 상태 속성 추가\n @property({ type: Boolean })\n isLoading: boolean = false\n\n /**\n * 컴포넌트 스타일\n */\n static styles = css`\n :host {\n display: flex;\n flex-direction: column;\n height: 100%;\n }\n\n #map {\n width: 100%;\n height: 100%;\n background-color: #0f1621;\n position: relative;\n }\n\n .map-container {\n flex: 1;\n position: relative;\n width: 100%;\n background-color: #0f1621;\n }\n\n .layer-controls {\n position: absolute;\n top: 10px;\n right: 10px;\n background-color: rgba(0, 0, 0, 0.5);\n border-radius: 5px;\n padding: 10px;\n color: white;\n z-index: 1000;\n font-size: 14px;\n }\n\n .layer-control {\n display: flex;\n align-items: center;\n margin-bottom: 8px;\n }\n\n .layer-control:last-child {\n margin-bottom: 0;\n }\n\n .layer-switch {\n position: relative;\n display: inline-block;\n width: 40px;\n height: 20px;\n margin-right: 10px;\n }\n\n .layer-switch input {\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .slider {\n position: absolute;\n cursor: pointer;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: #333;\n transition: 0.4s;\n border-radius: 20px;\n }\n\n .slider:before {\n position: absolute;\n content: '';\n height: 16px;\n width: 16px;\n left: 2px;\n bottom: 2px;\n background-color: white;\n transition: 0.4s;\n border-radius: 50%;\n }\n\n input:checked + .slider {\n background-color: #2196f3;\n }\n\n input:checked + .slider:before {\n transform: translateX(20px);\n }\n\n .layer-info {\n font-weight: 300;\n color: #eee;\n user-select: none;\n }\n\n /* 로딩 인디케이터 */\n .loading-indicator {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n background-color: rgba(0, 0, 0, 0.7);\n color: white;\n padding: 10px 15px;\n border-radius: 5px;\n font-size: 14px;\n z-index: 1000;\n display: flex;\n align-items: center;\n gap: 10px;\n }\n\n .spinner {\n width: 20px;\n height: 20px;\n border: 3px solid rgba(255, 255, 255, 0.3);\n border-radius: 50%;\n border-top-color: white;\n animation: spin 1s ease-in-out infinite;\n }\n\n @keyframes spin {\n to {\n transform: rotate(360deg);\n }\n }\n\n /* 바람 레이어 스타일 */\n .wind-layer {\n pointer-events: none; /* 마우스 이벤트 통과 */\n }\n\n .wind-layer canvas {\n image-rendering: optimizeSpeed; /* 성능 최적화 */\n opacity: 0; /* 초기에는 숨겨두고 로딩 후 보여줌 */\n transition: opacity 0.5s ease-in-out;\n background-color: transparent !important;\n mix-blend-mode: multiply; /* 배경과 자연스럽게 혼합 */\n animation: fadeIn 0.8s forwards;\n }\n\n @keyframes fadeIn {\n 0% {\n opacity: 0;\n }\n 100% {\n opacity: 0.9;\n }\n }\n `\n\n /**\n * 컴포넌트 초기화\n */\n connectedCallback() {\n super.connectedCallback()\n\n // 날씨 레이어 초기화\n this.weatherLayers = [new WindLayer()] //, new PrecipitationLayer(), new TemperatureLayer(), new SolarLayer()]\n\n // API 서비스 초기화\n this.weatherService = new WeatherService()\n }\n\n /**\n * 첫 번째 업데이트 후 호출 (DOM 접근 가능)\n */\n firstUpdated() {\n this.initMap()\n this.initWeatherData()\n }\n\n /**\n * 지도를 초기화합니다.\n */\n private initMap(): void {\n const mapElement = this.shadowRoot?.querySelector('#map') as HTMLElement\n if (!mapElement) {\n console.error('지도 컨테이너 요소를 찾을 수 없습니다')\n return\n }\n\n this.map = new Map({\n target: mapElement,\n pixelRatio: 2,\n layers: [\n new WebGLVectorLayer({\n source: new VectorSource({\n url: 'https://openlayers.org/data/vector/ocean.json',\n format: new GeoJSON()\n }),\n style: {\n 'fill-color': '#555555'\n }\n })\n ],\n view: new View({\n center: fromLonLat([this.longitude, this.latitude]),\n zoom: this.zoom,\n projection: 'EPSG:3857',\n maxZoom: 19,\n minZoom: 2,\n enableRotation: false\n })\n })\n\n for (const layer of this.weatherLayers) {\n layer.addToMap(this.map)\n }\n\n // 지도 이벤트 리스너 (스케일 변화나 지도 패닝에 따른 데이터 업데이트 비활성화)\n /* \n this.map.on('moveend', () => {\n console.log('지도 이동 감지')\n this.updateWeatherData()\n })\n */\n }\n\n /**\n * 초기 날씨 데이터를 가져옵니다.\n */\n private async initWeatherData() {\n if (!this.map) return\n\n try {\n this.isLoading = true\n console.log('초기 날씨 데이터 로딩 시작')\n\n const view = this.map.getView()\n const extent = view.calculateExtent(this.map.getSize() || [100, 100])\n const projectedExtent = transformExtent(extent, 'EPSG:3857', 'EPSG:4326')\n\n await this.fetchWeatherData(projectedExtent)\n console.log('초기 날씨 데이터 로딩 완료')\n\n // 데이터와 맵이 준비된 후 레이어에 설정\n this.weatherLayers.forEach(layer => {\n layer.setWeatherData(this.weatherData)\n })\n } catch (error) {\n console.error('초기 날씨 데이터 로딩 실패:', error)\n } finally {\n this.isLoading = false\n }\n }\n\n /**\n * 날씨 데이터를 가져옵니다.\n */\n private async fetchWeatherData(bounds?: number[]): Promise<void> {\n try {\n this.isLoading = true\n\n // 날씨 데이터 가져오기 (bounds가 없으면 기본 영역 사용)\n if (bounds) {\n const boundingBox: [[number, number], [number, number]] = [\n [bounds[0], bounds[1]],\n [bounds[2], bounds[3]]\n ]\n this.weatherData = await this.weatherService.fetchWeatherData(boundingBox)\n } else {\n // 기본 한국 영역 설정\n const defaultBounds: [[number, number], [number, number]] = [\n [124.0, 33.0],\n [132.0, 39.0]\n ]\n this.weatherData = await this.weatherService.fetchWeatherData(defaultBounds)\n }\n\n // 각 레이어 업데이트를 분리하여 비동기적으로 처리 OK\n this.updateLayersAsync()\n\n this.isLoading = false\n } catch (error) {\n console.error('날씨 데이터 가져오기 실패:', error)\n this.isLoading = false\n }\n }\n\n /**\n * 날씨 데이터를 업데이트합니다\n */\n private updateWeatherData() {\n if (!this.map) return\n\n // 이미 로딩 중이면 중복 요청 방지 P\n if (this.isLoading) {\n console.log('이미 날씨 데이터 업데이트가 진행 중입니다')\n return\n }\n\n const now = Date.now()\n // 너무 빈번한 업데이트 방지 (마지막 업데이트로부터 500ms 이내면 무시)\n if (now - this.lastUpdateTime < this.updateThrottleDelay) {\n if (!this.updatePending) {\n this.updatePending = true\n setTimeout(() => {\n this.updatePending = false\n this.updateWeatherData()\n }, this.updateThrottleDelay)\n }\n return\n }\n\n this.lastUpdateTime = now\n\n const view = this.map.getView()\n const extent = view.calculateExtent(this.map.getSize() || [100, 100])\n const projectedExtent = transformExtent(extent, 'EPSG:3857', 'EPSG:4326')\n\n this.isLoading = true\n console.log('날씨 데이터 업데이트 요청')\n\n this.fetchWeatherData(projectedExtent)\n .then(() => {\n console.log('날씨 데이터 업데이트 완료')\n })\n .catch(error => {\n console.error('날씨 데이터 업데이트 실패:', error)\n })\n }\n\n /**\n * 날씨 데이터와 활성 레이어가 변경될 때 레이어 업데이트\n */\n updated(changedProperties: any) {\n super.updated(changedProperties)\n\n if (changedProperties.has('weatherData') || changedProperties.has('activeLayers')) {\n // this.updateLayers()\n }\n }\n\n /**\n * 날씨 레이어를 비동기적으로 업데이트합니다.\n */\n private updateLayersAsync() {\n if (!this.map) return\n\n // 비동기 처리를 위해 setTimeout 사용\n setTimeout(() => {\n // this.updateLayers()\n }, 0)\n }\n\n render() {\n return html`\n\n ${\n this.isLoading\n ? html`\n <div class=\"loading-indicator\">\n <div class=\"spinner\"></div>\n <div>날씨 데이터 로딩 중...</div>\n </div>\n `\n : ''\n }\n </div>\n `\n }\n}\n"]}
|