@things-factory/weather 9.0.0-beta.41
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 +8 -0
- package/assets/images/wind.png +0 -0
- package/client/bootstrap.ts +1 -0
- package/client/index.ts +0 -0
- package/client/ol-components/webgl-canvas-layer.ts +413 -0
- package/client/ol-components/webgl-image-static.ts +174 -0
- package/client/ol-components/wind.ts +120 -0
- package/client/pages/weather-page.ts +318 -0
- package/client/route.ts +7 -0
- package/client/tsconfig.json +11 -0
- package/config/config.development.js +25 -0
- package/config/config.production.js +7 -0
- package/data/geo-json/countries-max-geo.json +1 -0
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +2 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/index.d.ts +0 -0
- package/dist-client/index.js +2 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/ol-components/webgl-canvas-layer.d.ts +61 -0
- package/dist-client/ol-components/webgl-canvas-layer.js +364 -0
- package/dist-client/ol-components/webgl-canvas-layer.js.map +1 -0
- package/dist-client/ol-components/webgl-image-static.d.ts +41 -0
- package/dist-client/ol-components/webgl-image-static.js +135 -0
- package/dist-client/ol-components/webgl-image-static.js.map +1 -0
- package/dist-client/ol-components/wind.d.ts +3 -0
- package/dist-client/ol-components/wind.js +104 -0
- package/dist-client/ol-components/wind.js.map +1 -0
- package/dist-client/pages/weather-page.d.ts +18 -0
- package/dist-client/pages/weather-page.js +300 -0
- package/dist-client/pages/weather-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +8 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/controllers/open-meteo-adapter.d.ts +4 -0
- package/dist-server/controllers/open-meteo-adapter.js +83 -0
- package/dist-server/controllers/open-meteo-adapter.js.map +1 -0
- package/dist-server/controllers/types.d.ts +55 -0
- package/dist-server/controllers/types.js +3 -0
- package/dist-server/controllers/types.js.map +1 -0
- package/dist-server/controllers/weather-controller.d.ts +12 -0
- package/dist-server/controllers/weather-controller.js +164 -0
- package/dist-server/controllers/weather-controller.js.map +1 -0
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +4 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/routers/geo-router.d.ts +59 -0
- package/dist-server/routers/geo-router.js +277 -0
- package/dist-server/routers/geo-router.js.map +1 -0
- package/dist-server/routers/weather-router.d.ts +1 -0
- package/dist-server/routers/weather-router.js +10 -0
- package/dist-server/routers/weather-router.js.map +1 -0
- package/dist-server/routes.d.ts +1 -0
- package/dist-server/routes.js +11 -0
- package/dist-server/routes.js.map +1 -0
- package/dist-server/services/chrome-renderer.d.ts +13 -0
- package/dist-server/services/chrome-renderer.js +137 -0
- package/dist-server/services/chrome-renderer.js.map +1 -0
- package/dist-server/services/headless-pool.d.ts +4 -0
- package/dist-server/services/headless-pool.js +89 -0
- package/dist-server/services/headless-pool.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/dist-server/utils/svg-utils.d.ts +35 -0
- package/dist-server/utils/svg-utils.js +89 -0
- package/dist-server/utils/svg-utils.js.map +1 -0
- package/dist-server/utils/tile-utils.d.ts +38 -0
- package/dist-server/utils/tile-utils.js +64 -0
- package/dist-server/utils/tile-utils.js.map +1 -0
- package/package.json +45 -0
- package/server/controllers/open-meteo-adapter.ts +91 -0
- package/server/controllers/types.ts +63 -0
- package/server/controllers/weather-controller.ts +176 -0
- package/server/index.ts +1 -0
- package/server/routers/geo-router.ts +297 -0
- package/server/routers/weather-router.ts +8 -0
- package/server/routes.ts +10 -0
- package/server/services/chrome-renderer.ts +175 -0
- package/server/services/data.json +8438 -0
- package/server/services/headless-pool.ts +97 -0
- package/server/tsconfig.json +10 -0
- package/server/utils/svg-utils.ts +108 -0
- package/server/utils/tile-utils.ts +77 -0
- package/things-factory.config.js +8 -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,120 @@
|
|
|
1
|
+
import { get as getProjection, transform } from 'ol/proj.js'
|
|
2
|
+
import DataTileSource from 'ol/source/DataTile.js'
|
|
3
|
+
import { createXYZ, wrapX } from 'ol/tilegrid.js'
|
|
4
|
+
|
|
5
|
+
const windData: Promise<{ data: ArrayBuffer; width: number; height: number }> = new Promise((resolve, reject) => {
|
|
6
|
+
const image = new Image()
|
|
7
|
+
image.onload = () => {
|
|
8
|
+
const canvas = document.createElement('canvas')
|
|
9
|
+
const width = image.width
|
|
10
|
+
const height = image.height
|
|
11
|
+
canvas.width = width
|
|
12
|
+
canvas.height = height
|
|
13
|
+
|
|
14
|
+
const context = canvas.getContext('2d')!
|
|
15
|
+
context.drawImage(image, 0, 0)
|
|
16
|
+
const data = context.getImageData(0, 0, width, height).data
|
|
17
|
+
resolve({ data, width, height })
|
|
18
|
+
}
|
|
19
|
+
image.onerror = () => {
|
|
20
|
+
reject(new Error('failed to load'))
|
|
21
|
+
}
|
|
22
|
+
image.src = '/assets/images/wind.png'
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
function bilinearInterpolation(xAlong, yAlong, v11, v21, v12, v22) {
|
|
26
|
+
const q11 = (1 - xAlong) * (1 - yAlong) * v11
|
|
27
|
+
const q21 = xAlong * (1 - yAlong) * v21
|
|
28
|
+
const q12 = (1 - xAlong) * yAlong * v12
|
|
29
|
+
const q22 = xAlong * yAlong * v22
|
|
30
|
+
return q11 + q21 + q12 + q22
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function interpolatePixels(xAlong, yAlong, p11, p21, p12, p22) {
|
|
34
|
+
return p11.map((_, i) => bilinearInterpolation(xAlong, yAlong, p11[i], p21[i], p12[i], p22[i]))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const dataTileGrid = createXYZ()
|
|
38
|
+
const dataTileSize = 256
|
|
39
|
+
|
|
40
|
+
const inputImageProjection = getProjection('EPSG:4326')!
|
|
41
|
+
const dataTileProjection = getProjection('EPSG:3857')!
|
|
42
|
+
|
|
43
|
+
const inputBands = 4
|
|
44
|
+
const dataBands = 3
|
|
45
|
+
|
|
46
|
+
// range of wind velocities
|
|
47
|
+
// these values are stretched between 0 and 255 in the png
|
|
48
|
+
const minU = -21.32
|
|
49
|
+
const maxU = 26.8
|
|
50
|
+
const deltaU = maxU - minU
|
|
51
|
+
const minV = -21.57
|
|
52
|
+
const maxV = 21.42
|
|
53
|
+
const deltaV = maxV - minV
|
|
54
|
+
|
|
55
|
+
const wind = new DataTileSource({
|
|
56
|
+
// transition must be 0, see https://github.com/openlayers/openlayers/issues/16119
|
|
57
|
+
transition: 0,
|
|
58
|
+
wrapX: true,
|
|
59
|
+
async loader(z, x, y) {
|
|
60
|
+
const { data: inputData, width: inputWidth, height: inputHeight } = await windData
|
|
61
|
+
|
|
62
|
+
const tileCoord = wrapX(dataTileGrid, [z, x, y], dataTileProjection)
|
|
63
|
+
const extent = dataTileGrid.getTileCoordExtent(tileCoord)
|
|
64
|
+
const resolution = dataTileGrid.getResolution(z)
|
|
65
|
+
const data = new Float32Array(dataTileSize * dataTileSize * dataBands)
|
|
66
|
+
for (let row = 0; row < dataTileSize; ++row) {
|
|
67
|
+
let offset = row * dataTileSize * dataBands
|
|
68
|
+
const mapY = extent[3] - row * resolution
|
|
69
|
+
for (let col = 0; col < dataTileSize; ++col) {
|
|
70
|
+
const mapX = extent[0] + col * resolution
|
|
71
|
+
const [lon, lat] = transform([mapX, mapY], dataTileProjection, inputImageProjection)
|
|
72
|
+
|
|
73
|
+
const x = (inputWidth * (lon + 180)) / 360
|
|
74
|
+
let x1 = Math.floor(x)
|
|
75
|
+
let x2 = Math.ceil(x)
|
|
76
|
+
const xAlong = x - x1
|
|
77
|
+
if (x1 < 0) {
|
|
78
|
+
x1 += inputWidth
|
|
79
|
+
}
|
|
80
|
+
if (x2 >= inputWidth) {
|
|
81
|
+
x2 -= inputWidth
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const y = (inputHeight * (90 - lat)) / 180
|
|
85
|
+
let y1 = Math.floor(y)
|
|
86
|
+
let y2 = Math.ceil(y)
|
|
87
|
+
const yAlong = y - y1
|
|
88
|
+
if (y1 < 0) {
|
|
89
|
+
y1 = 0
|
|
90
|
+
}
|
|
91
|
+
if (y2 >= inputHeight) {
|
|
92
|
+
y2 = inputHeight - 1
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const corners = [
|
|
96
|
+
[x1, y1],
|
|
97
|
+
[x2, y1],
|
|
98
|
+
[x1, y2],
|
|
99
|
+
[x2, y2]
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
const pixels = corners.map(([cx, cy]) => {
|
|
103
|
+
const inputOffset = (cy * 360 + cx) * inputBands
|
|
104
|
+
return [inputData[inputOffset], inputData[inputOffset + 1]]
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const interpolated = interpolatePixels(xAlong, yAlong, ...(pixels as [any, any, any, any]))
|
|
108
|
+
const u = minU + (deltaU * interpolated[0]) / 255
|
|
109
|
+
const v = minV + (deltaV * interpolated[1]) / 255
|
|
110
|
+
|
|
111
|
+
data[offset] = u
|
|
112
|
+
data[offset + 1] = v
|
|
113
|
+
offset += dataBands
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return data
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
export default wind
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { css, html } from 'lit'
|
|
2
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
3
|
+
import { PageView } from '@operato/shell'
|
|
4
|
+
|
|
5
|
+
import Map from 'ol/Map.js'
|
|
6
|
+
import View from 'ol/View.js'
|
|
7
|
+
import TileLayer from 'ol/layer/Tile.js'
|
|
8
|
+
import { fromLonLat } from 'ol/proj.js'
|
|
9
|
+
import 'ol/ol.css'
|
|
10
|
+
import { MVT } from 'ol/format'
|
|
11
|
+
import { DEVICE_PIXEL_RATIO } from 'ol/has.js'
|
|
12
|
+
import Flow from 'ol/layer/Flow.js'
|
|
13
|
+
|
|
14
|
+
import ImageLayer from 'ol/layer/Image'
|
|
15
|
+
import WebGLImageStatic from '../ol-components/webgl-image-static'
|
|
16
|
+
import { VectorTile, XYZ } from 'ol/source'
|
|
17
|
+
import VectorSource from 'ol/source/Vector'
|
|
18
|
+
import colormap from 'colormap'
|
|
19
|
+
|
|
20
|
+
import Heatmap from 'ol/layer/Heatmap.js'
|
|
21
|
+
import Feature from 'ol/Feature.js'
|
|
22
|
+
import Point from 'ol/geom/Point.js'
|
|
23
|
+
import { Stroke, Style } from 'ol/style.js'
|
|
24
|
+
|
|
25
|
+
import wind from '../ol-components/wind.js'
|
|
26
|
+
import VectorTileLayer from 'ol/layer/VectorTile'
|
|
27
|
+
|
|
28
|
+
const colors = colormap({
|
|
29
|
+
colormap: 'jet',
|
|
30
|
+
nshades: 10,
|
|
31
|
+
alpha: 0.3,
|
|
32
|
+
format: 'rgba'
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
@customElement('weather-page')
|
|
36
|
+
export class WeatherPage extends PageView {
|
|
37
|
+
static get styles() {
|
|
38
|
+
return css`
|
|
39
|
+
:host {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
height: 100%;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.map-container {
|
|
46
|
+
flex: 1;
|
|
47
|
+
display: flex;
|
|
48
|
+
position: relative;
|
|
49
|
+
background-color: #f9f9f9;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#map {
|
|
54
|
+
flex: 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.controls {
|
|
58
|
+
position: absolute;
|
|
59
|
+
top: 10px;
|
|
60
|
+
right: 10px;
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: 10px;
|
|
64
|
+
background: rgba(255, 255, 255, 0.8);
|
|
65
|
+
padding: 15px;
|
|
66
|
+
border-radius: 4px;
|
|
67
|
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
68
|
+
z-index: 10;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.weather-btn {
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
padding: 8px 12px;
|
|
74
|
+
background-color: #f0f0f0;
|
|
75
|
+
border: 1px solid #ddd;
|
|
76
|
+
border-radius: 4px;
|
|
77
|
+
font-weight: bold;
|
|
78
|
+
transition: all 0.3s;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.weather-btn.active {
|
|
82
|
+
background-color: #4281f5;
|
|
83
|
+
color: white;
|
|
84
|
+
border-color: #2d5ed9;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.refresh-btn {
|
|
88
|
+
margin-top: 10px;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
padding: 8px 12px;
|
|
91
|
+
background-color: #00c851;
|
|
92
|
+
color: white;
|
|
93
|
+
border: none;
|
|
94
|
+
border-radius: 4px;
|
|
95
|
+
font-weight: bold;
|
|
96
|
+
transition: all 0.3s;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.refresh-btn:hover {
|
|
100
|
+
background-color: #00a651;
|
|
101
|
+
}
|
|
102
|
+
`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@property({ type: String }) private activeType = 'temperature'
|
|
106
|
+
@state() private map: any
|
|
107
|
+
@state() private weatherLayer: any
|
|
108
|
+
@state() private windLayer: any
|
|
109
|
+
|
|
110
|
+
constructor() {
|
|
111
|
+
super()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
render() {
|
|
115
|
+
return html`
|
|
116
|
+
<div class="map-container">
|
|
117
|
+
<div id="map"></div>
|
|
118
|
+
|
|
119
|
+
<div class="controls">
|
|
120
|
+
<button
|
|
121
|
+
class="weather-btn ${this.activeType === 'temperature' ? 'active' : ''}"
|
|
122
|
+
@click=${() => this._changeWeatherType('temperature')}
|
|
123
|
+
>
|
|
124
|
+
온도
|
|
125
|
+
</button>
|
|
126
|
+
<button
|
|
127
|
+
class="weather-btn ${this.activeType === 'humidity' ? 'active' : ''}"
|
|
128
|
+
@click=${() => this._changeWeatherType('humidity')}
|
|
129
|
+
>
|
|
130
|
+
습도
|
|
131
|
+
</button>
|
|
132
|
+
<button
|
|
133
|
+
class="weather-btn ${this.activeType === 'wind' ? 'active' : ''}"
|
|
134
|
+
@click=${() => this._changeWeatherType('wind')}
|
|
135
|
+
>
|
|
136
|
+
바람
|
|
137
|
+
</button>
|
|
138
|
+
<button
|
|
139
|
+
class="weather-btn ${this.activeType === 'precipitation' ? 'active' : ''}"
|
|
140
|
+
@click=${() => this._changeWeatherType('precipitation')}
|
|
141
|
+
>
|
|
142
|
+
강수량
|
|
143
|
+
</button>
|
|
144
|
+
<button class="refresh-btn" @click=${this._refreshWeatherMap}>새로고침</button>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
`
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
connectedCallback(): void {
|
|
151
|
+
super.connectedCallback()
|
|
152
|
+
|
|
153
|
+
requestAnimationFrame(() => {
|
|
154
|
+
this._initMap()
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
disconnectedCallback(): void {
|
|
159
|
+
super.disconnectedCallback()
|
|
160
|
+
|
|
161
|
+
this.map.removeLayer(this.weatherLayer)
|
|
162
|
+
this.weatherLayer.dispose()
|
|
163
|
+
this.map.dispose()
|
|
164
|
+
|
|
165
|
+
this.map = null
|
|
166
|
+
this.weatherLayer = null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private _initMap() {
|
|
170
|
+
const lightOSM = new TileLayer({
|
|
171
|
+
source: new XYZ({
|
|
172
|
+
// url: 'https://{a-c}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png',
|
|
173
|
+
// url: 'https://{a-c}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png',
|
|
174
|
+
url: 'https://{a-c}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png',
|
|
175
|
+
// url: 'https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
|
|
176
|
+
// url: 'https://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
|
|
177
|
+
// url: 'https://api.maptiler.com/maps/toner-lines/{z}/{x}/{y}.png?key=uHikv5CybbRm7cDLjRca'
|
|
178
|
+
// attributions: '© MapTiler'
|
|
179
|
+
attributions: '© OpenStreetMap contributors, © CartoDB'
|
|
180
|
+
}),
|
|
181
|
+
background: 'tranparent',
|
|
182
|
+
opacity: 1
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const labelOSM = new TileLayer({
|
|
186
|
+
source: new XYZ({
|
|
187
|
+
url: 'https://{a-c}.basemaps.cartocdn.com/dark_only_labels/{z}/{x}/{y}{r}.png'
|
|
188
|
+
// url: 'https://{a-c}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png'
|
|
189
|
+
}),
|
|
190
|
+
background: 'tranparent',
|
|
191
|
+
opacity: 1
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
const vectorTile = new VectorTile({
|
|
195
|
+
format: new MVT({
|
|
196
|
+
layerName: 'countries',
|
|
197
|
+
layers: ['countries']
|
|
198
|
+
}),
|
|
199
|
+
url: `/geo/tiles/{z}/{x}/{y}.mvt`,
|
|
200
|
+
maxZoom: 10,
|
|
201
|
+
projection: 'EPSG:3857'
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const outlineLayer = new VectorTileLayer({
|
|
205
|
+
source: vectorTile,
|
|
206
|
+
style: new Style({
|
|
207
|
+
stroke: new Stroke({
|
|
208
|
+
color: '#858585',
|
|
209
|
+
width: 1
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
console.log('🛠 벡터 타일 레이어:', outlineLayer, outlineLayer.getSource())
|
|
215
|
+
|
|
216
|
+
/* heatmap */
|
|
217
|
+
|
|
218
|
+
const generateHeatmapData = () => {
|
|
219
|
+
const features: Feature[] = []
|
|
220
|
+
for (let i = 0; i < 20000; i++) {
|
|
221
|
+
// 더 많은 데이터 포인트 생성
|
|
222
|
+
const lon = Math.random() * 360 - 180 // 전 세계 경도 (-180 ~ 180)
|
|
223
|
+
const lat = Math.random() * 170 - 85 // 전 세계 위도 (-85 ~ 85)
|
|
224
|
+
|
|
225
|
+
const weight = Math.random() * 1 // 가중치 (0 ~ 2)
|
|
226
|
+
|
|
227
|
+
const feature = new Feature({
|
|
228
|
+
geometry: new Point(fromLonLat([lon, lat])),
|
|
229
|
+
weight: weight
|
|
230
|
+
})
|
|
231
|
+
features.push(feature)
|
|
232
|
+
}
|
|
233
|
+
return features
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const vectorSource = new VectorSource({
|
|
237
|
+
features: generateHeatmapData()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const heatmapLayer = new Heatmap({
|
|
241
|
+
source: vectorSource,
|
|
242
|
+
blur: 60, // 흐림 정도 증가
|
|
243
|
+
radius: 60, // 점의 반경 증가
|
|
244
|
+
weight: feature => feature.get('weight'), // 가중치 적용
|
|
245
|
+
gradient: colors.map(c => `rgba(${c.join(',')})`), // 색상 적용
|
|
246
|
+
opacity: 0.8
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
this.map = new Map({
|
|
250
|
+
target: this.renderRoot.querySelector('#map') as HTMLElement,
|
|
251
|
+
pixelRatio: Math.min(DEVICE_PIXEL_RATIO, 2),
|
|
252
|
+
layers: [lightOSM, heatmapLayer, outlineLayer, labelOSM],
|
|
253
|
+
view: new View({
|
|
254
|
+
center: fromLonLat([127.5, 36.5]),
|
|
255
|
+
zoom: 7.5
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// this._addWeatherLayer()
|
|
260
|
+
this._addWindLayer()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private _addWindLayer() {
|
|
264
|
+
if (this.windLayer) {
|
|
265
|
+
this.map.removeLayer(this.windLayer)
|
|
266
|
+
console.log('🚨 기존 날씨 레이어 제거')
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const maxSpeed = 20
|
|
270
|
+
|
|
271
|
+
const colorStops: (number | [number, number, number, number])[] = []
|
|
272
|
+
for (let i = 0; i < colors.length; ++i) {
|
|
273
|
+
colorStops.push((i * maxSpeed) / (colors.length - 1))
|
|
274
|
+
colorStops.push(colors[i])
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
this.windLayer = new Flow({
|
|
278
|
+
source: wind,
|
|
279
|
+
maxSpeed,
|
|
280
|
+
particles: 20000,
|
|
281
|
+
style: {
|
|
282
|
+
color: ['interpolate', ['linear'], ['get', 'speed'], ...colorStops]
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
this.map.addLayer(this.windLayer)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private _addWeatherLayer() {
|
|
290
|
+
if (this.weatherLayer) {
|
|
291
|
+
this.map.removeLayer(this.weatherLayer)
|
|
292
|
+
console.log('🚨 기존 날씨 레이어 제거')
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.weatherLayer = new ImageLayer({
|
|
296
|
+
source: new WebGLImageStatic({
|
|
297
|
+
url: `/weather/map/${this.activeType}.png`,
|
|
298
|
+
imageExtent: [-20037508.3427892, -20037508.3427892, 20037508.3427892, 20037508.3427892],
|
|
299
|
+
interpolation: true,
|
|
300
|
+
projection: 'EPSG:3857' // 좌표 시스템 설정
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
this.map.addLayer(this.weatherLayer)
|
|
305
|
+
|
|
306
|
+
console.log('🚨 날씨 레이어 추가', this.map.getLayers())
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private _changeWeatherType(type: string) {
|
|
310
|
+
if (this.activeType === type) return
|
|
311
|
+
this.activeType = type
|
|
312
|
+
this._addWeatherLayer()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private _refreshWeatherMap() {
|
|
316
|
+
this._addWeatherLayer()
|
|
317
|
+
}
|
|
318
|
+
}
|
package/client/route.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const DEFAULT_CACHE_TTL = 3600 // 1시간 (초 단위)
|
|
2
|
+
const DEFAULT_TILE_SIZE = 256
|
|
3
|
+
const DEFAULT_MAX_ZOOM = 14
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
weather: {
|
|
7
|
+
cacheTtl: DEFAULT_CACHE_TTL * 1000, // 밀리초 단위로 변환
|
|
8
|
+
|
|
9
|
+
// 타일 설정
|
|
10
|
+
tileSize: DEFAULT_TILE_SIZE,
|
|
11
|
+
maxZoom: DEFAULT_MAX_ZOOM,
|
|
12
|
+
|
|
13
|
+
// Mock 데이터 사용 여부
|
|
14
|
+
useMockData: true,
|
|
15
|
+
|
|
16
|
+
// 타일 생성 설정
|
|
17
|
+
defaultResolution: 8, // 기본 그리드 해상도
|
|
18
|
+
minResolution: 4, // 최소 그리드 해상도 (낮은 줌 레벨에서 사용)
|
|
19
|
+
maxResolution: 16, // 최대 그리드 해상도 (높은 줌 레벨에서 사용)
|
|
20
|
+
|
|
21
|
+
openWeatherMap: {
|
|
22
|
+
ApiKey: '41937147fdd666b7184c85f8388b845b'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|