@operato/scene-google-map 1.3.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +15 -0
  3. package/assets/favicon.ico +0 -0
  4. package/assets/images/spinner.png +0 -0
  5. package/dist/gmap-marker.d.ts +67 -0
  6. package/dist/gmap-marker.js +230 -0
  7. package/dist/gmap-marker.js.map +1 -0
  8. package/dist/gmap-path.d.ts +54 -0
  9. package/dist/gmap-path.js +296 -0
  10. package/dist/gmap-path.js.map +1 -0
  11. package/dist/google-map.d.ts +49 -0
  12. package/dist/google-map.js +178 -0
  13. package/dist/google-map.js.map +1 -0
  14. package/dist/index.d.ts +5 -0
  15. package/dist/index.js +8 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/templates/gmap-marker.d.ts +18 -0
  18. package/dist/templates/gmap-marker.js +20 -0
  19. package/dist/templates/gmap-marker.js.map +1 -0
  20. package/dist/templates/gmap-path.d.ts +21 -0
  21. package/dist/templates/gmap-path.js +25 -0
  22. package/dist/templates/gmap-path.js.map +1 -0
  23. package/dist/templates/google-map.d.ts +18 -0
  24. package/dist/templates/google-map.js +20 -0
  25. package/dist/templates/google-map.js.map +1 -0
  26. package/dist/templates/index.d.ts +53 -0
  27. package/dist/templates/index.js +5 -0
  28. package/dist/templates/index.js.map +1 -0
  29. package/helps/scene/component/gmap-map.ko.md +3 -0
  30. package/helps/scene/component/gmap-map.md +3 -0
  31. package/helps/scene/component/gmap-map.zh.md +3 -0
  32. package/helps/scene/component/gmap-marker.ko.md +3 -0
  33. package/helps/scene/component/gmap-marker.md +3 -0
  34. package/helps/scene/component/gmap-marker.zh.md +3 -0
  35. package/helps/scene/component/gmap-path.ko.md +3 -0
  36. package/helps/scene/component/gmap-path.md +3 -0
  37. package/helps/scene/component/gmap-path.zh.md +3 -0
  38. package/icons/gmap-marker.png +0 -0
  39. package/icons/gmap-path.png +0 -0
  40. package/icons/google-map.png +0 -0
  41. package/logs/.08636eb59927f12972f6774f5947c8507b3564c2-audit.json +15 -0
  42. package/logs/.5e5d741d8b7784a2fbad65eedc0fd46946aaf6f2-audit.json +15 -0
  43. package/logs/application-2024-01-01-16.log +9 -0
  44. package/logs/connections-2024-01-01-16.log +41 -0
  45. package/package.json +62 -0
  46. package/schema.graphql +3966 -0
  47. package/src/gmap-marker.ts +306 -0
  48. package/src/gmap-path.ts +365 -0
  49. package/src/google-map.ts +215 -0
  50. package/src/index.ts +8 -0
  51. package/src/templates/gmap-marker.ts +20 -0
  52. package/src/templates/gmap-path.ts +25 -0
  53. package/src/templates/google-map.ts +20 -0
  54. package/src/templates/index.ts +5 -0
  55. package/test/basic-test.html +67 -0
  56. package/test/index.html +24 -0
  57. package/test/unit/test-google-map.js +33 -0
  58. package/test/unit/util.js +22 -0
  59. package/things-scene.config.js +5 -0
  60. package/translations/en.json +6 -0
  61. package/translations/ja.json +6 -0
  62. package/translations/ko.json +6 -0
  63. package/translations/zh.json +6 -0
  64. package/tsconfig.json +22 -0
  65. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,306 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+
5
+ import { Component, InfoWindow, Properties, RectPath, Shape } from '@hatiolab/things-scene'
6
+ import GoogleMap from './google-map'
7
+
8
+ const NATURE = {
9
+ mutable: false,
10
+ resizable: true,
11
+ rotatable: true,
12
+ properties: [
13
+ {
14
+ type: 'id-input',
15
+ label: 'target-map',
16
+ name: 'targetMap',
17
+ property: {
18
+ component: 'google-map'
19
+ }
20
+ },
21
+ {
22
+ type: 'number',
23
+ label: 'latitude',
24
+ name: 'lat',
25
+ property: {
26
+ step: 0.000001,
27
+ max: 90,
28
+ min: -90
29
+ }
30
+ },
31
+ {
32
+ type: 'number',
33
+ label: 'longitude',
34
+ name: 'lng',
35
+ property: {
36
+ step: 0.000001,
37
+ max: 180,
38
+ min: -180
39
+ }
40
+ }
41
+ ],
42
+ 'value-property': 'latlng'
43
+ // help: 'scene/component/gmap-marker'
44
+ }
45
+
46
+ const MARKER_PATH =
47
+ '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'
48
+
49
+ export default class GMapMarker extends RectPath(Shape) {
50
+ private _infoWindow?: google.maps.InfoWindow
51
+ private _marker?: google.maps.Marker | null
52
+ private _map?: google.maps.Map | null
53
+ private _targetMap?: GoogleMap | null
54
+
55
+ dispose() {
56
+ this.marker && this.marker.setMap(null)
57
+
58
+ this.marker = null
59
+ delete this._infoWindow
60
+
61
+ super.dispose()
62
+ }
63
+
64
+ ready() {
65
+ super.ready()
66
+
67
+ if (this.isTemplate()) {
68
+ return
69
+ }
70
+
71
+ this.onchangeTargetMap()
72
+ }
73
+
74
+ get map() {
75
+ return this._map
76
+ }
77
+
78
+ get infoWindow() {
79
+ if (!this._infoWindow) this._infoWindow = new google.maps.InfoWindow()
80
+
81
+ return this._infoWindow
82
+ }
83
+
84
+ findInfoWindow(type: string) {
85
+ var eventSetting = (this.state.event && this.state.event[type]) || {}
86
+
87
+ var infoWindow =
88
+ /* event spec v1.0 */ eventSetting.infoWindow ||
89
+ /* event spec v1.1 */ (eventSetting.action == 'infoWindow' && eventSetting.target)
90
+
91
+ if (infoWindow) {
92
+ return this.root.findById(infoWindow)
93
+ }
94
+ }
95
+
96
+ setInfoContent(sceneInfoWindow: InfoWindow) {
97
+ var tpl = Component.template(sceneInfoWindow.model.frontSideTemplate)
98
+ var content = `<style>${sceneInfoWindow.model.style}</style>` + tpl(this)
99
+
100
+ this.infoWindow.setContent(content)
101
+ }
102
+
103
+ openInfoWindow(iw: InfoWindow) {
104
+ this.setInfoContent(iw)
105
+
106
+ if (!this.map) {
107
+ return
108
+ }
109
+
110
+ this.infoWindow.open(this.map, this.marker!)
111
+ }
112
+
113
+ onmarkerclick(e: Event) {
114
+ var iw = this.findInfoWindow('tap')
115
+ iw && this.openInfoWindow(iw)
116
+
117
+ this.trigger('click', e)
118
+ }
119
+
120
+ onmarkermouseover(e: Event) {
121
+ var iw = this.findInfoWindow('hover')
122
+ iw && this.openInfoWindow(iw)
123
+ }
124
+
125
+ onmarkermouseout(e: Event) {
126
+ var iw = this.findInfoWindow('hover')
127
+ iw && this.infoWindow.close()
128
+ }
129
+
130
+ set marker(marker) {
131
+ if (this._marker) {
132
+ this._marker.setMap(null)
133
+ google.maps.event.clearInstanceListeners(this._marker)
134
+
135
+ delete this._marker
136
+ }
137
+
138
+ if (marker) {
139
+ marker.addListener('click', this.onmarkerclick.bind(this))
140
+ marker.addListener('mouseover', this.onmarkermouseover.bind(this))
141
+ marker.addListener('mouseout', this.onmarkermouseout.bind(this))
142
+
143
+ this._marker = marker
144
+ }
145
+ }
146
+
147
+ get marker() {
148
+ if (!this._marker && this.map) {
149
+ let {
150
+ lat,
151
+ lng,
152
+ fillStyle: fillColor,
153
+ alpha: fillOpacity = 1,
154
+ strokeStyle: strokeColor,
155
+ lineWidth: strokeWeight
156
+ } = this.state
157
+
158
+ var marker = new google.maps.Marker({
159
+ position: {
160
+ lat: Number(lat) || 0,
161
+ lng: Number(lng) || 0
162
+ },
163
+ map: this.map,
164
+ icon: {
165
+ path: MARKER_PATH,
166
+ fillColor,
167
+ fillOpacity,
168
+ strokeColor,
169
+ strokeWeight
170
+ }
171
+ })
172
+
173
+ this.marker = marker
174
+ }
175
+
176
+ return this._marker
177
+ }
178
+
179
+ get hidden() {
180
+ return super.hidden || this.app.isViewMode
181
+ }
182
+
183
+ set hidden(hidden) {
184
+ super.hidden = hidden
185
+ }
186
+
187
+ render(context: CanvasRenderingContext2D) {
188
+ var { top, left, width, height } = this.state
189
+
190
+ context.translate(left, top)
191
+
192
+ // 마커 모양 그리기
193
+ context.beginPath()
194
+
195
+ context.moveTo(width / 2, height * 0.9)
196
+ context.bezierCurveTo(width / 2.3, height * 0.6, 0, height / 2, 0, height / 4)
197
+
198
+ context.ellipse(width / 2, height / 4, width / 2, height / 4, 0, Math.PI * 1, Math.PI * 0)
199
+
200
+ context.bezierCurveTo(width, height / 2, width / 1.7, height * 0.6, width / 2, height * 0.9)
201
+ context.closePath()
202
+
203
+ context.translate(-left, -top)
204
+ }
205
+
206
+ get controls() {
207
+ return []
208
+ }
209
+
210
+ onchangeTargetMap() {
211
+ if (this.targetMap) {
212
+ this._targetMap = null
213
+ this._map = null
214
+ }
215
+
216
+ var id = this.get('targetMap')
217
+ if (id !== undefined) {
218
+ this._targetMap = this.root.findById(id) as GoogleMap
219
+
220
+ if (this.targetMap) {
221
+ this._map = this.targetMap.map
222
+
223
+ if (!this.map) {
224
+ var listener = (after: any) => {
225
+ if ('map' in after) {
226
+ this._map = after.map
227
+ this.marker && this.marker.setMap(this.map!)
228
+
229
+ this.targetMap!.off('change', listener)
230
+ }
231
+ }
232
+ this.targetMap.on('change', listener)
233
+ } else {
234
+ this.marker && this.marker.setMap(this.map)
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ get targetMap() {
241
+ return this._targetMap
242
+ }
243
+
244
+ // get click_handler() {
245
+ // if (!this._click_handler)
246
+ // this._click_handler = this.onmarkerclick.bind(this);
247
+
248
+ // return this._click_handler;
249
+ // }
250
+
251
+ onchange(after: Properties, before: Properties) {
252
+ if ('targetMap' in after) {
253
+ this.onchangeTargetMap()
254
+ }
255
+
256
+ if ('lat' in after || 'lng' in after) {
257
+ var { lat, lng } = this.state
258
+ this.latlng = {
259
+ lat,
260
+ lng
261
+ }
262
+ }
263
+
264
+ if (('fillStyle' in after || 'strokeStyle' in after || 'lineWidth' in after) && this.marker) {
265
+ let {
266
+ fillStyle: fillColor,
267
+ alpha: fillOpacity = 1,
268
+ strokeStyle: strokeColor,
269
+ lineWidth: strokeWeight
270
+ } = this.state
271
+
272
+ this.marker.setIcon({
273
+ path: MARKER_PATH,
274
+ fillColor,
275
+ fillOpacity,
276
+ strokeColor,
277
+ strokeWeight
278
+ })
279
+ }
280
+
281
+ super.onchange && super.onchange(after, before)
282
+ }
283
+
284
+ get latlng() {
285
+ return {
286
+ lat: this.getState('lat'),
287
+ lng: this.getState('lng')
288
+ }
289
+ }
290
+
291
+ set latlng(latlng) {
292
+ var { lat, lng } = latlng
293
+ this.marker && this.marker.setPosition(new google.maps.LatLng(lat, lng))
294
+
295
+ this.setState({
296
+ lat,
297
+ lng
298
+ })
299
+ }
300
+
301
+ get nature() {
302
+ return NATURE
303
+ }
304
+ }
305
+
306
+ Component.register('gmap-marker', GMapMarker)
@@ -0,0 +1,365 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+
5
+ import { Component, InfoWindow, Properties, RectPath, Shape } from '@hatiolab/things-scene'
6
+ import GoogleMap from './google-map'
7
+
8
+ const NATURE = {
9
+ mutable: false,
10
+ resizable: true,
11
+ rotatable: true,
12
+ properties: [
13
+ {
14
+ type: 'id-input',
15
+ label: 'target-map',
16
+ name: 'targetMap',
17
+ property: {
18
+ component: 'google-map'
19
+ }
20
+ },
21
+ {
22
+ type: 'checkbox',
23
+ label: 'show-path',
24
+ name: 'showPath'
25
+ },
26
+ {
27
+ type: 'checkbox',
28
+ label: 'show-intermediate-markers',
29
+ name: 'showIntermediateMarkers'
30
+ },
31
+ {
32
+ type: 'checkbox',
33
+ label: 'start-end-marker-different-design',
34
+ name: 'startEndMarkerDifferentDesign'
35
+ }
36
+ ],
37
+ 'value-property': 'latlngs'
38
+ // help: 'scene/component/gmap-path'
39
+ }
40
+
41
+ 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'
42
+ const END_MARKER_PATH =
43
+ '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'
44
+ const START_MARKER_PATH =
45
+ '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'
46
+
47
+ export default class GMapPath extends RectPath(Shape) {
48
+ private _infoWindow?: google.maps.InfoWindow
49
+ private _markers: google.maps.Marker[] = []
50
+ private _map?: google.maps.Map | null
51
+ private _targetMap?: GoogleMap | null
52
+ private _trackPath?: google.maps.Polyline
53
+
54
+ dispose() {
55
+ this.markers && this.markers.forEach(marker => marker.setMap(null))
56
+
57
+ this.markers = []
58
+ delete this._infoWindow
59
+
60
+ super.dispose()
61
+ }
62
+
63
+ ready() {
64
+ super.ready()
65
+
66
+ if (this.isTemplate()) {
67
+ return
68
+ }
69
+
70
+ this.onchangeTargetMap()
71
+ }
72
+
73
+ get map() {
74
+ return this._map
75
+ }
76
+
77
+ findInfoWindow(type: string) {
78
+ var eventSetting = (this.state.event && this.state.event[type]) || {}
79
+
80
+ var infoWindow =
81
+ /* event spec v1.0 */ eventSetting.infoWindow ||
82
+ /* event spec v1.1 */ (eventSetting.action == 'infoWindow' && eventSetting.target)
83
+
84
+ if (infoWindow) {
85
+ return this.root.findById(infoWindow)
86
+ }
87
+ }
88
+
89
+ getInfoContent(sceneInfoWindow: InfoWindow, index: number) {
90
+ var tpl = Component.template(sceneInfoWindow.model.frontSideTemplate)
91
+ return (
92
+ `<style>${sceneInfoWindow.model.style}</style>` +
93
+ tpl({
94
+ data: this.data,
95
+ index
96
+ })
97
+ )
98
+ }
99
+
100
+ openInfoWindow(iw: InfoWindow, index: number) {
101
+ var content = this.getInfoContent(iw, index)
102
+
103
+ if (!this.map) return
104
+
105
+ var infoWindow = new google.maps.InfoWindow()
106
+ infoWindow.setContent(content)
107
+ infoWindow.open(this.map, this.markers![index])
108
+
109
+ return infoWindow
110
+ }
111
+
112
+ buildMarkers() {
113
+ if (!this.map) {
114
+ return
115
+ }
116
+
117
+ let {
118
+ latlngs = [],
119
+ fillStyle: fillColor,
120
+ alpha: fillOpacity = 1,
121
+ strokeStyle: strokeColor,
122
+ lineWidth: strokeWeight,
123
+ showIntermediateMarkers = false,
124
+ startEndMarkerDifferentDesign = true,
125
+ showPath = false
126
+ } = this.state
127
+
128
+ if (showIntermediateMarkers) {
129
+ var markers: google.maps.Marker[] = latlngs.map(({ lat, lng }: { lat: number; lng: number }, index: number) => {
130
+ if (startEndMarkerDifferentDesign) {
131
+ return new google.maps.Marker({
132
+ position: {
133
+ lat: Number(lat) || 0,
134
+ lng: Number(lng) || 0
135
+ },
136
+ map: this.map!,
137
+ icon: {
138
+ path: index == 0 ? START_MARKER_PATH : index + 1 == latlngs.length ? END_MARKER_PATH : EMPTY_MARKER_PATH,
139
+ fillColor,
140
+ fillOpacity,
141
+ strokeColor,
142
+ strokeWeight
143
+ }
144
+ })
145
+ } else {
146
+ return new google.maps.Marker({
147
+ position: {
148
+ lat: Number(lat) || 0,
149
+ lng: Number(lng) || 0
150
+ },
151
+ map: this.map!,
152
+ icon: {
153
+ path: EMPTY_MARKER_PATH,
154
+ fillColor,
155
+ fillOpacity,
156
+ strokeColor,
157
+ strokeWeight
158
+ }
159
+ })
160
+ }
161
+ })
162
+ } else {
163
+ var spots =
164
+ latlngs.length > 1 ? [latlngs[0], latlngs[latlngs.length - 1]] : latlngs.length == 1 ? [latlngs[0]] : []
165
+
166
+ var markers: google.maps.Marker[] = spots.map(({ lat, lng }, index) => {
167
+ if (startEndMarkerDifferentDesign) {
168
+ return new google.maps.Marker({
169
+ position: {
170
+ lat: Number(lat) || 0,
171
+ lng: Number(lng) || 0
172
+ },
173
+ map: this.map!,
174
+ icon: {
175
+ path: index == 0 ? START_MARKER_PATH : END_MARKER_PATH,
176
+ fillColor,
177
+ fillOpacity,
178
+ strokeColor,
179
+ strokeWeight
180
+ }
181
+ })
182
+ } else {
183
+ return new google.maps.Marker({
184
+ position: {
185
+ lat: Number(lat) || 0,
186
+ lng: Number(lng) || 0
187
+ },
188
+ map: this.map!,
189
+ icon: {
190
+ path: EMPTY_MARKER_PATH,
191
+ fillColor,
192
+ fillOpacity,
193
+ strokeColor,
194
+ strokeWeight
195
+ }
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: number) => {
215
+ marker.addListener('click', (e: Event) => {
216
+ var iw = this.findInfoWindow('tap')
217
+ iw && this.openInfoWindow(iw, index)
218
+
219
+ this.trigger('click', e)
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
+ 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
+ get hidden() {
270
+ return super.hidden || this.app.isViewMode
271
+ }
272
+
273
+ set hidden(hidden) {
274
+ super.hidden = hidden
275
+ }
276
+
277
+ render(context: CanvasRenderingContext2D) {
278
+ var { top, left, width, height } = this.state
279
+
280
+ context.translate(left, top)
281
+
282
+ // 마커 모양 그리기
283
+ context.beginPath()
284
+
285
+ context.moveTo(width / 2, height * 0.9)
286
+ context.bezierCurveTo(width / 2.3, height * 0.6, 0, height / 2, 0, height / 4)
287
+
288
+ context.ellipse(width / 2, height / 4, width / 2, height / 4, 0, Math.PI * 1, Math.PI * 0)
289
+
290
+ context.bezierCurveTo(width, height / 2, width / 1.7, height * 0.6, width / 2, height * 0.9)
291
+ context.closePath()
292
+
293
+ context.translate(-left, -top)
294
+ }
295
+
296
+ get controls() {
297
+ return []
298
+ }
299
+
300
+ onchangeTargetMap() {
301
+ if (this.targetMap) {
302
+ this._targetMap = null
303
+ this._map = null
304
+ }
305
+
306
+ var id = this.get('targetMap')
307
+ if (id !== undefined) {
308
+ this._targetMap = this.root.findById(id) as GoogleMap
309
+
310
+ if (this.targetMap) {
311
+ this._map = this.targetMap.map
312
+
313
+ if (!this.map) {
314
+ var listener = (after: any) => {
315
+ if ('map' in after) {
316
+ this._map = after.map
317
+ this.markers && this.markers.forEach(marker => marker.setMap(this.map!))
318
+
319
+ this.targetMap!.off('change', listener)
320
+ }
321
+ }
322
+ this.targetMap.on('change', listener)
323
+ } else {
324
+ this.markers && this.markers.forEach(marker => marker.setMap(this.map!))
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ get targetMap() {
331
+ return this._targetMap
332
+ }
333
+
334
+ onchange(after: Properties, before: Properties) {
335
+ if ('targetMap' in after) {
336
+ this.onchangeTargetMap()
337
+ }
338
+
339
+ if ('latlngs' in after) {
340
+ this.buildMarkers()
341
+ }
342
+
343
+ if ('fillStyle' in after || 'strokeStyle' in after || 'lineWidth' in after) {
344
+ this.buildMarkers()
345
+ }
346
+
347
+ super.onchange && super.onchange(after, before)
348
+ }
349
+
350
+ get latlngs() {
351
+ return this.getState('latlngs')
352
+ }
353
+
354
+ set latlngs(latlngs) {
355
+ this.setState({
356
+ latlngs
357
+ })
358
+ }
359
+
360
+ get nature() {
361
+ return NATURE
362
+ }
363
+ }
364
+
365
+ Component.register('gmap-path', GMapPath)