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

com.googlecode.blaisemath.graphics.impl.DelegatingPointSetGraphic Maven / Gradle / Ivy

There is a newer version: 3.0.16
Show newest version
package com.googlecode.blaisemath.graphics.impl;

/*
 * #%L
 * BlaiseGraphics
 * --
 * Copyright (C) 2009 - 2020 Elisha Peterson
 * --
 * Licensed 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.
 * #L%
 */

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Queues;
import com.google.common.collect.Sets;
import com.googlecode.blaisemath.annotation.InvokedFromThread;
import com.googlecode.blaisemath.coordinate.CoordinateChangeEvent;
import com.googlecode.blaisemath.coordinate.CoordinateListener;
import com.googlecode.blaisemath.coordinate.CoordinateManager;
import com.googlecode.blaisemath.graphics.DelegatingPrimitiveGraphic;
import com.googlecode.blaisemath.graphics.Graphic;
import com.googlecode.blaisemath.graphics.GraphicComposite;
import com.googlecode.blaisemath.graphics.Renderer;
import com.googlecode.blaisemath.style.ObjectStyler;
import com.googlecode.blaisemath.style.Styles;
import com.googlecode.blaisemath.util.swing.MoreSwingUtilities;
import org.checkerframework.checker.nullness.qual.Nullable;

import javax.swing.*;
import java.awt.geom.Point2D;
import java.util.*;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.googlecode.blaisemath.graphics.impl.LabeledPointGraphic.P_LABEL_RENDERER;
import static com.googlecode.blaisemath.graphics.PrimitiveGraphicSupport.P_RENDERER;
import com.googlecode.blaisemath.primitive.AnchoredText;

/**
 * Manages a collection of points that are maintained as separate {@link Graphic}s,
 * and therefore fully customizable. Points and their locations are handled by a {@link CoordinateManager},
 * which allows their locations to be safely modified from other threads.
 *
 * @param  the type of object being displayed
 * @param  type of canvas to render to
 *
 * @see BasicPointSetGraphic
 *
 * @author Elisha Peterson
 */
public class DelegatingPointSetGraphic extends GraphicComposite {

    private static final Logger LOG = Logger.getLogger(DelegatingPointSetGraphic.class.getName());
    
    private static final int DEFAULT_NODE_CACHE_SIZE = 20000;
    
    /** Key for flag allowing individual points to be selected */
    public static final String POINT_SELECTION_ENABLED = "point-selection-enabled";

    /** Graphic objects for individual points */
    protected final Map> points = Maps.newHashMap();
    /** Whether points can be dragged */
    protected boolean dragEnabled = false;

    /** Manages locations of points */
    protected CoordinateManager manager;
    /** Responds to coordinate update events. Also used as a lock object for updates. */
    private final CoordinateListener coordListener;
    /** Flag that indicates points are being updated, and no notification events should be sent. */
    protected boolean updating = false;
    /** Queue of updates to be processed */
    private final Queue updateQueue = Queues.newConcurrentLinkedQueue();
    
    /** Selects styles for graphics */
    protected ObjectStyler styler = ObjectStyler.create();
    /** Selects renderer for points */
    protected Renderer renderer;
    /** Renderer for point labels */
    protected Renderer textRenderer;

    //region CONSTRUCTORS
    
    /**
     * Construct with no points.
     */
    public DelegatingPointSetGraphic() {
        this(null, null);
    }
    
    /**
     * Construct with no points.
     * @param renderer draws points
     * @param labelRenderer draws labels
     */
    public DelegatingPointSetGraphic(@Nullable Renderer renderer,
                                     @Nullable Renderer labelRenderer) {
        this(CoordinateManager.create(DEFAULT_NODE_CACHE_SIZE), renderer, labelRenderer);
    }

    /**
     * Construct with given set of coordinate locations.
     * @param crdManager manages point locations
     * @param renderer used for drawing the points
     * @param labelRenderer draws labels
     */
    public DelegatingPointSetGraphic(CoordinateManager crdManager, 
            @Nullable Renderer renderer,
            @Nullable Renderer labelRenderer) {
        setRenderer(renderer);
        setLabelRenderer(labelRenderer);
        
        styler.setStyle(Styles.DEFAULT_POINT_STYLE);
        styler.setTipDelegate(Objects::toString);
        coordListener = this::handleCoordinateChange;
        setCoordinateManager(crdManager);
    }

    //endregion

    //region PROPERTIES

    /**
     * Returns true if individual points can be selected.
     * @return true if points can be selected
     */
    public boolean isPointSelectionEnabled() {
        return styleHints.contains(POINT_SELECTION_ENABLED);
    }

    public void setPointSelectionEnabled(boolean val) {
        if (isPointSelectionEnabled() != val) {
            setStyleHint(POINT_SELECTION_ENABLED, val);
            points.values().forEach(p -> p.setSelectionEnabled(val));
        }
    }

    /**
     * Manager responsible for tracking point locations
     * @return manager
     */
    public CoordinateManager getCoordinateManager() {
        return manager;
    }

    /**
     * Set manager responsible for tracking point locations
     * @param mgr manager
     */
    public final void setCoordinateManager(CoordinateManager mgr) {
        if (this.manager != checkNotNull(mgr)) {
            if (this.manager != null) {
                this.manager.removeCoordinateListener(coordListener);
            }
            this.manager = null;
            clearPendingUpdates();

            Set oldPoints = points.keySet();
            Set toRemove = Sets.newHashSet(oldPoints);
            // lock to ensure that no changes are made until after the listener has been setup
            synchronized (mgr) {
                this.manager = mgr;
                Map activePoints = manager.getActiveLocationCopy();
                toRemove.removeAll(activePoints.keySet());
                updatePointGraphics(activePoints, toRemove, false);
                this.manager.addCoordinateListener(coordListener);
            }
            super.graphicChanged(this);
        }
    }

    /**
     * Returns object used to style points
     * @return styler object styler
     */
    public ObjectStyler getStyler() {
        return styler;
    }

    /**
     * Sets object used to style points
     * @param styler object styler
     */
    public void setStyler(ObjectStyler styler) {
        if (this.styler != checkNotNull(styler)) {
            this.styler = styler;
            fireGraphicChanged();
        }
    }

    public @Nullable Renderer getRenderer() {
        return renderer;
    }

    public final void setRenderer(@Nullable Renderer renderer) {
        if (this.renderer != renderer) {
            Object old = this.renderer;
            this.renderer = renderer;
            updating = true;
            for (DelegatingPrimitiveGraphic dpg : points.values()) {
                dpg.setRenderer(renderer);
            }
            updating = false;
            fireGraphicChanged();
            pcs.firePropertyChange(P_RENDERER, old, renderer);
        }
    }

    public @Nullable Renderer getLabelRenderer() {
        return textRenderer;
    }

    public final void setLabelRenderer(@Nullable Renderer renderer) {
        if (this.textRenderer != renderer) {
            Object old = this.renderer;
            this.textRenderer = renderer;
            fireGraphicChanged();
            pcs.firePropertyChange(P_LABEL_RENDERER, old, renderer);
        }
    }

    public boolean isDragEnabled() {
        return dragEnabled;
    }

    public void setDragEnabled(boolean val) {
        if (this.dragEnabled != val) {
            this.dragEnabled = val;
            for (DelegatingPrimitiveGraphic dpg : points.values()) {
                dpg.setDragEnabled(val);
            }
        }
    }

    /**
     * Return source objects.
     * @return source objects
     */
    public Set getObjects() {
        return manager.getActive();
    }

    //endregion

    //region MUTATORS

    /**
     * Adds objects to the graphic
     * @param obj objects to put
     */
    public final void addObjects(Map obj) {
        manager.putAll(obj);
    }

    //endregion

    //region LOOKUPS

    public @Nullable DelegatingPrimitiveGraphic getPointGraphic(S source) {
        return points.get(source);
    }

    @Override
    public void initContextMenu(JPopupMenu menu, Graphic src, Point2D point, Object focus, Set> selection, G canvas) {
        Graphic gfc = graphicAt(point, canvas);
        super.initContextMenu(menu, this, point,
                gfc instanceof DelegatingPrimitiveGraphic ? ((DelegatingPrimitiveGraphic)gfc).getSourceObject() : focus,
                selection, canvas);
    }

    //endregion

    //region EVENTS

    @InvokedFromThread("unknown")
    private void handleCoordinateChange(final CoordinateChangeEvent evt) {
        updateQueue.add(evt);
        MoreSwingUtilities.invokeOnEventDispatchThread(this::processNextCoordinateChangeEvent);
    }
    
    @InvokedFromThread("EDT")
    private void processNextCoordinateChangeEvent() {
        if (!SwingUtilities.isEventDispatchThread()) {
            LOG.log(Level.WARNING, "processNextCoordinateChangeEvent() called from non-EDT");
        }
        CoordinateChangeEvent evt = updateQueue.poll();
        if (evt != null && evt.getSource() == manager) {
            updatePointGraphics(evt.getAdded(), evt.getRemoved(), true);
        }
    }
    
    @InvokedFromThread("EDT")
    private void clearPendingUpdates() {
        if (!SwingUtilities.isEventDispatchThread()) {
            LOG.log(Level.WARNING, "clearPendingUpdates() called from non-EDT");
        }
        updateQueue.clear();
    }
    
    @InvokedFromThread("EDT")
    private void updatePointGraphics(Map added, Set removed, boolean notify) {
        updating = true;
        boolean change = false;
        List> addMe = Lists.newArrayList();
        if (added != null) {
            for (Entry en : added.entrySet()) {
                S src = en.getKey();
                DelegatingPrimitiveGraphic dpg = points.get(src);
                if (dpg == null) {
                    LabeledPointGraphic lpg = new LabeledPointGraphic<>(en.getKey(), en.getValue(), styler);
                    lpg.setRenderer(renderer);
                    lpg.setLabelRenderer(textRenderer);
                    lpg.setDragEnabled(dragEnabled);
                    lpg.setSelectionEnabled(isPointSelectionEnabled());
                    points.put(src, lpg);
                    addMe.add(lpg);
                } else {
                    dpg.setPrimitive(en.getValue());
                    change = true;
                }
            }
        }
        Set> removeMe = Sets.newHashSet();
        if (removed != null) {
            for (S s : removed) {
                removeMe.add(points.get(s));
                points.remove(s);
            }
        }
        change = replaceGraphics(removeMe, addMe) || change;
        updating = false;
        if (change && notify) {
            fireGraphicChanged();
        }
    }
    
    @Override
    protected void fireGraphicChanged() {
        if (!updating) {
            super.fireGraphicChanged();
        }
    }

    @Override
    public void graphicChanged(Graphic source) {
        if (!updating && source instanceof LabeledPointGraphic) {
            LabeledPointGraphic dpg = (LabeledPointGraphic) source;
            Point2D prim = dpg.getPrimitive();
            manager.put(dpg.getSourceObject(), prim instanceof Point2D.Double ? (Point2D.Double) prim
                    : new Point2D.Double(prim.getX(), prim.getY()));
        }
        if (!updating) {
            super.graphicChanged(source);
        }
    }

    //endregion
    
}