Esempio n. 1
0
    def test_extent_metatile(self):
        self.assertEqual(self.ftg.extent(TileCoord(1, 0, 0, 3)),
                         (420000, 349998.5, 420001.5, 350000))

        self.assertEqual(self.ftg2.extent(TileCoord(1, 0, 0, 3)),
                         (420000, 349923.2, 420076.8, 350000))
Esempio n. 2
0
    def test_extent_metatile(self) -> None:
        assert self.ftg.extent(TileCoord(1, 0, 0, 3)) == (420000, 349998.5,
                                                          420001.5, 350000)

        assert self.ftg2.extent(TileCoord(1, 0, 0, 3)) == (420000, 349923.2,
                                                           420076.8, 350000)
Esempio n. 3
0
 def test_children_root(self) -> None:
     tc = TileCoord(0, 0, 0)
     assert sorted(self.ftg.children(tc)) == sorted(self.qtg.children(tc))
Esempio n. 4
0
 def test_extent_metatile(self) -> None:
     assert self.ftg.extent(TileCoord(1, 4, 6, 2)) == (428000, 334000,
                                                       432000, 338000)
Esempio n. 5
0
 def test_tilecoord(self) -> None:
     assert self.ftg.tilecoord(1, 428000, 336000) == TileCoord(1, 4, 7)
     assert self.ftg.tilecoord(1, 430000, 334000) == TileCoord(1, 5, 8)
     assert self.ftg.tilecoord(1, 432000, 332000) == TileCoord(1, 6, 9)
Esempio n. 6
0
 def test_init_kwargs(self) -> None:
     tile = Tile(TileCoord(0, 0, 0), kwarg=None)
     self.assertEqual(tile.kwarg, None)
Esempio n. 7
0
 def test_extent(self) -> None:
     assert self.ftg.extent(TileCoord(1, 4, 6)) == (428000, 336000, 430000,
                                                    338000)
     assert self.ftg.extent(TileCoord(1, 5, 7)) == (430000, 334000, 432000,
                                                    336000)
Esempio n. 8
0
 def test_parent(self):
     self.assertEqual(self.qtg.parent(TileCoord(5, 11, 21)),
                      TileCoord(4, 5, 10))
Esempio n. 9
0
 def test_parent_root(self):
     self.assertEqual(self.qtg.parent(TileCoord(0, 0, 0)), None)
Esempio n. 10
0
 def test_parent(self):
     tc = TileCoord(3, 3, 5)
     self.assertEqual(self.ftg.parent(tc), self.qtg.parent(tc))
Esempio n. 11
0
 def test_extent_z0(self):
     self.assertEqual(self.qtg.extent(TileCoord(0, 0, 0)),
                      (0.0, 1.0, 2.0, 3.0))
Esempio n. 12
0
 def test_children_root(self):
     tc = TileCoord(0, 0, 0)
     self.assertEqual(sorted(self.ftg.children(tc)),
                      sorted(self.qtg.children(tc)))
Esempio n. 13
0
 def test_children(self):
     tc = TileCoord(2, 2, 3)
     self.assertEqual(sorted(self.ftg.children(tc)),
                      sorted(self.qtg.children(tc)))
Esempio n. 14
0
    def test_extent_metatile_border(self):
        self.assertEqual(self.ftg.extent(TileCoord(1, 0, 0, 3), 50),
                         (419999.75, 349998.25, 420001.75, 350000.25))

        self.assertEqual(self.ftg2.extent(TileCoord(1, 0, 0, 3), 50),
                         (419995, 349918.2, 420081.8, 350005))
Esempio n. 15
0
 def _tilecoord(self, match):
     return TileCoord(*map(int, match.groups()))
Esempio n. 16
0
 def test_roots(self):
     self.assertEqual(list(self.qtg.roots()), [TileCoord(0, 0, 0)])
Esempio n. 17
0
    def serve(self, path, params, **kwargs):
        dimensions = []
        metadata = {}

        if path:
            if tuple(path[: len(self.static_path)]) == tuple(self.static_path):
                body, mime = self._get(  # pylint: disable=not-callable
                    "/".join(path[len(self.static_path) :]), **kwargs
                )
                if mime is not None:
                    return self.response(
                        body,
                        {
                            "Content-Type": mime,
                            "Expires": (
                                datetime.datetime.utcnow() + datetime.timedelta(hours=self.expires_hours)
                            ).isoformat(),
                            "Cache-Control": "max-age={}".format((3600 * self.expires_hours)),
                            "Access-Control-Allow-Origin": "*",
                            "Access-Control-Allow-Methods": "GET",
                        },
                        **kwargs,
                    )
                else:  # pragma: no cover
                    return body
            elif len(path) >= 1 and path[0] != self.wmts_path:  # pragma: no cover
                return self.error(
                    404,
                    "Type '{}' don't exists, allows values: '{}' or '{}'".format(
                        path[0], self.wmts_path, "/".join(self.static_path)
                    ),
                    **kwargs,
                )
            path = path[1:]  # remove type

        if path:
            if len(path) == 2 and path[0] == "1.0.0" and path[1].lower() == "wmtscapabilities.xml":
                params["SERVICE"] = "WMTS"
                params["VERSION"] = "1.0.0"
                params["REQUEST"] = "GetCapabilities"
            elif len(path) < 7:
                return self.error(400, "Not enough path", **kwargs)
            else:
                params["SERVICE"] = "WMTS"
                params["VERSION"] = path[0]

                params["LAYER"] = path[1]
                params["STYLE"] = path[2]

                if params["LAYER"] in self.layers:
                    layer = tilegeneration.layers[params["LAYER"]]
                else:
                    return self.error(400, "Wrong Layer '{}'".format(params["LAYER"]), **kwargs)

                index = 3
                dimensions = path[index : index + len(layer["dimensions"])]
                for dimension in layer["dimensions"]:
                    metadata["dimension_" + dimension["name"]] = path[index]
                    params[dimension["name"].upper()] = path[index]
                    index += 1

                last = path[-1].split(".")
                if len(path) < index + 4:  # pragma: no cover
                    return self.error(400, "Not enough path", **kwargs)
                params["TILEMATRIXSET"] = path[index]
                params["TILEMATRIX"] = path[index + 1]
                params["TILEROW"] = path[index + 2]
                if len(path) == index + 4:
                    params["REQUEST"] = "GetTile"
                    params["TILECOL"] = last[0]
                    if last[1] != layer["extension"]:  # pragma: no cover
                        return self.error(400, "Wrong extension '{}'".format(last[1]), **kwargs)
                elif len(path) == index + 6:
                    params["REQUEST"] = "GetFeatureInfo"
                    params["TILECOL"] = path[index + 3]
                    params["I"] = path[index + 4]
                    params["J"] = last[0]
                    params["INFO_FORMAT"] = layer.get("info_formats", ["application/vnd.ogc.gml"])[0]
                else:  # pragma: no cover
                    return self.error(400, "Wrong path length", **kwargs)

                params["FORMAT"] = layer["mime_type"]
        else:
            if "SERVICE" not in params or "REQUEST" not in params or "VERSION" not in params:
                return self.error(400, "Not all required parameters are present", **kwargs)

        if params["SERVICE"] != "WMTS":
            return self.error(400, "Wrong Service '{}'".format(params["SERVICE"]), **kwargs)
        if params["VERSION"] != "1.0.0":
            return self.error(400, "Wrong Version '{}'".format(params["VERSION"]), **kwargs)

        if params["REQUEST"] == "GetCapabilities":
            if "wmtscapabilities_file" in self.cache:
                wmtscapabilities_file = self.cache["wmtscapabilities_file"]
                body, mime = self._get(wmtscapabilities_file, **kwargs)  # pylint: disable=not-callable
            else:
                body = controller.get_wmts_capabilities(tilegeneration, self.cache).encode("utf-8")
                mime = "application/xml"
            if mime is not None:
                return self.response(
                    body,
                    headers={
                        "Content-Type": "application/xml",
                        "Expires": (
                            datetime.datetime.utcnow() + datetime.timedelta(hours=self.expires_hours)
                        ).isoformat(),
                        "Cache-Control": "max-age={}".format((3600 * self.expires_hours)),
                        "Access-Control-Allow-Origin": "*",
                        "Access-Control-Allow-Methods": "GET",
                    },
                    **kwargs,
                )
            else:  # pragma: no cover
                return body

        if (
            "FORMAT" not in params
            or "LAYER" not in params
            or "TILEMATRIXSET" not in params
            or "TILEMATRIX" not in params
            or "TILEROW" not in params
            or "TILECOL" not in params
        ):  # pragma: no cover
            return self.error(400, "Not all required parameters are present", **kwargs)

        if not path:
            if params["LAYER"] in self.layers:
                layer = tilegeneration.layers[params["LAYER"]]
            else:
                return self.error(400, "Wrong Layer '{}'".format(params["LAYER"]), **kwargs)

            for dimension in layer["dimensions"]:
                value = (
                    params[dimension["name"].upper()]
                    if dimension["name"].upper() in params
                    else dimension["default"]
                )
                dimensions.append(value)
                metadata["dimension_" + dimension["name"]] = value

        if params["STYLE"] != layer["wmts_style"]:
            return self.error(400, "Wrong Style '{}'".format(params["STYLE"]), **kwargs)
        if params["TILEMATRIXSET"] != layer["grid"]:
            return self.error(400, "Wrong TileMatrixSet '{}'".format(params["TILEMATRIXSET"]), **kwargs)

        metadata["layer"] = layer["name"]
        tile = Tile(
            TileCoord(
                # TODO fix for matrix_identifier = resolution
                int(params["TILEMATRIX"]),
                int(params["TILECOL"]),
                int(params["TILEROW"]),
            ),
            metadata=metadata,
        )

        if params["REQUEST"] == "GetFeatureInfo":
            if "I" not in params or "J" not in params or "INFO_FORMAT" not in params:  # pragma: no cover
                return self.error(400, "Not all required parameters are present", **kwargs)
            if "query_layers" in layer:
                return self.forward(
                    layer["url"]
                    + "?"
                    + urlencode(
                        {
                            "SERVICE": "WMS",
                            "VERSION": layer["version"],
                            "REQUEST": "GetFeatureInfo",
                            "LAYERS": layer["layers"],
                            "QUERY_LAYERS": layer["query_layers"],
                            "STYLES": params["STYLE"],
                            "FORMAT": params["FORMAT"],
                            "INFO_FORMAT": params["INFO_FORMAT"],
                            "WIDTH": layer["grid_ref"]["tile_size"],
                            "HEIGHT": layer["grid_ref"]["tile_size"],
                            "SRS": layer["grid_ref"]["srs"],
                            "BBOX": layer["grid_ref"]["obj"].extent(tile.tilecoord),
                            "X": params["I"],
                            "Y": params["J"],
                        }
                    ),
                    no_cache=True,
                    **kwargs,
                )
            else:  # pragma: no cover
                return self.error(400, "Layer '{}' not queryable".format(layer["name"]), **kwargs)

        if params["REQUEST"] != "GetTile":
            return self.error(400, "Wrong Request '{}'".format(params["REQUEST"]), **kwargs)

        if params["FORMAT"] != layer["mime_type"]:
            return self.error(400, "Wrong Format '{}'".format(params["FORMAT"]), **kwargs)

        if tile.tilecoord.z > self.max_zoom_seed[layer["name"]]:  # pragma: no cover
            return self._map_cache(layer, tile, params, kwargs)

        if layer["name"] in self.filters:
            layer_filter = self.filters[layer["name"]]
            meta_size = layer["meta_size"]
            meta_tilecoord = (
                TileCoord(
                    # TODO fix for matrix_identifier = resolution
                    tile.tilecoord.z,
                    tile.tilecoord.x / meta_size * meta_size,
                    tile.tilecoord.y / meta_size * meta_size,
                    meta_size,
                )
                if meta_size != 1
                else tile.tilecoord
            )
            if not layer_filter.filter_tilecoord(meta_tilecoord, layer["name"]):  # pragma: no cover
                return self._map_cache(layer, tile, params, kwargs)

        store_ref = "/".join([params["LAYER"]] + list(dimensions))
        if store_ref in self.stores:  # pragma: no cover
            store = self.stores[store_ref]
        else:  # pragma: no cover
            return self.error(
                400,
                "No store found for layer '{}' and dimensions {}".format(
                    layer["name"], ", ".join(dimensions)
                ),
                **kwargs,
            )

        tile = store.get_one(tile)
        if tile:
            if tile.error:
                return self.error(500, tile.error, **kwargs)

            return self.response(
                tile.data,
                headers={
                    "Content-Type": tile.content_type,
                    "Expires": (
                        datetime.datetime.utcnow() + datetime.timedelta(hours=self.expires_hours)
                    ).isoformat(),
                    "Cache-Control": "max-age={}".format((3600 * self.expires_hours)),
                    "Access-Control-Allow-Origin": "*",
                    "Access-Control-Allow-Methods": "GET",
                    "Tile-Backend": "Cache",
                },
                **kwargs,
            )
        else:
            return self.error(204, **kwargs)
Esempio n. 18
0
 def test_root_parents(self):
     for root_z in set(root.z for root in self.ftg.roots()):
         self.assertEquals(self.ftg.parent(TileCoord(root_z, 0, 0)), None)
Esempio n. 19
0
 def test_empty(self) -> None:
     tile = Tile(TileCoord(0, 0, 0))
     self.assertEqual(tile.content_type, None)
     self.assertEqual(tile.content_encoding, None)
     self.assertEqual(tile.data, None)
     self.assertEqual(tile.error, None)
Esempio n. 20
0
 def test_root_zero(self):
     self.assertEquals(self.ftg.parent(TileCoord(0, 0, 0)), None)
Esempio n. 21
0
 def test_extent_border(self) -> None:
     assert self.ftg.extent(TileCoord(1, 4, 6),
                            5) == (427900, 335900, 430100, 338100)
Esempio n. 22
0
 def test_extent_border(self):
     self.assertEqual(self.ftg.extent(TileCoord(1, 4, 6), 5),
                      (427900, 335900, 430100, 338100))
Esempio n. 23
0
 def test_extent_metatile_border(self) -> None:
     assert self.ftg.extent(TileCoord(1, 4, 6, 2),
                            5) == (427900, 333900, 432100, 338100)
Esempio n. 24
0
 def test_extent_metatile(self):
     self.assertEqual(self.ftg.extent(TileCoord(1, 4, 6, 2)),
                      (428000, 334000, 432000, 338000))
Esempio n. 25
0
 def test_extent(self) -> None:
     assert self.ftg.extent(TileCoord(0, 0, 0)) == (420000.0, 349000.0,
                                                    421000.0, 350000.0)
     assert self.ftg.extent(TileCoord(2, 0, 0)) == (420000.0, 349750.0,
                                                    420250.0, 350000.0)
Esempio n. 26
0
 def test_extent_metatile_border(self):
     self.assertEqual(self.ftg.extent(TileCoord(1, 4, 6, 2), 5),
                      (427900, 333900, 432100, 338100))
Esempio n. 27
0
 def test_children(self) -> None:
     tc = TileCoord(2, 2, 3)
     assert sorted(self.ftg.children(tc)) == sorted(self.qtg.children(tc))
Esempio n. 28
0
    def serve(self, path, params, **kwargs):
        dimensions = []
        metadata = {}

        if path:
            if len(path) >= 1 and path[0] == self.static_path:
                body, mime = self._get('/'.join(path[1:]), **kwargs)
                if mime is not None:
                    return self.response(
                        body, {
                            'Content-Type':
                            mime,
                            'Expires':
                            (datetime.datetime.utcnow() + datetime.timedelta(
                                hours=self.expires_hours)).isoformat(),
                            'Cache-Control':
                            "max-age={}".format((3600 * self.expires_hours)),
                            'Access-Control-Allow-Origin':
                            '*',
                            'Access-Control-Allow-Methods':
                            'GET',
                        }, **kwargs)
                else:  # pragma: no cover
                    return body
            elif len(path
                     ) >= 1 and path[0] != self.wmts_path:  # pragma: no cover
                return self.error(
                    404, "Type '{}' don't exists, allows values: '{}' or '{}'".
                    format(path[0], self.wmts_path,
                           self.static_path), **kwargs)
            path = path[1:]  # remove type

            if len(path) == 2 and path[0] == '1.0.0' and path[1].lower(
            ) == 'wmtscapabilities.xml':
                params['SERVICE'] = 'WMTS'
                params['VERSION'] = '1.0.0'
                params['REQUEST'] = 'GetCapabilities'
            elif len(path) < 7:
                return self.error(400, "Not enough path", **kwargs)
            else:
                params['SERVICE'] = 'WMTS'
                params['VERSION'] = path[0]

                params['LAYER'] = path[1]
                params['STYLE'] = path[2]

                if params['LAYER'] in self.layers:
                    layer = self.tilegeneration.layers[params['LAYER']]
                else:
                    return self.error(
                        400, "Wrong Layer '{}'".format(params['LAYER']),
                        **kwargs)

                index = 3
                dimensions = path[index:index + len(layer['dimensions'])]
                for dimension in layer['dimensions']:
                    metadata["dimension_" + dimension['name']] = path[index]
                    params[dimension['name'].upper()] = path[index]
                    index += 1

                last = path[-1].split('.')
                if len(path) < index + 4:  # pragma: no cover
                    return self.error(400, "Not enough path", **kwargs)
                params['TILEMATRIXSET'] = path[index]
                params['TILEMATRIX'] = path[index + 1]
                params['TILEROW'] = path[index + 2]
                if len(path) == index + 4:
                    params['REQUEST'] = 'GetTile'
                    params['TILECOL'] = last[0]
                    if last[1] != layer['extension']:  # pragma: no cover
                        return self.error(
                            400, "Wrong extension '{}'".format(last[1]),
                            **kwargs)
                elif len(path) == index + 6:
                    params['REQUEST'] = 'GetFeatureInfo'
                    params['TILECOL'] = path[index + 3]
                    params['I'] = path[index + 4]
                    params['J'] = last[0]
                    params['INFO_FORMAT'] = layer.get(
                        'info_formats', ['application/vnd.ogc.gml'])[0]
                else:  # pragma: no cover
                    return self.error(400, "Wrong path length", **kwargs)

                params['FORMAT'] = layer['mime_type']
        else:
            if \
                    'SERVICE' not in params or \
                    'REQUEST' not in params or \
                    'VERSION' not in params:
                return self.error(400,
                                  "Not all required parameters are present",
                                  **kwargs)

        if params['SERVICE'] != 'WMTS':
            return self.error(400,
                              "Wrong Service '{}'".format(params['SERVICE']),
                              **kwargs)
        if params['VERSION'] != '1.0.0':
            return self.error(400,
                              "Wrong Version '{}'".format(params['VERSION']),
                              **kwargs)

        if params['REQUEST'] == 'GetCapabilities':
            if 'wmtscapabilities_file' in self.cache:
                wmtscapabilities_file = self.cache['wmtscapabilities_file']
                body, mime = self._get(wmtscapabilities_file, **kwargs)
            else:
                body = controller.get_wmts_capabilities(
                    self.tilegeneration, self.cache).encode('utf-8')
                mime = "application/xml"
            if mime is not None:
                return self.response(
                    body,
                    headers={
                        'Content-Type':
                        "application/xml",
                        'Expires':
                        (datetime.datetime.utcnow() + datetime.timedelta(
                            hours=self.expires_hours)).isoformat(),
                        'Cache-Control':
                        "max-age={}".format((3600 * self.expires_hours)),
                        'Access-Control-Allow-Origin':
                        '*',
                        'Access-Control-Allow-Methods':
                        'GET',
                    },
                    **kwargs)
            else:  # pragma: no cover
                return body

        if \
                'FORMAT' not in params or \
                'LAYER' not in params or \
                'TILEMATRIXSET' not in params or \
                'TILEMATRIX' not in params or \
                'TILEROW' not in params or \
                'TILECOL' not in params:  # pragma: no cover
            return self.error(400, "Not all required parameters are present",
                              **kwargs)

        if not path:
            if params['LAYER'] in self.layers:
                layer = self.tilegeneration.layers[params['LAYER']]
            else:
                return self.error(400,
                                  "Wrong Layer '{}'".format(params['LAYER']),
                                  **kwargs)

            for dimension in layer['dimensions']:
                value = params[dimension['name'].upper()] \
                    if dimension['name'].upper() in params \
                    else dimension['default']
                dimensions.append(value)
                metadata["dimension_" + dimension['name']] = value

        if params['STYLE'] != layer['wmts_style']:
            return self.error(400, "Wrong Style '{}'".format(params['STYLE']),
                              **kwargs)
        if params['TILEMATRIXSET'] != layer['grid']:
            return self.error(
                400,
                "Wrong TileMatrixSet '{}'".format(params['TILEMATRIXSET']),
                **kwargs)

        tile = Tile(
            TileCoord(
                # TODO fix for matrix_identifier = resolution
                int(params['TILEMATRIX']),
                int(params['TILECOL']),
                int(params['TILEROW']),
            ),
            metadata=metadata)

        if params['REQUEST'] == 'GetFeatureInfo':
            if \
                    'I' not in params or \
                    'J' not in params or \
                    'INFO_FORMAT' not in params:  # pragma: no cover
                return self.error(400,
                                  "Not all required parameters are present",
                                  **kwargs)
            if 'query_layers' in layer:
                return self.forward(layer['url'] + '?' + urlencode(
                    {
                        'SERVICE': 'WMS',
                        'VERSION': layer['version'],
                        'REQUEST': 'GetFeatureInfo',
                        'LAYERS': layer['layers'],
                        'QUERY_LAYERS': layer['query_layers'],
                        'STYLES': params['STYLE'],
                        'FORMAT': params['FORMAT'],
                        'INFO_FORMAT': params['INFO_FORMAT'],
                        'WIDTH': layer['grid_ref']['tile_size'],
                        'HEIGHT': layer['grid_ref']['tile_size'],
                        'SRS': layer['grid_ref']['srs'],
                        'BBOX': layer['grid_ref']['obj'].extent(
                            tile.tilecoord),
                        'X': params['I'],
                        'Y': params['J'],
                    }),
                                    no_cache=True,
                                    **kwargs)
            else:  # pragma: no cover
                return self.error(
                    400, "Layer '{}' not queryable".format(layer['name']),
                    **kwargs)

        if params['REQUEST'] != 'GetTile':
            return self.error(400,
                              "Wrong Request '{}'".format(params['REQUEST']),
                              **kwargs)

        if params['FORMAT'] != layer['mime_type']:
            return self.error(400,
                              "Wrong Format '{}'".format(params['FORMAT']),
                              **kwargs)

        if tile.tilecoord.z > self.max_zoom_seed[
                layer['name']]:  # pragma: no cover
            return self.forward(self.mapcache_baseurl + '?' +
                                urlencode(params),
                                headers=self.mapcache_header,
                                **kwargs)

        if layer['name'] in self.filters:
            layer_filter = self.filters[layer['name']]
            meta_size = layer['meta_size']
            meta_tilecoord = TileCoord(
                # TODO fix for matrix_identifier = resolution
                tile.tilecoord.z,
                tile.tilecoord.x / meta_size * meta_size,
                tile.tilecoord.y / meta_size * meta_size,
                meta_size,
            ) if meta_size != 1 else tile.tilecoord
            if not layer_filter.filter_tilecoord(
                    meta_tilecoord):  # pragma: no cover
                return self.forward(self.mapcache_baseurl + '?' +
                                    urlencode(params),
                                    headers=self.mapcache_header,
                                    **kwargs)

        store_ref = '/'.join([params['LAYER']] + list(dimensions))
        if store_ref in self.stores:  # pragma: no cover
            store = self.stores[store_ref]
        else:  # pragma: no cover
            return self.error(
                400, "No store found for layer '{}' and dimensions {}".format(
                    layer['name'], ', '.join(dimensions)), **kwargs)

        tile = store.get_one(tile)
        if tile:
            if tile.error:
                return self.error(500, tile.error, **kwargs)

            return self.response(
                tile.data,
                headers={
                    'Content-Type':
                    tile.content_type,
                    'Expires':
                    (datetime.datetime.utcnow() +
                     datetime.timedelta(hours=self.expires_hours)).isoformat(),
                    'Cache-Control':
                    "max-age={}".format((3600 * self.expires_hours)),
                    'Access-Control-Allow-Origin':
                    '*',
                    'Access-Control-Allow-Methods':
                    'GET',
                },
                **kwargs)
        else:
            return self.error(204, **kwargs)
Esempio n. 29
0
 def test_parent(self) -> None:
     tc = TileCoord(3, 3, 5)
     assert self.ftg.parent(tc) == self.qtg.parent(tc)
Esempio n. 30
0
 def test_extent(self):
     self.assertEqual(self.ftg.extent(TileCoord(1, 4, 6)),
                      (428000, 336000, 430000, 338000))
     self.assertEqual(self.ftg.extent(TileCoord(1, 5, 7)),
                      (430000, 334000, 432000, 336000))