OsmRenderable2d.java

package nl.tudelft.simulation.dsol.animation.gis.osm;

import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.rmi.RemoteException;

import javax.naming.NamingException;

import org.djutils.draw.bounds.Bounds2d;
import org.djutils.draw.bounds.Bounds3d;
import org.djutils.draw.point.OrientedPoint3d;
import org.djutils.draw.point.Point2d;
import org.djutils.logger.CategoryLogger;

import nl.tudelft.simulation.dsol.animation.d2.RenderableScale;
import nl.tudelft.simulation.dsol.animation.gis.GisMapInterface;
import nl.tudelft.simulation.dsol.animation.gis.GisRenderable2d;
import nl.tudelft.simulation.dsol.animation.gis.transform.CoordinateTransform;
import nl.tudelft.simulation.naming.context.Contextualized;
import nl.tudelft.simulation.naming.context.util.ContextUtil;

/**
 * This renderable draws OSM maps.
 * <p>
 * Copyright (c) 2020-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
 * for project information <a href="https://simulation.tudelft.nl/dsol/manual/" target="_blank">DSOL Manual</a>. The DSOL
 * project is distributed under a three-clause BSD-style license, which can be found at
 * <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">DSOL License</a>.
 * </p>
 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
 */
public class OsmRenderable2d implements GisRenderable2d
{
    /** */
    private static final long serialVersionUID = 20200108L;

    /** the map to display. */
    @SuppressWarnings("checkstyle:visibilitymodifier")
    protected GisMapInterface map = null;

    /** the image cached image. */
    @SuppressWarnings("checkstyle:visibilitymodifier")
    protected BufferedImage cachedImage = null;

    /** the cached extent. */
    @SuppressWarnings("checkstyle:visibilitymodifier")
    protected Bounds2d cachedExtent = new Bounds2d(0, 0, 0, 0);

    /** the cached screenSize. */
    @SuppressWarnings("checkstyle:visibilitymodifier")
    protected Dimension cachedScreenSize = new Dimension();

    /** the location of the map. */
    @SuppressWarnings("checkstyle:visibilitymodifier")
    protected OrientedPoint3d location = null;

    /** the bounds of the map. */
    @SuppressWarnings("checkstyle:visibilitymodifier")
    protected Bounds3d bounds = null;

    /**
     * constructs a new GisRenderable2d.
     * @param contextProvider Contextualized; the object that can provide the context to store the animation objects
     * @param map MapInterface; the map to use.
     */
    public OsmRenderable2d(final Contextualized contextProvider, final GisMapInterface map)
    {
        this(contextProvider, map, new CoordinateTransform.NoTransform());
    }

    /**
     * constructs a new GisRenderable2d.
     * @param contextProvider Contextualized; the object that can provide the context to store the animation objects
     * @param map MapInterface; the map to use.
     * @param coordinateTransform CoordinateTransform; the transformation of (x, y) coordinates to (x', y') coordinates.
     */
    public OsmRenderable2d(final Contextualized contextProvider, final GisMapInterface map,
            final CoordinateTransform coordinateTransform)
    {
        this(contextProvider, map, coordinateTransform, -Double.MAX_VALUE);
    }

    /**
     * constructs a new GisRenderable2d based on an existing Map.
     * @param contextProvider Contextualized; the object that can provide the context to store the animation objects
     * @param map MapInterface; the map to use.
     * @param coordinateTransform CoordinateTransform; the transformation of (x, y) coordinates to (x', y') coordinates.
     * @param z double; the z-value to use
     */
    public OsmRenderable2d(final Contextualized contextProvider, final GisMapInterface map,
            final CoordinateTransform coordinateTransform, final double z)
    {
        try
        {
            this.map = map;
            this.location = new OrientedPoint3d(this.cachedExtent.midPoint().getX(), this.cachedExtent.midPoint().getY(), z);
            this.bounds = new Bounds3d(this.cachedExtent.getDeltaX(), this.cachedExtent.getDeltaY(), 0.0);
            this.bind2Context(contextProvider);
        }
        catch (Exception exception)
        {
            CategoryLogger.always().warn(exception, "<init>");
        }
    }

    /**
     * binds a renderable2D to the context. The reason for specifying this in an independent method instead of adding the code
     * in the constructor is related to the RFE submitted by van Houten that in specific distributed context, such binding must
     * be overwritten.
     * @param contextProvider Contextualized; the object that can provide the context to store the animation objects
     */
    protected void bind2Context(final Contextualized contextProvider)
    {
        try
        {
            ContextUtil.lookupOrCreateSubContext(contextProvider.getContext(), "animation/2D")
                    .bindObject(Integer.toString(System.identityHashCode(this)), this);
        }
        catch (NamingException | RemoteException exception)
        {
            CategoryLogger.always().warn(exception, "<init>");
        }
    }

    /** {@inheritDoc} */
    @Override
    public void paintComponent(final Graphics2D graphics, final Bounds2d extent, final Dimension screen,
            final RenderableScale renderableScale, final ImageObserver observer)
    {
        try
        {
            this.map.setDrawBackground(false);

            // is the extent or the screen size still the same
            if (extent.equals(this.cachedExtent) && screen.equals(this.cachedScreenSize) && this.map.isSame())
            {
                graphics.drawImage(this.cachedImage, 0, 0, null);
                return;
            }
            this.map.setExtent(extent);
            this.map.getImage().setSize(screen);
            this.cacheImage();
            this.paintComponent(graphics, extent, screen, renderableScale, observer);
        }
        catch (Exception exception)
        {
            CategoryLogger.always().warn(exception, "paint");
        }
    }

    /** {@inheritDoc} */
    @Override
    public OsmRenderable2d getSource()
    {
        return this;
    }

    /** {@inheritDoc} */
    @Override
    public Bounds3d getBounds()
    {
        return this.bounds;
    }

    /** {@inheritDoc} */
    @Override
    public OrientedPoint3d getLocation()
    {
        return this.location;
    }

    /**
     * @return map the Shapefile map
     */
    @Override
    public GisMapInterface getMap()
    {
        return this.map;
    }

    /**
     * caches the GIS map by creating an image. This prevents continuous rendering.
     * @throws Exception on graphicsProblems and network connection failures.
     */
    private void cacheImage() throws Exception
    {
        this.cachedImage = new BufferedImage((int) this.map.getImage().getSize().getWidth(),
                (int) this.map.getImage().getSize().getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
        Graphics2D bg = this.cachedImage.createGraphics();
        this.map.drawMap(bg);
        bg.dispose();
        this.cachedScreenSize = (Dimension) this.map.getImage().getSize().clone();
        this.cachedExtent = this.map.getExtent();
        this.location = new OrientedPoint3d(this.cachedExtent.midPoint().getX(), this.cachedExtent.midPoint().getY(),
                -Double.MIN_VALUE);
        this.bounds = new Bounds3d(this.cachedExtent.getDeltaX(), this.cachedExtent.getDeltaY(), 0.0);
    }

    /** {@inheritDoc} */
    @Override
    public void destroy(final Contextualized contextProvider)
    {
        try
        {
            ContextUtil.lookupOrCreateSubContext(contextProvider.getContext(), "animation/2D")
                    .unbindObject(Integer.toString(System.identityHashCode(this)));
        }
        catch (Throwable throwable)
        {
            CategoryLogger.always().warn(throwable, "finalize");
        }
    }

    /** {@inheritDoc} */
    @Override
    public boolean contains(final Point2d pointWorldCoordinates, final Bounds2d extent)
    {
        return false;
    }

    /** {@inheritDoc} */
    @Override
    public long getId()
    {
        return -1; // drawn before the rest in case all z-values are the same
    }

}