@opengis/gis 0.2.185 → 0.2.187
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/package.json +79 -77
- package/server/routes/map/fonts/fontawesome-webfont.ttf +0 -0
- package/server/routes/map/fonts/glyphicons-regular.ttf +0 -0
- package/server/routes/map/fonts/icomoon_180219.ttf +0 -0
- package/server/routes/map/fonts/index.js +1933 -0
- package/server/routes/map/fonts/maki.ttf +0 -0
- package/server/routes/map/fonts/map-icons.ttf +0 -0
- package/server/routes/map/fonts/typicons.ttf +0 -0
- package/server/routes/map/functions/gis.icon.js +134 -28
- package/server/routes/map/map.route.js +3 -0
- package/server/routes/map/map.route.test.js +190 -0
- package/server/routes/registers/functions/getTableColumnMeta.js +6 -6
- package/server/routes/registers/functions/gis.suggest.js +6 -6
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
|
|
5
6
|
import * as solidIcons from '@fortawesome/free-solid-svg-icons';
|
|
6
7
|
import sharp from 'sharp';
|
|
8
|
+
import pathBounds from 'svg-path-bounds';
|
|
9
|
+
import { openSync } from 'fontkit';
|
|
10
|
+
|
|
7
11
|
import { BadRequestError, NotFoundError } from '@opengis/fastify-table/errors.js';
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
import fonts from '../fonts/index.js';
|
|
14
|
+
|
|
15
|
+
const iconFilenamePattern = /^([a-z][a-z0-9]*)-(sm|s|m|l|xl)-(fa|ar|ab|ty|mi|gl|ma|mn)-([\p{L}\p{N}_-]+)\+([a-z]+|[a-f0-9]{3,8})\.(png|svg)$/iu;
|
|
16
|
+
|
|
10
17
|
const validColorLengths = new Set([3, 4, 6, 8]);
|
|
18
|
+
const validColors = new Set(['red', 'green', 'black', 'gray', 'yellow', 'white', 'blue', 'orange', 'purple', 'pink', 'brown']);
|
|
11
19
|
|
|
12
20
|
const iconSizeByCode = {
|
|
13
21
|
s: 24,
|
|
@@ -57,31 +65,75 @@ function parseIconFilename(filename) {
|
|
|
57
65
|
const match = iconFilenamePattern.exec(filename);
|
|
58
66
|
if (!match) return null;
|
|
59
67
|
|
|
60
|
-
const [, pinName, sizeCode, iconType, iconValue,
|
|
61
|
-
|
|
68
|
+
const [, pinName, sizeCode, iconType, iconValue, colorEl, format] = match;
|
|
69
|
+
const isHexColor = colorEl && /[a-f0-9]{3,8}/.test(colorEl.toLowerCase()); // hex or plain text
|
|
70
|
+
if (isHexColor && !validColorLengths.has(colorEl.length)) return null;
|
|
71
|
+
if (!isHexColor && !validColors.has(colorEl)) return null;
|
|
72
|
+
|
|
73
|
+
const iconFont = iconType.toLowerCase();
|
|
74
|
+
|
|
75
|
+
const iconValue1 = iconFont === 'fa'
|
|
76
|
+
? iconValue.toLowerCase()
|
|
77
|
+
: iconValue.toUpperCase();
|
|
62
78
|
|
|
63
79
|
return {
|
|
64
80
|
filename: filename.toLowerCase(),
|
|
65
81
|
pinName: pinName.toLowerCase(),
|
|
66
82
|
size: iconSizeByCode[sizeCode.toLowerCase()],
|
|
67
|
-
iconType:
|
|
68
|
-
iconValue:
|
|
69
|
-
|
|
70
|
-
: iconValue.toUpperCase(),
|
|
71
|
-
color: `#${hexColor}`,
|
|
83
|
+
iconType: iconFont,
|
|
84
|
+
iconValue: iconValue1,
|
|
85
|
+
color: isHexColor ? `#${colorEl}` : colorEl,
|
|
72
86
|
format: format.toLowerCase(),
|
|
73
87
|
};
|
|
74
88
|
}
|
|
75
89
|
|
|
76
|
-
|
|
90
|
+
const test = {};
|
|
91
|
+
|
|
92
|
+
function findIcon(iconName, iconFont) {
|
|
77
93
|
const exportName = `fa${iconName.replace(/(^|-)([a-z0-9])/g, (_, __, char) => char.toUpperCase())}`;
|
|
78
94
|
const exportedIcon = solidIcons[exportName];
|
|
79
95
|
|
|
80
96
|
if (exportedIcon?.prefix === 'fas') return exportedIcon;
|
|
81
97
|
|
|
82
|
-
|
|
98
|
+
const icon1 = Object.values(solidIcons).find(icon => (
|
|
83
99
|
icon?.prefix === 'fas' && icon.iconName === iconName
|
|
84
100
|
));
|
|
101
|
+
|
|
102
|
+
if (icon1) return icon1;
|
|
103
|
+
|
|
104
|
+
const { ar, font: fontName } = fonts[iconFont] || {};
|
|
105
|
+
const iconCode = fonts[iconFont].ar?.[iconName.toLowerCase()];
|
|
106
|
+
|
|
107
|
+
if (!fontName || !iconCode || !ar?.[iconName.toLowerCase()]) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
112
|
+
if (process.platform === 'win32') test[fontName] = 1;
|
|
113
|
+
const fontPath = path.join(dirname, `../fonts/${fontName}`);
|
|
114
|
+
|
|
115
|
+
if (!existsSync(fontPath)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const font = openSync(fontPath);
|
|
120
|
+
const unicodePoint = Number.parseInt(iconCode, 16);
|
|
121
|
+
const glyph = font.glyphForCodePoint(unicodePoint);
|
|
122
|
+
|
|
123
|
+
const { minX, minY, maxX, maxY } = glyph.bbox;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
prefix: font,
|
|
127
|
+
iconName: iconName.toLowerCase(),
|
|
128
|
+
icon: [
|
|
129
|
+
maxX - minX,
|
|
130
|
+
maxY - minY,
|
|
131
|
+
iconName.toLowerCase(),
|
|
132
|
+
99,
|
|
133
|
+
glyph.path.toSVG(),
|
|
134
|
+
],
|
|
135
|
+
bbox: glyph.bbox,
|
|
136
|
+
};
|
|
85
137
|
}
|
|
86
138
|
|
|
87
139
|
async function readFileIfExists(filePath, encoding) {
|
|
@@ -107,30 +159,66 @@ function getPinPlacement(pinName, width, height) {
|
|
|
107
159
|
};
|
|
108
160
|
}
|
|
109
161
|
|
|
110
|
-
function createFontAwesomeSvg(icon, placement, color) {
|
|
111
|
-
const [width, height, , , pathData] = icon.icon;
|
|
112
|
-
const paths = Array.isArray(pathData) ? pathData : [pathData];
|
|
113
|
-
const x = placement.x - placement.iconSize / 2;
|
|
114
|
-
const y = placement.y - placement.iconSize / 2;
|
|
115
|
-
|
|
116
|
-
return `<svg data-gis-icon-layer="foreground" x="${x}" y="${y}" width="${placement.iconSize}" height="${placement.iconSize}" viewBox="0 0 ${width} ${height}" fill="${color}">
|
|
117
|
-
${paths.map(iconPath => `<path d="${iconPath}"/>`).join('')}
|
|
118
|
-
</svg>`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
162
|
function createSymbolSvg(symbol, placement, color) {
|
|
122
163
|
const fontSize = Math.round(placement.iconSize * 0.75);
|
|
123
164
|
|
|
124
165
|
return `<text data-gis-icon-layer="foreground" x="${placement.x}" y="${placement.y}" fill="${color}" font-family="Arial, sans-serif" font-size="${fontSize}" font-weight="700" text-anchor="middle" dominant-baseline="middle">${symbol}</text>`;
|
|
125
166
|
}
|
|
126
167
|
|
|
168
|
+
function createFontIconSvg(icon, placement, color) {
|
|
169
|
+
const [, , , , pathData] = icon.icon;
|
|
170
|
+
const paths = Array.isArray(pathData) ? pathData : [pathData];
|
|
171
|
+
|
|
172
|
+
const bounds = paths.reduce(
|
|
173
|
+
(result, path) => {
|
|
174
|
+
const [minX, minY, maxX, maxY] = pathBounds(path);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
minX: Math.min(result.minX, minX),
|
|
178
|
+
minY: Math.min(result.minY, minY),
|
|
179
|
+
maxX: Math.max(result.maxX, maxX),
|
|
180
|
+
maxY: Math.max(result.maxY, maxY),
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
minX: Infinity,
|
|
185
|
+
minY: Infinity,
|
|
186
|
+
maxX: -Infinity,
|
|
187
|
+
maxY: -Infinity,
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const width = bounds.maxX - bounds.minX;
|
|
192
|
+
const height = bounds.maxY - bounds.minY;
|
|
193
|
+
|
|
194
|
+
const scale = placement.iconSize / Math.max(width, height);
|
|
195
|
+
|
|
196
|
+
const centerX = (bounds.minX + bounds.maxX) / 2;
|
|
197
|
+
const centerY = (bounds.minY + bounds.maxY) / 2;
|
|
198
|
+
|
|
199
|
+
return `<g
|
|
200
|
+
data-gis-icon-layer="foreground"
|
|
201
|
+
transform="
|
|
202
|
+
translate(${placement.x} ${placement.y})
|
|
203
|
+
scale(${scale} ${-scale})
|
|
204
|
+
translate(${-centerX} ${-centerY})
|
|
205
|
+
"
|
|
206
|
+
fill="${color}"
|
|
207
|
+
>
|
|
208
|
+
${paths.map(path => `<path d="${path}"/>`).join('')}
|
|
209
|
+
</g>`;
|
|
210
|
+
}
|
|
211
|
+
|
|
127
212
|
function createForegroundSvg(options, placement) {
|
|
128
|
-
if (options.
|
|
213
|
+
if (options.iconValue && options.iconValue.length === 1) {
|
|
129
214
|
return createSymbolSvg(options.iconValue, placement, options.color);
|
|
130
215
|
}
|
|
131
216
|
|
|
132
|
-
const icon = findIcon(options.iconValue);
|
|
133
|
-
|
|
217
|
+
const icon = findIcon(options.iconValue, options.iconType);
|
|
218
|
+
|
|
219
|
+
return icon
|
|
220
|
+
? createFontIconSvg(icon, placement, options.color)
|
|
221
|
+
: null;
|
|
134
222
|
}
|
|
135
223
|
|
|
136
224
|
function setSvgViewport(svg, size, width, height) {
|
|
@@ -152,19 +240,36 @@ async function createPinSvg({
|
|
|
152
240
|
}) {
|
|
153
241
|
const templatePath = path.join(pinDirectory, `${pinName}.svg`);
|
|
154
242
|
const template = await readFileIfExists(templatePath, 'utf8');
|
|
243
|
+
|
|
155
244
|
if (!template) return null;
|
|
156
245
|
|
|
157
246
|
const width = getSvgAttribute(template, 'width', 512);
|
|
158
247
|
const height = getSvgAttribute(template, 'height', 512);
|
|
248
|
+
|
|
159
249
|
const placement = getPinPlacement(pinName, width, height);
|
|
250
|
+
|
|
160
251
|
const coloredTemplate = placement.recolorBackground
|
|
161
252
|
? template.replace(/#ff0000/gi, color)
|
|
162
253
|
: template;
|
|
163
|
-
|
|
164
|
-
const
|
|
254
|
+
|
|
255
|
+
const svg = setSvgViewport(
|
|
256
|
+
coloredTemplate,
|
|
257
|
+
size,
|
|
258
|
+
width,
|
|
259
|
+
height
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const foreground = createForegroundSvg(
|
|
263
|
+
{ ...iconOptions, color },
|
|
264
|
+
placement,
|
|
265
|
+
);
|
|
266
|
+
|
|
165
267
|
if (!foreground) return null;
|
|
166
268
|
|
|
167
|
-
return svg.replace(
|
|
269
|
+
return svg.replace(
|
|
270
|
+
/<\/svg>\s*$/i,
|
|
271
|
+
`${foreground}</svg>`
|
|
272
|
+
);
|
|
168
273
|
}
|
|
169
274
|
|
|
170
275
|
async function createIcon(options) {
|
|
@@ -204,7 +309,8 @@ export default async function gisIconApi({ name, nocache }) {
|
|
|
204
309
|
throw BadRequestError('invalid icon name');
|
|
205
310
|
}
|
|
206
311
|
|
|
207
|
-
|
|
312
|
+
// fixed icon size of 32px
|
|
313
|
+
const image = await getIcon({ ...options, size: 32 }, nocache);
|
|
208
314
|
|
|
209
315
|
if (!image) {
|
|
210
316
|
throw NotFoundError('icon not found');
|
|
@@ -149,6 +149,9 @@ export default function route(app) {
|
|
|
149
149
|
sql: query.sql,
|
|
150
150
|
pointZoom: query.pointZoom,
|
|
151
151
|
}, pg);
|
|
152
|
+
if (process.platform === 'win32' && typeof result === 'string') {
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
152
155
|
return reply.headers(result.headers).send(result.data);
|
|
153
156
|
});
|
|
154
157
|
}
|
|
@@ -56,6 +56,7 @@ describe('map api test', () => {
|
|
|
56
56
|
const res = await injectWithHeaders({
|
|
57
57
|
method: 'GET',
|
|
58
58
|
url: `/api/gis-icon/pin6-m-fa-plus-square+7ed957.png`,
|
|
59
|
+
query: { nocache: 1 },
|
|
59
60
|
headers: { uid: user.id, user_type: user.type }
|
|
60
61
|
});
|
|
61
62
|
expect(res.statusCode).toBe(200);
|
|
@@ -67,6 +68,7 @@ describe('map api test', () => {
|
|
|
67
68
|
const res = await injectWithHeaders({
|
|
68
69
|
method: 'GET',
|
|
69
70
|
url: `/api/icon/pin6-m-fa-plus-square+7ed957.png`,
|
|
71
|
+
query: { nocache: 1 },
|
|
70
72
|
headers: { uid: user.id, user_type: user.type }
|
|
71
73
|
});
|
|
72
74
|
expect(res.statusCode).toBe(200);
|
|
@@ -78,6 +80,7 @@ describe('map api test', () => {
|
|
|
78
80
|
const res = await injectWithHeaders({
|
|
79
81
|
method: 'GET',
|
|
80
82
|
url: `/api/gis-icon/pin6-m-fa-plus-square+7ed957.svg`,
|
|
83
|
+
query: { nocache: 1 },
|
|
81
84
|
headers: { uid: user.id, user_type: user.type }
|
|
82
85
|
});
|
|
83
86
|
expect(res.statusCode).toBe(200);
|
|
@@ -90,6 +93,7 @@ describe('map api test', () => {
|
|
|
90
93
|
const res = await injectWithHeaders({
|
|
91
94
|
method: 'GET',
|
|
92
95
|
url: `/api/gis-icon/pin6-m-ar-E+ff4500.svg`,
|
|
96
|
+
query: { nocache: 1 },
|
|
93
97
|
headers: { uid: user.id, user_type: user.type }
|
|
94
98
|
});
|
|
95
99
|
expect(res.statusCode).toBe(200);
|
|
@@ -105,6 +109,7 @@ describe('map api test', () => {
|
|
|
105
109
|
const res = await injectWithHeaders({
|
|
106
110
|
method: 'GET',
|
|
107
111
|
url: `/api/gis-icon/${pin}-m-fa-plus-square+7ed957.svg`,
|
|
112
|
+
query: { nocache: 1 },
|
|
108
113
|
headers: { uid: user.id, user_type: user.type }
|
|
109
114
|
});
|
|
110
115
|
expect(res.statusCode).toBe(200);
|
|
@@ -127,6 +132,7 @@ describe('map api test', () => {
|
|
|
127
132
|
const res = await injectWithHeaders({
|
|
128
133
|
method: 'GET',
|
|
129
134
|
url: `/api/gis-icon/${pin}-m-fa-plus-square+7ed957.svg`,
|
|
135
|
+
query: { nocache: 1 },
|
|
130
136
|
headers: { uid: user.id, user_type: user.type }
|
|
131
137
|
});
|
|
132
138
|
expect(res.statusCode).toBe(200);
|
|
@@ -137,6 +143,190 @@ describe('map api test', () => {
|
|
|
137
143
|
},
|
|
138
144
|
);
|
|
139
145
|
|
|
146
|
+
test('GET /gis-icon with typicons font (ty)', async () => {
|
|
147
|
+
const res = await injectWithHeaders({
|
|
148
|
+
method: 'GET',
|
|
149
|
+
url: `/api/gis-icon/pin-m-ty-anchor+ff0000.svg`,
|
|
150
|
+
query: { nocache: 1 },
|
|
151
|
+
headers: { uid: user.id, user_type: user.type }
|
|
152
|
+
});
|
|
153
|
+
expect(res.statusCode).toBe(200);
|
|
154
|
+
expect(res.headers['content-type']).toBe('image/svg+xml');
|
|
155
|
+
expect(res.body).toContain('<svg');
|
|
156
|
+
expect(res.body).toContain('fill="#ff0000"');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('GET /gis-icon with maki font (ma)', async () => {
|
|
160
|
+
const res = await injectWithHeaders({
|
|
161
|
+
method: 'GET',
|
|
162
|
+
url: `/api/gis-icon/pin-m-ma-zoo+00ff00.svg`,
|
|
163
|
+
query: { nocache: 1 },
|
|
164
|
+
headers: { uid: user.id, user_type: user.type }
|
|
165
|
+
});
|
|
166
|
+
expect(res.statusCode).toBe(200);
|
|
167
|
+
expect(res.headers['content-type']).toBe('image/svg+xml');
|
|
168
|
+
expect(res.body).toContain('<svg');
|
|
169
|
+
expect(res.body).toContain('fill="#00ff00"');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('GET /gis-icon with fontawesome font (fa)', async () => {
|
|
173
|
+
const res = await injectWithHeaders({
|
|
174
|
+
method: 'GET',
|
|
175
|
+
url: `/api/gis-icon/pin-m-fa-heart+ff0000.svg`,
|
|
176
|
+
query: { nocache: 1 },
|
|
177
|
+
headers: { uid: user.id, user_type: user.type }
|
|
178
|
+
});
|
|
179
|
+
expect(res.statusCode).toBe(200);
|
|
180
|
+
expect(res.headers['content-type']).toBe('image/svg+xml');
|
|
181
|
+
expect(res.body).toContain('<svg');
|
|
182
|
+
expect(res.body).toContain('fill="#ff0000"');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('GET /gis-icon with map icons font (mi)', async () => {
|
|
186
|
+
const res = await injectWithHeaders({
|
|
187
|
+
method: 'GET',
|
|
188
|
+
url: `/api/gis-icon/pin-m-mi-hospital+0000ff.svg`,
|
|
189
|
+
query: { nocache: 1 },
|
|
190
|
+
headers: { uid: user.id, user_type: user.type }
|
|
191
|
+
});
|
|
192
|
+
expect(res.statusCode).toBe(200);
|
|
193
|
+
expect(res.headers['content-type']).toBe('image/svg+xml');
|
|
194
|
+
expect(res.body).toContain('<svg');
|
|
195
|
+
expect(res.body).toContain('fill="#0000ff"');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('GET /gis-icon with glyphicons font (gl)', async () => {
|
|
199
|
+
const res = await injectWithHeaders({
|
|
200
|
+
method: 'GET',
|
|
201
|
+
url: `/api/gis-icon/pin-m-gl-heart+ffff00.svg`,
|
|
202
|
+
query: { nocache: 1 },
|
|
203
|
+
headers: { uid: user.id, user_type: user.type }
|
|
204
|
+
});
|
|
205
|
+
expect(res.statusCode).toBe(200);
|
|
206
|
+
expect(res.headers['content-type']).toBe('image/svg+xml');
|
|
207
|
+
expect(res.body).toContain('<svg');
|
|
208
|
+
expect(res.body).toContain('fill="#ffff00"');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('GET /gis-icon with mongolia font (mn)', async () => {
|
|
212
|
+
const res = await injectWithHeaders({
|
|
213
|
+
method: 'GET',
|
|
214
|
+
url: `/api/gis-icon/pin-m-mn-book+ffa500.svg`,
|
|
215
|
+
query: { nocache: 1 },
|
|
216
|
+
headers: { uid: user.id, user_type: user.type }
|
|
217
|
+
});
|
|
218
|
+
expect(res.statusCode).toBe(200);
|
|
219
|
+
expect(res.headers['content-type']).toBe('image/svg+xml');
|
|
220
|
+
expect(res.body).toContain('<svg');
|
|
221
|
+
expect(res.body).toContain('fill="#ffa500"');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Test different pin types with various fonts
|
|
225
|
+
test.each([
|
|
226
|
+
['ty', 'anchor'],
|
|
227
|
+
['ma', 'zoo'],
|
|
228
|
+
['fa', 'heart'],
|
|
229
|
+
['mi', 'hospital'],
|
|
230
|
+
['gl', 'heart'],
|
|
231
|
+
['mn', 'book']
|
|
232
|
+
])('GET /gis-icon with different pins and %s font', async (fontType, icon) => {
|
|
233
|
+
const res = await injectWithHeaders({
|
|
234
|
+
method: 'GET',
|
|
235
|
+
url: `/api/gis-icon/pin2-m-${fontType}-${icon}+7ed957.svg`,
|
|
236
|
+
query: { nocache: 1 },
|
|
237
|
+
headers: { uid: user.id, user_type: user.type }
|
|
238
|
+
});
|
|
239
|
+
expect(res.statusCode).toBe(200);
|
|
240
|
+
expect(res.headers['content-type']).toBe('image/svg+xml');
|
|
241
|
+
expect(res.body).toContain('<svg');
|
|
242
|
+
expect(res.body).toContain('fill="#7ed957"');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Test different sizes
|
|
246
|
+
test.each(['s', 'm', 'l', 'xl'])('GET /gis-icon with different sizes (%s)', async (size) => {
|
|
247
|
+
const res = await injectWithHeaders({
|
|
248
|
+
method: 'GET',
|
|
249
|
+
url: `/api/gis-icon/pin-${size}-fa-star+red.svg`,
|
|
250
|
+
query: { nocache: 1 },
|
|
251
|
+
headers: { uid: user.id, user_type: user.type }
|
|
252
|
+
});
|
|
253
|
+
expect(res.statusCode).toBe(200);
|
|
254
|
+
expect(res.headers['content-type']).toBe('image/svg+xml');
|
|
255
|
+
expect(res.body).toContain('<svg');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Test PNG format
|
|
259
|
+
test('GET /gis-icon with PNG format', async () => {
|
|
260
|
+
const res = await injectWithHeaders({
|
|
261
|
+
method: 'GET',
|
|
262
|
+
url: `/api/gis-icon/pin-m-fa-star+blue.png`,
|
|
263
|
+
query: { nocache: 1 },
|
|
264
|
+
headers: { uid: user.id, user_type: user.type }
|
|
265
|
+
});
|
|
266
|
+
expect(res.statusCode).toBe(200);
|
|
267
|
+
expect(res.headers['content-type']).toBe('image/png');
|
|
268
|
+
expect(res.body.includes('PNG')).toBeTruthy();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Test hex color formats
|
|
272
|
+
test.each([
|
|
273
|
+
['3-digit hex', 'f00'],
|
|
274
|
+
['4-digit hex', 'f00f'],
|
|
275
|
+
['6-digit hex', 'ff0000'],
|
|
276
|
+
['8-digit hex', 'ff0000ff']
|
|
277
|
+
])('GET /gis-icon with %s color', async (format, color) => {
|
|
278
|
+
const res = await injectWithHeaders({
|
|
279
|
+
method: 'GET',
|
|
280
|
+
url: `/api/gis-icon/pin-m-fa-star+${color}.svg`,
|
|
281
|
+
query: { nocache: 1 },
|
|
282
|
+
headers: { uid: user.id, user_type: user.type }
|
|
283
|
+
});
|
|
284
|
+
expect(res.statusCode).toBe(200);
|
|
285
|
+
expect(res.headers['content-type']).toBe('image/svg+xml');
|
|
286
|
+
expect(res.body).toContain('<svg');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Test named colors
|
|
290
|
+
test.each([
|
|
291
|
+
'red', 'green', 'black', 'gray', 'yellow', 'white', 'blue', 'orange', 'purple', 'pink', 'brown'
|
|
292
|
+
])('GET /gis-icon with named color %s', async (color) => {
|
|
293
|
+
const res = await injectWithHeaders({
|
|
294
|
+
method: 'GET',
|
|
295
|
+
url: `/api/gis-icon/pin-m-fa-star+${color}.svg`,
|
|
296
|
+
query: { nocache: 1 },
|
|
297
|
+
headers: { uid: user.id, user_type: user.type }
|
|
298
|
+
});
|
|
299
|
+
expect(res.statusCode).toBe(200);
|
|
300
|
+
expect(res.headers['content-type']).toBe('image/svg+xml');
|
|
301
|
+
expect(res.body).toContain('<svg');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Test invalid icon name returns 400
|
|
305
|
+
test('GET /gis-icon with invalid name returns 400', async () => {
|
|
306
|
+
const res = await injectWithHeaders({
|
|
307
|
+
method: 'GET',
|
|
308
|
+
url: `/api/gis-icon/invalid-name.svg`,
|
|
309
|
+
query: { nocache: 1 },
|
|
310
|
+
headers: { uid: user.id, user_type: user.type }
|
|
311
|
+
});
|
|
312
|
+
expect(res.statusCode).toBe(400);
|
|
313
|
+
expect(res.json()).toHaveProperty('message');
|
|
314
|
+
expect(res.json().message).toContain('invalid icon name');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Test icon not found returns 404
|
|
318
|
+
test('GET /gis-icon with non-existent icon returns 404', async () => {
|
|
319
|
+
const res = await injectWithHeaders({
|
|
320
|
+
method: 'GET',
|
|
321
|
+
url: `/api/gis-icon/pin-m-fa-nonexistenticon+red.svg`,
|
|
322
|
+
query: { nocache: 1 },
|
|
323
|
+
headers: { uid: user.id, user_type: user.type }
|
|
324
|
+
});
|
|
325
|
+
expect(res.statusCode).toBe(404);
|
|
326
|
+
expect(res.json()).toHaveProperty('message');
|
|
327
|
+
expect(res.json().message).toContain('icon not found');
|
|
328
|
+
});
|
|
329
|
+
|
|
140
330
|
test('GET /maps', async () => {
|
|
141
331
|
const res = await injectWithHeaders({
|
|
142
332
|
method: 'GET', url: `/api/maps`,
|
|
@@ -88,11 +88,11 @@ export default async function getTableColumnMeta(
|
|
|
88
88
|
? `with c(id,text) as (select id, id as text from (select unnest("${queryColumn.replace(
|
|
89
89
|
/"/g,
|
|
90
90
|
""
|
|
91
|
-
)}") as id from ${tableName} t ${sqlTable})q group by id) select id, text from c`
|
|
91
|
+
)}"::text[]) as id from ${tableName} t ${sqlTable})q group by id) select id, text from c`
|
|
92
92
|
: `with c(id,text) as (select id, id as text, count(*) from (select unnest("${queryColumn.replace(
|
|
93
93
|
/"/g,
|
|
94
94
|
""
|
|
95
|
-
)}") as id from ${tableName} t ${sqlTable})q group by id limit ${defaultLimit}) select * from c`;
|
|
95
|
+
)}"::text[]) as id from ${tableName} t ${sqlTable})q group by id limit ${defaultLimit}) select * from c`;
|
|
96
96
|
|
|
97
97
|
return {
|
|
98
98
|
arr,
|
|
@@ -103,17 +103,17 @@ export default async function getTableColumnMeta(
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const original = filtered
|
|
106
|
-
? `with c(id,text) as (select rtrim( replace( replace( replace( encode("${queryColumn.replace(/"/g, '')}"::bytea, 'base64'), '+', '' ), '-', '' ), '/', '' ), '=' ) as id, "${queryColumn.replace(
|
|
106
|
+
? `with c(id,text) as (select rtrim( replace( replace( replace( encode("${queryColumn.replace(/"/g, '')}"::text::bytea, 'base64'), '+', '' ), '-', '' ), '/', '' ), '=' ) as id, "${queryColumn.replace(
|
|
107
107
|
/"/g,
|
|
108
108
|
""
|
|
109
|
-
)}" as text from ${tableName} t ${sqlTable} group by "${queryColumn.replace(
|
|
109
|
+
)}"::text as text from ${tableName} t ${sqlTable} group by "${queryColumn.replace(
|
|
110
110
|
/"/g,
|
|
111
111
|
""
|
|
112
112
|
)}") select id, text from c`
|
|
113
|
-
: `with c(id,text) as (select rtrim( replace( replace( replace( encode("${queryColumn.replace(/"/g, '')}"::bytea, 'base64'), '+', '' ), '-', '' ), '/', '' ), '=' ) as id, "${queryColumn.replace(
|
|
113
|
+
: `with c(id,text) as (select rtrim( replace( replace( replace( encode("${queryColumn.replace(/"/g, '')}"::text::bytea, 'base64'), '+', '' ), '-', '' ), '/', '' ), '=' ) as id, "${queryColumn.replace(
|
|
114
114
|
/"/g,
|
|
115
115
|
""
|
|
116
|
-
)}" as text, count(*) from ${tableName} t ${sqlTable} group by "${queryColumn.replace(
|
|
116
|
+
)}"::text as text, count(*) from ${tableName} t ${sqlTable} group by "${queryColumn.replace(
|
|
117
117
|
/"/g,
|
|
118
118
|
""
|
|
119
119
|
)}" limit ${defaultLimit}) select * from c`;
|
|
@@ -83,12 +83,12 @@ export default async function gisSuggest({ token, key, val, uid, lang = 'ua', li
|
|
|
83
83
|
if (arr && !cls) {
|
|
84
84
|
const sqlCls = count
|
|
85
85
|
? `select value, count(*) from (select ${pg.pgType?.[dataTypeID]?.includes("[]")
|
|
86
|
-
? `unnest("${name}")`
|
|
87
|
-
: `"${name}"`
|
|
86
|
+
? `unnest("${name}"::text[])`
|
|
87
|
+
: `"${name}"::text`
|
|
88
88
|
} as value from ${table} where 1=1)q group by value`
|
|
89
89
|
: `select array_agg(distinct value)::text[] from (select ${pg.pgType?.[dataTypeID]?.includes("[]")
|
|
90
|
-
? `unnest("${name}")`
|
|
91
|
-
: `"${name}"`
|
|
90
|
+
? `unnest("${name}"::text[])`
|
|
91
|
+
: `"${name}"::text`
|
|
92
92
|
} as value from ${table} where 1=1)q`;
|
|
93
93
|
|
|
94
94
|
if (pg?.pk?.[registry.table_name] && !cls) {
|
|
@@ -184,8 +184,8 @@ export default async function gisSuggest({ token, key, val, uid, lang = 'ua', li
|
|
|
184
184
|
"''"
|
|
185
185
|
)} o
|
|
186
186
|
WHERE ${pg.pgType[dataTypeID]?.includes("[]")
|
|
187
|
-
? `c.text = ANY(o."${name}")`
|
|
188
|
-
: `o."${name}" IS NOT DISTINCT FROM c.text`
|
|
187
|
+
? `c.text = ANY(o."${name}"::text)`
|
|
188
|
+
: `o."${name}"::text IS NOT DISTINCT FROM c.text`
|
|
189
189
|
} ) and ${whereQuery || "true"}
|
|
190
190
|
${order} LIMIT $${args.length}::bigint`
|
|
191
191
|
: `with c(id,text) as ( ${clsMeta.original} where ${whereQuery} ${order}) select * from c LIMIT $${args.length}::bigint`;
|