You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/01/18 16:04:43 UTC

[sis-site] branch main updated: Add a "How to…" section with three first items.

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sis-site.git


The following commit(s) were added to refs/heads/main by this push:
     new 9845d209 Add a "How to…" section with three first items.
9845d209 is described below

commit 9845d209a66ca0c431ca1de075e1396f2139823c
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Jan 18 11:09:36 2023 +0100

    Add a "How to…" section with three first items.
---
 content/howto/_index.md                            |  11 ++
 .../raster_values_at_geographic_coordinates.md     | 185 +++++++++++++++++++++
 content/howto/rasters_bigger_than_memory.md        | 133 +++++++++++++++
 content/howto/resample_and_save_raster.md          | 147 ++++++++++++++++
 layouts/_default/baseof.html                       |   1 +
 layouts/partials/menu.html                         |   1 +
 6 files changed, 478 insertions(+)

diff --git a/content/howto/_index.md b/content/howto/_index.md
new file mode 100644
index 00000000..ba3671f0
--- /dev/null
+++ b/content/howto/_index.md
@@ -0,0 +1,11 @@
+---
+title: How to
+---
+
+Java code examples for performing some tasks with Apache {{% SIS %}}.
+
+# Rasters
+
+* [Get raster values at geographic coordinates](howto/raster_values_at_geographic_coordinates.html)
+* [Handle rasters bigger than memory](howto/rasters_bigger_than_memory.html)
+* [Resample a raster and write to a file](howto/resample_and_save_raster.html)
diff --git a/content/howto/raster_values_at_geographic_coordinates.md b/content/howto/raster_values_at_geographic_coordinates.md
new file mode 100644
index 00000000..334f075d
--- /dev/null
+++ b/content/howto/raster_values_at_geographic_coordinates.md
@@ -0,0 +1,185 @@
+---
+title: Get raster values at geographic coordinates
+---
+
+This example reads a netCDF file and fetches values at given coordinates.
+The coordinates can be expressed in different Coordinate Reference System (CRS).
+Conversions from geographic coordinates to pixel coordinates,
+followed by conversions from raster data to units of measurement,
+are done automatically.
+Raster rata and spatiotemporal coordinates can have more than two dimensions.
+
+This example uses data in netCDF format.
+A netCDF file can contain an arbitrary amount of variables.
+For this reason, the data store implements the `Aggregate` interface
+and the desired variable must be specified.
+A similar code can be used for reading data in other
+formats supported by Apache {{% SIS %}} such as GeoTIFF,
+but not all formats are aggregates.
+For some file formats, the data store implements directly
+the `GridCoverageResource` interface instead of `Aggregate`.
+
+
+# Direct dependencies
+
+Maven coordinates                   | Module info                     | Remarks
+----------------------------------- | ------------------------------- | --------------------
+`org.apache.sis.storage:sis-netcdf` | `org.apache.sis.storage.netcdf` |
+`edu.ucar:cdm-core`                 |                                 | For netCDF-4 or HDF5
+
+The `cdm-core` dependency can be omitted for netCDF-3 (a.k.a. "classic"),
+GeoTIFF or any other [formats supported by Apache SIS](../formats.html).
+For the dependencies required for reading GeoTIFF instead of netCDF files,
+see the [rasters bigger than memory](rasters_bigger_than_memory.html) snippet.
+
+
+# Code snippet
+
+The file name, resource name and geographic coordinates
+in following snippet need to be updated for yours data.
+
+{{< highlight java >}}
+import java.io.File;
+import java.util.Map;
+import javax.measure.Unit;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.Aggregate;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStores;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.geometry.GeneralDirectPosition;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.measure.Units;
+
+public class RasterValuesAtGeographicCoordinates {
+    /**
+     * Demo entry point.
+     *
+     * @param  args  ignored.
+     * @throws DataStoreException if an error occurred while reading the raster.
+     */
+    public static void main(String[] args) throws DataStoreException {
+        try (DataStore store = DataStores.open(new File("CMEMS.nc"))) {
+            /*
+             * See what is inside this file. One of the components listed
+             * below can be given in argument to `findResource(String)`.
+             */
+            printComponents(store);
+            /*
+             * The following code read fully the specified resource.
+             * For reading only a subset, or for handling data bigger
+             * than memory, see "How to..." in Apache SIS web site.
+             */
+            Resource resource = store.findResource("sea_surface_height_above_geoid");
+            GridCoverage data = ((GridCoverageResource) resource).read(null, null);
+            System.out.printf("Information about the selected resource:%n%s%n", data);
+            /*
+             * Switch to a view of the data in the units of measurement.
+             * Then get the unit of measurement of the first band (0).
+             * If no unit is specified, fallback on dimensionless unit.
+             */
+            data = data.forConvertedValues(true);
+            int band = 0;
+            Unit<?> unit = data.getSampleDimensions().get(band).getUnits().orElse(Units.UNITY);
+            /*
+             * Get raster values at geographic coordinates expressed in WGS84.
+             * Coordinate values in this example are in (latitude, longitude) order.
+             * Any compatible coordinate reference system (CRS) can be used below,
+             * Apache SIS will automatically transform to the CRS used by the raster.
+             * If the raster data are three-dimensional, a 3D CRS should be specified.
+             */
+            System.out.println("Evaluation at some (latitude, longitude) coordinates:");
+            var point = new GeneralDirectPosition(CommonCRS.WGS84.geographic());
+            GridCoverage.Evaluator eval = data.evaluator();
+            /*
+             * If the data are three-dimensional but we still want to use two-dimensional
+             * coordinates, we need to specify a default value for the temporal dimension.
+             * This code set the default to slice 0 (the first slice) in dimension 2.
+             * Omit this line if the data are two-dimensional or if `point` has a 3D CRS.
+             */
+            eval.setDefaultSlice(Map.of(2, 0L));
+            /*
+             * The same `Evaluator` can be reused as often as needed for evaluating
+             * at many points.
+             */
+            point.setCoordinate(40, -10);           // 40°N 10°W
+            double[] values = eval.apply(point);
+            System.out.printf("- Value at %s is %g %s.%n", point, values[band], unit);
+
+            point.setCoordinate(30, -15);           // 30°N 15°W
+            values = eval.apply(point);
+            System.out.printf("- Value at %s is %g %s.%n", point, values[band], unit);
+        }
+    }
+
+    /**
+     * Lists the components found in the given data store.
+     * They are the values that can be given to {@link DataStore#findResource(String).
+     *
+     * @param  store  the data store from which to get the components.
+     * @throws DataStoreException if an error occurred while reading the raster.
+     */
+    private static void printComponents(DataStore store) throws DataStoreException {
+        if (store instanceof Aggregate a) {
+            System.out.println("Components found in the data store:");
+            for (Resource component : a.components()) {
+                component.getIdentifier().ifPresent((id) -> System.out.println("- " + id));
+            }
+        } else {
+            System.out.println("The data store is not an aggregate.");
+        }
+        System.out.println();
+    }
+}
+{{< / highlight >}}
+
+
+# Output
+
+The output depends on the raster data and the locale.
+Below is an example:
+
+```
+Components found in the data store:
+- sea_surface_height_above_geoid
+- sea_water_velocity
+
+Information about the selected resource:
+Raster
+  ├─Coverage domain
+  │   ├─Grid extent
+  │   │   ├─Column: [0 …  864]  (865 cells)
+  │   │   ├─Row:    [0 … 1080] (1081 cells)
+  │   │   └─Time:   [0 …   95]   (96 cells)
+  │   ├─Geographic extent
+  │   │   ├─Lower bound:  25°59′09″N  19°00′50″W  2022-05-16T00:00:00Z
+  │   │   └─Upper bound:  56°00′50″N  05°00′50″E  2022-05-17T00:00:00Z
+  │   ├─Envelope
+  │   │   ├─Geodetic longitude: -19.01388888888889 … 5.013888888888888   ∆Lon = 0.02777778°
+  │   │   ├─Geodetic latitude:   25.98611111111111 … 56.013888888888886  ∆Lat = 0.02777778°
+  │   │   └─time:                        634,392.0 … 634,416.0           ∆t   = 0.25 h
+  │   ├─Coordinate reference system
+  │   │   └─time latitude longitude
+  │   └─Conversion (origin in a cell center)
+  │       └─┌                                                              ┐
+  │         │ 0.027777777777777776  0                     0        -19.000 │
+  │         │ 0                     0.027777777777777776  0         26.000 │
+  │         │ 0                     0                     0.25  634392.125 │
+  │         │ 0                     0                     0          1     │
+  │         └                                                              ┘
+  └─Sample dimensions
+      └─┌────────────────────┬────────────────────────┬────────────────────┐
+        │       Values       │        Measures        │        Name        │
+        ╞════════════════════╧════════════════════════╧════════════════════╡
+        │ zos                                                              │
+        ├────────────────────┬────────────────────────┬────────────────────┤
+        │           -32,767  │ NaN #0                 │ Fill value         │
+        │ [-10,000 … 10,000] │ [-10.0000 … 10.0000] m │ Sea surface height │
+        └────────────────────┴────────────────────────┴────────────────────┘
+
+Evaluation at some (latitude, longitude) coordinates:
+- Value at POINT(40 -10) is 0.188000 m.
+- Value at POINT(30 -15) is 0.619000 m.
+```
diff --git a/content/howto/rasters_bigger_than_memory.md b/content/howto/rasters_bigger_than_memory.md
new file mode 100644
index 00000000..c6d89544
--- /dev/null
+++ b/content/howto/rasters_bigger_than_memory.md
@@ -0,0 +1,133 @@
+---
+title: Handle rasters bigger than memory
+---
+
+This example opens a big GeoTIFF file without reading the tiles immediately.
+Instead, tiles will be read only when requested by a call to the Java2D `RenderedImage.getTile(int, int)` method.
+Loaded tiles are cached by soft references, i.e. they may be discarted and reloaded when needed again.
+This approach allows processing of raster data larger than memory,
+provided that the application does not request all tiles at once.
+It integrates well with operations provided by Apache {{% SIS %}} such as
+[raster resampling](resample_and_save_raster.html) and
+[getting values at geographic coordinates](raster_values_at_geographic_coordinates.html).
+
+The example in this page works with pixel coordinates.
+For working with geographic coordinates, see
+[values at geographic coordinates](raster_values_at_geographic_coordinates.html) snippet.
+
+
+# Direct dependencies
+
+Maven coordinates                           | Module info                           | Remarks
+------------------------------------------- | ------------------------------------- | -----------------------------
+`org.apache.sis.storage:sis-geotiff`        | `org.apache.sis.storage.geotiff`      |
+`org.apache.sis.non-free:sis-embedded-data` | `org.apache.sis.referencing.database` | Optional. Non-Apache license.
+
+The [EPSG dependency](../epsg.html) may or may not be needed,
+depending how the Coordinate Reference System (CRS) is encoded in the GeoTIFF file.
+
+
+# Code snippet
+
+The file name in following snippet need to be updated for yours data.
+
+{{< highlight java >}}
+import java.io.File;
+import java.util.Collection;
+import java.awt.Rectangle;
+import java.awt.image.Raster;
+import java.awt.image.RenderedImage;
+import java.awt.image.ImagingOpException;
+import org.apache.sis.image.ImageProcessor;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.Aggregate;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStores;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.RasterLoadingStrategy;
+import org.apache.sis.coverage.grid.GridCoverage;
+
+public class RasterBiggerThanMemory {
+    /**
+     * Demo entry point.
+     *
+     * @param  args  ignored.
+     * @throws DataStoreException if an error occurred while reading the raster.
+     * @throws ImagingOpException unchecked exception thrown if an error occurred while loading a tile.
+     */
+    public static void main(String[] args) throws DataStoreException {
+        try (DataStore store = DataStores.open(new File("TM250m.tiff"))) {
+            /*
+             * This data store is an aggregate because a GeoTIFF file may contain many images.
+             * Not all data stores are aggregate, so the following casts do not apply to all.
+             * For this example, we know that the file is GeoTIFF and we take the first image.
+             */
+            Collection<? extends Resource> allImages = ((Aggregate) store).components();
+            GridCoverageResource firstImage = (GridCoverageResource) allImages.iterator().next();
+            /*
+             * Following line requests to load data at `RenderedImage.getTile(…)` invocation time.
+             * This is the key line of code for handling rasters larger than memory, but is effective
+             * only if the file is tiled as in, for example, Cloud Optimized GeoTIFF (COG) convention.
+             * Without this line, the default is to load all data at `GridCoverageResource.read(…)`
+             * invocation time.
+             */
+            firstImage.setLoadingStrategy(RasterLoadingStrategy.AT_GET_TILE_TIME);
+            GridCoverage data = firstImage.read(null, null);
+            System.out.printf("Information about the selected image:%n%s%n", data);
+            /*
+             * Get an arbitrary tile, then get an arbitrary sample value in an arbitrary band
+             * (the blue channel) of that tile.
+             */
+            RenderedImage image = data.render(null);
+            System.out.printf("The image has %d × %d tiles.%n", image.getNumXTiles(), image.getNumYTiles());
+
+            Raster tile = image.getTile(130, 80);               // This is where tile loading actually happen.
+            System.out.printf("Got a tile starting at coordinates %d, %d.%n", tile.getMinX(), tile.getMinY());
+            System.out.printf("A sample value in a tile: %d%n", tile.getSample(93710, 57680, 2));
+            /*
+             * If we know in advance which tiles will be requested, specifying them in advance allows
+             * the GeoTIFF reader to use a better strategy than loading the tiles in random order.
+             */
+            var processor = new ImageProcessor();
+            image = processor.prefetch(image, new Rectangle(90000, 50000, 1000, 1000));
+            tile = image.getTile(130, 80);
+            System.out.printf("Same, but from prefetched image: %d%n%n", tile.getSample(93710, 57680, 2));
+        }
+    }
+}
+{{< / highlight >}}
+
+
+# Output
+
+The output depends on the raster data and the locale.
+Below is an example:
+
+```
+Information about the selected image:
+CompressedSubset
+  └─Coverage domain
+      ├─Grid extent
+      │   ├─Column: [0 … 172799] (172800 cells)
+      │   └─Row:    [0 …  86399]  (86400 cells)
+      ├─Geographic extent
+      │   ├─Lower bound:  90°00′00″S  180°00′00″W
+      │   └─Upper bound:  90°00′00″N  180°00′00″E
+      ├─Envelope
+      │   ├─Geodetic longitude: -180.000 … 180.000  ∆Lon = 0.00208333°
+      │   └─Geodetic latitude:   -90.000 … 90.000   ∆Lat = 0.00208333°
+      ├─Coordinate reference system
+      │   └─CRS:84 — WGS 84
+      └─Conversion (origin in a cell center)
+          └─┌                                                                    ┐
+            │ 0.0020833333333333333   0                      -179.99895833333332 │
+            │ 0                      -0.0020833333333333333    89.99895833333333 │
+            │ 0                       0                         1                │
+            └                                                                    ┘
+
+The image has 240 × 120 tiles.
+Got a tile starting at coordinates 93600, 57600.
+A sample value in a tile: 20
+Same, but from prefetched image: 20
+```
diff --git a/content/howto/resample_and_save_raster.md b/content/howto/resample_and_save_raster.md
new file mode 100644
index 00000000..039299ff
--- /dev/null
+++ b/content/howto/resample_and_save_raster.md
@@ -0,0 +1,147 @@
+---
+title: Resample a raster and write to a file
+---
+
+This example reads a raster in a GeoTIFF file
+and reprojects it to a different Coordinate Reference System (CRS).
+The result is saved as a World File in PNG format.
+
+
+# Direct dependencies
+
+Maven coordinates                           | Module info                           | Remarks
+------------------------------------------- | ------------------------------------- | -----------------------------
+`org.apache.sis.storage:sis-geotiff`        | `org.apache.sis.storage.geotiff`      |
+`org.apache.sis.non-free:sis-embedded-data` | `org.apache.sis.referencing.database` | Non-Apache license.
+
+The [EPSG dependency](../epsg.html) is necessary for this example
+because a Coordinate Reference System (CRS) is instantiated from its EPSG code.
+But it would also be possible to specify a CRS without EPSG code,
+for example using Well Known Text (WKT) format.
+
+
+# Code snippet
+
+The file name in following snippet need to be updated for yours data.
+
+{{< highlight java >}}
+import java.nio.file.Paths;
+import java.util.Collection;
+import java.awt.image.ImagingOpException;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.Aggregate;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStores;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridCoverageProcessor;
+import org.apache.sis.image.Interpolation;
+import org.apache.sis.referencing.CRS;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.util.FactoryException;
+
+public class ResampleAndSaveRaster {
+    /**
+     * Demo entry point.
+     *
+     * @param  args  ignored.
+     * @throws DataStoreException if an error occurred while reading the raster.
+     * @throws FactoryException   if an error occurred while creating the Coordinate Reference System (CRS).
+     * @throws TransformException if an error occurred while transforming coordinates to the target CRS.
+     * @throws ImagingOpException unchecked exception thrown if an error occurred while resampling a tile.
+     */
+    public static void main(String[] args) throws DataStoreException, FactoryException, TransformException {
+        try (DataStore store = DataStores.open(Paths.get("Airport.tiff"))) {
+            /*
+             * This data store is an aggregate because a GeoTIFF file may contain many images.
+             * Not all data stores are aggregate, so the following casts do not apply to all.
+             * For this example, we know that the file is GeoTIFF and we take the first image.
+             */
+            Collection<? extends Resource> allImages = ((Aggregate) store).components();
+            GridCoverageResource firstImage = (GridCoverageResource) allImages.iterator().next();
+            /*
+             * The following code read fully the specified resource.
+             * For reading only a subset, or for handling data bigger
+             * than memory, see "How to..." in Apache SIS web site.
+             */
+            GridCoverage data = firstImage.read(null, null);
+            System.out.printf("Information about the selected image:%n%s%n", data);
+            /*
+             * Reproject to "WGS 84 / World Mercator" (EPSG::3395) using bilinear interpolation.
+             * This example lets Apache SIS choose the output grid size and resolution.
+             * But it is possible to specify those aspects if desired.
+             */
+            var processor = new GridCoverageProcessor();
+            processor.setInterpolation(Interpolation.BILINEAR);
+            data = processor.resample(data, CRS.forCode("EPSG::3395"));
+            System.out.printf("Information about the image after reprojection:%n%s%n", data);
+            /*
+             * TODO: Apache SIS is missing an `DataStores.write(…)` convenience method.
+             * Writing a TIFF World File is possible but requires use of internal API.
+             * A public convenience method will be added in next version.
+             */
+        }
+    }
+}
+{{< / highlight >}}
+
+
+# Output
+
+The output depends on the raster data and the locale.
+Below is an example:
+
+```
+Information about the image after reprojection:
+GridCoverage2D
+  ├─Coverage domain
+  │   ├─Grid extent
+  │   │   ├─Column: [0 … 8191] (8192 cells)
+  │   │   └─Row:    [0 … 8191] (8192 cells)
+  │   ├─Geographic extent
+  │   │   ├─Lower bound:  48°59′20″N  02°31′33″E
+  │   │   └─Upper bound:  49°01′08″N  02°34′16″E
+  │   ├─Envelope
+  │   │   ├─Easting:    465,341.6 … 468,618.39999999997  ∆E = 0.4 m
+  │   │   └─Northing: 5,426,352.8 … 5,429,629.6          ∆N = 0.4 m
+  │   ├─Coordinate reference system
+  │   │   └─EPSG:32631 — WGS 84 / UTM zone 31N
+  │   └─Conversion (origin in a cell center)
+  │       └─┌                      ┐
+  │         │ 0.4   0     465341.8 │
+  │         │ 0    -0.4  5429629.4 │
+  │         │ 0     0          1   │
+  │         └                      ┘
+  └─Image layout
+      ├─Origin: 0, 0
+      ├─Tile size: 8,192 × 128
+      ├─Data type: byte
+      └─Image is opaque.
+
+Information about the image after reprojection:
+GridCoverage2D
+  ├─Coverage domain
+  │   ├─Grid extent
+  │   │   ├─Dimension 0: [0 … 8239] (8240 cells)
+  │   │   └─Dimension 1: [0 … 8240] (8241 cells)
+  │   ├─Geographic extent
+  │   │   ├─Lower bound:  48°59′20″N  02°31′33″E
+  │   │   └─Upper bound:  49°01′08″N  02°34′16″E
+  │   ├─Envelope
+  │   │   ├─Easting:   281,190.4273301751 … 286,207.11249780044  ∆E = 0.60882102 m
+  │   │   └─Northing: 6,240,752.860382801 … 6,245,770.154371441  ∆N = 0.60882102 m
+  │   ├─Coordinate reference system
+  │   │   └─EPSG:3395 — WGS 84 / World Mercator
+  │   └─Conversion (origin in a cell center)
+  │       └─┌                                                            ┐
+  │         │ 0.6088210154885099   0                  281190.73174068285 │
+  │         │ 0                   -0.60882101548851  6245769.8499609330  │
+  │         │ 0                    0                       1             │
+  │         └                                                            ┘
+  └─Image layout
+      ├─Origin: 0, 0
+      ├─Tile size: 1,648 × 201
+      ├─Data type: byte
+      └─Image is opaque.
+```
diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html
index c3b8ed2d..06911070 100644
--- a/layouts/_default/baseof.html
+++ b/layouts/_default/baseof.html
@@ -37,6 +37,7 @@
       <li><a class="nav-link {{ if eq .Page.RelPermalink "/" }} active {{ else }} text-white {{ end }}" href="/index.html">Home</a></li>
       <li><a class="nav-link text-white" href="http://www.apache.org/licenses/">License</a></li>
       <li><a class="nav-link {{ if eq .Page.RelPermalink "/downloads.html" }} active {{ else }} text-white {{ end }}" href="/downloads.html">Downloads</a></li>
+      <li><a class="nav-link {{ if strings.Contains .Page.RelPermalink "/howto" }} active {{ else }} text-white {{ end }}" href="/howto.html">How to…</a></li>
       <li><a class="nav-link {{ if eq .Page.RelPermalink "/standards.html" }} active {{ else }} text-white {{ end }}" href="/standards.html">Standards</a></li>
       <li><a class="nav-link {{ if eq .Page.RelPermalink "/formats.html" }} active {{ else }} text-white {{ end }}" href="/formats.html">Data formats</a></li>
       <li><a class="nav-link {{ if eq .Page.RelPermalink "/epsg.html" }} active {{ else }} text-white {{ end }}" href="/epsg.html">EPSG Database</a></li>
diff --git a/layouts/partials/menu.html b/layouts/partials/menu.html
index e3f52afe..14337b90 100644
--- a/layouts/partials/menu.html
+++ b/layouts/partials/menu.html
@@ -24,6 +24,7 @@
         <ul class="dropdown-menu" aria-labelledby="menuDocumentation">
           <li><a class="dropdown-item" href="/apidocs/index.html">Online Javadoc</a></li>
           <li><a class="dropdown-item" href="/book/en/developer-guide.html">Developer Guide</a></li>
+          <li><a class="dropdown-item" href="/howto.html">How to…</a></li>
           <li><a class="dropdown-item" href="/formats.html">Supported formats</a></li>
           <li><a class="dropdown-item" href="/tables/CoordinateReferenceSystems.html">Supported CRS</a></li>
           <li><a class="dropdown-item" href="/tables/CoordinateOperationMethods.html">Map Projections</a></li>