All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openstreetmap.atlas.checks.validation.areas.ShadowDetectionCheck Maven / Gradle / Ivy

package org.openstreetmap.atlas.checks.validation.areas;

import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import org.openstreetmap.atlas.checks.base.BaseCheck;
import org.openstreetmap.atlas.checks.flag.CheckFlag;
import org.openstreetmap.atlas.exception.CoreException;
import org.openstreetmap.atlas.geography.Altitude;
import org.openstreetmap.atlas.geography.GeometricSurface;
import org.openstreetmap.atlas.geography.MultiPolygon;
import org.openstreetmap.atlas.geography.Polygon;
import org.openstreetmap.atlas.geography.Rectangle;
import org.openstreetmap.atlas.geography.atlas.Atlas;
import org.openstreetmap.atlas.geography.atlas.items.Area;
import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity;
import org.openstreetmap.atlas.geography.atlas.items.AtlasObject;
import org.openstreetmap.atlas.geography.atlas.items.Relation;
import org.openstreetmap.atlas.geography.atlas.items.complex.RelationOrAreaToMultiPolygonConverter;
import org.openstreetmap.atlas.geography.index.PackedSpatialIndex;
import org.openstreetmap.atlas.geography.index.RTree;
import org.openstreetmap.atlas.geography.index.SpatialIndex;
import org.openstreetmap.atlas.tags.BuildingLevelsTag;
import org.openstreetmap.atlas.tags.BuildingMinLevelTag;
import org.openstreetmap.atlas.tags.BuildingPartTag;
import org.openstreetmap.atlas.tags.BuildingTag;
import org.openstreetmap.atlas.tags.HeightTag;
import org.openstreetmap.atlas.tags.MinHeightTag;
import org.openstreetmap.atlas.tags.RelationTypeTag;
import org.openstreetmap.atlas.tags.annotations.validation.Validators;
import org.openstreetmap.atlas.utilities.collections.Iterables;
import org.openstreetmap.atlas.utilities.configuration.Configuration;
import org.openstreetmap.atlas.utilities.tuples.Tuple;

import com.google.common.collect.Range;

/**
 * This flags buildings that are floating in 3D, thus casting a shadow on the base map when rendered
 * in 3D. Buildings are defined as Areas with a building or building:part tag or are part of a
 * building relation, or a relation of type multipolygon with a building tag.
 *
 * @author bbreithaupt
 */
public class ShadowDetectionCheck extends BaseCheck
{

    private static final long serialVersionUID = -6968080042879358551L;

    private static final List FALLBACK_INSTRUCTIONS = Arrays.asList(
            "The building(s) and/or building part(s) float(s) above the ground. Please check the height/building:levels "
                    + "and min_height/building:min_level tags for all of the buildings parts.",
            "Relation {0,number,#} is floating.");

    // OSM standard level conversion factor
    private static final double LEVEL_TO_METERS_CONVERSION = 3.5;
    private static final String ZERO_STRING = "0";
    private static final RelationOrAreaToMultiPolygonConverter MULTI_POLYGON_CONVERTER = new RelationOrAreaToMultiPolygonConverter();

    private final Map> relationSpatialIndices = new HashMap<>();

    /**
     * The default constructor that must be supplied. The Atlas Checks framework will generate the
     * checks with this constructor, supplying a configuration that can be used to adjust any
     * parameters that the check uses during operation.
     *
     * @param configuration
     *            the JSON configuration for this check
     */
    public ShadowDetectionCheck(final Configuration configuration)
    {
        super(configuration);
    }

    /**
     * This function will validate if the supplied atlas object is valid for the check.
     *
     * @param object
     *            the atlas object supplied by the Atlas-Checks framework for evaluation
     * @return {@code true} if this object should be checked
     */
    @Override
    public boolean validCheckForObject(final AtlasObject object)
    {
        return !this.isFlagged(object.getIdentifier())
                && (object instanceof Area
                        || (object instanceof Relation && ((Relation) object).isMultiPolygon()))
                && this.hasMinKey(object)
                && (this.isBuildingOrPart(object) || this.isBuildingRelationMember(object));
    }

    /**
     * This is the actual function that will check to see whether the object needs to be flagged.
     *
     * @param object
     *            the atlas object supplied by the Atlas-Checks framework for evaluation
     * @return an optional {@link CheckFlag} object that
     */
    @Override
    protected Optional flag(final AtlasObject object)
    {
        // Gather connected building parts and check for a connection to the ground
        final Tuple, Boolean> connectedParts = this.getConnectedParts(object);
        if (connectedParts.getSecond())
        {
            final CheckFlag flag;
            // If object is a relation, flatten it and add a relation instruction
            if (object instanceof Relation)
            {
                flag = this.createFlag(((Relation) object).flatten(),
                        this.getLocalizedInstruction(0));
                flag.addInstruction(this.getLocalizedInstruction(1, object.getOsmIdentifier()));
            }
            else
            {
                flag = this.createFlag(object, this.getLocalizedInstruction(0));
            }
            // Flag all the connected floating parts together
            for (final AtlasObject part : connectedParts.getFirst())
            {
                this.markAsFlagged(part.getIdentifier());
                if (!part.equals(object))
                {
                    if (part instanceof Relation)
                    {
                        flag.addObjects(((Relation) part).flatten());
                        flag.addInstruction(
                                this.getLocalizedInstruction(1, part.getOsmIdentifier()));
                    }
                    else
                    {
                        flag.addObject(part);
                    }
                }
            }
            return Optional.of(flag);
        }

        connectedParts.getFirst().forEach(part -> this.markAsFlagged(part.getIdentifier()));
        return Optional.empty();
    }

    @Override
    protected List getFallbackInstructions()
    {
        return FALLBACK_INSTRUCTIONS;
    }

    /**
     * Create a new spatial index that pre filters building relations. Pre-filtering drastically
     * decreases runtime by eliminating very large non-building relations. Copied from
     * {@link org.openstreetmap.atlas.geography.atlas.AbstractAtlas}.
     *
     * @return A newly created spatial index
     */
    private SpatialIndex buildRelationSpatialIndex(final Atlas atlas)
    {
        final SpatialIndex index = new PackedSpatialIndex(new RTree<>())
        {
            private static final long serialVersionUID = -3139831928323333246L;

            @Override
            protected Long compress(final Relation item)
            {
                return item.getIdentifier();
            }

            @Override
            protected boolean isValid(final Relation item, final Rectangle bounds)
            {
                return item.intersects(bounds);
            }

            @Override
            protected Relation restore(final Long packed)
            {
                return atlas.relation(packed);
            }
        };
        atlas.relations(relation -> relation.isMultiPolygon() && BuildingTag.isBuilding(relation))
                .forEach(index::add);
        return index;
    }

    /**
     * Uses a BFS to gather all connected building parts and check for a connection to the ground.
     *
     * @param startingPart
     *            {@link AtlasObject} to start the walker from
     * @return a {@link Tuple} of a {@link Set} of connected {@link AtlasObject} building parts and
     *         a {@link Boolean} indicating if they are floating
     */
    private Tuple, Boolean> getConnectedParts(final AtlasObject startingPart)
    {
        final Set connectedParts = new HashSet<>();
        final ArrayDeque toCheck = new ArrayDeque<>();
        boolean isFloating = true;
        connectedParts.add(startingPart);
        toCheck.add(startingPart);

        while (!toCheck.isEmpty())
        {
            final AtlasObject checking = toCheck.poll();

            // If a connection to the ground is found the parts are not floating
            if (!this.isOffGround(checking))
            {
                isFloating = false;
            }
            // Get parts connected in 3D
            final Set neighboringParts = new HashSet<>();
            final Rectangle checkingBounds = checking.bounds();
            // Get Areas
            neighboringParts
                    .addAll(Iterables.asSet(checking.getAtlas().areasIntersecting(checkingBounds,
                            area -> this.neighboringPart(area, checking, connectedParts))));
            // Get Relations
            if (!this.relationSpatialIndices.containsKey(checking.getAtlas()))
            {
                this.relationSpatialIndices.put(checking.getAtlas(),
                        this.buildRelationSpatialIndex(checking.getAtlas()));
            }
            neighboringParts.addAll(Iterables
                    .asSet(this.relationSpatialIndices.get(checking.getAtlas()).get(checkingBounds,
                            relation -> this.neighboringPart(relation, checking, connectedParts))));
            // Add the parts to the Set and Queue
            connectedParts.addAll(neighboringParts);
            toCheck.addAll(neighboringParts);
        }
        return Tuple.createTuple(connectedParts, isFloating);
    }

    /**
     * Checks if an {@link AtlasObject} has a tag defining the minimum height of a building.
     *
     * @param object
     *            {@link AtlasObject} to check
     * @return true if {@code object} has a tag defining the minimum height of a building
     */
    private boolean hasMinKey(final AtlasObject object)
    {
        return Validators.hasValuesFor(object, BuildingMinLevelTag.class)
                || Validators.hasValuesFor(object, MinHeightTag.class);
    }

    /**
     * Checks if an {@link AtlasObject} is a building or building:part that is valid for this check.
     *
     * @param object
     *            {@link AtlasObject} to check
     * @return true if {@code object} has a {@code building:part=yes} tag
     */
    private boolean isBuildingOrPart(final AtlasObject object)
    {
        return (BuildingTag.isBuilding(object)
                // Ignore roofs, as the are often used for items that have supports that are too
                // small to effectively map (such as a carport)
                && Validators.isNotOfType(object, BuildingTag.class, BuildingTag.ROOF))
                || Validators.isNotOfType(object, BuildingPartTag.class, BuildingPartTag.NO);
    }

    /**
     * Checks if an {@link AtlasObject} is a outline or part member of a building relation. This is
     * an equivalent tagging to building=* or building:part=yes.
     *
     * @param object
     *            {@link AtlasObject} to check
     * @return true if the object is part of any relation where it has role outline or part
     */
    private boolean isBuildingRelationMember(final AtlasObject object)
    {
        return object instanceof AtlasEntity && ((AtlasEntity) object).relations().stream()
                .anyMatch(relation -> Validators.isOfType(relation, RelationTypeTag.class,
                        RelationTypeTag.BUILDING)
                        && relation.members().stream()
                                .anyMatch(member -> member.getEntity().equals(object)
                                        && (member.getRole().equals("outline"))
                                        || member.getRole().equals("part")));
    }

    /**
     * Checks if an {@link AtlasObject} has tags indicating it is off the ground.
     *
     * @param object
     *            {@link AtlasObject} to check
     * @return true if the area is off the ground
     */
    private boolean isOffGround(final AtlasObject object)
    {
        final double minHeight;
        final double minLevel;
        try
        {
            minHeight = Double
                    .parseDouble(object.getOsmTags().getOrDefault(MinHeightTag.KEY, ZERO_STRING));
            minLevel = Double.parseDouble(
                    object.getOsmTags().getOrDefault(BuildingMinLevelTag.KEY, ZERO_STRING));
        }
        // We want to ignore but propagate AtlasObjects with bad tag values
        catch (final NumberFormatException badTagValue)
        {
            return true;
        }
        return minHeight > 0 || minLevel > 0;
    }

    /**
     * Checks if two {@link AtlasObject}s are building parts and overlap each other.
     *
     * @param part
     *            a known building part to check against
     * @return true if {@code object} is a building part and overlaps {@code part}
     */
    private boolean neighboringPart(final AtlasObject object, final AtlasObject part,
            final Set checked)
    {
        try
        {
            // Get the polygons of the parts, either single or multi
            final GeometricSurface partPolygon = part instanceof Area ? ((Area) part).asPolygon()
                    : MULTI_POLYGON_CONVERTER.convert((Relation) part);
            final GeometricSurface objectPolygon = object instanceof Area
                    ? ((Area) object).asPolygon()
                    : MULTI_POLYGON_CONVERTER.convert((Relation) object);
            // Check if it is a building part, and overlaps.
            return !checked.contains(object)
                    && (this.isBuildingOrPart(object) || this.isBuildingRelationMember(object))
                    // Check 2D overlap
                    && (partPolygon instanceof Polygon
                            ? objectPolygon.overlaps((Polygon) partPolygon)
                            : objectPolygon.overlaps((MultiPolygon) partPolygon))
                    // Check 3D overlap
                    && this.neighborsHeightContains(part, object);
        }
        // Ignore malformed MultiPolygons
        catch (final CoreException invalidMultiPolygon)
        {
            return false;
        }
    }

    /**
     * Given two {@link AtlasObject}s, checks that they have any intersecting or touching height
     * values. The range of height values for the {@link AtlasObject}s are calculated using height
     * and layer tags. Height tags get precedence over level tags. Heights are calculated from level
     * tags using a conversion factor. A {@code min_height} or {@code building:min_layer} tag must
     * exist for {@code part}. All other tags will use defaults if not found.
     *
     * @param part
     *            {@link AtlasObject} being checked
     * @param neighbor
     *            {@link AtlasObject} being checked against
     * @return true if {@code part} intersects or touches {@code neighbor}, by default neighbor is
     *         flat on the ground.
     */
    private boolean neighborsHeightContains(final AtlasObject part, final AtlasObject neighbor)
    {
        final Map neighborTags = neighbor.getOsmTags();
        final Map partTags = part.getOsmTags();

        try
        {
            // Set partMinHeight
            final double partMinHeight = MinHeightTag.get(part).map(Altitude::asMeters).orElseGet(
                    () -> partTags.containsKey(BuildingMinLevelTag.KEY)
                            ? Double.parseDouble(partTags.get(BuildingMinLevelTag.KEY))
                                    * LEVEL_TO_METERS_CONVERSION
                            : 0);

            // Set partMaxHeight
            final double partMaxHeight = HeightTag.get(part).map(Altitude::asMeters)
                    .orElseGet(() -> partTags.containsKey(BuildingLevelsTag.KEY)
                            ? Double.parseDouble(partTags.get(BuildingLevelsTag.KEY))
                                    * LEVEL_TO_METERS_CONVERSION
                            // Default to 0 height above the minimum
                            : partMinHeight);

            // Set neighborMinHeight
            final double neighborMinHeight = MinHeightTag.get(neighbor).map(Altitude::asMeters)
                    .orElseGet(() -> neighborTags.containsKey(BuildingMinLevelTag.KEY)
                            ? Double.parseDouble(neighborTags.get(BuildingMinLevelTag.KEY))
                                    * LEVEL_TO_METERS_CONVERSION
                            // Default to 0
                            : 0);

            // Set neighborMaxHeight
            final double neighborMaxHeight = HeightTag.get(neighbor).map(Altitude::asMeters)
                    .orElseGet(() -> neighborTags.containsKey(BuildingLevelsTag.KEY)
                            ? Double.parseDouble(neighborTags.get(BuildingLevelsTag.KEY))
                                    * LEVEL_TO_METERS_CONVERSION
                            // Default to 0
                            : 0);

            // Check the range of heights for overlap.
            return Range.closed(partMinHeight, partMaxHeight)
                    .isConnected(Range.closed(neighborMinHeight, neighborMaxHeight));
        }
        // Ignore buildings with a min value larger than its height
        // Ignore features with bad tags (like 2;10)
        catch (final IllegalArgumentException exc)
        {
            return false;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy