| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sis.referencing.report; |
| |
| import java.util.Locale; |
| import java.util.Set; |
| import java.util.Map; |
| import java.util.HashSet; |
| import java.util.TreeMap; |
| import java.util.NavigableMap; |
| import java.util.Optional; |
| import java.io.File; |
| import java.io.IOException; |
| import org.opengis.metadata.Identifier; |
| import org.opengis.util.FactoryException; |
| import org.opengis.util.InternationalString; |
| import org.opengis.referencing.IdentifiedObject; |
| import org.opengis.referencing.cs.CartesianCS; |
| import org.opengis.referencing.cs.SphericalCS; |
| import org.opengis.referencing.cs.CoordinateSystem; |
| import org.opengis.referencing.crs.CompoundCRS; |
| import org.opengis.referencing.crs.VerticalCRS; |
| import org.opengis.referencing.crs.GeodeticCRS; |
| import org.opengis.referencing.crs.GeographicCRS; |
| import org.opengis.referencing.crs.EngineeringCRS; |
| import org.opengis.referencing.crs.DerivedCRS; |
| import org.opengis.referencing.crs.CoordinateReferenceSystem; |
| import org.opengis.referencing.crs.CRSAuthorityFactory; |
| import org.opengis.referencing.datum.Datum; |
| import org.opengis.referencing.datum.RealizationMethod; |
| import org.opengis.referencing.operation.OperationMethod; |
| import org.apache.sis.metadata.iso.citation.Citations; |
| import org.apache.sis.referencing.CRS; |
| import org.apache.sis.referencing.CommonCRS; |
| import org.apache.sis.referencing.IdentifiedObjects; |
| import org.apache.sis.referencing.internal.DeprecatedCode; |
| import org.apache.sis.util.CharSequences; |
| import org.apache.sis.util.ComparisonMode; |
| import org.apache.sis.util.Deprecable; |
| import org.apache.sis.util.Utilities; |
| import org.apache.sis.util.Version; |
| import org.apache.sis.util.privy.Constants; |
| import org.apache.sis.referencing.crs.AbstractCRS; |
| import org.apache.sis.referencing.cs.AxesConvention; |
| import org.apache.sis.util.iso.DefaultNameSpace; |
| import org.apache.sis.util.logging.Logging; |
| |
| // Test dependencies |
| import static org.junit.jupiter.api.Assertions.*; |
| import org.opengis.test.report.AuthorityCodesReport; |
| |
| |
| /** |
| * Generates a list of supported Coordinate Reference Systems in the current directory. |
| * This class is for manual execution after the EPSG database has been updated, |
| * or the projection implementations changed. |
| * |
| * <p><b>WARNING:</b> |
| * this class implements heuristic rules for nicer sorting (e.g. of CRS having numbers as Roman letters). |
| * Those heuristic rules were determined specifically for the EPSG dataset expanded with WMS codes. |
| * This class is not likely to produce good results for any other authorities, and many need to be updated |
| * after any upgrade of the EPSG dataset.</p> |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| */ |
| public final class CoordinateReferenceSystems extends AuthorityCodesReport { |
| /** |
| * The titles of some sections where to group CRS. By default CRS are grouped by datum names. |
| * But if a name is listed in this map, then that alternative name will be used for grouping purpose. |
| * Sometimes the change is only cosmetic (e.g. "Reseau Geodesique Francais" → "Réseau Géodésique Français"). |
| * But sometimes the changes have the effect of merging different datum under the same section. |
| * For example, we merge the "Arc 1950" and "Arc 1960" sections into a single "Arc" section, |
| * since those sections were small and we do not want to scatter the HTML page with too many sections. |
| * However, we do not merge "NAD83" and "NAD83(HARN)" because those sections are already quite large, |
| * and merging them will result in a too large section. |
| * |
| * <p>The decision to merge or not is arbitrary. Generally, we try to avoid small sections (less that 5 CRS) |
| * but without merging together unrelated datum. Every CRS having a datum whose name <em>starts</em> with a |
| * value in the left column will be reported in the section given in the right column.</p> |
| */ |
| private static final NavigableMap<String,String> SECTION_TITLES = new TreeMap<>(); |
| static { |
| rd("American Samoa", "American Samoa"); |
| rd("Arc", "Arc"); |
| rd("Ancienne Triangulation Francaise", "Ancienne Triangulation Française"); |
| rd("Australian Geodetic Datum", "Australian Geodetic Datum"); |
| rd("Australian Height Datum", "Australian Height Datum"); |
| rd("Azores Central Islands", "Azores Islands"); |
| rd("Azores Occidental Islands", "Azores Islands"); |
| rd("Azores Oriental Islands", "Azores Islands"); |
| rd("Baltic", "Baltic"); |
| rd("Batavia", "Batavia"); |
| rd("Bermuda", "Bermuda"); |
| rd("Bogota 1975", "Bogota 1975"); |
| rd("Carthage", "Carthage"); |
| rd("Bern 1938", "Bern / CH1903"); |
| rd("Cais", "Cais"); |
| rd("Cayman Brac", "Cayman Islands"); |
| rd("Cayman Islands", "Cayman Islands"); |
| rd("CH1903", "Bern / CH1903"); |
| rd("CH1903+", "Bern / CH1903"); |
| rd("Canadian Geodetic Vertical Datum", "Canadian Geodetic Vertical Datum"); |
| rd("Chatham Islands Datum", "Chatham Islands Datum"); |
| rd("Corrego Alegre", "Corrego Alegre"); |
| rd("Croatian Terrestrial Reference System", "Croatian Reference System"); |
| rd("Croatian Vertical Reference Datum", "Croatian Reference System"); |
| rd("Danger 1950", "Saint Pierre et Miquelon 1950"); |
| rd("Dansk", "Dansk"); |
| rd("Dealul Piscului", "Dealul Piscului"); |
| rd("Deutsches Haupthoehennetz", "Deutsches Haupthoehennetz"); |
| rd("Douala", "Douala"); |
| rd("Dunedin", "Dunedin"); |
| rd("Dunedin-Bluff", "Dunedin"); |
| rd("EGM2008 geoid", "EGM geoid"); |
| rd("EGM84 geoid", "EGM geoid"); |
| rd("EGM96 geoid", "EGM geoid"); |
| rd("Egypt", "Egypt"); |
| rd("EPSG example", "EPSG example"); |
| rd("Estonia", "Estonia"); |
| rd("European Datum", "European Datum"); |
| rd("European Terrestrial Reference Frame", "European Terrestrial Reference Frame"); |
| rd("European Vertical Reference Frame", "European Vertical Reference Frame"); |
| rd("Fahud", "Fahud"); |
| rd("Fao", "Fao"); |
| rd("Fehmarnbelt", "Fehmarnbelt"); |
| rd("Faroe Datum", "Faroe Islands"); |
| rd("Faroe Islands", "Faroe Islands"); |
| rd("fk89", "Faroe Islands"); |
| rd("Fiji", "Fiji"); |
| rd("Gan 1970", "Gandajika"); |
| rd("Grand Cayman", "Grand Cayman"); |
| rd("Greek", "Greek"); |
| rd("Greenland", "Greenland"); |
| rd("Guadeloupe", "Guadeloupe"); |
| rd("Guam", "Guam"); |
| rd("Gunung Segara", "Gunung Segara"); |
| rd("Helsinki", "Helsinki"); |
| rd("High Water", "High Water"); |
| rd("Higher High Water", "High Water"); |
| rd("Highest Astronomical Tide", "High Water"); |
| rd("Hong Kong", "Hong Kong"); |
| rd("Hungarian", "Hungarian Datum"); |
| rd("IG05", "Israeli Grid"); |
| rd("IGb", "IGb"); |
| rd("IGN", "IGN"); |
| rd("IGS", "IGS"); |
| rd("Indian", "Indian"); |
| rd("International Great Lakes Datum", "International Great Lakes Datum"); |
| rd("International Terrestrial Reference Frame", "International Terrestrial Reference Frame"); |
| rd("Islands Net", "Islands Net"); |
| rd("Israeli Geodetic Datum", "Israeli Geodetic Datum"); |
| rd("Jamaica", "Jamaica"); |
| rd("Japanese Geodetic Datum 2000", "Japanese Geodetic Datum 2000"); |
| rd("Japanese Geodetic Datum 2011", "Japanese Geodetic Datum 2011"); |
| rd("Japanese Standard Levelling Datum", "Japanese Standard Levelling Datum"); |
| rd("Kalianpur", "Kalianpur"); |
| rd("Kertau", "Kertau"); |
| rd("KOC Construction Datum", "KOC Construction Datum / Well Datum"); |
| rd("KOC Well Datum", "KOC Construction Datum / Well Datum"); |
| rd("Korean Datum", "Korean Datum"); |
| rd("Kuwait Oil Company", "Kuwait Oil Company / Kuwait Utility"); |
| rd("Kuwait PWD", "Kuwait Oil Company / Kuwait Utility"); |
| rd("Kuwait Utility", "Kuwait Oil Company / Kuwait Utility"); |
| rd("Lao", "Lao"); |
| rd("Latvia", "Latvia"); |
| rd("Lisbon", "Lisbon"); |
| rd("Lower Low Water Large Tide", "Low Water"); |
| rd("Lowest Astronomical Tide", "Low Water"); |
| rd("Macao", "Macao"); |
| rd("Makassar", "Makassar"); |
| rd("Manoca", "Manoca"); |
| rd("Martinique", "Martinique"); |
| rd("Maupiti", "Maupiti"); |
| rd("Mean High Water", "Mean Sea Level"); |
| rd("Mean Higher High Water", "Mean Sea Level"); |
| rd("Mean Low Water", "Mean Sea Level"); |
| rd("Mean Lower Low Water", "Mean Sea Level"); |
| rd("Missao Hidrografico Angola y Sao Tome 1951", "Missao Hidrografico Angola y Sao Tome"); |
| rd("Mhast (offshore)", "Missao Hidrografico Angola y Sao Tome"); |
| rd("Mhast (onshore)", "Missao Hidrografico Angola y Sao Tome"); |
| rd("Militar-Geographische Institut (Ferro)", "Militar-Geographische Institut"); |
| rd("MOMRA", "MOMRA"); |
| rd("Monte Mario (Rome)", "Monte Mario"); |
| rd("Moorea", "Moorea"); |
| rd("Nahrwan", "Nahrwan"); |
| rd("Naparima", "Naparima"); |
| rd("Nivellement General de la Corse", "Nivellement Général Corse / France / Nouvelle-Calédonie / Polynésie Française / Luxembourd / Guyanais"); |
| rd("Nivellement General de la France", "Nivellement Général Corse / France / Nouvelle-Calédonie / Polynésie Française / Luxembourd / Guyanais"); |
| rd("Nivellement General de Nouvelle Caledonie", "Nivellement Général Corse / France / Nouvelle-Calédonie / Polynésie Française / Luxembourd / Guyanais"); |
| rd("Nivellement General de Polynesie Francaise", "Nivellement Général Corse / France / Nouvelle-Calédonie / Polynésie Française / Luxembourd / Guyanais"); |
| rd("Nivellement General du Luxembourg", "Nivellement Général Corse / France / Nouvelle-Calédonie / Polynésie Française / Luxembourd / Guyanais"); |
| rd("Nivellement General Guyanais", "Nivellement Général Corse / France / Nouvelle-Calédonie / Polynésie Française / Luxembourd / Guyanais"); |
| rd("NGO 1948", "NGO 1948"); |
| rd("Nouvelle Triangulation Francaise", "Nouvelle Triangulation Française"); |
| rd("NAD83 Canadian Spatial Reference System", "North American Datum 1983 — Canadian Spatial Reference System"); |
| rd("NAD83 (Continuously Operating Reference Station 1996)", "North American Datum 1983 — Continuously Operating Reference Station 1996"); // For better sort order. |
| rd("NAD83 (Federal Base Network)", "North American Datum 1983 — Federal Base Network"); |
| rd("NAD83 (High Accuracy Reference Network)", "North American Datum 1983 — High Accuracy Reference Network"); |
| rd("NAD83 (High Accuracy Reference Network - Corrected)", "North American Datum 1983 — High Accuracy Reference Network"); |
| rd("NAD83 (National Spatial Reference System 2007)", "North American Datum 1983 — National Spatial Reference System 2007"); |
| rd("NAD83 (National Spatial Reference System 2011)", "North American Datum 1983 — National Spatial Reference System 2011"); |
| rd("NAD83 (National Spatial Reference System MA11)", "North American Datum 1983 — National Spatial Reference System MA11 / PA11"); |
| rd("NAD83 (National Spatial Reference System PA11)", "North American Datum 1983 — National Spatial Reference System MA11 / PA11"); |
| rd("North American Datum of 1983 (CSRS)", "North American Datum 1983 — CSRS"); |
| rd("North American Datum of 1983 (CSRS96)", "North American Datum 1983 — CSRS"); |
| rd("New Zealand Vertical Datum", "New Zealand Vertical Datum"); |
| rd("Norway Normal Null", "Norway Normal Null"); |
| rd("Ordnance Datum Newlyn", "Ordnance Datum Newlyn"); |
| rd("OSGB", "OSGB"); |
| rd("Parametry Zemli 1990", "Parametry Zemli 1990"); |
| rd("PDO Height Datum 1993", "PDO Survey / Height Datum 1993"); |
| rd("PDO Survey Datum 1993", "PDO Survey / Height Datum 1993"); |
| rd("Pitcairn", "Pitcairn"); |
| rd("Port Moresby", "Port Moresby"); |
| rd("Porto Santo", "Porto Santo"); |
| rd("Posiciones Geodésicas Argentinas", "Posiciones Geodésicas Argentinas"); |
| rd("Puerto Rico", "Puerto Rico"); |
| rd("Qatar", "Qatar"); |
| rd("Qornoq", "Qornoq"); |
| rd("Reseau Geodesique de Nouvelle Caledonie", "Réseau Géodésique de Nouvelle-Calédonie"); |
| rd("Reseau National Belge", "Réseau National Belge"); |
| rd("Reunion", "Réunion"); |
| rd("Rikets hojdsystem", "Rikets hojdsystem"); |
| rd("Santa Cruz", "Santa Cruz"); |
| rd("Serbian", "Serbian Reference System / Network"); |
| rd("Sierra Leone", "Sierra Leone"); |
| rd("SIRGAS", "SIRGAS"); |
| rd("Slovenia", "Slovenia"); |
| rd("Slovenian", "Slovenia"); |
| rd("South American Datum", "South American Datum"); |
| rd("Sri Lanka", "Sri Lanka"); |
| rd("Stockholm 1938", "Stockholm 1938"); |
| rd("St. Helena", "St. Helena"); |
| rd("System of the Unified Trigonometrical Cadastral Network", "System of the Unified Trigonometrical Cadastral Network"); |
| rd("Tahaa", "Tahaa"); |
| rd("Tahiti", "Tahiti"); |
| rd("Taiwan", "Taiwan"); |
| rd("Tananarive 1925", "Tananarive 1925"); |
| rd("Tokyo", "Tokyo"); |
| rd("Viti Levu", "Viti Levu"); |
| rd("Voirol", "Voirol"); |
| rd("WGS 72 Transit Broadcast Ephemeris", "World Geodetic System 1972 — Transit Broadcast Ephemeris"); |
| rd("World Geodetic System 1984", "World Geodetic System 1984"); |
| rd("Yellow Sea", "Yellow Sea"); |
| } |
| |
| /** |
| * The datums from the above list which are deprecated, but that we do not want to replace by the non-deprecated |
| * datum. We disable some replacements when they allow better sorting of deprecated CRS. |
| */ |
| private static final Set<String> KEEP_DEPRECATED_DATUM = Set.of( |
| "Dealul Piscului 1970"); // Datum does not exist but is an alias for S-42 in Romania. |
| |
| /** |
| * Shortcut for {@link #SECTION_TITLES} initialization. |
| * {@code "rd"} stands for "rename datum". |
| */ |
| private static void rd(final String datum, final String display) { |
| assertNull(datum, SECTION_TITLES.put(datum, display)); |
| } |
| |
| /** |
| * Words to ignore in a datum name in order to detect if a CRS name is the acronym of the datum name. |
| */ |
| private static final Set<String> DATUM_WORDS_TO_IGNORE = Set.of( |
| "of", // VIVD: Virgin Islands Vertical Datum of 2009 |
| "de", // RRAF: Reseau de Reference des Antilles Francaises |
| "des", // RGAF: Reseau Geodesique des Antilles Francaises |
| "la", // RGR: Reseau Geodesique de la Reunion |
| "et", // RGSPM: Reseau Geodesique de Saint Pierre et Miquelon |
| "para", // SIRGAS: Sistema de Referencia Geocentrico para America del Sur 1995 |
| "del", // SIRGAS: Sistema de Referencia Geocentrico para America del Sur 1995 |
| "las", // SIRGAS: Sistema de Referencia Geocentrico para las AmericaS 2000 |
| "Tides"); // MLWS: Mean Low Water Spring Tides |
| |
| /** |
| * The keywords before which to cut the CRS names when sorting by alphabetical order. |
| * The main intent here is to preserve the "far west", "west", "central west", "central", |
| * "central east", "east", "far east" order. |
| */ |
| private static final String[] CUT_BEFORE = { |
| " far west", // "MAGNA-SIRGAS / Colombia Far West zone" |
| " far east", |
| " west", // "Bogota 1975 / Colombia West zone" |
| " east", // "Bogota 1975 / Colombia East Central zone" |
| " central", // "Korean 1985 / Central Belt" (between "East Belt" and "West Belt") |
| " old central", // "NAD Michigan / Michigan Old Central" |
| " bogota zone", // "Bogota 1975 / Colombia Bogota zone" |
| // Do not declare "North" and "South" as it causes confusion with "WGS 84 / North Pole" and other cases. |
| }; |
| |
| /** |
| * The keywords after which to cut the CRS names when sorting by alphabetical order. |
| * |
| * Note: alphabetical sorting of Roman numbers work for zones from I to VIII inclusive. |
| * If there is more zones (for example with "JGD2000 / Japan Plane Rectangular"), then |
| * we need to cut before those numbers in order to use sorting by EPSG codes instead. |
| * |
| * Note 2: if alphabetical sorting is okay for Roman numbers, it is actually preferable |
| * because it give better position of names with height like "zone II + NGF IGN69 height". |
| */ |
| private static final String[] CUT_AFTER = { |
| " cs ", // "JGD2000 / Japan Plane Rectangular CS IX" |
| " tm", // "ETRS89 / TM35FIN(E,N)" — we want to not interleave them between "TM35" and "TM36". |
| " dktm", // "ETRS89 / DKTM1 + DVR90 height" |
| "-gk", // "ETRS89 / ETRS-GK19FIN" |
| " philippines zone ", // "Luzon 1911 / Philippines zone IV" |
| " california zone ", // "NAD27 / California zone V" |
| " ngo zone ", // "NGO 1948 (Oslo) / NGO zone I" |
| " lambert zone ", // "NTF (Paris) / Lambert zone II + NGF IGN69 height" |
| "fiji 1956 / utm zone " // Two zones: 60S and 1S with 60 before 1. |
| }; |
| |
| /** |
| * The symbol to write in from of EPSG code of CRS having an axis order different |
| * then the (longitude, latitude) one. |
| */ |
| private static final char YX_ORDER = '\u21B7'; |
| |
| /** |
| * The factory which create CRS instances. |
| */ |
| private final CRSAuthorityFactory factory; |
| |
| /** |
| * The datum from the {@link #SECTION_TITLES} that we didn't found after we processed all codes. |
| * Used for verification purpose only. |
| */ |
| private final Set<String> unusedDatumMapping; |
| |
| /** |
| * Creates a new instance. |
| */ |
| private CoordinateReferenceSystems() throws FactoryException { |
| super(null); |
| unusedDatumMapping = new HashSet<>(SECTION_TITLES.keySet()); |
| properties.setProperty("TITLE", "Apache SIS™ Coordinate Reference System (CRS) codes"); |
| properties.setProperty("PRODUCT.NAME", "Apache SIS™"); |
| properties.setProperty("PRODUCT.VERSION", getVersion()); |
| properties.setProperty("PRODUCT.URL", "https://sis.apache.org"); |
| properties.setProperty("JAVADOC.GEOAPI", "https://www.geoapi.org/snapshot/javadoc"); |
| properties.setProperty("FACTORY.NAME", "EPSG"); |
| properties.setProperty("FACTORY.VERSION", "9.9.1"); |
| properties.setProperty("FACTORY.VERSION.SUFFIX", ", together with other sources"); |
| properties.setProperty("PRODUCT.VERSION.SUFFIX", " (provided that <a href=\"https://sis.apache.org/epsg.html\">a connection to an EPSG database exists</a>)"); |
| properties.setProperty("DESCRIPTION", "<p><b>Notation:</b></p>\n" + |
| "<ul>\n" + |
| " <li>The " + YX_ORDER + " symbol in front of authority codes (${PERCENT.ANNOTATED} of them) identifies" + |
| " left-handed coordinate systems (for example with <var>latitude</var> axis before <var>longitude</var>).</li>\n" + |
| " <li>The <del>codes with a strike</del> (${PERCENT.DEPRECATED} of them) identify deprecated CRS." + |
| " In some cases, the remarks column indicates the replacement.</li>\n" + |
| "</ul>"); |
| factory = CRS.getAuthorityFactory(null); |
| add(factory); |
| } |
| |
| /** |
| * Generates the HTML report. |
| * |
| * @param args ignored. |
| * @throws FactoryException if an error occurred while fetching the CRS. |
| * @throws IOException if an error occurred while writing the HTML file. |
| */ |
| @SuppressWarnings("UseOfSystemOutOrSystemErr") |
| public static void main(final String[] args) throws FactoryException, IOException { |
| Locale.setDefault(Locale.US); // We have to use this hack for now because exceptions are formatted in the current locale. |
| final CoordinateReferenceSystems writer = new CoordinateReferenceSystems(); |
| final File file = writer.write(new File("CoordinateReferenceSystems.html")); |
| System.out.println("Created " + file.getAbsolutePath()); |
| if (!writer.unusedDatumMapping.isEmpty()) { |
| System.out.println(); |
| System.out.println("WARNING: the following datums were expected but not found. Maybe their spelling changed?"); |
| for (final String name : writer.unusedDatumMapping) { |
| System.out.print(" - "); |
| System.out.println(name); |
| } |
| } |
| } |
| |
| /** |
| * Returns the current Apache SIS version, with the {@code -SNAPSHOT} trailing part omitted. |
| * |
| * @return the current Apache SIS version. |
| */ |
| private static String getVersion() { |
| String version = Version.SIS.toString(); |
| final int snapshot = version.lastIndexOf('-'); |
| if (snapshot >= 2) { |
| version = version.substring(0, snapshot); |
| } |
| return version; |
| } |
| |
| /** |
| * Creates the text to show in the "Remarks" column for the given CRS. |
| */ |
| private String getRemark(final CoordinateReferenceSystem crs) { |
| if (crs instanceof GeographicCRS) { |
| return (crs.getCoordinateSystem().getDimension() == 3) ? "Geographic 3D" : "Geographic"; |
| } |
| if (crs instanceof DerivedCRS derived) { |
| final OperationMethod method = derived.getConversionFromBase().getMethod(); |
| final Identifier identifier = IdentifiedObjects.getIdentifier(method, Citations.EPSG); |
| if (identifier != null) { |
| return "<a href=\"CoordinateOperationMethods.html#" + identifier.getCode() |
| + "\">" + method.getName().getCode().replace('_', ' ') + "</a>"; |
| } |
| } |
| if (crs instanceof GeodeticCRS) { |
| final CoordinateSystem cs = crs.getCoordinateSystem(); |
| if (cs instanceof CartesianCS) { |
| return "Geocentric (Cartesian coordinate system)"; |
| } else if (cs instanceof SphericalCS) { |
| return "Geocentric (spherical coordinate system)"; |
| } |
| return "Geodetic"; |
| } |
| if (crs instanceof VerticalCRS vertical) { |
| final Optional<RealizationMethod> method = vertical.getDatum().getRealizationMethod(); |
| if (method.isPresent()) { |
| return CharSequences.camelCaseToSentence(method.get().name().toLowerCase(getLocale())) + " realization method"; |
| } |
| } |
| if (crs instanceof CompoundCRS compound) { |
| final StringBuilder buffer = new StringBuilder(); |
| for (final CoordinateReferenceSystem component : compound.getComponents()) { |
| if (buffer.length() != 0) { |
| buffer.append(" + "); |
| } |
| buffer.append(getRemark(component)); |
| } |
| return buffer.toString(); |
| } |
| if (crs instanceof EngineeringCRS) { |
| return "Engineering (" + crs.getCoordinateSystem().getName().getCode() + ')'; |
| } |
| return ""; |
| } |
| |
| /** |
| * Omits the trailing number, if any. |
| * For example if the given name is "Abidjan 1987", then this method returns "Abidjan". |
| */ |
| private static String omitTrailingNumber(String name) { |
| int i = CharSequences.skipTrailingWhitespaces(name, 0, name.length()); |
| while (i != 0) { |
| final char c = name.charAt(--i); |
| if (c < '0' || c > '9') { |
| name = name.substring(0, CharSequences.skipTrailingWhitespaces(name, 0, i+1)); |
| break; |
| } |
| } |
| return name; |
| } |
| |
| /** |
| * If the first word of the CRS name seems to be an acronym of the datum name, |
| * puts that acronym in a {@code <abbr title="datum name">...</abbr>} element. |
| */ |
| static String insertAbbreviationTitle(final String crsName, final String datumName) { |
| int s = crsName.indexOf(' '); |
| if (s < 0) s = crsName.length(); |
| int p = crsName.indexOf('('); |
| if (p >= 0 && p < s) s = p; |
| p = datumName.indexOf('('); |
| if (p < 0) p = datumName.length(); |
| final String acronym = crsName.substring(0, s); |
| final String ar = omitTrailingNumber(acronym); |
| final String dr = omitTrailingNumber(datumName.substring(0, p)); |
| if (dr.startsWith(ar)) { |
| return crsName; // Avoid redudancy between CRS name and datum name. |
| } |
| /* |
| * If the first CRS word does not seem to be an acronym of the datum name, verify |
| * if there is some words that we should ignore in the datum name and try again. |
| */ |
| if (!CharSequences.isAcronymForWords(ar, dr)) { |
| final String[] words = (String[]) CharSequences.split(dr, ' '); |
| int n = 0; |
| for (final String word : words) { |
| if (!DATUM_WORDS_TO_IGNORE.contains(word)) { |
| words[n++] = word; |
| } |
| } |
| if (n == words.length || n < 2) { |
| return crsName; |
| } |
| final StringBuilder b = new StringBuilder(); |
| for (int i=0; i<n; i++) { |
| if (i != 0) b.append(' '); |
| b.append(words[i]); |
| } |
| if (!CharSequences.isAcronymForWords(ar, b)) { |
| return crsName; |
| } |
| } |
| return "<abbr title=\"" + datumName + "\">" + acronym + "</abbr>" + crsName.substring(s); |
| } |
| |
| /** |
| * Invoked when a CRS has been successfully created. This method modifies the default |
| * {@link org.opengis.test.report.AuthorityCodesReport.Row} attribute values created |
| * by GeoAPI. |
| * |
| * @param code the authority code of the created object. |
| * @param object the object created from the given authority code. |
| * @return the created row, or {@code null} if the row should be ignored. |
| */ |
| @Override |
| protected Row createRow(final String code, final IdentifiedObject object) { |
| final Row row = super.createRow(code, object); |
| final CoordinateReferenceSystem crs = (CoordinateReferenceSystem) object; |
| final CoordinateReferenceSystem crsXY = AbstractCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED); |
| if (!Utilities.deepEquals(crs.getCoordinateSystem(), crsXY.getCoordinateSystem(), ComparisonMode.IGNORE_METADATA)) { |
| row.annotation = YX_ORDER; |
| } |
| CoordinateReferenceSystem replacement = crs; |
| row.remark = getRemark(crs); |
| /* |
| * If the object is deprecated, find the replacement. |
| * We do not take the whole comment because it may be pretty long. |
| */ |
| if (object instanceof Deprecable dep) { |
| row.isDeprecated = dep.isDeprecated(); |
| if (row.isDeprecated) { |
| String replacedBy = null; |
| InternationalString i18n = object.getRemarks(); |
| for (final Identifier id : object.getIdentifiers()) { |
| if (id instanceof Deprecable did && did.isDeprecated()) { |
| i18n = did.getRemarks(); |
| if (id instanceof DeprecatedCode dc) { |
| replacedBy = dc.replacedBy; |
| } |
| break; |
| } |
| } |
| if (i18n != null) { |
| row.remark = i18n.toString(getLocale()); |
| } |
| /* |
| * If a replacement exists for a deprecated CRS, use the datum of the replacement instead of |
| * the datum of the deprecated CRS for determining in which section to put the CRS. The reason |
| * is that some CRS are deprecated because they were associated to the wrong datum, in which |
| * case the deprecated CRS would appear in the wrong section if we do not apply this correction. |
| */ |
| if (!KEEP_DEPRECATED_DATUM.contains(CRS.getSingleComponents(crs).get(0).getDatum().getName().getCode())) { |
| if (replacedBy != null) try { |
| replacement = factory.createCoordinateReferenceSystem("EPSG:" + replacedBy); |
| } catch (FactoryException e) { |
| // Ignore - keep the datum of the deprecated object. |
| } |
| } |
| } |
| } |
| ((ByName) row).setup(CRS.getSingleComponents(replacement).get(0).getDatum(), unusedDatumMapping); |
| return row; |
| } |
| |
| /** |
| * Invoked when a CRS creation failed. This method modifies the default |
| * {@link org.opengis.test.report.AuthorityCodesReport.Row} attribute values |
| * created by GeoAPI. |
| * |
| * @param code the authority code of the object to create. |
| * @param exception the exception that occurred while creating the identified object. |
| * @return the created row, or {@code null} if the row should be ignored. |
| */ |
| @Override |
| protected Row createRow(final String code, final FactoryException exception) { |
| if (code.startsWith(Constants.PROJ4 + DefaultNameSpace.DEFAULT_SEPARATOR)) { |
| return null; |
| } |
| final Row row = super.createRow(code, exception); |
| try { |
| row.name = factory.getDescriptionText(CoordinateReferenceSystem.class, code).get().toString(getLocale()); |
| } catch (FactoryException e) { |
| Logging.unexpectedException(null, CoordinateReferenceSystems.class, "createRow", e); |
| } |
| if (code.startsWith("AUTO2:")) { |
| // It is normal to be unable to instantiate an "AUTO" CRS, |
| // because those authority codes need parameters. |
| row.hasError = false; |
| row.remark = "Projected"; |
| ((ByName) row).setup(CommonCRS.WGS84.datum(), unusedDatumMapping); |
| } else { |
| row.remark = exception.getLocalizedMessage(); |
| ((ByName) row).setup(null, unusedDatumMapping); |
| } |
| return row; |
| } |
| |
| /** |
| * Invoked by {@link AuthorityCodesReport} for creating a new row instance. |
| * |
| * @return the new row instance. |
| */ |
| @Override |
| protected Row newRow() { |
| return new ByName(); |
| } |
| |
| |
| |
| |
| /** |
| * A row with a natural ordering that use the first part of the name before to use the authority code. |
| * We use only the part of the name prior some keywords (e.g. {@code "zone"}). |
| * For example if the following codes: |
| * |
| * <pre class="text"> |
| * EPSG:32609 WGS 84 / UTM zone 9N |
| * EPSG:32610 WGS 84 / UTM zone 10N</pre> |
| * |
| * We compare only the "WGS 84 / UTM" string, then the code. This is a reasonably easy way to keep a more |
| * natural ordering ("9" sorted before "10", "UTM North" projections kept together and same for South). |
| */ |
| private static final class ByName extends Row { |
| /** |
| * A string derived from the {@link #name} to use for sorting. |
| */ |
| private String reducedName; |
| |
| /** |
| * The datum name, or {@code null} if unknown. |
| * If non-null, this is used for grouping CRS names by sections. |
| */ |
| String section; |
| |
| /** |
| * Creates a new row. |
| */ |
| ByName() { |
| } |
| |
| /** |
| * Computes the {@link #reducedName} field value. |
| */ |
| final void setup(final Datum datum, final Set<String> unusedDatumMapping) { |
| final String datumName; |
| if (datum != null) { |
| datumName = datum.getName().getCode(); |
| } else { |
| // Temporary patch (TODO: remove after we implemented the missing methods in SIS) |
| if (name.startsWith("NSIDC EASE-Grid")) { |
| datumName = "Unspecified datum"; |
| } else if (code.equals("EPSG:2163")) { |
| datumName = "Unspecified datum"; |
| } else if (code.equals("EPSG:5818")) { |
| datumName = "Seismic bin grid datum"; |
| } else { |
| datumName = null; // Keep ordering based on the name. |
| } |
| } |
| if (datumName != null) { |
| final String prefix; |
| final Map.Entry<String,String> group = SECTION_TITLES.floorEntry(datumName); |
| if (group != null && datumName.startsWith(prefix = group.getKey())) { |
| unusedDatumMapping.remove(prefix); |
| section = group.getValue(); |
| } else { |
| section = datumName; |
| } |
| } |
| /* |
| * Get a copy of the name in all lower case. |
| */ |
| final StringBuilder b = new StringBuilder(name); |
| for (int i=0; i<b.length(); i++) { |
| b.setCharAt(i, Character.toLowerCase(b.charAt(i))); |
| } |
| /* |
| * Cut the string to a shorter length if we find a keyword. |
| * This will result in many string equals, which will then be sorted by EPSG codes. |
| * This is useful when the EPSG codes give a better ordering than the alphabetic one |
| * (for example with Roman numbers). |
| */ |
| int s = 0; |
| for (final String keyword : CUT_BEFORE) { |
| int i = b.lastIndexOf(keyword); |
| if (i > 0 && (s == 0 || i < s)) s = i; |
| } |
| for (final String keyword : CUT_AFTER) { |
| int i = b.lastIndexOf(keyword); |
| if (i >= 0) { |
| i += keyword.length(); |
| if (i > s) s = i; |
| } |
| } |
| if (s != 0) b.setLength(s); |
| uniformizeZoneNumber(b); |
| reducedName = b.toString(); |
| if (datumName != null) { |
| name = insertAbbreviationTitle(name, datumName); |
| } |
| } |
| |
| /** |
| * If the string ends with a number optionally followed by "N" or "S", replaces the hemisphere |
| * symbol by a sign and makes sure that the number uses at least 3 digits (e.g. "2N" → "+002"). |
| * This string will be used for better sorting order. |
| */ |
| private static void uniformizeZoneNumber(final StringBuilder b) { |
| if (b.indexOf("/") < 0) { |
| /* |
| * Do not process names like "WGS 84". We want to process only names like "WGS 84 / UTM zone 2N", |
| * otherwise the replacement of "WGS 84" by "WGS 084" causes unexpected sorting. |
| */ |
| return; |
| } |
| int i = b.length(); |
| char c = b.charAt(i - 1); |
| if (c == ')') { |
| // Ignore suffix like " (ftUS)". |
| i = b.lastIndexOf(" ("); |
| if (i < 0) return; |
| c = b.charAt(i - 1); |
| } |
| char sign; |
| switch (c) { |
| default: sign = 0; break; |
| case 'e': case 'n': sign = '+'; i--; break; |
| case 'w': case 's': sign = '-'; i--; break; |
| } |
| int upper = i; |
| do { |
| if (i == 0) return; |
| c = b.charAt(--i); |
| } while (c >= '0' && c <= '9'); |
| switch (upper - ++i) { |
| case 2: b.insert(i, '0'); upper++; break; // Found 2 digits. |
| case 1: b.insert(i, "00"); upper+=2; break; // Only one digit found. |
| case 0: return; // No digit. |
| } |
| if (sign != 0) { |
| b.insert(i, sign); |
| upper++; |
| } |
| b.setLength(upper); |
| } |
| |
| /** |
| * Compares this row with the given row for ordering by name. |
| */ |
| @Override |
| public int compareTo(final Row o) { |
| int n = reducedName.compareTo(((ByName) o).reducedName); |
| if (n == 0) { |
| n = super.compareTo(o); |
| } |
| return n; |
| } |
| } |
| |
| /** |
| * Sorts the rows, then inserts sections between CRS instances that use different datums. |
| */ |
| @Override |
| protected void sortRows() { |
| super.sortRows(); |
| @SuppressWarnings("SuspiciousToArrayCall") |
| final ByName[] data = rows.toArray(ByName[]::new); |
| final Map<String,String> sections = new TreeMap<>(); |
| for (final ByName row : data) { |
| final String section = row.section; |
| if (section != null) { |
| sections.put(CharSequences.toASCII(section).toString().toLowerCase(), section); |
| } |
| } |
| rows.clear(); |
| /* |
| * Recopy the rows, but section-by-section. We do this sorting here instead of in the Row.compareTo(Row) |
| * method in order to preserve the alphabetical order of rows with unknown datum. |
| * Algorithm below is inefficient, but this class should be rarely used anyway and only by site maintainer. |
| */ |
| for (final String section : sections.values()) { |
| final Row separator = new Row(); |
| separator.isSectionHeader = true; |
| separator.name = section; |
| rows.add(separator); |
| boolean found = false; |
| for (int i=0; i<data.length; i++) { |
| final ByName row = data[i]; |
| if (row != null) { |
| if (row.section != null) { |
| found = section.equals(row.section); |
| } |
| if (found) { |
| rows.add(row); |
| data[i] = null; |
| found = true; |
| } |
| } |
| } |
| } |
| boolean found = false; |
| for (final ByName row : data) { |
| if (row != null) { |
| if (!found) { |
| final Row separator = new Row(); |
| separator.isSectionHeader = true; |
| separator.name = "Unknown"; |
| rows.add(separator); |
| } |
| rows.add(row); |
| found = true; |
| } |
| } |
| } |
| } |