@opengis/gis 0.2.160 → 0.2.162

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/{CardIcon-CklSJTtC.js → CardIcon-DcriKHRQ.js} +1 -1
  2. package/dist/{EntityTablePage-BzWYtKWK.js → EntityTablePage-44UmAQh0.js} +1 -1
  3. package/dist/{ExtentOutlineLayer.vue_vue_type_script_setup_true_lang-Rw9Gg_TG.js → ExtentOutlineLayer.vue_vue_type_script_setup_true_lang-BAaobvwj.js} +1 -1
  4. package/dist/{HeaderActions.vue_vue_type_script_setup_true_lang-Ddh9Cmkh.js → HeaderActions.vue_vue_type_script_setup_true_lang-CLEpHbVL.js} +1 -1
  5. package/dist/{MapSettings-8X4kAyPU.js → MapSettings-C7qDO9gp.js} +1 -1
  6. package/dist/{RastersTablePage-Dj-flTFQ.js → RastersTablePage-DmOpbseA.js} +1 -1
  7. package/dist/{cartocss-CilZ3kmC.js → cartocss-DGvNrARb.js} +5 -5
  8. package/dist/{import-utils-C5dvkoG1.js → import-utils-DYDa34GH.js} +1 -1
  9. package/dist/index-DCKH-TcG.js +13556 -0
  10. package/dist/index.css +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.umd.cjs +55 -42
  13. package/dist/{raster-CO1uIsxP.js → raster-CAi6K97q.js} +3 -3
  14. package/dist/{register-dMnCEwXf.js → register-C6IbQrEK.js} +3 -3
  15. package/dist/service-B_C_D5nN.js +3471 -0
  16. package/dist/{vs-datatable-B989esR4.js → vs-datatable-aAXTRfDg.js} +2 -3
  17. package/module/test/map/main.json +10 -10
  18. package/package.json +2 -2
  19. package/server/plugins/mapnik/map.proto +93 -1
  20. package/server/routes/gis/index.mjs +3 -0
  21. package/server/routes/gis/services/create.route.js +96 -0
  22. package/server/routes/map/controllers/gis.convert.js +3 -2
  23. package/server/routes/map/controllers/pointHeight.js +55 -0
  24. package/server/routes/map/index.mjs +52 -1
  25. package/server/routes/map/vtile1.js +1 -1
  26. package/dist/index-CIF8ZBn4.js +0 -11606
  27. package/dist/service-CPxxx_z0.js +0 -2628
@@ -1,7 +1,7 @@
1
1
  import { defineComponent as F, mergeModels as O, defineAsyncComponent as I, useModel as R, ref as y, watch as K, openBlock as o, createElementBlock as s, createElementVNode as e, createVNode as $, unref as x, withCtx as S, createStaticVNode as V, useSlots as P, computed as H, Fragment as b, createTextVNode as U, createCommentVNode as N, renderList as A, normalizeStyle as Q, toDisplayString as M, normalizeClass as z, withModifiers as X, createBlock as Y, resolveDynamicComponent as ee, withDirectives as te, renderSlot as ne } from "vue";
2
2
  import { notify as E } from "@opengis/core";
3
3
  import { Edit as le, Trash as oe } from "lucide-vue-next";
4
- import { _ as u } from "./index-CIF8ZBn4.js";
4
+ import { _ as u } from "./index-DCKH-TcG.js";
5
5
  const se = { class: "flex items-center justify-center col-span-3 gap-2" }, re = { class: "flex justify-end p-[20px] gap-[10px] border-t w-full" }, ie = /* @__PURE__ */ F({
6
6
  __name: "vs-datatable-controls",
7
7
  props: /* @__PURE__ */ O({
@@ -725,12 +725,11 @@ const kt = /* @__PURE__ */ u(pt, [["render", wt]]), W = {
725
725
  ])
726
726
  ])) : (o(), s(b, { key: 1 }, [
727
727
  a.slot && g(a.slot) ? ne(i.$slots, a.slot, {
728
- key: 0,
729
728
  row: l,
730
729
  column: a,
731
730
  value: l[a.key],
732
731
  rowIndex: f
733
- }, void 0, !0) : a.type === "color" ? (o(), s("div", zt, [
732
+ }, void 0, !0, 0) : a.type === "color" ? (o(), s("div", zt, [
734
733
  e("div", {
735
734
  class: z(["w-4 h-4 rounded-full align-middle", `bg-[${l[a.key]}]`])
736
735
  }, null, 2)
@@ -1,32 +1,32 @@
1
1
  {
2
2
  "history": true,
3
3
  "height": "calc(100vh - 65px)",
4
- "center": [
4
+ "center": [
5
5
  34.65450,
6
- 48.51174
6
+ 48.51174
7
7
  ],
8
8
  "zoom": 11,
9
9
  "widgets": [
10
-
10
+
11
11
  {
12
12
  "type": "catalog",
13
13
  "position": "top-left"
14
-
14
+
15
15
  },
16
16
  {
17
17
  "type": "legend",
18
- "position": "bottom-right"
18
+ "position": "bottom-right"
19
19
  },
20
-
20
+
21
21
  {
22
22
  "type": "basemaps",
23
23
  "position": "bottom-left",
24
- "title": "Базові карти",
24
+ "title": "Базові карти",
25
25
  "visible": true
26
26
  }
27
-
27
+
28
28
  ],
29
-
29
+
30
30
  "tools": [
31
31
  "home",
32
32
  "geolocation",
@@ -36,4 +36,4 @@
36
36
  "print",
37
37
  "add-data"
38
38
  ]
39
- }
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengis/gis",
3
- "version": "0.2.160",
3
+ "version": "0.2.162",
4
4
  "type": "module",
5
5
  "author": "Softpro",
6
6
  "main": "./dist/index.js",
@@ -72,4 +72,4 @@
72
72
  "vue-router": "4.5.1",
73
73
  "vuedraggable": "^4.1.0"
74
74
  }
75
- }
75
+ }
@@ -10,7 +10,12 @@ package map;
10
10
  service MapService {
11
11
  rpc PrintMap(PrintMapRequest) returns (PrintMapOut);
12
12
  rpc RenderTile(RenderTileRequest) returns (RenderTileOut);
13
+ rpc GetTile(GetTileRequest) returns (GetTileOut);
14
+ rpc VectorTile(VectorTileRequest) returns (VectorTileOut);
15
+ rpc PoolClear(PoolClearRequest) returns (PoolClearOut);
13
16
  rpc ClearTile(ClearTileRequest) returns (ClearTileOut);
17
+ rpc ReadFile(ReadFileRequest) returns (ReadFileOut);
18
+ rpc WriteFile(WriteFileRequest) returns (WriteFileOut);
14
19
  rpc ReadDir(ReadDirRequest) returns (ReadDirOut);
15
20
  rpc FileStat(FileStatRequest) returns (FileStatOut);
16
21
  rpc FileSearch(FileSearchRequest) returns (FileSearchOut);
@@ -20,8 +25,10 @@ service MapService {
20
25
  rpc ConvertCog(ConvertCogRequest) returns (stream JobProgress); // streaming COG conversion
21
26
  rpc ConvertVector(ConvertVectorRequest) returns (ConvertVectorOut); // vector file/archive -> GeoJSON
22
27
  rpc ImportData(ImportDataRequest) returns (stream JobProgress); // streaming SHP -> PostGIS import
28
+ rpc DumpTable(DumpTableRequest) returns (stream JobProgress); // streaming PostGIS -> SHP dump
23
29
  rpc UploadXML(UploadXMLRequest) returns (UploadXMLOut);
24
30
  rpc UploadRaster(UploadRasterRequest) returns (UploadRasterOut);
31
+ rpc UploadChunk(UploadChunkRequest) returns (UploadChunkOut);
25
32
  rpc GetRasterStatus(GetRasterStatusRequest) returns (GetStatusOut);
26
33
  rpc GetStat(GetStatRequest) returns (GetStatOut);
27
34
  rpc GetLogs(GetLogsRequest) returns (GetLogsOut);
@@ -43,6 +50,7 @@ message PrintMapRequest {
43
50
  int32 opacity = 8; // 0-255
44
51
  repeated string colors = 9; // [ #00FF00, #0000FF]
45
52
  repeated string layers = 10;
53
+ optional bool ignoreBusy = 11; // when true fallback to one-off renderer if pool is exhausted
46
54
  }
47
55
 
48
56
  message PrintMapOut {
@@ -65,6 +73,8 @@ message RenderTileRequest {
65
73
  bool meta = 10; // when true return tile metadata/path only (without base64 image payload)
66
74
  optional bool base64 = 11; // optional output mode flag: true -> base64, false -> raw bytes
67
75
  optional string format = 12; // optional tile image format: png|jpg|jpeg|webp
76
+ optional bool ignoreBusy = 13; // when true fallback to one-off renderer if pool is exhausted
77
+ optional int32 maxZoom = 14; // optional per-request max zoom override
68
78
  }
69
79
 
70
80
  message RenderTileOut {
@@ -77,16 +87,71 @@ message RenderTileOut {
77
87
  bytes data = 7; // raw PNG bytes (used when request.base64=false)
78
88
  }
79
89
 
90
+ message GetTileRequest {
91
+ string path = 1; // tile path relative to tiles root
92
+ bool meta = 2; // when true return metadata/path only without binary payload
93
+ }
94
+
95
+ message GetTileOut {
96
+ string tile_path = 1; // normalized relative tile path under tiles/
97
+ bytes data = 2; // raw tile bytes when meta=false
98
+ string err = 3;
99
+ }
100
+
101
+ message VectorTileRequest {
102
+ string path = 1; // vector tile folder/path relative to tiles root
103
+ int32 z = 2;
104
+ int32 x = 3;
105
+ int32 y = 4;
106
+ bool meta = 5; // when true return metadata/path only without binary payload
107
+ }
108
+
109
+ message VectorTileOut {
110
+ string tile_path = 1; // normalized relative tile path under tiles/
111
+ bytes data = 2; // raw vector tile bytes when meta=false
112
+ string err = 3;
113
+ }
114
+
80
115
  message ClearTileRequest {
81
116
  string name = 1;
82
117
  string path = 2;
83
118
  }
84
119
 
120
+ message PoolClearRequest {
121
+ string path = 1;
122
+ }
123
+
124
+ message PoolClearOut {
125
+ string result = 1;
126
+ string err = 2;
127
+ }
128
+
85
129
  message ClearTileOut {
86
130
  string result = 1;
87
131
  string err = 2;
88
132
  }
89
133
 
134
+ message ReadFileRequest {
135
+ string path = 1;
136
+ }
137
+
138
+ message ReadFileOut {
139
+ string data = 1;
140
+ string path = 2;
141
+ string err = 3;
142
+ }
143
+
144
+ message WriteFileRequest {
145
+ string path = 1;
146
+ string data = 2;
147
+ }
148
+
149
+ message WriteFileOut {
150
+ string status = 1;
151
+ string path = 2;
152
+ string err = 3;
153
+ }
154
+
90
155
  message FileStatRequest {
91
156
  string path = 1;
92
157
  }
@@ -210,6 +275,7 @@ message ConvertVectorRequest {
210
275
  string format = 2; // optional input format hint: zip|shp|kml|gml|geojson|json
211
276
  int32 srs = 3; // target EPSG code, default 4326
212
277
  string filename = 4; // optional original filename for extension/layer detection
278
+ string encoding = 5; // optional encoding, default UTF-8
213
279
  }
214
280
 
215
281
  message ConvertVectorOut {
@@ -221,13 +287,22 @@ message ConvertVectorOut {
221
287
  message ImportDataRequest {
222
288
  string path = 1; // path to .shp file or directory under vector root
223
289
  bool recursive = 2; // process nested folders when path is directory
224
- bool force = 3; // overwrite existing tables when true
290
+ optional bool force = 3; // overwrite existing tables when true; defaults to true when omitted
225
291
  string mode = 4; // import mode: file|type (aliases: 1|2)
226
292
  string db = 5; // PostgreSQL URL: postgresql://user:pass@host:port/dbname
227
293
  string schema = 6; // target schema override
228
294
  string type = 7; // optional geometry type override, e.g. LineString
229
295
  }
230
296
 
297
+ message DumpTableRequest {
298
+ string path = 1; // target directory under vector/root where SHP files will be written
299
+ optional bool overwrite = 2; // overwrite existing shapefiles when true
300
+ repeated string include = 3; // optional allow-list of table names to dump
301
+ optional bool explodeMulti = 4; // dump multipart geometries as separate single-part features
302
+ repeated string ignore = 5; // optional deny-list of table names to skip
303
+ int32 limit = 6; // optional max number of tables to dump after filtering
304
+ }
305
+
231
306
  message UploadXMLRequest {
232
307
  string name = 1;
233
308
  string xml = 2;
@@ -253,6 +328,21 @@ message UploadRasterOut {
253
328
  bool finished = 3;
254
329
  }
255
330
 
331
+ message UploadChunkRequest {
332
+ string path = 1;
333
+ int64 part = 2;
334
+ int64 offset = 3;
335
+ bytes data = 4;
336
+ int64 filesize = 5;
337
+ }
338
+
339
+ message UploadChunkOut {
340
+ string status = 1;
341
+ int64 uploaded = 2;
342
+ bool finished = 3;
343
+ string path = 4;
344
+ }
345
+
256
346
  message GetRasterStatusRequest {
257
347
  string path = 1;
258
348
  bool md5 = 2;
@@ -266,6 +356,8 @@ message GetStatusOut {
266
356
 
267
357
  message GetStatRequest {
268
358
  string period = 1; // "5m", "1h", "1d"
359
+ string path = 2; // optional map_id or XML/map path filter
360
+ bool recursive = 3; // when path is a directory/map prefix, include nested maps
269
361
  }
270
362
 
271
363
  message GetStatOut {
@@ -25,6 +25,7 @@ import checkXML from './checkXML.js';
25
25
 
26
26
  import getCartocss from './cartocss/get.cartocss.js';
27
27
  import addCartocss from './cartocss/add.cartocss.js';
28
+ import createRoute from './services/create.route.js';
28
29
 
29
30
  const mapInfoPointSchema = {
30
31
  querystring: {
@@ -83,6 +84,8 @@ async function route(app) {
83
84
 
84
85
  app.get('/dashboard', publicParams, getDashboard);
85
86
 
87
+ app.get('/routes', publicParams, createRoute);
88
+
86
89
  app.get('/gis-css/:id', publicParams, getCartocss);
87
90
  app.post('/gis-css/:id', { config: { policy: 'L0', allowSql: true, package: 'gis' } }, addCartocss);
88
91
  }
@@ -0,0 +1,96 @@
1
+ const ROUTE_URL = 'https://data.gki.com.ua/api/osrm-routes'
2
+
3
+ function parseCoordPair(raw) {
4
+ const [lngRaw, latRaw] = String(raw || '').split(',')
5
+ const lng = Number(lngRaw)
6
+ const lat = Number(latRaw)
7
+
8
+ if (
9
+ !Number.isFinite(lng)
10
+ || !Number.isFinite(lat)
11
+ || lng < -180
12
+ || lng > 180
13
+ || lat < -90
14
+ || lat > 90
15
+ ) {
16
+ throw new Error('invalid coordinate')
17
+ }
18
+
19
+ return [lng, lat]
20
+ }
21
+
22
+ function buildRoutePoints(from, to, via) {
23
+ const points = [parseCoordPair(from)]
24
+ if (via) {
25
+ for (const segment of String(via).split(';')) {
26
+ const trimmed = segment.trim()
27
+ if (trimmed) points.push(parseCoordPair(trimmed))
28
+ }
29
+ }
30
+ points.push(parseCoordPair(to))
31
+ return points
32
+ }
33
+
34
+ function routeOptionsQuery(query) {
35
+ const params = new URLSearchParams()
36
+ for (const key of ['optimize', 'surface', 'weight', 'mode']) {
37
+ const value = typeof query?.[key] === 'string' ? query[key].trim() : ''
38
+ if (value) params.set(key, value)
39
+ }
40
+ const qs = params.toString()
41
+ return qs ? `&${qs}` : ''
42
+ }
43
+
44
+ function buildSegmentsBody(points) {
45
+ const segments = []
46
+ for (let i = 0; i < points.length - 1; i += 1) {
47
+ segments.push([
48
+ [points[i][0], points[i][1]],
49
+ [points[i + 1][0], points[i + 1][1]],
50
+ ])
51
+ }
52
+ return JSON.stringify(segments)
53
+ }
54
+
55
+ export default async function createRouteHandler(req, reply) {
56
+ try {
57
+ const points = buildRoutePoints(req.query.from, req.query.to, req.query.via)
58
+ const url = `${ROUTE_URL}?steps=true&alternatives=true${routeOptionsQuery(req.query)}`
59
+
60
+ const response = await fetch(url, {
61
+ method: 'POST',
62
+ headers: {
63
+ 'Content-Type': 'application/json',
64
+ Accept: 'application/json',
65
+ },
66
+ body: buildSegmentsBody(points),
67
+ signal: AbortSignal.timeout(10_000),
68
+ }).catch(error => ({
69
+ ok: false,
70
+ status: 502,
71
+ headers: new Headers({ 'content-type': 'application/json' }),
72
+ text: async () => JSON.stringify({ error: String(error), code: 502 }),
73
+ }))
74
+
75
+ const contentType = response.headers?.get?.('content-type') || 'application/json'
76
+ const text = await response.text()
77
+ let payload = text
78
+
79
+ if (contentType.includes('application/json')) {
80
+ try {
81
+ payload = text ? JSON.parse(text) : null
82
+ } catch {
83
+ payload = text
84
+ }
85
+ }
86
+
87
+ return reply
88
+ .status(response.status || 200)
89
+ .header('Content-Type', contentType)
90
+ .send(payload)
91
+ } catch (err) {
92
+ const message = err?.message || 'external route provider request failed'
93
+ const statusCode = /invalid coordinate/i.test(message) ? 400 : 502
94
+ return reply.status(statusCode).send({ error: message, code: statusCode })
95
+ }
96
+ }
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
 
3
- import mapnik from "../../../plugins/mapnik/funcs/mapnik.js";
4
3
  import { downloadFile } from "@opengis/fastify-table/utils.js";
4
+ import mapnik from "../../../plugins/mapnik/funcs/mapnik.js";
5
5
 
6
6
  const { ConvertVector } = mapnik();
7
7
 
@@ -15,7 +15,7 @@ const ALLOWED_FORMAT = new Set(["zip", "shp", "kml", "gml", "geojson", "json"]);
15
15
  * @body {number} [srid=4326] - цільовий EPSG код
16
16
  */
17
17
  export default async function gisConvert({ body = {} }, reply) {
18
- const { filepath, srid = 4326 } = body;
18
+ const { filepath, srid = 4326, encoding } = body;
19
19
 
20
20
  if (!filepath || typeof filepath !== "string") {
21
21
  return reply
@@ -42,6 +42,7 @@ export default async function gisConvert({ body = {} }, reply) {
42
42
  data,
43
43
  format: ext,
44
44
  srs: +srid,
45
+ encoding,
45
46
  filename: path.basename(filepath),
46
47
  }).catch((err) => ({ err }));
47
48
 
@@ -0,0 +1,55 @@
1
+ const DEFAULT_ENDPOINT = 'https://api.opentopodata.org/v1/srtm90m';
2
+
3
+ function parseLatLng(query) {
4
+ const lat = Number(query?.lat);
5
+ const lng = Number(query?.lng);
6
+ if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
7
+ throw new Error('lat and lng are required');
8
+ }
9
+ if (Math.abs(lat) > 90 || Math.abs(lng) > 180) {
10
+ throw new Error('coordinates out of range');
11
+ }
12
+ return { lat, lng };
13
+ }
14
+
15
+ function getEndpoint() {
16
+ const configured = process.env.ELEVATION_API_URL?.trim();
17
+ return configured || DEFAULT_ENDPOINT;
18
+ }
19
+
20
+ async function fetchHeight(lat, lng) {
21
+ const base = getEndpoint();
22
+ const url = new URL(base);
23
+ url.searchParams.set('locations', `${lat},${lng}`);
24
+
25
+ const controller = new AbortController();
26
+ const timeout = setTimeout(() => controller.abort(), 12_000);
27
+
28
+ try {
29
+ const response = await fetch(url, { signal: controller.signal });
30
+ if (!response.ok) {
31
+ throw new Error(`Elevation API returned ${response.status}`);
32
+ }
33
+
34
+ const body = await response.json();
35
+ const result = body?.results?.[0] || null;
36
+ if (!result || result.elevation == null) return null;
37
+ const height = Number(result.elevation);
38
+ return Number.isFinite(height) ? height : null;
39
+ }
40
+ finally {
41
+ clearTimeout(timeout);
42
+ }
43
+ }
44
+
45
+ export default async function pointHeight(ctx = {}) {
46
+ const query = ctx?.query ?? ctx ?? {};
47
+ const { lat, lng } = parseLatLng(query);
48
+ const height = await fetchHeight(lat, lng);
49
+
50
+ return {
51
+ lat,
52
+ lng,
53
+ height,
54
+ };
55
+ }
@@ -26,6 +26,28 @@ import addWidget from './widgets/add.widget.js';
26
26
  import delWidget from './widgets/del.widget.js';
27
27
 
28
28
  import gisConvert from './controllers/gis.convert.js';
29
+ import pointHeight from './controllers/pointHeight.js';
30
+
31
+ const gisConvertSchema = {
32
+ body: {
33
+ type: 'object',
34
+ properties: {
35
+ encoding: {
36
+ type: 'string',
37
+ // python is case insensitive and ignores "-"
38
+ enum: ['LATIN-1', 'CP1251', 'UTF-8'],
39
+ },
40
+ srid: {
41
+ type: 'number',
42
+ minimum: 1,
43
+ },
44
+ filepath: {
45
+ type: 'string',
46
+ },
47
+ },
48
+ required: ['filepath'],
49
+ },
50
+ };
29
51
 
30
52
  const schemaInfo = {
31
53
  type: 'object',
@@ -74,6 +96,27 @@ const mapFormatSchema = {
74
96
  },
75
97
  };
76
98
 
99
+ const pointHeightSchema = {
100
+ querystring: {
101
+ type: 'object',
102
+ required: ['lat', 'lng'],
103
+ properties: {
104
+ lat: { type: 'number', minimum: -90, maximum: 90 },
105
+ lng: { type: 'number', minimum: -180, maximum: 180 },
106
+ },
107
+ },
108
+ response: {
109
+ 200: {
110
+ type: 'object',
111
+ properties: {
112
+ lat: { type: 'number' },
113
+ lng: { type: 'number' },
114
+ height: { anyOf: [{ type: 'number' }, { type: 'null' }] },
115
+ },
116
+ },
117
+ },
118
+ };
119
+
77
120
  const publicParams = {
78
121
  config: { policy: 'L0' },
79
122
  schema: schemaInfo,
@@ -184,6 +227,14 @@ export default async function route(app) {
184
227
  );
185
228
  }
186
229
 
230
+ if (!app.hasRoute({ method: 'GET', url: '/api/coords/height' })) {
231
+ app.get(
232
+ '/coords/height',
233
+ { ...publicParams, schema: pointHeightSchema },
234
+ pointHeight,
235
+ );
236
+ }
237
+
187
238
  if (!app.hasRoute({ method: 'POST', url: '/api/gis-parse-geo' })) {
188
239
  app.post('/gis-parse-geo', publicParams, parseGeoFile);
189
240
  }
@@ -191,7 +242,7 @@ export default async function route(app) {
191
242
  if (!app.hasRoute({ method: 'POST', url: '/api/gis-convert' })) {
192
243
  app.post(
193
244
  '/gis-convert',
194
- { config: { policy: 'L0' }, schema: {}, package: 'gis' },
245
+ { config: { policy: 'L0' }, schema: gisConvertSchema, package: 'gis' },
195
246
  gisConvert,
196
247
  );
197
248
  }
@@ -20,7 +20,7 @@ const mercator = new Sphericalmercator({ size: 256 });
20
20
  import {
21
21
  getColumnCLS, getTemplate, getMeta, getFilterSQL, getSelect,
22
22
  } from '@opengis/fastify-table/utils.js';
23
- import yaml from 'js-yaml';
23
+ import * as yaml from 'js-yaml';
24
24
  import rootFolder from '../../plugins/mapnik/funcs/rootFolder.mjs';
25
25
 
26
26
  const propsCache = {};