@operato/scene-openlayers 1.2.54

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +13 -0
  3. package/assets/favicon.ico +0 -0
  4. package/assets/images/spinner.png +0 -0
  5. package/db.sqlite +0 -0
  6. package/dist/editors/index.d.ts +0 -0
  7. package/dist/editors/index.js +2 -0
  8. package/dist/editors/index.js.map +1 -0
  9. package/dist/groups/geography.d.ts +6 -0
  10. package/dist/groups/geography.js +48 -0
  11. package/dist/groups/geography.js.map +1 -0
  12. package/dist/groups/index.d.ts +7 -0
  13. package/dist/groups/index.js +3 -0
  14. package/dist/groups/index.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +3 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/ol-marker.d.ts +79 -0
  19. package/dist/ol-marker.js +247 -0
  20. package/dist/ol-marker.js.map +1 -0
  21. package/dist/openlayers.d.ts +37 -0
  22. package/dist/openlayers.js +211 -0
  23. package/dist/openlayers.js.map +1 -0
  24. package/dist/templates/index.d.ts +14 -0
  25. package/dist/templates/index.js +4 -0
  26. package/dist/templates/index.js.map +1 -0
  27. package/dist/templates/ol-marker copy.d.ts +14 -0
  28. package/dist/templates/ol-marker copy.js +16 -0
  29. package/dist/templates/ol-marker copy.js.map +1 -0
  30. package/dist/templates/ol-marker.d.ts +14 -0
  31. package/dist/templates/ol-marker.js +16 -0
  32. package/dist/templates/ol-marker.js.map +1 -0
  33. package/dist/templates/ol-path.d.ts +14 -0
  34. package/dist/templates/ol-path.js +16 -0
  35. package/dist/templates/ol-path.js.map +1 -0
  36. package/dist/templates/openlayers.d.ts +14 -0
  37. package/dist/templates/openlayers.js +16 -0
  38. package/dist/templates/openlayers.js.map +1 -0
  39. package/dist/tsconfig.tsbuildinfo +1 -0
  40. package/icons/ol-marker-template.png +0 -0
  41. package/icons/ol-path-template.png +0 -0
  42. package/icons/openlayers-template.png +0 -0
  43. package/logs/.08636eb59927f12972f6774f5947c8507b3564c2-audit.json +15 -0
  44. package/logs/.5e5d741d8b7784a2fbad65eedc0fd46946aaf6f2-audit.json +15 -0
  45. package/logs/application-2023-09-02-17.log +15 -0
  46. package/logs/connections-2023-09-02-17.log +76 -0
  47. package/package.json +63 -0
  48. package/schema.gql +3702 -0
  49. package/src/editors/index.ts +0 -0
  50. package/src/groups/geography.ts +48 -0
  51. package/src/groups/index.ts +3 -0
  52. package/src/index.ts +2 -0
  53. package/src/ol-marker.ts +318 -0
  54. package/src/ol-path.ts_x +368 -0
  55. package/src/openlayers.ts +256 -0
  56. package/src/templates/index.ts +4 -0
  57. package/src/templates/ol-marker.ts +16 -0
  58. package/src/templates/ol-path.ts +16 -0
  59. package/src/templates/openlayers.ts +16 -0
  60. package/things-scene.config.js +7 -0
  61. package/translations/en.json +3 -0
  62. package/translations/ko.json +3 -0
  63. package/translations/ms.json +3 -0
  64. package/translations/zh.json +3 -0
  65. package/tsconfig.json +23 -0
  66. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,368 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+
5
+ import { Component, RectPath, Shape } from '@hatiolab/things-scene'
6
+
7
+ const NATURE = {
8
+ mutable: false,
9
+ resizable: true,
10
+ rotatable: true,
11
+ properties: [
12
+ {
13
+ type: 'id-input',
14
+ label: 'target-map',
15
+ name: 'targetMap',
16
+ property: {
17
+ component: 'google-map'
18
+ }
19
+ },
20
+ {
21
+ type: 'checkbox',
22
+ label: 'show-path',
23
+ name: 'showPath'
24
+ },
25
+ {
26
+ type: 'checkbox',
27
+ label: 'show-intermediate-markers',
28
+ name: 'showIntermediateMarkers'
29
+ },
30
+ {
31
+ type: 'checkbox',
32
+ label: 'start-end-marker-different-design',
33
+ name: 'startEndMarkerDifferentDesign'
34
+ }
35
+ ],
36
+ 'value-property': 'latlngs'
37
+ // help: 'scene/component/gmap-path'
38
+ }
39
+
40
+ const EMPTY_MARKER_PATH = 'M 0,0 C -2,-20 -10,-22 -10,-30 A 10,10 0 1,1 10,-30 C 10,-22 2,-20 0,0 z'
41
+ const END_MARKER_PATH =
42
+ 'M 0,0 C -2,-20 -10,-22 -10,-30 A 10,10 0 1,1 10,-30 C 10,-22 2,-20 0,0 z M -2,-30 a 2,2 0 1,1 4,0 2,2 0 1,1 -4,0'
43
+ const START_MARKER_PATH =
44
+ 'M 0,0 C -2,-20 -10,-22 -10,-30 A 10,10 0 1,1 10,-30 C 10,-22 2,-20 0,0 z m -3,-34 l 0,8 l 8,-4 l -8,-4 z m -0,-0 l 0,8 l 8,-4 l -8,-4'
45
+
46
+ export default class GMapPath extends RectPath(Shape) {
47
+ _infoWindow: any
48
+ _map: any
49
+
50
+ dispose() {
51
+ this.markers && this.markers.forEach(marker => marker.setMap(null))
52
+
53
+ this.markers = null
54
+ delete this._infoWindow
55
+
56
+ super.dispose()
57
+ }
58
+
59
+ ready() {
60
+ super.ready()
61
+
62
+ if (this.isTemplate()) {
63
+ return
64
+ }
65
+
66
+ this.onchangeTargetMap()
67
+ }
68
+
69
+ get map() {
70
+ return this._map
71
+ }
72
+
73
+ findInfoWindow(type) {
74
+ var eventSetting = (this.state.event && this.state.event[type]) || {}
75
+
76
+ var infoWindow =
77
+ /* event spec v1.0 */ eventSetting.infoWindow ||
78
+ /* event spec v1.1 */ (eventSetting.action == 'infoWindow' && eventSetting.target)
79
+
80
+ if (infoWindow) {
81
+ return this.root.findById(infoWindow)
82
+ }
83
+ }
84
+
85
+ getInfoContent(sceneInfoWindow, index) {
86
+ var tpl = Component.template(sceneInfoWindow.model.frontSideTemplate)
87
+ return (
88
+ `<style>${sceneInfoWindow.model.style}</style>` +
89
+ tpl({
90
+ data: this.data,
91
+ index
92
+ })
93
+ )
94
+ }
95
+
96
+ openInfoWindow(iw, index) {
97
+ var content = this.getInfoContent(iw, index)
98
+
99
+ if (!this.map) return
100
+
101
+ var infoWindow = new google.maps.InfoWindow()
102
+ infoWindow.setContent(content)
103
+ infoWindow.open(this.map, this.markers[index])
104
+
105
+ return infoWindow
106
+ }
107
+
108
+ buildMarkers() {
109
+ if (!this.map) {
110
+ return
111
+ }
112
+
113
+ let {
114
+ latlngs = [],
115
+ fillStyle: fillColor,
116
+ alpha: fillOpacity = 1,
117
+ strokeStyle: strokeColor,
118
+ lineWidth: strokeWeight,
119
+ showIntermediateMarkers = false,
120
+ startEndMarkerDifferentDesign = true,
121
+ showPath = false
122
+ } = this.state
123
+
124
+ if (showIntermediateMarkers) {
125
+ var markers = latlngs.map(({ lat, lng }, index) => {
126
+ if (startEndMarkerDifferentDesign) {
127
+ return new google.maps.Marker({
128
+ position: {
129
+ lat: Number(lat) || 0,
130
+ lng: Number(lng) || 0
131
+ },
132
+ map: this.map,
133
+ icon: {
134
+ path: index == 0 ? START_MARKER_PATH : index + 1 == latlngs.length ? END_MARKER_PATH : EMPTY_MARKER_PATH,
135
+ fillColor,
136
+ fillOpacity,
137
+ strokeColor,
138
+ strokeWeight
139
+ },
140
+ index
141
+ })
142
+ } else {
143
+ return new google.maps.Marker({
144
+ position: {
145
+ lat: Number(lat) || 0,
146
+ lng: Number(lng) || 0
147
+ },
148
+ map: this.map,
149
+ icon: {
150
+ path: EMPTY_MARKER_PATH,
151
+ fillColor,
152
+ fillOpacity,
153
+ strokeColor,
154
+ strokeWeight
155
+ },
156
+ index
157
+ })
158
+ }
159
+ })
160
+ } else {
161
+ var spots =
162
+ latlngs.length > 1 ? [latlngs[0], latlngs[latlngs.length - 1]] : latlngs.length == 1 ? [latlngs[0]] : []
163
+
164
+ var markers = spots.map(({ lat, lng }, index) => {
165
+ if (startEndMarkerDifferentDesign) {
166
+ return new google.maps.Marker({
167
+ position: {
168
+ lat: Number(lat) || 0,
169
+ lng: Number(lng) || 0
170
+ },
171
+ map: this.map,
172
+ icon: {
173
+ path: index == 0 ? START_MARKER_PATH : END_MARKER_PATH,
174
+ fillColor,
175
+ fillOpacity,
176
+ strokeColor,
177
+ strokeWeight
178
+ },
179
+ index
180
+ })
181
+ } else {
182
+ return new google.maps.Marker({
183
+ position: {
184
+ lat: Number(lat) || 0,
185
+ lng: Number(lng) || 0
186
+ },
187
+ map: this.map,
188
+ icon: {
189
+ path: EMPTY_MARKER_PATH,
190
+ fillColor,
191
+ fillOpacity,
192
+ strokeColor,
193
+ strokeWeight
194
+ },
195
+ index
196
+ })
197
+ }
198
+ })
199
+ }
200
+
201
+ if (showPath) {
202
+ this.trackPath = new google.maps.Polyline({
203
+ path: latlngs,
204
+ geodesic: true,
205
+ strokeColor: '#FF0000',
206
+ strokeOpacity: 1,
207
+ strokeWeight: 4,
208
+ map: this.map
209
+ })
210
+ }
211
+
212
+ var infowindows = new Array(markers.length)
213
+
214
+ markers.forEach((marker, index) => {
215
+ marker.addListener('click', e => {
216
+ var iw = this.findInfoWindow('tap')
217
+ iw && this.openInfoWindow(iw, index)
218
+
219
+ this.trigger('click', e.ya)
220
+ })
221
+ marker.addListener('mouseover', () => {
222
+ var iw = this.findInfoWindow('hover')
223
+ if (!iw) return
224
+ infowindows[index] = this.openInfoWindow(iw, index)
225
+ })
226
+ marker.addListener('mouseout', () => {
227
+ var infowindow = infowindows[index]
228
+ infowindow && infowindow.close()
229
+ infowindows[index] = null
230
+ })
231
+ })
232
+
233
+ this.markers = markers
234
+ }
235
+
236
+ set markers(markers) {
237
+ if (this._markers) {
238
+ this._markers.forEach(marker => {
239
+ marker.setMap(null)
240
+ google.maps.event.clearInstanceListeners(marker)
241
+ })
242
+
243
+ delete this._markers
244
+ }
245
+
246
+ this._markers = markers
247
+ }
248
+
249
+ get markers() {
250
+ if (!this._markers) {
251
+ this.buildMarkers()
252
+ }
253
+
254
+ return this._markers
255
+ }
256
+
257
+ get trackPath() {
258
+ return this._trackPath
259
+ }
260
+
261
+ set trackPath(trackPath) {
262
+ if (this.trackPath) {
263
+ this.trackPath.setMap(null)
264
+ }
265
+
266
+ this._trackPath = trackPath
267
+ }
268
+
269
+ render(context) {
270
+ var { top, left, width, height } = this.state
271
+
272
+ context.translate(left, top)
273
+
274
+ // 마커 모양 그리기
275
+ context.beginPath()
276
+
277
+ context.moveTo(width / 2, height * 0.9)
278
+ context.bezierCurveTo(width / 2.3, height * 0.6, 0, height / 2, 0, height / 4)
279
+
280
+ context.ellipse(width / 2, height / 4, width / 2, height / 4, 0, Math.PI * 1, Math.PI * 0)
281
+
282
+ context.bezierCurveTo(width, height / 2, width / 1.7, height * 0.6, width / 2, height * 0.9)
283
+ context.closePath()
284
+
285
+ context.translate(-left, -top)
286
+ }
287
+
288
+ get controls() {}
289
+
290
+ onchangeTargetMap() {
291
+ if (this.targetMap) {
292
+ this._targetMap = null
293
+ this._map = null
294
+ }
295
+
296
+ var id = this.get('targetMap')
297
+ if (id !== undefined) {
298
+ this._targetMap = this.root.findById(id)
299
+
300
+ if (this.targetMap) {
301
+ this._map = this.targetMap.map
302
+
303
+ if (!this.map) {
304
+ var listener = after => {
305
+ if ('map' in after) {
306
+ this._map = after.map
307
+ this.markers && this.markers.forEach(marker => marker.setMap(this.map))
308
+
309
+ this.targetMap.off('change', listener)
310
+ }
311
+ }
312
+ this.targetMap.on('change', listener)
313
+ } else {
314
+ this.markers && this.markers.forEach(marker => marker.setMap(this.map))
315
+ }
316
+ }
317
+ }
318
+ }
319
+
320
+ get targetMap() {
321
+ return this._targetMap
322
+ }
323
+
324
+ onchange(after, before) {
325
+ if ('targetMap' in after) {
326
+ this.onchangeTargetMap()
327
+ }
328
+
329
+ if ('latlngs' in after) {
330
+ this.buildMarkers()
331
+ }
332
+
333
+ if (('fillStyle' in after || 'strokeStyle' in after || 'lineWidth' in after) && this.marker) {
334
+ let {
335
+ fillStyle: fillColor,
336
+ alpha: fillOpacity = 1,
337
+ strokeStyle: strokeColor,
338
+ lineWidth: strokeWeight
339
+ } = this.state
340
+
341
+ this.marker.setIcon({
342
+ path: MARKER_PATH,
343
+ fillColor,
344
+ fillOpacity,
345
+ strokeColor,
346
+ strokeWeight
347
+ })
348
+ }
349
+
350
+ super.onchange && super.onchange(after, before)
351
+ }
352
+
353
+ get latlngs() {
354
+ return this.getState('latlngs')
355
+ }
356
+
357
+ set latlngs(latlngs) {
358
+ this.setState({
359
+ latlngs
360
+ })
361
+ }
362
+
363
+ get nature() {
364
+ return NATURE
365
+ }
366
+ }
367
+
368
+ Component.register('gmap-path', GMapPath)
@@ -0,0 +1,256 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+
5
+ // @ts-ignore
6
+ import OpenLayersStyle from '!!text-loader!ol/ol.css'
7
+
8
+ import { Feature, Map, View } from 'ol'
9
+ import { Circle as CircleStyle, Fill, Icon, Stroke, Style } from 'ol/style.js'
10
+ import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
11
+ import { Vector as VectorSource, OSM } from 'ol/source'
12
+ import { fromLonLat } from 'ol/proj'
13
+ import { Geometry, Point } from 'ol/geom'
14
+
15
+ import { Component, HTMLOverlayContainer, Properties, ComponentNature, error } from '@hatiolab/things-scene'
16
+
17
+ const MARKER_PATH =
18
+ 'M 0,0 C -2,-20 -10,-22 -10,-30 A 10,10 0 1,1 10,-30 C 10,-22 2,-20 0,0 z M -2,-30 a 2,2 0 1,1 4,0 2,2 0 1,1 -4,0'
19
+
20
+ const NATURE: ComponentNature = {
21
+ mutable: false,
22
+ resizable: true,
23
+ rotatable: true,
24
+ properties: [
25
+ {
26
+ type: 'number',
27
+ label: 'latitude',
28
+ name: 'lat',
29
+ property: {
30
+ step: 0.000001,
31
+ max: 90,
32
+ min: -90
33
+ }
34
+ },
35
+ {
36
+ type: 'number',
37
+ label: 'longitude',
38
+ name: 'lng',
39
+ property: {
40
+ step: 0.000001,
41
+ min: -180,
42
+ max: 180
43
+ }
44
+ },
45
+ {
46
+ type: 'number',
47
+ label: 'zoom',
48
+ name: 'zoom'
49
+ },
50
+ {
51
+ type: 'boolean',
52
+ label: 'show-marker',
53
+ name: 'showMarker'
54
+ }
55
+ ],
56
+ 'value-property': 'latlng',
57
+ help: 'scene/component/openlayers'
58
+ }
59
+
60
+ function getGlobalScale(component: Component) {
61
+ var scale = { x: 1, y: 1 }
62
+ var parent = component
63
+
64
+ while (parent) {
65
+ let { x, y } = parent.get('scale') || { x: 1, y: 1 }
66
+ scale.x *= x || 1
67
+ scale.y *= y || 1
68
+
69
+ parent = parent.parent
70
+ }
71
+ return scale
72
+ }
73
+
74
+ export default class Openlayers extends HTMLOverlayContainer {
75
+ static markerStyle: Style = new Style({
76
+ image: new Icon({
77
+ src:
78
+ 'data:image/svg+xml;charset=utf-8,' +
79
+ encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">' + MARKER_PATH + '</svg>'),
80
+ anchor: [0.5, 1]
81
+ })
82
+ })
83
+
84
+ _anchor?: HTMLDivElement
85
+ _map: Map | null = null
86
+ _listenTo?: Component
87
+ _listener?: Function
88
+ _vectorSource?: VectorSource<Geometry>
89
+
90
+ get eventMap() {
91
+ return {
92
+ 'model-layer': {
93
+ '(self)': {
94
+ change: (after: any) => {
95
+ after.scale && this.rescale()
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ /*
103
+ * 부모의 스케일의 역으로 transform해서, scale을 1로 맞추어준다.
104
+ */
105
+ rescale() {
106
+ var anchor = this._anchor
107
+ if (!anchor) {
108
+ return
109
+ }
110
+
111
+ var scale = getGlobalScale(this)
112
+
113
+ var sx = 1 / scale.x
114
+ var sy = 1 / scale.y
115
+
116
+ var transform = `scale(${sx}, ${sy})`
117
+
118
+ anchor!.style.transform = transform
119
+ anchor!.style.transformOrigin = '0px 0px'
120
+
121
+ var { width, height } = this.state
122
+ anchor.style.width = width * scale.x + 'px'
123
+ anchor.style.height = height * scale.y + 'px'
124
+ }
125
+
126
+ createElement() {
127
+ super.createElement()
128
+ this._anchor = document.createElement('div')
129
+
130
+ const style = document.createElement('style')
131
+ style.textContent = `
132
+ ${OpenLayersStyle}
133
+ `
134
+
135
+ this.element.appendChild(style)
136
+ this.element.appendChild(this._anchor)
137
+
138
+ const { lat, lng, zoom, showMarker } = this.state
139
+
140
+ // 지도의 중심 좌표
141
+ const center = fromLonLat([lng || 126.9783882, lat || 37.5666103])
142
+
143
+ // 타일 레이어 생성 (배경 지도)
144
+ const tileLayer = new TileLayer({
145
+ source: new OSM({
146
+ attributions: ''
147
+ })
148
+ })
149
+
150
+ // 벡터 레이어 생성
151
+ const styles: { [name: string]: Style } = {
152
+ route: new Style({
153
+ stroke: new Stroke({
154
+ width: 6,
155
+ color: [237, 212, 0, 0.8]
156
+ })
157
+ }),
158
+ marker: Openlayers.markerStyle,
159
+ circle: new Style({
160
+ image: new CircleStyle({
161
+ radius: 7,
162
+ stroke: new Stroke({
163
+ color: 'black',
164
+ width: 2
165
+ })
166
+ })
167
+ })
168
+ }
169
+
170
+ const vectorSource = new VectorSource()
171
+ const vectorLayer = new VectorLayer({
172
+ source: vectorSource,
173
+ style: function (feature) {
174
+ return styles[feature.get('type')]
175
+ }
176
+ })
177
+
178
+ // 지도 생성
179
+ const map = new Map({
180
+ target: this._anchor,
181
+ layers: [tileLayer, vectorLayer],
182
+ view: new View({
183
+ center,
184
+ zoom
185
+ })
186
+ })
187
+
188
+ this._map = map
189
+ this._vectorSource = vectorSource
190
+
191
+ if (showMarker) {
192
+ const marker = new Feature({
193
+ type: 'circle',
194
+ geometry: new Point(center)
195
+ })
196
+
197
+ this._vectorSource.addFeatures([marker])
198
+ }
199
+
200
+ this.rescale()
201
+ }
202
+
203
+ get tagName() {
204
+ return 'div'
205
+ }
206
+
207
+ get map() {
208
+ return this._map
209
+ }
210
+
211
+ dispose() {
212
+ super.dispose()
213
+
214
+ delete this._anchor
215
+ }
216
+
217
+ setElementProperties(div: HTMLDivElement) {
218
+ this.rescale()
219
+ }
220
+
221
+ onchange(after: Properties, before: Properties) {
222
+ if (after.zoom) {
223
+ const view = this.map?.getView()
224
+ view?.setCenter(after.zoom)
225
+ }
226
+
227
+ if ('lat' in after || 'lng' in after) {
228
+ let { lat, lng } = this.state
229
+ const view = this.map?.getView()
230
+ view?.setCenter(fromLonLat([lng, lat]))
231
+ }
232
+
233
+ super.onchange(after, before)
234
+
235
+ this.rescale()
236
+ }
237
+
238
+ get latlng() {
239
+ const { lat, lng } = this.state
240
+ return { lat, lng }
241
+ }
242
+
243
+ set latlng(latlng) {
244
+ this.setState(latlng)
245
+ }
246
+
247
+ get vectorSource() {
248
+ return this._vectorSource
249
+ }
250
+
251
+ get nature() {
252
+ return NATURE
253
+ }
254
+ }
255
+
256
+ Component.register('openlayers', Openlayers)
@@ -0,0 +1,4 @@
1
+ import openlayers from './openlayers'
2
+ import olMarker from './ol-marker'
3
+
4
+ export default [openlayers, olMarker]
@@ -0,0 +1,16 @@
1
+ const icon = new URL('../../icons/ol-marker-template.png', import.meta.url).href
2
+
3
+ export default {
4
+ type: 'ol-marker',
5
+ description: 'ol-marker',
6
+ // group: 'geographic',
7
+ group: 'etc',
8
+ icon,
9
+ model: {
10
+ type: 'ol-marker',
11
+ left: 10,
12
+ top: 10,
13
+ width: 55,
14
+ height: 100
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ const icon = new URL('../../icons/ol-path-template.png', import.meta.url).href
2
+
3
+ export default {
4
+ type: 'ol-path',
5
+ description: 'ol-path',
6
+ // group: 'geographic',
7
+ group: 'etc',
8
+ icon,
9
+ model: {
10
+ type: 'ol-path',
11
+ left: 10,
12
+ top: 10,
13
+ width: 100,
14
+ height: 20
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ const icon = new URL('../../icons/openlayers-template.png', import.meta.url).href
2
+
3
+ export default {
4
+ type: 'openlayers',
5
+ description: 'openlayers',
6
+ // group: 'geographic',
7
+ group: 'etc',
8
+ icon,
9
+ model: {
10
+ type: 'openlayers',
11
+ left: 10,
12
+ top: 10,
13
+ width: 100,
14
+ height: 20
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ import groups from './dist/groups'
2
+ import templates from './dist/templates'
3
+
4
+ export default {
5
+ templates,
6
+ groups
7
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "keyword": "keyword"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "keyword": "키워드"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "keyword": "[ms] keyword"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "keyword": "关键词"
3
+ }