1 /*
2 SPDX-FileCopyrightText: 2016 Friedrich W. H. Kossebau <kossebau@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7 #include "TestUtils.h"
8
9 #include <GeoSceneEquirectTileProjection.h>
10 #include <GeoSceneMercatorTileProjection.h>
11 #include <GeoDataLatLonBox.h>
12 #include <TileId.h>
13
14
15 namespace Marble
16 {
17
18 class TileProjectionTest : public QObject
19 {
20 Q_OBJECT
21
22 private Q_SLOTS:
23 void testTypeEquirect();
24 void testTypeMercator();
25
26 void testLevelZeroColumnsRowsEquirect();
27 void testLevelZeroColumnsRowsMercator();
28
29 void testTileIndexesEquirect_data();
30 void testTileIndexesEquirect();
31 void testTileIndexesMercator_data();
32 void testTileIndexesMercator();
33
34 void testGeoCoordinatesEquirect_data();
35 void testGeoCoordinatesEquirect();
36 void testGeoCoordinatesMercator_data();
37 void testGeoCoordinatesMercator();
38
39 private:
40 void testLevelZeroColumnsRows(GeoSceneAbstractTileProjection& projection);
41 };
42
43
testLevelZeroColumnsRows(GeoSceneAbstractTileProjection & projection)44 void TileProjectionTest::testLevelZeroColumnsRows(GeoSceneAbstractTileProjection& projection)
45 {
46 // test default
47 QCOMPARE(projection.levelZeroColumns(), 1);
48 QCOMPARE(projection.levelZeroRows(), 1);
49
50 // test setting a different value
51 const int levelZeroColumns = 4;
52 const int levelZeroRows = 6;
53
54 projection.setLevelZeroColumns(levelZeroColumns);
55 projection.setLevelZeroRows(levelZeroRows);
56
57 QCOMPARE(projection.levelZeroColumns(), levelZeroColumns);
58 QCOMPARE(projection.levelZeroRows(), levelZeroRows);
59 }
60
testLevelZeroColumnsRowsEquirect()61 void TileProjectionTest::testLevelZeroColumnsRowsEquirect()
62 {
63 GeoSceneEquirectTileProjection projection;
64 testLevelZeroColumnsRows(projection);
65 }
66
testLevelZeroColumnsRowsMercator()67 void TileProjectionTest::testLevelZeroColumnsRowsMercator()
68 {
69 GeoSceneMercatorTileProjection projection;
70 testLevelZeroColumnsRows(projection);
71 }
72
testTypeEquirect()73 void TileProjectionTest::testTypeEquirect()
74 {
75 GeoSceneEquirectTileProjection projection;
76 QCOMPARE(projection.type(), GeoSceneAbstractTileProjection::Equirectangular);
77 }
78
testTypeMercator()79 void TileProjectionTest::testTypeMercator()
80 {
81 GeoSceneMercatorTileProjection projection;
82 QCOMPARE(projection.type(), GeoSceneAbstractTileProjection::Mercator);
83 }
84
85
testTileIndexesEquirect_data()86 void TileProjectionTest::testTileIndexesEquirect_data()
87 {
88 QTest::addColumn<qreal>("westLon");
89 QTest::addColumn<qreal>("northLat");
90 QTest::addColumn<qreal>("eastLon");
91 QTest::addColumn<qreal>("southLat");
92 QTest::addColumn<int>("zoomLevel");
93 QTest::addColumn<int>("expectedTileXWest");
94 QTest::addColumn<int>("expectedTileYNorth");
95 QTest::addColumn<int>("expectedTileXEast");
96 QTest::addColumn<int>("expectedTileYSouth");
97
98 // zoomlevel zero: 1 tile
99 // bounds matching the tile map
100 addRow() << qreal(-M_PI) << qreal(+M_PI * 0.5)
101 << qreal(+M_PI) << qreal(-M_PI * 0.5)
102 << 0
103 << 0 << 0 << 0 << 0;
104 // bounds inside the 1 tile
105 addRow() << qreal(-M_PI*0.5) << qreal(+M_PI * 0.25)
106 << qreal(+M_PI*0.5) << qreal(-M_PI * 0.25)
107 << 0
108 << 0 << 0 << 0 << 0;
109 // bounds west and north on tile map borders, with normal border values
110 addRow() << qreal(-M_PI) << qreal(+M_PI * 0.5)
111 << qreal(+M_PI*0.5) << qreal(-M_PI * 0.25)
112 << 0
113 << 0 << 0 << 0 << 0;
114 // bounds west and north on tile map borders, with border values from other map border sides
115 addRow() << qreal(+M_PI) << qreal(-M_PI * 0.5)
116 << qreal(+M_PI*0.5) << qreal(-M_PI * 0.25)
117 << 0
118 << 0 << 0 << 0 << 0;
119
120 // zoomlevel 1: 2 tiles per dimension
121 // bounds matching the tile map
122 addRow() << qreal(-M_PI) << qreal(+M_PI * 0.5)
123 << qreal(+M_PI) << qreal(-M_PI * 0.5)
124 << 1
125 << 0 << 0 << 1 << 1;
126 // bounds inside the 4 tiles
127 addRow() << qreal(-M_PI*0.5) << qreal(+M_PI * 0.25)
128 << qreal(+M_PI*0.5) << qreal(-M_PI * 0.25)
129 << 1
130 << 0 << 0 << 1 << 1;
131 // bounds matching the most north-west tile, with normal border values
132 addRow() << qreal(-M_PI) << qreal(+M_PI * 0.5)
133 << qreal(0) << qreal(0)
134 << 1
135 << 0 << 0 << 0 << 0;
136 // bounds matching the most north-west tile, with border values from other map border sides
137 addRow() << qreal(+M_PI) << qreal(-M_PI * 0.5)
138 << qreal(0) << qreal(0)
139 << 1
140 << 0 << 0 << 0 << 0;
141 // bounds matching the most south-east tile, with normal border values
142 addRow() << qreal(0) << qreal(0)
143 << qreal(+M_PI) << qreal(-M_PI * 0.5)
144 << 1
145 << 1 << 1 << 1 << 1;
146 // bounds matching the most south-east tile, with border values from other map border sides
147 addRow() << qreal(0) << qreal(0)
148 << qreal(-M_PI) << qreal(+M_PI * 0.5)
149 << 1
150 << 1 << 1 << 1 << 1;
151
152 // zoomlevel 9: 2^8==512 tiles per dimension
153 // bounds matching the tile map
154 addRow() << qreal(-M_PI) << qreal(+M_PI * 0.5)
155 << qreal(+M_PI) << qreal(-M_PI * 0.5)
156 << 9
157 << 0 << 0 << 511 << 511;
158 // bounds inside the outer tiles
159 addRow() << qreal(-M_PI*(511/512.0)) << qreal(+M_PI * 0.5 * (511/512.0))
160 << qreal(+M_PI*(511/512.0)) << qreal(-M_PI * 0.5 * (511/512.0))
161 << 9
162 << 0 << 0 << 511 << 511;
163 // bounds matching the most north-west tile, with normal border values
164 addRow() << qreal(-M_PI) << qreal(+M_PI * 0.5)
165 << qreal(-M_PI*(255/256.0)) << qreal(+M_PI * 0.5 *(255/256.0))
166 << 9
167 << 0 << 0 << 0 << 0;
168 // bounds matching the most north-west tile, with border values from other map border sides
169 addRow() << qreal(+M_PI) << qreal(-M_PI * 0.5)
170 << qreal(-M_PI*(255/256.0)) << qreal(+M_PI * 0.5 *(255/256.0))
171 << 9
172 << 0 << 0 << 0 << 0;
173 // bounds matching the most south-east tile, with normal border values
174 addRow() << qreal(+M_PI*(255/256.0)) << qreal(-M_PI * 0.5 *(255/256.0))
175 << qreal(+M_PI) << qreal(-M_PI * 0.5)
176 << 9
177 << 511 << 511 << 511 << 511;
178 // bounds matching the most south-east tile, with border values from other map border sides
179 addRow() << qreal(+M_PI*(255/256.0)) << qreal(-M_PI * 0.5 *(255/256.0))
180 << qreal(-M_PI) << qreal(+M_PI * 0.5)
181 << 9
182 << 511 << 511 << 511 << 511;
183 }
184
185
testTileIndexesEquirect()186 void TileProjectionTest::testTileIndexesEquirect()
187 {
188 QFETCH(qreal, westLon);
189 QFETCH(qreal, northLat);
190 QFETCH(qreal, eastLon);
191 QFETCH(qreal, southLat);
192 QFETCH(int, zoomLevel);
193 QFETCH(int, expectedTileXWest);
194 QFETCH(int, expectedTileYNorth);
195 QFETCH(int, expectedTileXEast);
196 QFETCH(int, expectedTileYSouth);
197
198 GeoDataLatLonBox latLonBox(northLat, southLat, eastLon, westLon);
199
200 const GeoSceneEquirectTileProjection projection;
201
202 const QRect rect = projection.tileIndexes(latLonBox, zoomLevel);
203
204 QCOMPARE(rect.left(), expectedTileXWest);
205 QCOMPARE(rect.top(), expectedTileYNorth);
206 QCOMPARE(rect.right(), expectedTileXEast);
207 QCOMPARE(rect.bottom(), expectedTileYSouth);
208 }
209
210
testTileIndexesMercator_data()211 void TileProjectionTest::testTileIndexesMercator_data()
212 {
213 QTest::addColumn<qreal>("westLon");
214 QTest::addColumn<qreal>("northLat");
215 QTest::addColumn<qreal>("eastLon");
216 QTest::addColumn<qreal>("southLat");
217 QTest::addColumn<int>("zoomLevel");
218 QTest::addColumn<int>("expectedTileXWest");
219 QTest::addColumn<int>("expectedTileYNorth");
220 QTest::addColumn<int>("expectedTileXEast");
221 QTest::addColumn<int>("expectedTileYSouth");
222
223 // zoomlevel zero: 1 tile
224 // bounds matching the tile map up to 90 degree latitude
225 addRow() << qreal(-M_PI) << qreal(+M_PI * 0.5)
226 << qreal(+M_PI) << qreal(-M_PI * 0.5)
227 << 0
228 << 0 << 0 << 0 << 0;
229 // bounds matching the tile map with 85 degree latitude limit
230 addRow() << qreal(-M_PI) << qreal(85.0 * DEG2RAD)
231 << qreal(+M_PI) << qreal(-85.0 * DEG2RAD)
232 << 0
233 << 0 << 0 << 0 << 0;
234 // bounds inside the 1 tile
235 addRow() << qreal(-M_PI*0.5) << qreal(+M_PI * 0.25)
236 << qreal(+M_PI*0.5) << qreal(-M_PI * 0.25)
237 << 0
238 << 0 << 0 << 0 << 0;
239 // bounds west and north on tile map borders, with normal border values
240 addRow() << qreal(-M_PI) << qreal(+M_PI * 0.5)
241 << qreal(+M_PI*0.5) << qreal(-M_PI * 0.25)
242 << 0
243 << 0 << 0 << 0 << 0;
244 // bounds west and north on tile map borders, with border values from other map border sides
245 addRow() << qreal(+M_PI) << qreal(-M_PI * 0.5)
246 << qreal(+M_PI*0.5) << qreal(-M_PI * 0.25)
247 << 0
248 << 0 << 0 << 0 << 0;
249
250 // zoomlevel 1: 2 tiles per dimension
251 // bounds matching the tile map up to 90 degree latitude
252 addRow() << qreal(-M_PI) << qreal(+M_PI * 0.5)
253 << qreal(+M_PI) << qreal(-M_PI * 0.5)
254 << 1
255 << 0 << 0 << 1 << 1;
256 // bounds matching the tile map with 85 degree latitude limit
257 addRow() << qreal(-M_PI) << qreal(85.0 * DEG2RAD)
258 << qreal(+M_PI) << qreal(-85.0 * DEG2RAD)
259 << 1
260 << 0 << 0 << 1 << 1;
261 // bounds inside the 4 tiles
262 addRow() << qreal(-M_PI*0.5) << qreal(+M_PI * 0.25)
263 << qreal(+M_PI*0.5) << qreal(-M_PI * 0.25)
264 << 1
265 << 0 << 0 << 1 << 1;
266 // bounds matching the most north-west tile, with normal border values
267 addRow() << qreal(-M_PI) << qreal(+M_PI * 0.5)
268 << qreal(0) << qreal(0)
269 << 1
270 << 0 << 0 << 0 << 0;
271 // bounds matching the most north-west tile, with border values from other map border sides
272 addRow() << qreal(+M_PI) << qreal(-M_PI * 0.5)
273 << qreal(0) << qreal(0)
274 << 1
275 << 0 << 0 << 0 << 0;
276 // bounds matching the most south-east tile, with normal border values
277 addRow() << qreal(0) << qreal(0)
278 << qreal(+M_PI) << qreal(-M_PI * 0.5)
279 << 1
280 << 1 << 1 << 1 << 1;
281 // bounds matching the most south-east tile, with border values from other map border sides
282 addRow() << qreal(0) << qreal(0)
283 << qreal(-M_PI) << qreal(+M_PI * 0.5)
284 << 1
285 << 1 << 1 << 1 << 1;
286
287 // zoomlevel 9: 2^8==512 tiles per dimension
288 // GeoSceneMercatorTileProjection bounds latitude value at +/- 85.0 degree (so not at 85.05113),
289 // which results in some tiles missed at the outer sides.
290 // bounds matching the tile map up to 90 degree latitude
291 addRow() << qreal(-M_PI) << qreal(+M_PI * 0.5)
292 << qreal(+M_PI) << qreal(-M_PI * 0.5)
293 << 9
294 << 0 << 5 << 511 << 506;
295 // bounds matching the tile map with 85 degree latitude limit
296 addRow() << qreal(-M_PI) << qreal(85.0 * DEG2RAD)
297 << qreal(+M_PI) << qreal(-85.0 * DEG2RAD)
298 << 9
299 << 0 << 5 << 511 << 506;
300 // bounds inside the outer tiles
301 addRow() << qreal(-M_PI*(511/512.0)) << qreal(+M_PI * 0.5 * (511/512.0))
302 << qreal(+M_PI*(511/512.0)) << qreal(-M_PI * 0.5 * (511/512.0))
303 << 9
304 << 0 << 5 << 511 << 506;
305 // bounds matching the most north-west tile, with normal border values
306 addRow() << qreal(-M_PI) << qreal(+M_PI * 0.5)
307 << qreal(-M_PI*(255/256.0)) << qreal(+M_PI * 0.5 *(255/256.0))
308 << 9
309 << 0 << 5 << 0 << 5;
310 // bounds matching the most north-west tile, with border values from other map border sides
311 addRow() << qreal(+M_PI) << qreal(-M_PI * 0.5)
312 << qreal(-M_PI*(255/256.0)) << qreal(+M_PI * 0.5 *(255/256.0))
313 << 9
314 << 0 << 5 << 0 << 5;
315 // bounds matching the most south-east tile, with normal border values
316 addRow() << qreal(+M_PI*(255/256.0)) << qreal(-M_PI * 0.5 *(255/256.0))
317 << qreal(+M_PI) << qreal(-M_PI * 0.5)
318 << 9
319 << 511 << 506 << 511 << 506;
320 // bounds matching the most south-east tile, with border values from other map border sides
321 addRow() << qreal(+M_PI*(255/256.0)) << qreal(-M_PI * 0.5 *(255/256.0))
322 << qreal(-M_PI) << qreal(+M_PI * 0.5)
323 << 9
324 << 511 << 506 << 511 << 506;
325 }
326
327
testTileIndexesMercator()328 void TileProjectionTest::testTileIndexesMercator()
329 {
330 QFETCH(qreal, westLon);
331 QFETCH(qreal, northLat);
332 QFETCH(qreal, eastLon);
333 QFETCH(qreal, southLat);
334 QFETCH(int, zoomLevel);
335 QFETCH(int, expectedTileXWest);
336 QFETCH(int, expectedTileYNorth);
337 QFETCH(int, expectedTileXEast);
338 QFETCH(int, expectedTileYSouth);
339
340 GeoDataLatLonBox latLonBox(northLat, southLat, eastLon, westLon);
341
342 const GeoSceneMercatorTileProjection projection;
343
344 const QRect rect = projection.tileIndexes(latLonBox, zoomLevel);
345
346 QCOMPARE(rect.left(), expectedTileXWest);
347 QCOMPARE(rect.top(), expectedTileYNorth);
348 QCOMPARE(rect.right(), expectedTileXEast);
349 QCOMPARE(rect.bottom(), expectedTileYSouth);
350 }
351
352
testGeoCoordinatesEquirect_data()353 void TileProjectionTest::testGeoCoordinatesEquirect_data()
354 {
355 QTest::addColumn<int>("tileX");
356 QTest::addColumn<int>("tileY");
357 QTest::addColumn<int>("zoomLevel");
358 QTest::addColumn<qreal>("expectedWesternTileEdgeLon");
359 QTest::addColumn<qreal>("expectedNorthernTileEdgeLat");
360 QTest::addColumn<qreal>("expectedEasternTileEdgeLon");
361 QTest::addColumn<qreal>("expectedSouthernTileEdgeLat");
362
363 // zoomlevel zero: 1 tile
364 addRow() << 0 << 0 << 0 << qreal(-M_PI) << qreal(+M_PI * 0.5)
365 << qreal(+M_PI) << qreal(-M_PI * 0.5);
366
367 // zoomlevel 1: 2 tiles per dimension
368 addRow() << 0 << 0 << 1 << qreal(-M_PI) << qreal(+M_PI * 0.5)
369 << qreal(0) << qreal(0);
370 addRow() << 0 << 1 << 1 << qreal(-M_PI) << qreal(0)
371 << qreal(0) << qreal(-M_PI * 0.5);
372 addRow() << 1 << 0 << 1 << qreal(0) << qreal(+M_PI * 0.5)
373 << qreal(+M_PI) << qreal(0);
374 addRow() << 1 << 1 << 1 << qreal(0) << qreal(0)
375 << qreal(+M_PI) << qreal(-M_PI * 0.5);
376
377 // zoomlevel 9: 2^8==512 tiles per dimension
378 addRow() << 0 << 0 << 9 << qreal(-M_PI) << qreal(+M_PI * 0.5)
379 << qreal(-M_PI * (255/256.0)) << qreal(+M_PI * 0.5 * (255/256.0));
380 addRow() << 0 << 256 << 9 << qreal(-M_PI) << qreal(0)
381 << qreal(-M_PI * (255/256.0)) << qreal(-M_PI * 0.5 * (1/256.0));
382 addRow() << 256 << 0 << 9 << qreal(0) << qreal(+M_PI * 0.5)
383 << qreal(M_PI * (1/256.0)) << qreal(+M_PI * 0.5 * (255/256.0));
384 addRow() << 511 << 511 << 9 << qreal(M_PI * (255/256.0)) << qreal(-M_PI * 0.5 * (255/256.0))
385 << qreal(+M_PI) << qreal(-M_PI * 0.5);
386 }
387
388
testGeoCoordinatesEquirect()389 void TileProjectionTest::testGeoCoordinatesEquirect()
390 {
391 QFETCH(int, tileX);
392 QFETCH(int, tileY);
393 QFETCH(int, zoomLevel);
394 QFETCH(qreal, expectedWesternTileEdgeLon);
395 QFETCH(qreal, expectedNorthernTileEdgeLat);
396 QFETCH(qreal, expectedEasternTileEdgeLon);
397 QFETCH(qreal, expectedSouthernTileEdgeLat);
398
399 const GeoSceneEquirectTileProjection projection;
400
401 // method variants with GeoDataLatLonBox
402 const GeoDataLatLonBox latLonBox = projection.geoCoordinates(zoomLevel, tileX, tileY);
403
404 QCOMPARE(latLonBox.west(), expectedWesternTileEdgeLon);
405 QCOMPARE(latLonBox.north(), expectedNorthernTileEdgeLat);
406 QCOMPARE(latLonBox.east(), expectedEasternTileEdgeLon);
407 QCOMPARE(latLonBox.south(), expectedSouthernTileEdgeLat);
408
409 TileId tileId(QStringLiteral("testmap"), zoomLevel, tileX, tileY);
410 const GeoDataLatLonBox latLonBox2 = projection.geoCoordinates(tileId);
411
412 QCOMPARE(latLonBox2.west(), expectedWesternTileEdgeLon);
413 QCOMPARE(latLonBox2.north(), expectedNorthernTileEdgeLat);
414 QCOMPARE(latLonBox2.east(), expectedEasternTileEdgeLon);
415 QCOMPARE(latLonBox2.south(), expectedSouthernTileEdgeLat);
416 }
417
testGeoCoordinatesMercator_data()418 void TileProjectionTest::testGeoCoordinatesMercator_data()
419 {
420 QTest::addColumn<int>("tileX");
421 QTest::addColumn<int>("tileY");
422 QTest::addColumn<int>("zoomLevel");
423 QTest::addColumn<qreal>("expectedWesternTileEdgeLon");
424 QTest::addColumn<qreal>("expectedNorthernTileEdgeLat");
425 QTest::addColumn<qreal>("expectedEasternTileEdgeLon");
426 QTest::addColumn<qreal>("expectedSouthernTileEdgeLat");
427
428 const qreal absMaxLat = DEG2RAD * 85.05113;
429
430 // zoomlevel zero: 1 tile
431 addRow() << 0 << 0 << 0 << qreal(-M_PI) << qreal(+absMaxLat)
432 << qreal(+M_PI) << qreal(-absMaxLat);
433
434 // zoomlevel 1: 2 tiles per dimension
435 addRow() << 0 << 0 << 1 << qreal(-M_PI) << qreal(+absMaxLat)
436 << qreal(0) << qreal(0);
437 addRow() << 0 << 1 << 1 << qreal(-M_PI) << qreal(0)
438 << qreal(0) << qreal(-absMaxLat);
439 addRow() << 1 << 0 << 1 << qreal(0) << qreal(+absMaxLat)
440 << qreal(+M_PI) << qreal(0);
441 addRow() << 1 << 1 << 1 << qreal(0) << qreal(0)
442 << qreal(+M_PI) << qreal(-absMaxLat);
443
444 // zoomlevel 9: 2^8==512 tiles per dimension
445 addRow() << 0 << 0 << 9 << qreal(-M_PI) << qreal(+absMaxLat)
446 << qreal(-M_PI * (255/256.0)) << qreal(+1.48336);
447 addRow() << 0 << 256 << 9 << qreal(-M_PI) << qreal(0)
448 << qreal(-M_PI * (255/256.0)) << qreal(-0.0122715);
449 addRow() << 256 << 0 << 9 << qreal(0) << qreal(+absMaxLat)
450 << qreal(M_PI * (1/256.0)) << qreal(+1.48336);
451 addRow() << 511 << 511 << 9 << qreal(M_PI * (255/256.0)) << qreal(-1.48336)
452 << qreal(+M_PI) << qreal(-absMaxLat);
453 }
454
455
testGeoCoordinatesMercator()456 void TileProjectionTest::testGeoCoordinatesMercator()
457 {
458 QFETCH(int, tileX);
459 QFETCH(int, tileY);
460 QFETCH(int, zoomLevel);
461 QFETCH(qreal, expectedWesternTileEdgeLon);
462 QFETCH(qreal, expectedNorthernTileEdgeLat);
463 QFETCH(qreal, expectedEasternTileEdgeLon);
464 QFETCH(qreal, expectedSouthernTileEdgeLat);
465
466 const GeoSceneMercatorTileProjection projection;
467
468 // method variants with GeoDataLatLonBox
469 const GeoDataLatLonBox latLonBox = projection.geoCoordinates(zoomLevel, tileX, tileY);
470
471 QCOMPARE(latLonBox.west(), expectedWesternTileEdgeLon);
472 QFUZZYCOMPARE(latLonBox.north(), expectedNorthernTileEdgeLat, 0.00001);
473 QCOMPARE(latLonBox.east(), expectedEasternTileEdgeLon);
474 QFUZZYCOMPARE(latLonBox.south(), expectedSouthernTileEdgeLat, 0.00001);
475
476 TileId tileId(QStringLiteral("testmap"), zoomLevel, tileX, tileY);
477 const GeoDataLatLonBox latLonBox2 = projection.geoCoordinates(tileId);
478
479 QCOMPARE(latLonBox2.west(), expectedWesternTileEdgeLon);
480 QFUZZYCOMPARE(latLonBox2.north(), expectedNorthernTileEdgeLat, 0.00001);
481 QCOMPARE(latLonBox2.east(), expectedEasternTileEdgeLon);
482 QFUZZYCOMPARE(latLonBox2.south(), expectedSouthernTileEdgeLat, 0.00001);
483 }
484
485 } // namespace Marble
486
487 QTEST_MAIN(Marble::TileProjectionTest)
488
489 #include "TestTileProjection.moc"
490