blob: 5b6e5bdf66cb857911c6eba11d1b22681f9bb3ee [file] [log] [blame]
/*
* 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.gazetteer;
import java.util.Map;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.ConcurrentModificationException;
import java.util.function.Function;
import org.opengis.util.InternationalString;
import org.opengis.metadata.extent.GeographicExtent;
import org.apache.sis.metadata.iso.extent.DefaultGeographicDescription;
import org.apache.sis.metadata.iso.citation.DefaultOrganisation;
import org.apache.sis.referencing.gazetteer.internal.Resources;
import org.apache.sis.util.CorruptedObjectException;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.iso.Types;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.metadata.citation.Party;
import org.opengis.referencing.gazetteer.ReferenceSystemUsingIdentifiers;
/**
* Helper class for building the description of a location. Temporary instances of this class can be used
* during the construction of <i>spatial reference systems using geographic identifiers</i>.
* Since {@code ModifiableLocationType} instances are modifiable, they should not be published directly.
* Instead, unmodifiable {@linkplain #snapshot snapshots} should be published.
* The same {@code ModifiableLocationType} instance can be used for many snapshots.
*
* <h2>Example</h2>
* The following code creates 3 levels of location types: <var>administrative areas</var>, which contain
* <var>towns</var>, which themselves contain <var>streets</var>. Note that the {@code street} location
* type has two parents, {@code town} and {@code area}, because a street can be outside any town and
* directly under the authority of an administrative area instead.
*
* {@snippet lang="java" :
* ModifiableLocationType area = new ModifiableLocationType("administrative area");
* ModifiableLocationType town = new ModifiableLocationType("town");
* ModifiableLocationType street = new ModifiableLocationType("street");
*
* area .setTheme("local administration");
* town .setTheme("built environment");
* street.setTheme("access");
*
* area .setDefinition("area of responsibility of highest level local authority");
* town .setDefinition("city or town");
* street.setDefinition("thoroughfare providing access to properties");
*
* town .addParent(area);
* street.addParent(area);
* street.addParent(town);
* }
*
* A string representation of the {@code area} location type is as below:
*
* <pre class="text">
* administrative area………………… area of responsibility of highest level local authority
*   ├─town……………………………………………… city or town
*   │   └─street……………………………… thoroughfare providing access to properties
*   └─street………………………………………… thoroughfare providing access to properties</pre>
*
* <h2>Inheritance of property values</h2>
* According ISO 19112:2003, all properties except the collection of
* {@linkplain #getParents() parents} and {@linkplain #getChildren() children} are mandatory.
* Those mandatory properties are the {@linkplain #getName() name}, {@linkplain #getTheme() theme},
* {@linkplain #getIdentifications() identifications}, {@linkplain #getDefinition() definition},
* {@linkplain #getTerritoryOfUse() territory of use} and {@linkplain #getOwner() owner}.
* However, in Apache SIS implementation, only the name is truly mandatory;
* SIS is tolerant to missing value for all other properties.
* But in the hope to improve ISO compliance, values of undefined properties are inherited
* from the parents (if any) provided that all parents define the same values.
*
* <h2>Example</h2>
* If an <var>administrative area</var> is in some {@linkplain #getTerritoryOfUse() territory of use},
* then all children of the administrative area (namely <var>towns</var> and <var>streets</var>) can
* reasonably presumed to be in the same territory of use. That territory can be specified only once
* as below:
*
* {@snippet lang="java" :
* area.setTerritoryOfUse("Japan");
* }
*
* Then, the towns and streets automatically inherit the same value for that property,
* unless they are explicitly given another value.
*
* <h2>Limitation</h2>
* This class is not serializable and is not thread-safe. For thread safety or for serialization,
* a {@linkplain #snapshot snapshots} of this location type should be taken.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.8
*
* @see AbstractLocation
* @see ReferencingByIdentifiers
*
* @since 0.8
*/
public class ModifiableLocationType extends AbstractLocationType { // Not Serializable on intent.
/**
* Name of the location type.
*/
private final InternationalString name;
/**
* Property used as the defining characteristic of the location type.
*/
private InternationalString theme;
/**
* Method(s) of uniquely identifying location instances.
*/
private final Map<String, InternationalString> identifications;
/**
* The way in which location instances are defined.
*/
private InternationalString definition;
/**
* Geographic area within which the location type occurs.
*/
private GeographicExtent territoryOfUse;
/**
* Name of organization or class of organization able to create and destroy location instances.
*/
private Party owner;
/**
* Parent location types (location types of which this location type is a sub-division).
*/
private final Map<String, ModifiableLocationType> parents;
/**
* Child location types (location types which sub-divides this location type).
*/
private final Map<String, ModifiableLocationType> children;
/**
* Creates a new location type of the given name.
*
* @param name the location type name.
*/
public ModifiableLocationType(final CharSequence name) {
ArgumentChecks.ensureNonEmpty("name", name);
this.name = Types.toInternationalString(name);
identifications = new LinkedHashMap<>();
parents = new LinkedHashMap<>();
children = new LinkedHashMap<>();
}
/**
* Returns the name of the location type.
* This name is specified at construction time and cannot be changed.
*
* <h4>Examples</h4>
* “administrative area”, “town”, “locality”, “street”, “property”.
*
* @return name of the location type.
*/
@Override
public InternationalString getName() {
return name;
}
/**
* If all parents return the same value for the given property, returns that value.
* Otherwise returns {@code null}.
*/
private <E> E inherit(final Function<ModifiableLocationType, E> property) {
E common = null;
for (final ModifiableLocationType parent : parents.values()) {
final E value = property.apply(parent);
if (value != null) {
if (common == null) {
common = value;
} else if (!value.equals(common)) {
return null;
}
}
}
return common;
}
/**
* Returns the property used as the defining characteristic of the location type.
* If no theme has been explicitly set, then this method inherits the value from
* the parents providing that all parents specify the same theme.
*
* @return property used as the defining characteristic of the location type,
* or {@code null} if no value has been defined or can be inherited.
*
* @see ReferencingByIdentifiers#getTheme()
*/
@Override
public InternationalString getTheme() {
return (theme != null) ? theme : inherit(ModifiableLocationType::getTheme);
}
/**
* Sets the property used as the defining characteristic of the location type.
*
* <h4>Examples</h4>
* <q>local administration</q> for administrative areas,
* <q>built environment</q> for towns or properties,
* <q>access</q> for streets,
* <q>electoral</q>,
* <q>postal</q>.
*
* @param value the new theme, or {@code null} for inheriting a value from the parents.
*/
public void setTheme(final CharSequence value) {
theme = Types.toInternationalString(value);
}
/**
* Returns the method(s) of uniquely identifying location instances.
* If no methods have been explicitly set, then this method inherits the values from
* the parents providing that all parents specify the same methods.
*
* <p>The collection returned by this method is unmodifiable. For adding or removing an identification,
* use {@link #addIdentification(CharSequence)} or {@link #removeIdentification(CharSequence)}.</p>
*
* <h4>Examples</h4>
* Some identification methods are “name”, “code”, “unique street reference number” and “geographic address”.
* A location using “name” identifications may have the “Spain” {@linkplain AbstractLocation#getGeographicIdentifier()
* geographic identifier}, and a location using “postcode” identifications may have the “SW1P 3AD” geographic identifier.
*
* @return method(s) of uniquely identifying location instances,
* or an empty list if no value has been defined or can be inherited.
*
* @see AbstractLocation#getGeographicIdentifier()
*/
@Override
public Collection<InternationalString> getIdentifications() {
return identifications.isEmpty() ? inherit(ModifiableLocationType::getIdentifications)
: Collections.unmodifiableCollection(identifications.values());
}
/**
* Adds a method of uniquely identifying location instances.
*
* <h4>Examples</h4>
* “name”, “code”, “unique street reference number”, “geographic address”.
*
* @param value the method to add.
* @throws IllegalArgumentException if the given value is already defined.
*/
public void addIdentification(final CharSequence value) {
ArgumentChecks.ensureNonEmpty("value", value);
final String key = value.toString();
if (identifications.putIfAbsent(key, Types.toInternationalString(value)) != null) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.ElementAlreadyPresent_1, key));
}
}
/**
* Removes a method of uniquely identifying location instances.
*
* @param value the method to remove.
* @throws IllegalArgumentException if the given value is not found.
*/
public void removeIdentification(final CharSequence value) {
ArgumentChecks.ensureNonEmpty("value", value);
final String key = value.toString();
if (identifications.remove(key) == null) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.ElementNotFound_1, key));
}
}
/**
* Returns the way in which location instances are defined.
* If no definition has been explicitly set, then this method inherits the value from
* the parents providing that all parents specify the same definition.
*
* @return the way in which location instances are defined,
* or {@code null} if no value has been defined or can be inherited.
*/
@Override
public InternationalString getDefinition() {
return (definition != null) ? definition : inherit(ModifiableLocationType::getDefinition);
}
/**
* Sets the way in which location instances are defined.
*
* @param value the new identification.
*/
public void setDefinition(final CharSequence value) {
definition = Types.toInternationalString(value);
}
/**
* Returns the geographic area within which the location type occurs.
* If no geographic area has been explicitly set, then this method inherits the value from
* the parents providing that all parents specify the same geographic area.
*
* @return geographic area within which the location type occurs,
* or {@code null} if no value has been defined or can be inherited.
*
* @see ReferencingByIdentifiers#getDomains()
*/
@Override
public GeographicExtent getTerritoryOfUse() {
return (territoryOfUse != null) ? territoryOfUse : inherit(ModifiableLocationType::getTerritoryOfUse);
}
/**
* Sets the geographic area within which the location type occurs. The given value is typically an instance of
* {@link org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox}. For an alternative where only the
* territory name is specified, see {@link #setTerritoryOfUse(String)}.
*
* @param value the new geographic extent.
*/
public void setTerritoryOfUse(final GeographicExtent value) {
territoryOfUse = value;
}
/**
* Sets the name of the geographic area within which the location type occurs.
*
* <h4>Examples</h4>
* The geographic domain for a location type “rivers” might be “North America”.
*
* @param identifier the identifier of the geographic extent.
*/
public void setTerritoryOfUse(final String identifier) {
territoryOfUse = (identifier != null) ? new DefaultGeographicDescription(null, identifier) : null;
}
/**
* Returns the name of organization or class of organization able to create and destroy location instances.
* If no organization has been explicitly set, then this method inherits the value from
* the parents providing that all parents specify the same organization.
*
* @return organization or class of organization able to create and destroy location instances,
* or {@code null} if no value has been defined or can be inherited.
*
* @see AbstractLocation#getAdministrator()
* @see ReferencingByIdentifiers#getOverallOwner()
*/
@Override
public Party getOwner() {
return (owner != null) ? owner : inherit(ModifiableLocationType::getOwner);
}
/**
* Sets the organization or class of organization able to create and destroy location instances.
* The given value is typically an instance of {@link DefaultOrganisation}.
* For an alternative where only the organization name is specified, see {@link #setOwner(CharSequence)}.
*
* @param value the new owner.
*/
public void setOwner(final Party value) {
owner = value;
}
/**
* Sets the name of the organization or class of organization able to create and destroy location instances.
*
* @param name the organization name.
*/
public void setOwner(final CharSequence name) {
owner = (name != null) ? new DefaultOrganisation(name, null, null, null) : null;
}
/**
* Returns the parent location types (location types of which this location type is a sub-division).
* A location type can have more than one possible parent. For example, the parent of a location type named
* <q>street</q> could be <q>locality</q>, <q>town</q> or <q>administrative area</q>.
*
* <p>The collection returned by this method is unmodifiable. For adding or removing a parent,
* use {@link #addParent(ModifiableLocationType)} or {@link #removeParent(ModifiableLocationType)}.</p>
*
* @return parent location types, or an empty collection if none.
*
* @see AbstractLocation#getParents()
*/
@Override
public final Collection<ModifiableLocationType> getParents() {
return Collections.unmodifiableCollection(parents.values());
}
/**
* Returns the child location types (location types which sub-divides this location type).
* The collection returned by this method is unmodifiable. For adding or removing a child,
* use <code>child.{@linkplain #addParent addParent}(this)</code>
* or <code>child.{@linkplain #removeParent removeParent}(this)</code>.
*
* @return child location types, or an empty collection if none.
*
* @see AbstractLocation#getChildren()
*/
@Override
public final Collection<ModifiableLocationType> getChildren() {
return Collections.unmodifiableCollection(children.values());
}
/**
* Adds the given element to the list of parents.
*
* @param parent the parent to add.
* @throws IllegalStateException if this location type already have a parent of the same name.
* @throws IllegalArgumentException if the given parent already have a child of the same name as this location type.
*/
public void addParent(final ModifiableLocationType parent) {
final String parentName = parent.name.toString();
if (parents.putIfAbsent(parentName, parent) != null) {
throw new IllegalStateException(Resources.format(Resources.Keys.ParentAlreadyExists_1, parentName));
}
final String childName = name.toString();
if (parent.children.putIfAbsent(childName, this) != null) {
if (parents.remove(parentName) != parent) {
throw new ConcurrentModificationException(); // Paranoiac check.
}
throw new IllegalArgumentException(Resources.format(Resources.Keys.ChildAlreadyExists_1, parentName));
}
/*
* Following verification is not very efficient (lot of verifications are repeated every time that a new
* parent is added), and is redundant with the verification performed by snapshot(…) method. For now we
* perform this verification here on the assumption that the tree is small, and that reporting errors
* early make development easier. However if performance become a concern, we could remove this code
* and perform the verification when first needed in getParents() and getChildren(), or make equals(…)
* and toString() methods tolerance to cycles.
*/
try {
parent.checkForCycles();
} catch (IllegalArgumentException e) {
parent.children.remove(childName); // Rollback
parents.remove(parentName);
throw e;
}
}
/**
* Removes the given element from the list of parent.
*
* @param parent the parent to remove.
* @throws IllegalArgumentException if the given parent has not been found.
*/
public void removeParent(final ModifiableLocationType parent) {
final String key = parent.name.toString();
final ModifiableLocationType removed = parents.remove(key);
if (removed == null) {
throw new IllegalArgumentException(Resources.format(Resources.Keys.LocationTypeNotFound_1, key));
}
if (removed.children.remove(name.toString()) != this || removed != parent) {
throw new CorruptedObjectException();
}
}
/**
* Returns the reference system that comprises this location type. For {@code ModifiableLocationType}s,
* the reference system is always null. The reference system is defined when the location types are
* given to the {@link ReferencingByIdentifiers} constructor for example.
*
* @return {@code null}.
*
* @see ReferencingByIdentifiers#getLocationTypes()
*/
@Override
public ReferenceSystemUsingIdentifiers getReferenceSystem() {
return null;
}
}