content: /**
   * Copyright 2020 Alexander Herzog
   *
   * 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.
   */
  package ui.modeleditor;
  
  import java.awt.AWTEvent;
  import java.awt.BasicStroke;
  import java.awt.Color;
  import java.awt.Component;
  import java.awt.Container;
  import java.awt.Cursor;
  import java.awt.Dimension;
  import java.awt.Font;
  import java.awt.FontMetrics;
  import java.awt.GradientPaint;
  import java.awt.Graphics;
  import java.awt.Graphics2D;
  import java.awt.GraphicsEnvironment;
  import java.awt.Image;
  import java.awt.MouseInfo;
  import java.awt.Point;
  import java.awt.Rectangle;
  import java.awt.RenderingHints;
  import java.awt.Toolkit;
  import java.awt.Window;
  import java.awt.datatransfer.Clipboard;
  import java.awt.datatransfer.DataFlavor;
  import java.awt.datatransfer.Transferable;
  import java.awt.datatransfer.UnsupportedFlavorException;
  import java.awt.dnd.DnDConstants;
  import java.awt.dnd.DropTarget;
  import java.awt.dnd.DropTargetAdapter;
  import java.awt.dnd.DropTargetDropEvent;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.event.FocusAdapter;
  import java.awt.event.FocusEvent;
  import java.awt.event.FocusListener;
  import java.awt.event.InputEvent;
  import java.awt.event.KeyEvent;
  import java.awt.event.KeyListener;
  import java.awt.event.MouseEvent;
  import java.awt.event.MouseListener;
  import java.awt.event.MouseMotionListener;
  import java.awt.event.MouseWheelEvent;
  import java.awt.event.MouseWheelListener;
  import java.awt.im.InputContext;
  import java.awt.image.BufferedImage;
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.File;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.OutputStreamWriter;
  import java.io.Serializable;
  import java.io.Writer;
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Optional;
  import java.util.Set;
  import java.util.Timer;
  import java.util.TimerTask;
  import java.util.TooManyListenersException;
  import java.util.concurrent.LinkedBlockingQueue;
  import java.util.concurrent.ThreadFactory;
  import java.util.concurrent.ThreadPoolExecutor;
  import java.util.concurrent.TimeUnit;
  import java.util.function.BiConsumer;
  import java.util.function.Consumer;
  import java.util.function.Function;
  import java.util.stream.Stream;
  
  import javax.imageio.ImageIO;
  import javax.swing.Icon;
  import javax.swing.ImageIcon;
  import javax.swing.JDialog;
  import javax.swing.JMenu;
  import javax.swing.JMenuItem;
  import javax.swing.JPanel;
  import javax.swing.JPopupMenu;
  import javax.swing.JViewport;
  import javax.swing.KeyStroke;
  import javax.swing.SwingUtilities;
  
  import org.apache.batik.dom.GenericDOMImplementation;
  import org.apache.batik.svggen.SVGGraphics2D;
  import org.apache.commons.math3.util.FastMath;
  import org.apache.poi.xwpf.usermodel.XWPFDocument;
  import org.apache.poi.xwpf.usermodel.XWPFParagraph;
  import org.apache.poi.xwpf.usermodel.XWPFRun;
  import org.w3c.dom.DOMImplementation;
  import org.w3c.dom.Document;
  
  import de.erichseifert.vectorgraphics2d.VectorGraphics2D;
  import de.erichseifert.vectorgraphics2d.eps.EPSProcessor;
  import de.erichseifert.vectorgraphics2d.intermediate.CommandSequence;
  import de.erichseifert.vectorgraphics2d.util.PageSize;
  import language.Language;
  import mathtools.NumberTools;
  import mathtools.distribution.tools.FileDropperData;
  import simulator.editmodel.EditModel;
  import simulator.editmodel.EditModelDark;
  import simulator.runmodel.SimulationData;
  import systemtools.BaseDialog;
  import systemtools.GUITools;
  import systemtools.ImageTools;
  import systemtools.MsgBox;
  import systemtools.statistics.XWPFDocumentPictureTools;
  import tools.SetupData;
  import ui.EditorPanel;
  import ui.MainFrame;
  import ui.ModelChanger;
  import ui.dialogs.UndoRedoDialog;
  import ui.images.Images;
  import ui.modeleditor.coreelements.ModelElement;
  import ui.modeleditor.coreelements.ModelElementBox;
  import ui.modeleditor.coreelements.ModelElementBoxHistogramDialog;
  import ui.modeleditor.coreelements.ModelElementPosition;
  import ui.modeleditor.descriptionbuilder.ModelDescriptionBuilderSingleStation;
  import ui.modeleditor.descriptionbuilder.ModelDescriptionBuilderStyled;
  import ui.modeleditor.elements.AnimationExpression;
  import ui.modeleditor.elements.ElementAnimationClickable;
  import ui.modeleditor.elements.ElementWithAnimationDisplay;
  import ui.modeleditor.elements.FontCache;
  import ui.modeleditor.elements.ModelElementAnimationBar;
  import ui.modeleditor.elements.ModelElementAnimationBarChart;
  import ui.modeleditor.elements.ModelElementAnimationLCD;
  import ui.modeleditor.elements.ModelElementAnimationLineDiagram;
  import ui.modeleditor.elements.ModelElementAnimationPointerMeasuring;
  import ui.modeleditor.elements.ModelElementAnimationRecord;
  import ui.modeleditor.elements.ModelElementAnimationTextValue;
  import ui.modeleditor.elements.ModelElementDashboard;
  import ui.modeleditor.elements.ModelElementEdge;
  import ui.modeleditor.elements.ModelElementImage;
  import ui.modeleditor.elements.ModelElementInput;
  import ui.modeleditor.elements.ModelElementNote;
  import ui.modeleditor.elements.ModelElementSourceTable;
  import ui.modeleditor.elements.ModelElementSub;
  import ui.modeleditor.elements.ModelElementText;
  import ui.parameterseries.ParameterCompareTemplatesDialog;
  import ui.parameterseries.ParameterCompareTemplatesDialog.TemplateMode;
  import ui.parameterseries.ParameterCompareTemplatesDialog.TemplateRecord;
  import ui.tools.FlatLaFHelper;
  import ui.tools.GlassInfo;
  import ui.tools.InputContextFix;
  
  /**
   * Zeichenflche fr die Modell-Elemente<br>
   * Achtung: <code>ModelSurfacePanel</code> sollte als bergeordnetes Element in ein <code>JScrollPane</code> eingebettet werden.
   * @author Alexander Herzog
   */
  public final class ModelSurfacePanel extends JPanel {
  	/**
  	 * Serialisierungs-ID der Klasse
  	 * @see Serializable
  	 */
  	private static final long serialVersionUID = 2829918440976903115L;
  
  	/**
  	 * Erlaubt das Bearbeiten von Elementen sowie das Verschieben im ansonsten Read-Only-Modus.
  	 * Das Lschen von Elementen und das Erstellen neuer Elemente bleibt aber verboten.
  	 * @see #allowEditorDialogs()
  	 */
  	private boolean allowChangeOperationsOnReadOnly=false;
  
  	/**
  	 * Gre der Zeichenflche
  	 */
  	public static final int SIZE=100_000;
  
  	/**
  	 * Minimaler Zoomfaktor
  	 */
  	public static final double ZOOM_MIN=0.2;
  
  	/**
  	 * Maximaler Zoomfaktor
  	 */
  	public static final double ZOOM_MAX=4;
  
  	/**
  	 * nderung des Zoomfaktors pro Mausrad-Schritt
  	 */
  	public static final double ZOOM_STEP=0.2;
  
  	/**
  	 * Anteil einer Bildschirmseite, um die das Fenster bei einem Mausrad-Schritt gescrollt werden soll
  	 */
  	public static final double WHEEL_SCROLL_FRACTION=25;
  
  	/**
  	 * Maximale Undo-Schritte
  	 */
  	public static final int MAX_UNDO_STEPS=20;
  
  	/**
  	 * Minimaler Abstand in Sekunden zwischen der Aufzeichnung von zwei Undo-Schritten
  	 */
  	public static final int MIN_UNDO_TIME_DELTA=1;
  
  	/**
  	 * Betriebsart der Zeichenflche
  	 * @see ModelSurfacePanel#getMode()
  	 * @see ModelSurfacePanel#startAddElement(ModelElementPosition)
  	 * @see ModelSurfacePanel#pasteFromClipboard()
  	 * @see ModelSurfacePanel#clipboardData
  	 */
  	public enum ClickMode {
  		/** Betriebsmodus: Normal */
  		MODE_NORMAL,
  
  		/** Betriebsmodus: Element hinzufgen */
  		MODE_ADD_ELEMENT,
  
  		/** Betriebsmodus: Kante hinzufgen (Auswahl des ersten zu verbindenden Elements) */
  		MODE_ADD_EDGE_STEP1,
  
  		/** Betriebsmodus: Kante hinzufgen (Auswahl des zweiten zu verbindenden Elements) */
  		MODE_ADD_EDGE_STEP2,
  
  		/** Betriebsmodus: Inhalt der Zwischenablage (Elemente) einfgen */
  		MODE_INSERT_ELEMENTS_FROM_CLIPBOARD,
  
  		/** Betriebsmodus: Elemente aus Vorlage einfgen */
  		MODE_INSERT_ELEMENTS_FROM_TEMPLATE,
  
  		/** Betriebsmodus: Inhalt der Zwischenablage (Text) einfgen */
  		MODE_INSERT_TEXT,
  
  		/** Betriebsmodus: Inhalt der Zwischenablage (Bild) einfgen */
  		MODE_INSERT_IMAGE
  	}
  
  	/**
  	 * Modus zum automatischen Hinzufgen von Verbindungskanten
  	 * @see ModelSurfacePanel#getAutoConnect()
  	 * @see ModelSurfacePanel#autoConnect(ModelElement, ModelElement)
  	 */
  	public enum ConnectMode {
  		/** Neue Elemente nicht automatisch verbinden */
  		OFF,
  
  		/** Neues Element jeweils mit dem vorherigen verbinden */
  		AUTO,
  
  		/** Nchstliegendes Element suchen und verbinden */
  		SMART
  	}
  
  	/** Ist die Rckgngig-Funktion verfgbar? ({@link #disableUndo()} */
  	private boolean canUndo=true;
  	/** Nur-Lese-Status */
  	private final boolean readOnly;
  	/** Darf der Menpunkt "Modelleigenschaften" im Kontextmen angezeigt werden? */
  	private boolean showEditModelProperties=true;
  	/** Gibt an, ob gerade eine Operation, die evtl. mehrere Elemente umfasst, luft und daher Statusausgaben usw. zunchst nicht verarbeitet werden sollen. */
  	private boolean operationRunning=false;
  
  	/** Zugehriges Modell */
  	private transient EditModel model=new EditModel();
  	/** Zeichenflche die dargestellt werden soll */
  	private transient ModelSurface surface=null;
  	/** Kundendaten aus {@link #model} */
  	private transient ModelClientData clientData;
  	/** Fertigungsplne aus {@link #model} */
  	private transient ModelSequences sequences;
  	/** Optionale (Animations-)Simulationsdaten ({@link #setAnimationSimulationData(SimulationData, ModelSurfaceAnimator)}) */
  	private transient SimulationData simData;
  	/** Optionales Animationssystem-Objekt der Hauptebene ({@link #setAnimationSimulationData(SimulationData, ModelSurfaceAnimator)}) */
  	private transient ModelSurfaceAnimator mainAnimator;
  
  	/** Reagiert auf {@link ModelSurface#addRedrawListener(Runnable)} */
  	private final transient Runnable requestRedrawListener;
  	/** Reagiert auf {@link ModelSurface#addRequestCopyListener(Runnable)} */
  	private final transient Runnable requestCopyListener;
  	/** Reagiert auf {@link ModelSurface#addRequestCutListener(Runnable)} */
  	private final transient Runnable requestCutListener;
  	/** Aktueller Zoomfaktor */
  	private double zoom;
  	/** Aktueller Raster-Anzeige-Modus */
  	private ModelSurface.Grid raster;
  	/** Farben fr den Hintergrund */
  	private Color[] colors=new Color[]{ModelSurface.DEFAULT_BACKGROUND_COLOR,ModelSurface.DEFAULT_RASTER_COLOR};
  	/** Optionales Hintergrundbild */
  	private BufferedImage backgroundImage;
  	/** Hashwert des Hintergrundbildes {@link #backgroundImage} (wird automatisch in {@link #setBackgroundImage(BufferedImage, double, ui.modeleditor.ModelSurface.BackgroundImageMode)} gesetzt */
  	private String backgroundImageHash;
  	/** Skalierungsfaktor fr das optionale Hintergrundbild */
  	private double backgroundImageScale;
  	/** Soll das Hintergrundbild, sofern {@link #backgroundImage} ungleich <code>null</code> ist, berhaupt gezeichnet werden? */
  	private ModelSurface.BackgroundImageMode backgroundImageMode;
  	/** Mausposition beim Start einer Drag&amp;drop-Operation */
  	private Point dragStartMousePosition=null;
  	/** Per Drag&amp;drop zu verschiebendes Element */
  	private transient ModelElement dragElement;
  	/** Per Drag&amp;drop zu kopierendes Element */
  	private transient ModelElement dragCopyElement;
  	/** Eckpunkt an dem ein Element in der Gre verndert wird */
  	private int dragStartElementBorderPointNr;
  	/** Startposition fr die Grennderung eines Elements */
  	private Point dragStartElementPosition;
  	/** Auswahlbereich */
  	private Rectangle selectBox;
  
  	/**
  	 * Aktueller Betriebsmodus
  	 * @see #getMode()
  	 * @see #setMode(ClickMode)
  	 */
  	private ClickMode mode=ClickMode.MODE_NORMAL;
  
  	/** Hinzuzufgendes Element */
  	private transient ModelElement modeAddElement;
  	/** Weitere Elemente die relativ zu {@link #modeAddElement} ausgerichtet und hinzugefgt werden sollen */
  	private transient ModelElement[] modeAddElementMore;
  	/** Element von dem aus nach dem Einfgen eines neuen Elements eine Verbindung zu dem neuen Element hergestellt werden soll */
  	private transient ModelElementBox modeAddElementSource;
  	/** Datenobjekt zum Einfgen aus der Zwischenablage */
  	private transient ByteArrayInputStream clipboardData;
  	/** Text zum Einfgen aus der Zwischenablage */
  	private String clipboardText;
  	/** Bild zum Einfgen aus der Zwischenablage */
  	private transient BufferedImage clipboardImage;
  
  	/**
  	 * Listener, die benachrichtigt werden sollen, wenn sich der Selektionmodus ndert
  	 * @see #fireStateChangeListener()
  	 */
  	private final transient List<ActionListener> stateChangeListener;
  
  	/**
  	 * Listener, die benachrichtigt werden sollen, wenn sich die Verfgbarkeit von Undo/Redo-Schritten ndert
  	 * @see #fireUndoRedoDoneListener()
  	 */
  	private final transient List<ActionListener> undoRedoDoneListener;
  
  	/**
  	 * Listen, die ber nderungen am Zoomfaktor benachrichtigt werden sollen
  	 */
  	private final transient List<ActionListener> zoomChangeListeners;
  
  	/**
  	 * Hlt das Cursor-Objekt fr "Verknpfung hier nicht mglich" vor
  	 */
  	private transient Cursor cursorNotAllowed;
  
  	/**
  	 * Kopie des letzten Status der Zeichenflchendaten
  	 * zu Erkennung, ob es Vernderungen gab
  	 */
  	private transient ModelSurface lastSurfaceState=null;
  
  	/**
  	 * Zeitpunkt (in System-Millisekunden) an dem die Zeichenflche
  	 * das letzte Mal auf nderungen berprft wurde, um so prfen
  	 * zu knnen, ob {@link #MIN_UNDO_TIME_DELTA} berschritten  wurde
  	 * und ggf. ein weiterer Rckgngig-Schritt erfasst werden soll.
  	 */
  	private long lastSurfaceStateTime=0;
  
  	/**
  	 * Liste mit Zeichenflchen-Puffer fr Rckgngig-Operationen
  	 */
  	private final transient List<ModelSurface> undoBuffer;
  
  	/**
  	 * Liste mit Zeichenflchen-Puffer fr Wiederholen-Operationen
  	 */
  	private final transient List<ModelSurface> redoBuffer;
  
  	/**
  	 * Optionales UserPaint-Objekt, welches beim Zeichnen zustzlich aufgerufen wird
  	 * @see #setAdditionalUserPaint(UserPaint)
  	 */
  	private transient UserPaint additionalUserPaint;
  
  	/**
  	 * Informationen zur linken Symbolleiste auf der leeren Zeichenflche anzeigen?
  	 */
  	private final boolean useInfoPaint;
  
  	/**
  	 * Optionales Callback, das zu einem Element weitere Tooltip-Daten (aus der Statistik) liefert
  	 * @see #setAdditionalTooltipGetter(Function)
  	 */
  	private transient Function<ModelElement,String> additionalTooltipGetter;
  
  	/**
  	 * Gibt an, ob Kanten zu neu eingefgten Elementen wenn mglich automatisch hinzugefgt werden sollen.
  	 * @see #getAutoConnect()
  	 * @see #setAutoConnect(ConnectMode)
  	 */
  	private ConnectMode autoConnect=ConnectMode.OFF;
  
  	/** Vertikale Positionen fr die Infotexte auf der Zeichenflche */
  	private transient int[] infoPositions;
  	/** Klick-Bereiche fr die Infotexte auf der Zeichenflche */
  	private transient Rectangle[] linkPositions;
  
  	/**
  	 * Konstruktor der Klasse <code>ModelSurfacePanel</code><br>
  	 * Achtung: <code>ModelSurfacePanel</code> sollte als bergeordnetes Element in ein <code>JScrollPane</code> eingebettet werden.
  	 * @param readOnly	Legt fest, ob nderungen an dem Modell vorgenommen werden drfen.
  	 * @param useInfoPaint	Informationen zur linken Symbolleiste auf der leeren Zeichenflche anzeigen
  	 */
  	public ModelSurfacePanel(final boolean readOnly, final boolean useInfoPaint) {
  		super();
  		this.readOnly=readOnly;
  		this.useInfoPaint=useInfoPaint;
  		stateChangeListener=new ArrayList<>();
  		undoRedoDoneListener=new ArrayList<>();
  		zoomChangeListeners=new ArrayList<>();
  		undoBuffer=new ArrayList<>();
  		redoBuffer=new ArrayList<>();
  
  		setZoom(SetupData.getSetup().lastZoom);
  		raster=ModelSurface.Grid.LINES;
  
  		requestRedrawListener=()->repaint();
  		requestCopyListener=()->copyToClipboard();
  		requestCutListener=()->{copyToClipboard(); deleteSelectedElements();};
  
  		addMouseListener(new ModelSurfacePanelMouseListener());
  		addMouseMotionListener(new ModelSurfacePanelMouseMotionListener());
  		addMouseWheelListener(new ModelSurfacePanelMouseWheelListener());
  		addKeyListener(new ModelSurfacePanelKeyListener());
  
  		setFocusable(true);
  		SwingUtilities.invokeLater(()->{requestFocus(); requestFocusInWindow();});
  
  		if (!GraphicsEnvironment.isHeadless()) try {
  			final BufferedImage image=ImageIO.read(Images.MODELEDITOR_NOT_ALLOWED_EDGE.getURLs()[0]);
  			cursorNotAllowed=Toolkit.getDefaultToolkit().createCustomCursor(image,new Point(image.getWidth()/2,image.getHeight()/2),"NotAllowed");
  		} catch (IOException e) {cursorNotAllowed=null;}
  		if (cursorNotAllowed==null) cursorNotAllowed=Cursor.getDefaultCursor();
  	}
  
  	/**
  	 * Konstruktor der Klasse <code>ModelSurfacePanel</code><br>
  	 * Achtung: <code>ModelSurfacePanel</code> sollte als bergeordnetes Element in ein <code>JScrollPane</code> eingebettet werden.
  	 */
  	public ModelSurfacePanel() {
  		this(false,false);
  	}
  
  	@Override
  	public InputContext getInputContext() {
  		return new InputContextFix(super.getInputContext());
  	}
  
  	/**
  	 * Deaktiviert das Undo/Redo-System fr diese Zeichenflche.<br>
  	 * Dies sollte ber den Konstruktor von {@link EditorPanel} fr den Fall,
  	 * dass es sich um einen Viewer oder eine Unter-Zeichenflche handelt,
  	 * durchgefhrt werden.
  	 */
  	public void disableUndo() {
  		canUndo=false;
  	}
  
  	/**
  	 * Legt fest, ob der Menpunkt "Modelleigenschaften" im Kontextmen angezeigt werden darf
  	 * @param showEditModelProperties	Soll der Menpunkt "Modelleigenschaften" im Kontextmen angezeigt werden?
  	 */
  	public void setShowEditModelProperties(final boolean showEditModelProperties) {
  		this.showEditModelProperties=showEditModelProperties;
  	}
  
  	/**
  	 * Setzt das {@link ModelSurface}-Element welches die Modell-Elemente beinhaltet;
  	 * dabei wird kein Undo-Schritt erfasst
  	 * @param model	Element vom Typ <code>EditModel</code> (wird bentigt, um die Liste der globalen Variablen zu laden)
  	 * @param surface	Darzustellendes <code>ModelSurface</code>-Element
  	 */
  	private void setSurfaceNoUndoCheck(final EditModel model, final ModelSurface surface) {
  		if (surface==this.surface) return;
  
  		flyOutOriginalZoom=-1;
  
  		final EditorPanel editorPanel=(this.surface==null)?null:this.surface.getEditorPanel();
  
  		if (this.surface!=null) {
  			this.surface.removeRedrawListener(requestRedrawListener);
  			this.surface.removeRequestCopyListener(requestCopyListener);
  			this.surface.removeRequestCutListener(requestCutListener);
  		}
  		this.model=model;
  		this.surface=surface;
  		if (this.surface!=null) {
  			this.surface.setHeatMapIntensityGetter(heatMapIntensityGetter);
  			this.surface.addRedrawListener(requestRedrawListener);
  			this.surface.addRequestCopyListener(requestCopyListener);
  			this.surface.addRequestCutListener(requestCutListener);
  			model.surface=surface;
  			this.surface.setEditorPanel(editorPanel);
  		}
  		SwingUtilities.invokeLater(()->{
  			repaint();
  			fireStateChangeListener();
  		});
  	}
  
  	/**
  	 * Setzt das {@link ModelSurface}-Element welches die Modell-Elemente beinhaltet
  	 * @param model	Element vom Typ <code>EditModel</code> (wird bentigt, um die Liste der globalen Variablen zu laden)
  	 * @param surface	Darzustellendes <code>ModelSurface</code>-Element
  	 * @param clientData	Kundendaten-Objekt (beim ndern der Namen im Surface werden hier ggf. neue Kundentypen als Kopien von vorhandenen angelegt)
  	 * @param sequences	Fertigungsplne-Objekt
  	 */
  	public void setSurface(final EditModel model, final ModelSurface surface, final ModelClientData clientData, final ModelSequences sequences) {
  		setSurfaceNoUndoCheck(model,surface);
  		surface.addStateChangeListener(()->fireStateChangeListener());
  		if (clientData!=null) this.clientData=clientData;
  		if (sequences!=null) this.sequences=sequences;
  		checkIfModelChanged(false);
  	}
  
  	/**
  	 * Liefert das aktuelle <code>ModelSurface</code>-Element zurck
  	 * @return	Dargestelltes <code>ModelSurface</code>-Element
  	 */
  	public ModelSurface getSurface() {
  		return surface;
  	}
  
  	/**
  	 * Fgt einen Listener hinzu, der benachrichtigt wird, wenn sich der Zoomfaktor ndert
  	 * @param zoomChangeListener	Zu benachrichtigender Listener
  	 */
  	public void addZoomChangeListener(final ActionListener zoomChangeListener) {
  		if (zoomChangeListeners.indexOf(zoomChangeListener)<0) zoomChangeListeners.add(zoomChangeListener);
  	}
  
  	/**
  	 * Entfernt einen Listener aus der Liste der im Falle einer nderung des Zoomfaktors zu benachrichtigenden Listener
  	 * @param zoomChangeListener	In Zukunft nicht mehr zu benachrichtigender Listener
  	 * @return	Gibt <code>true</code> zurck, wenn der Listener erfolgreich aus der Liste entfernt werden konnte
  	 */
  	public boolean removeZoomChangeListener(final ActionListener zoomChangeListener) {
  		return zoomChangeListeners.remove(zoomChangeListener);
  	}
  
  	/**
  	 * Benachrichtigt die Listen, dass der Zoomfaktor gendert wurde
  	 * @see #zoomChangeListeners
  	 */
  	private void fireZoomChangeListeners() {
  		final ActionEvent event=new ActionEvent(this,AWTEvent.RESERVED_ID_MAX+1,NumberTools.formatNumber(getZoom()));
  		for (ActionListener listener: zoomChangeListeners) listener.actionPerformed(event);
  	}
  
  	/**
  	 * Liefert den aktuell eingestellten Zoomfaktor
  	 * @return Aktueller Zoomfaktor
  	 */
  	public double getZoom() {
  		return zoom;
  	}
  
  	/**
  	 * Verringert den Zoomfaktor.
  	 */
  	public void zoomOut() {
  		setZoom(getZoom()-ZOOM_STEP);
  	}
  
  	/**
  	 * Vergrert den Zoomfaktor.
  	 */
  	public void zoomIn() {
  		setZoom(getZoom()+ZOOM_STEP);
  	}
  
  	/**
  	 * Stellt den Standard-Zoomfaktor wieder her.
  	 */
  	public void zoomDefault() {
  		setZoom(1);
  	}
  
  	/**
  	 * Stellt den Zoomfaktor ein.
  	 * @param zoom	Neuer Zoomfaktor
  	 */
  	public void setZoom(final double zoom) {
  		setZoom(zoom,true,-1,-1,true);
  	}
  
  	/**
  	 * Stellt den Zoomfaktor ein.
  	 * @param zoom	Neuer Zoomfaktor
  	 * @param keepMiddlePosition	So die bisherige zentrale Modellposition beibehalten werden (durch eine Verschiebung der oberen linken Kante des sichtbaren Bereichs)?
  	 * @param x	Wenn die Modellposition beibehalten werden soll und hier und fr y ein nichtnegativer Wert bergeben wird, wird diese Position statt der Mitte festgehalten
  	 * @param y	Wenn die Modellposition beibehalten werden soll und hier und fr x ein nichtnegativer Wert bergeben wird, wird diese Position statt der Mitte festgehalten
  	 * @param fireListeners	Sollen am Ende der Zoom-Listener ausgelst werden?
  	 */
  	private void setZoom(final double zoom, final boolean keepMiddlePosition, final int x, final int y, final boolean fireListeners) {
  		if (zoom<0.01 || zoom>10) return;
  		if (zoom==this.zoom) return;
  
  		final double oldZoom=this.zoom;
  		this.zoom=zoom;
  
  		if (keepMiddlePosition) {
  			final Rectangle rect=getViewRect();
  			if (rect!=null) {
  				final double newW=rect.width/zoom;
  				final double newH=rect.height/zoom;
  				final double newX;
  				final double newY;
  				if (x>=0 && y>=0) {
  					final double mx=x/oldZoom;
  					final double my=y/oldZoom;
  					final double xPart=(x-rect.x)/((double)(rect.width));
  					final double yPart=(y-rect.y)/((double)(rect.height));
  					newX=Math.max(0,mx-newW*xPart);
  					newY=Math.max(0,my-newH*yPart);
  				} else {
  					final double oldX=rect.x/oldZoom;
  					final double oldY=rect.y/oldZoom;
  					final double oldW=rect.width/oldZoom;
  					final double oldH=rect.height/oldZoom;
  					final double mx=oldX+oldW/2;
  					final double my=oldY+oldH/2;
  					newX=Math.max(0,mx-newW/2);
  					newY=Math.max(0,my-newH/2);
  				}
  				setTopPosition(new Point((int)Math.round(newX*zoom),(int)Math.round(newY*zoom)));
  			}
  		}
  
  		if (fireListeners) {
  			fireZoomChangeListeners();
  			final SetupData setup=SetupData.getSetup();
  			if (zoom!=setup.lastZoom) {
  				setup.lastZoom=zoom;
  				setup.saveSetup();
  			}
  		}
  		repaint();
  	}
  
  	/**
  	 * Zentriert das Modell auf der Zeichenflche
  	 */
  	public void centerModel() {
  		if (surface==null || !(getParent() instanceof JViewport)) return;
  
  		final Point point=surface.getModelCenter();
  
  		final JViewport viewport=(JViewport)getParent();
  		final Rectangle viewArea=viewport.getViewRect();
  
  		point.x=(int)Math.round(point.x*zoom);
  		point.y=(int)Math.round(point.y*zoom);
  
  		point.translate(-(int)Math.round(viewArea.width/2.0),-(int)Math.round(viewArea.height/2.0));
  		point.x=Math.max(0,point.x);
  		point.y=Math.max(0,point.y);
  
  		viewport.setViewPosition(point);
  
  		repaint();
  	}
  
  	/**
  	 * Scrollt ganz nach oben links.
  	 */
  	public void scrollToTop() {
  		if (surface==null || !(getParent() instanceof JViewport)) return;
  
  		final JViewport viewport=(JViewport)getParent();
  		viewport.setViewPosition(new Point(0,0));
  
  		repaint();
  	}
  
  	/**
  	 * Liefert die Position der linken oberen Ecke.
  	 * @return	Position der linken oberen Ecke (oder <code>null</code>, wenn diese nicht ermittelt werden konnte)
  	 * @see #setTopPosition(Point)
  	 */
  	public Point getTopPosition() {
  		if (surface==null || !(getParent() instanceof JViewport)) return null;
  		final JViewport viewport=(JViewport)getParent();
  		return viewport.getViewPosition();
  	}
  
  	/**
  	 * Stellt die Position der linken oberen Ecke ein.
  	 * @param topPosition	Position der linken oberen Ecke
  	 * @see #getTopPosition()
  	 */
  	public void setTopPosition(final Point topPosition) {
  		if (topPosition==null) return;
  		if (surface==null || !(getParent() instanceof JViewport)) return;
  		final JViewport viewport=(JViewport)getParent();
  		viewport.setViewPosition(topPosition);
  	}
  
  	/**
  	 * Liefert den tatschlich dargestellten Bereich der Zeichenflche.
  	 * @return	Aktuell dargestellter Bereich der Zeichenflche
  	 */
  	private Rectangle getViewRect() {
  		if (surface==null || !(getParent() instanceof JViewport)) return null;
  		final JViewport viewport=(JViewport)getParent();
  		return viewport.getViewRect();
  	}
  
  	/**
  	 * Skalierungsfaktor fr einen einzelnen Schritt beim Fly-Out-Zoom
  	 * @see #flyOutZoom()
  	 * @see #flyOutZoomStep(double)
  	 * @see #flyInZoom(Point)
  	 * @see #flyInZoomStep(Point)
  	 */
  	private static final double FLY_ZOOM_STEP_FACTOR=0.8;
  
  	/**
  	 * Ausgangszoomfaktor beim Auslsen des Fly-Out-Zooms.
  	 * @see #flyOutZoom()
  	 */
  	private double flyOutOriginalZoom=-1;
  
  	/**
  	 * Startet den Fly-Out-Zoom.
  	 * @see #flyOutZoomStep(double)
  	 */
  	public void flyOutZoom() {
  		if (flyOutOriginalZoom>0) return;
  		if (getTopPosition()==null) return;
  		flyOutOriginalZoom=zoom;
  		GlassInfo.info(this,Language.tr("Main.Menu.View.FlyOutZoom.Info"),650,true);
  		SwingUtilities.invokeLater(()->flyOutZoomStep(zoom));
  	}
  
  	/**
  	 * Fhrt einen einzelnen Schritt des Fly-Out-Zooms aus.
  	 * @param newZoom	Einzustellender Zoomfaktor
  	 * @see #flyOutZoom()
  	 */
  	private void flyOutZoomStep(final double newZoom) {
  		if (newZoom>0.2) {
  			final Point top=getTopPosition();
  
  			final double oldZoom=zoom;
  			setZoom(newZoom,false,-1,-1,false);
  			int x=(int)Math.round(top.x*newZoom/oldZoom);
  			int y=(int)Math.round(top.y*newZoom/oldZoom);
  
  			final Rectangle view=getViewRect();
  			final Point modelSize=surface.getLowerRightModelCorner();
  			if (view.width>modelSize.x*newZoom) x=0;
  			if (view.height>modelSize.y*newZoom) y=0;
  
  			setTopPosition(new Point(x,y));
  
  			final double nextZoom=newZoom*FLY_ZOOM_STEP_FACTOR;
  			SwingUtilities.invokeLater(()->flyOutZoomStep(nextZoom));
  		} else {
  			setZoom(0.2,false,-1,-1,true);
  		}
  	}
  
  	/**
  	 * Startet den Fly-In-Zoom (d.h. macht den Fly-Out-Zoom rckgngig).
  	 * @param center	Zu zentrierender Punkt
  	 * @see #flyInZoom(Point)
  	 */
  	private void flyInZoom(final Point center) {
  		if (flyOutOriginalZoom<0) return;
  		SwingUtilities.invokeLater(()->flyInZoomStep(center));
  	}
  
  	/**
  	 * Fhrt einen einzelnen Schritt des Fly-In-Zooms aus.
  	 * @param center	Zu zentrierender Punkt
  	 */
  	private void flyInZoomStep(final Point center) {
  		final double newZoom=zoom/FLY_ZOOM_STEP_FACTOR;
  		if (newZoom<flyOutOriginalZoom) {
  			final Point newCenter=new Point((int)Math.round(center.x/FLY_ZOOM_STEP_FACTOR),(int)Math.round(center.y/FLY_ZOOM_STEP_FACTOR));
  
  			final Rectangle rect=getViewRect();
  			final int x=Math.max(0,newCenter.x-rect.width/2);
  			final int y=Math.max(0,newCenter.y-rect.height/2);
  
  			setZoom(newZoom,false,-1,-1,false);
  			final Point newTop=new Point(x,y);
  			setTopPosition(newTop);
  
  			SwingUtilities.invokeLater(()->flyInZoomStep(newCenter));
  		} else {
  			setZoom(flyOutOriginalZoom,false,-1,-1,true);
  			flyOutOriginalZoom=-1;
  		}
  	}
  
  	/**
  	 * Zeigt den Dialog zur Konfiguration des Diagramme-Dashboards an.
  	 */
  	public void showDashboard() {
  		final ModelElementDashboard dashboard;
  
  		final boolean isInSurface;
  
  		/* Schon ein entsprechendes Element vorhanden? */
  		final Optional<ModelElementDashboard> dashboardOptional=surface.getElements().stream().filter(element->element instanceof ModelElementDashboard).map(element->((ModelElementDashboard)element)).findFirst();
  		if (dashboardOptional.isPresent()) {
  			dashboard=dashboardOptional.get();
  			isInSurface=true;
  		} else {
  			/* Ggf. neues Element anlegen */
  			dashboard=new ModelElementDashboard(model,surface);
  			isInSurface=false;
  		}
  
  		dashboard.showSubEditDialog(this,readOnly,false);
  
  		if (dashboard.getSubSurface().getElementCount()==0) {
  			if (isInSurface) surface.remove(dashboard);
  		} else {
  			if (!isInSurface) surface.add(dashboard);
  		}
  	}
  
  	/**
  	 * Gibt an, ob nderungen an dem Modell vorgenommen werden drfen.
  	 * @return	Nur-Lese-Status
  	 */
  	public boolean isReadOnly() {
  		return readOnly;
  	}
  
  	/**
  	 * Gibt an, ob ein Raster auf der Zeichenflche angezeigt wird
  	 * @return	Rasteranzeige
  	 */
  	public ModelSurface.Grid getRaster() {
  		return raster;
  	}
  
  	/**
  	 * Stellt ein, ob ein Raster auf der Zeichenflche angezeigt wird
  	 * @param raster	Rasteranzeige
  	 */
  	public void setRaster(final ModelSurface.Grid raster) {
  		if (raster==this.raster || raster==null) return;
  		this.raster=raster;
  		repaint();
  	}
  
  	/**
  	 * Listener, die benachrichtigt werden sollen, wenn eine Datei auf der Komponente abgelegt wird
  	 * @see #dropFile(File, Point)
  	 */
  	private List<ActionListener> fileDropListeners=new ArrayList<>();
  
  	/**
  	 * Fgt einen Listener hinzu, der benachrichtigt wird, wenn eine Datei auf der Komponente abgelegt wird
  	 * @param fileDropListener	Zu benachrichtigender Listener (der Dateiname ist ber die <code>getActionCommand()</code>-Methode des bergebenen <code>ActionEvent</code>-Objekts abrufbar)
  	 */
  	public void addFileDropListener(final ActionListener fileDropListener) {
  		if (fileDropListeners.indexOf(fileDropListener)<0) fileDropListeners.add(fileDropListener);
  	}
  
  	/**
  	 * Entfernt einen Listener aus der Liste der im Falle einer auf dieser Komponente abgelegten Datei zu benachrichtigenden Listener
  	 * @param fileDropListener	In Zukunft nicht mehr zu benachrichtigender Listener
  	 * @return	Gibt <code>true</code> zurck, wenn der Listener erfolgreich aus der Liste entfernt werden konnte
  	 */
  	public boolean removeFileDropListener(final ActionListener fileDropListener) {
  		return fileDropListeners.remove(fileDropListener);
  	}
  
  	/**
  	 * Reagiert auf das Ablegen einer Datei auf der Zeichenflche
  	 * @param file	Per Drag&amp;drop abgelegte Datei
  	 * @param point	Position auf der Zeichenflche
  	 */
  	private void dropFile(final File file, final Point point) {
  		BufferedImage image=null;
  		try {image=ImageIO.read(file);} catch (IOException e) {image=null;}
  		if (image!=null) {
  			ModelElementImage element=new ModelElementImage(model,surface);
  			element.setImage(image);
  			int x,y;
  			if (point==null) {
  				JViewport viewport=(JViewport)getParent();
  				final Point p=viewport.getViewPosition();
  				x=(int)Math.round((p.x+5)/zoom/5)*5;
  				y=(int)Math.round((p.y+5)/zoom/5)*5;
  			} else {
  				x=(int)Math.round(point.x/zoom/5)*5;
  				y=(int)Math.round(point.y/zoom/5)*5;
  			}
  			element.setPosition(new Point(x,y));
  			surface.add(element);
  			setMode(ClickMode.MODE_NORMAL);
  		} else {
  			final ActionEvent event=FileDropperData.getActionEvent(null,file,this,point);
  			for (ActionListener listener: fileDropListeners) listener.actionPerformed(event);
  		}
  	}
  
  	/**
  	 * Stellt ein, ob das Objekt per Drag&amp;Drop bertragene <code>ModelElementPosition</code>-Vorlageelemente annehmen soll.
  	 * @param acceptTemplateElements	Wird <code>true</code> bergeben, so werden Vorlagen per Drag&amp;Drop angenommen.
  	 */
  	public void setDropAccept(final boolean acceptTemplateElements) {
  		if (acceptTemplateElements) {
  			final DropTarget dt=new DropTarget();
  			setDropTarget(dt);
  			try {
  				dt.addDropTargetListener(new DropTargetAdapter() {
  					private boolean dropFileList(final DropTargetDropEvent dtde, final Transferable transfer) {
  						try {
  							final Object obj=transfer.getTransferData(DataFlavor.javaFileListFlavor);
  							if (obj instanceof List) for (Object entry: ((List<?>)obj)) if (entry instanceof File) {
  								dropFile((File)entry,dtde.getLocation());
  								break;
  							}
  							return true;
  						} catch (UnsupportedFlavorException | IOException e) {return false;}
  					}
  
  					private boolean dropElement(final DropTargetDropEvent dtde, final Transferable transfer) {
  						try {
  							final Object obj=transfer.getTransferData(DataFlavor.stringFlavor);
  							if (!(obj instanceof String)) return false;
  							final ModelElementPosition element=ModelElementCatalog.getCatalog().getMenuElement((String)obj);
  							if (element!=null) {
  								final Point point=dtde.getLocation();
  								addElement(element,new Point(point.x+10,point.y+10));
  							}
  							return true;
  						} catch (UnsupportedFlavorException | IOException e) {return false;}
  					}
  
  					@Override
  					public void drop(final DropTargetDropEvent dtde) {
  						dtde.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE); /* Sonst knnen keine Dateien abgelegt werden */
  						final Transferable transfer=dtde.getTransferable();
  
  						/* Datei(en) abgelegt */
  						if (transfer.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
  							if (dropFileList(dtde,transfer)) return;
  						}
  
  						/* Modell-Element aus dem Vorlagenbereich */
  						if (transfer.isDataFlavorSupported(DataFlavor.stringFlavor)) {
  							if (dropElement(dtde,transfer)) return;
  						}
  						dtde.rejectDrop();
  					}
  				});
  			} catch (TooManyListenersException e1) {}
  		} else {
  			setDropTarget(null);
  		}
  	}
  
  	/**
  	 * Liefert den aktuellen Betriebsmodus zurck.
  	 * @return	Aktueller Betriebsmodus
  	 */
  	public ClickMode getMode() {
  		return mode;
  	}
  
  	/**
  	 * Stellt den aktuellen Betriebsmodus ein.
  	 * @param mode	Neuer Betriebsmodus
  	 * @see #getMode()
  	 */
  	private void setMode(final ClickMode mode) {
  		boolean fireStateChange=(this.mode!=mode);
  		this.mode=mode;
  		if (mode==ClickMode.MODE_ADD_ELEMENT) {
  			setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
  		} else {
  			setCursor(Cursor.getDefaultCursor());
  			modeAddElementSource=null;
  		}
  		if (mode==ClickMode.MODE_NORMAL) {
  			modeAddElement=null;
  			modeAddElementMore=null;
  
  		}
  		if (fireStateChange) fireStateChangeListener();
  	}
  
  	/**
  	 * Startet die verzgerte Reaktion auf nderungen an dem Modell
  	 * (bzw. hlt die Auslsung der Listener zurck)
  	 * @see #finishDelayStateChangeListener()
  	 */
  	private void startDelayStateChangeListener() {
  		delayStateChangeListener=true;
  		needToFireStateChangeListener=false;
  	}
  
  	/**
  	 * Beendet die per {@link #startDelayStateChangeListener()}
  	 * gestartete Verzgerung der {@link #stateChangeListener}
  	 * und lst diese falls mittlerweile ntig aus.
  	 * @see #startDelayStateChangeListener()
  	 */
  	private void finishDelayStateChangeListener() {
  		delayStateChangeListener=false;
  		if (needToFireStateChangeListener) fireStateChangeListener();
  		needToFireStateChangeListener=false;
  	}
  
  	/**
  	 * Soll die Auslsung der {@link #stateChangeListener} verzgert werden?
  	 * @see #fireStateChangeListener()
  	 * @see #startDelayStateChangeListener()
  	 * @see #finishDelayStateChangeListener()
  	 */
  	private boolean delayStateChangeListener;
  
  	/**
  	 * Gibt es momentan zurckgehaltene {@link #stateChangeListener}-Auslsungen?
  	 * @see #fireStateChangeListener()
  	 * @see #startDelayStateChangeListener()
  	 * @see #finishDelayStateChangeListener()
  	 */
  	private boolean needToFireStateChangeListener;
  
  	/**
  	 * Lst die Listener, die benachrichtigt werden sollen, wenn sich der Selektionmodus ndert, aus.
  	 * @see #stateChangeListener
  	 */
  	public void fireStateChangeListener() {
  		if (delayStateChangeListener) {needToFireStateChangeListener=true; return;}
  		final ActionEvent event=new ActionEvent(this,AWTEvent.RESERVED_ID_MAX+1,"statechange");
  		for (ActionListener listener: stateChangeListener) listener.actionPerformed(event);
  	}
  
  	/**
  	 * Fgt einen Listener, der benachrichtigt wird, wenn sich der Selektionmodus ndert, zu der Liste aller Listener hinzu.
  	 * @param listener	Hinzuzufgender Listener
  	 */
  	public void addStateChangeListener(final ActionListener listener) {
  		if (stateChangeListener.indexOf(listener)<0) stateChangeListener.add(listener);
  	}
  
  	/**
  	 * Entfernt einen Listener, der benachrichtigt wird, wenn sich der Selektionmodus ndert, aus der Liste aller Listener.
  	 * @param listener	Zu entfernender Listener
  	 * @return	Gibt <code>true</code> zurck, wenn sich der Listener bisher in der Liste befand
  	 */
  	public boolean removeStateChangeListener(final ActionListener listener) {
  		return stateChangeListener.remove(listener);
  	}
  
  	/**
  	 * Lst die Listener, die benachrichtigt werden sollen, wenn sich die Verfgbarkeit von Undo/Redo-Schritten ndert, aus.
  	 * @see #undoRedoDoneListener
  	 */
  	private void fireUndoRedoDoneListener() {
  		final ActionEvent event=new ActionEvent(this,AWTEvent.RESERVED_ID_MAX+1,"undoredodone");
  		for (ActionListener listener: undoRedoDoneListener) listener.actionPerformed(event);
  	}
  
  	/**
  	 * Fgt einen Listener, der benachrichtigt wird, wenn sich die Verfgbarkeit von Undo/Redo-Schritten ndert, zu der Liste aller Listener hinzu.
  	 * @param listener	Hinzuzufgender Listener
  	 */
  	public void addUndoRedoDoneListener(final ActionListener listener) {
  		if (undoRedoDoneListener.indexOf(listener)<0) undoRedoDoneListener.add(listener);
  	}
  
  	/**
  	 * Entfernt einen Listener, der benachrichtigt wird, wenn sich die Verfgbarkeit von Undo/Redo-Schritten ndert, aus der Liste aller Listener.
  	 * @param listener	Zu entfernender Listener
  	 * @return	Gibt <code>true</code> zurck, wenn sich der Listener bisher in der Liste befand
  	 */
  	public boolean removeUndoRedoDoneListener(final ActionListener listener) {
  		return undoRedoDoneListener.remove(listener);
  	}
  
  	/**
  	 * ffnet den Bearbeiten-Dialog fr das gewhlte Element.
  	 */
  	public void editSelectedElement() {
  		showElementProperties(surface.getSelectedElement(),0);
  	}
  
  	/**
  	 * Lscht die gewhlten Elemente.
  	 */
  	public void deleteSelectedElements() {
  		if (!readOnly) {
  			startDelayStateChangeListener();
  			try {
  				final List<ModelElement> list=surface.getSelectedArea(true);
  				if (list!=null && list.size()>0) {
  					for (ModelElement listElement: new ArrayList<>(list)) surface.remove(listElement);
  					fireStateChangeListener();
  				}
  
  				final ModelElement element=surface.getSelectedElement();
  				if (element!=null) {
  					surface.remove(element);
  					fireStateChangeListener();
  				}
  			} finally {
  				finishDelayStateChangeListener();
  			}
  		}
  	}
  
  	/**
  	 * Lscht das gewhlte Element und versucht die Lcke mit einer neuen Kante zu schlieen.
  	 */
  	public void deleteSelectedElementAndCloseGap() {
  		if (!readOnly) {
  			startDelayStateChangeListener();
  			try {
  				final ModelElement element=surface.getSelectedElement();
  				if (element!=null) {
  					surface.removeAndCloseGap(element);
  					fireStateChangeListener();
  				}
  			} finally {
  				finishDelayStateChangeListener();
  			}
  		}
  	}
  
  	/**
  	 * Whlt alle Elemente aus.
  	 */
  	public void selectAll() {
  		surface.setSelectedElement(null);
  		operationRunning=true;
  		try {
  			surface.setSelectedAreaAll();
  		} finally {
  			operationRunning=false;
  		}
  		fireSelectionListener();
  	}
  
  	/**
  	 * Scrollt zu einem bestimmten Element und whlt es aus
  	 * @param id	ID des auszuwhlenden Elements
  	 */
  	public void selectAndScrollToElement(final int id) {
  		if (id<0) return;
  		if (surface==null || !(getParent() instanceof JViewport)) return;
  
  		ModelElement element=surface.getById(id);
  		if (element==null) return;
  		if (!surface.isVisibleOnLayer(element)) return;
  		surface.setSelectedElement(element);
  		fireSelectionListener();
  
  		Point point=element.getMiddlePosition(false);
  		if (point==null) point=element.getPosition(false);
  		if (point!=null) {
  
  			JViewport viewport=(JViewport)getParent();
  			Rectangle viewArea=viewport.getViewRect();
  
  			point.translate(-viewArea.width/2,-viewArea.height/2);
  			point.x=Math.max(0,point.x);
  			point.y=Math.max(0,point.y);
  			viewport.setViewPosition(point);
  		}
  
  		repaint();
  	}
  
  	/**
  	 * Bricht das Hinzufgen eines Elements oder einer Kante ab.
  	 */
  	public void cancelAdd() {
  		setMode(ClickMode.MODE_NORMAL);
  	}
  
  	/**
  	 * Beginnt mit dem Hinzufgen keiner Verbindungskante.
  	 */
  	public void startAddEdge() {
  		if (readOnly) return;
  		surface.setSelectedElement(null);
  		fireSelectionListener();
  		setMode(ClickMode.MODE_ADD_EDGE_STEP1);
  	}
  
  	/**
  	 * Beginnt mit dem Hinzufgen eines Elements (welches keine Kante ist).
  	 * Nach dem Einfgen soll es mit dem Ausgangselement verbunden werden.
  	 * @param connectionSource	Ausgangselement, mit dem das neue Element verbunden werden soll
  	 * @param element	Vorlage fr das hinzuzufgende Element
  	 */
  	public void startAddElement(final ModelElementBox connectionSource, final ModelElementPosition element) {
  		startAddElement(element);
  		modeAddElementSource=connectionSource;
  	}
  
  	/**
  	 * Beginnt mit dem Hinzufgen eines Elements (welches keine Kante ist).
  	 * @param element	Vorlage fr das hinzuzufgende Element
  	 */
  	public void startAddElement(final ModelElementPosition element) {
  		if (readOnly) return;
  		surface.setSelectedElement(null);
  		fireSelectionListener();
  		modeAddElement=element;
  		modeAddElementMore=null;
  		modeAddElementSource=null;
  		setMode(ClickMode.MODE_ADD_ELEMENT);
  	}
  
  	/**
  	 * Beginnt mit dem Hinzufgen eines Elements (welches keine Kante ist).
  	 * @param element	Vorlage fr das hinzuzufgende Element
  	 * @param elements	Optionale Vorlagen fr weitere hinzuzufgende Elemente (kann <code>null</code> sein)
  	 */
  	public void startAddElementMulti(final ModelElementPosition element, final ModelElementPosition[] elements) {
  		if (readOnly) return;
  		surface.setSelectedElement(null);
  		fireSelectionListener();
  		modeAddElement=element;
  		modeAddElementMore=elements;
  		modeAddElementSource=null;
  		setMode(ClickMode.MODE_ADD_ELEMENT);
  	}
  
  	/**
  	 * Versucht zwei Stationen ber eine Verbindungskante
  	 * zu verbinden (Funktion zur automatischen Verbindung neuer Stationen).
  	 * @param element1	Ausgangsstation
  	 * @param element2	Zielstation
  	 * @return	Liefert <code>true</code>, wenn eine Verbindungskante eingefgt werden konnte
  	 * @see #addElement(ModelElementPosition, Point)
  	 */
  	private boolean autoConnect(final ModelElement element1, final ModelElement element2) {
  		if (!(element1 instanceof ModelElementPosition)) return false;
  		if (!(element2 instanceof ModelElementPosition)) return false;
  
  		final ModelElementPosition box1=(ModelElementPosition)element1;
  		final ModelElementPosition box2=(ModelElementPosition)element2;
  
  		if (!box1.canAddEdgeOut() || !box2.canAddEdgeIn()) return false;
  
  		final ModelElementEdge edge=new ModelElementEdge(model,surface,box1,box2);
  		if (!box1.addEdgeOut(edge)) return false;
  		if (!box2.addEdgeIn(edge)) {box1.removeConnectionNotify(edge); return false;}
  		surface.add(edge);
  
  		return true;
  	}
  
  	/**
  	 * Versucht zwei Stationen ber eine Verbindungskante
  	 * zu verbinden (Funktion zur intelligenten Verbindung neuer Stationen).
  	 * @param lastSelected	Zuletzt selektierte Station (wird in Kombination mit dem Abstand bercksichtigt; die Verbindung muss also nicht notwendig von dieser Station ausgehen)
  	 * @param element	Zielstation
  	 * @return	Liefert <code>true</code>, wenn eine Verbindungskante eingefgt werden konnte
  	 * @see #addElement(ModelElementPosition, Point)
  	 */
  	private boolean smartConnect(final ModelElement lastSelected, final ModelElement element) {
  		if (!(element instanceof ModelElementPosition)) return false;
  		final ModelElementPosition box=(ModelElementPosition)element;
  		if (!box.canAddEdgeIn()) return false;
  
  		ModelElementPosition bestElement=null;
  		double bestDistance=Double.MAX_VALUE;
  		final Point boxPoint=box.getPosition(true);
  
  		for (ModelElement sourceVertex: surface.getElements()) {
  			if (sourceVertex==element) continue;
  			if (!(sourceVertex instanceof ModelElementPosition)) continue;
  			final ModelElementPosition sourceBox=(ModelElementPosition)sourceVertex;
  			if (!sourceBox.canAddEdgeOut()) continue;
  
  			final Point sourcePoint=sourceBox.getPosition(true);
  			final double x=(sourcePoint.x-boxPoint.x);
  			final double y=(sourcePoint.y-boxPoint.y);
  			double distance=Math.sqrt(x*x+y*y);
  			if (sourceBox==lastSelected) distance*=0.9; /* Das zu letzt gewhlte Element bekommt einen kleinen Vorsprung. */
  			if (distance<bestDistance) {
  				bestDistance=distance;
  				bestElement=sourceBox;
  			}
  		}
  
  		if (bestElement!=null) return autoConnect(bestElement,box);
  		return false;
  	}
  
  	/**
  	 * Fgt ein Element in das Modell ein.
  	 * @param template	Vorlage, von der eine Kopie eingefgt werden soll
  	 * @param point	Position, an der das Element eingefgt werden soll
  	 */
  	public void addElement(final ModelElementPosition template, final Point point) {
  		if (readOnly) return;
  		if (surface==null) return;
  
  		surface.setSelectedElement(null);
  		fireSelectionListener();
  
  		final ModelElement element=template.clone(model,surface);
  		int x=(int)Math.round(point.x/zoom/5)*5;
  		int y=(int)Math.round(point.y/zoom/5)*5;
  		element.setPosition(new Point(x,y));
  		surface.add(element);
  
  		switch (autoConnect) {
  		case OFF:
  			/* Nichts */
  			break;
  		case AUTO:
  			autoConnect(surface.getLastSelectedPositionElement(),element);
  			break;
  		case SMART:
  			smartConnect(surface.getLastSelectedPositionElement(),element);
  			break;
  		default:
  			break;
  		}
  
  		setMode(ClickMode.MODE_NORMAL);
  		surface.setSelectedElement(element);
  		fireSelectionListener();
  		fireStateChangeListener();
  	}
  
  	/**
  	 * Wird aufgerufen, wenn das Modell verndert wurde.
  	 * @param previousModel	Vorheriges Modell (fr die Rckgngig-Funktion)
  	 */
  	private void modelChanged(final ModelSurface previousModel) {
  		if (!canUndo) return;
  		undoBuffer.add(previousModel.clone(false,previousModel.getResources().clone(),previousModel.getSchedules().clone(),previousModel.getParentSurface(),null));
  		while (undoBuffer.size()>MAX_UNDO_STEPS) undoBuffer.remove(0);
  
  		redoBuffer.clear();
  
  		fireUndoRedoDoneListener();
  	}
  
  	/**
  	 * Prft, ob das Modell verndert wurde
  	 * @param forceCheck	Prfung erzwingen
  	 */
  	private void checkIfModelChanged(final boolean forceCheck) {
  		if (!canUndo || readOnly) return;
  		if ((forceCheck || (mode==ClickMode.MODE_NORMAL && dragStartElementPosition==null)) && surface!=null) {
  
  			if (lastSurfaceState!=null && lastSurfaceStateTime+MIN_UNDO_TIME_DELTA*1000<System.currentTimeMillis() && !surface.equalsModelSurface(lastSurfaceState)) {
  				modelChanged(lastSurfaceState);
  				lastSurfaceState=null;
  			}
  		}
  		if (lastSurfaceState==null && surface!=null) {
  			lastSurfaceState=surface.clone(false,surface.getResources().clone(),surface.getSchedules().clone(),surface.getParentSurface(),null);
  			lastSurfaceStateTime=System.currentTimeMillis();
  		}
  	}
  
  	/**
  	 * Gibt an, ob Rckgngig-Schritte vorhanden sind
  	 * @return	Gibt <code>true</code> zurck, wenn die Rckgngig-Funktion zur Verfgung steht
  	 * @see #doUndo()
  	 */
  	public boolean canUndo() {
  		return undoBuffer.size()>0;
  	}
  
  	/**
  	 * Gibt an, ob Wiederholen-Schritte vorhanden sind
  	 * @return	Gibt <code>true</code> zurck, wenn die Wiederholen-Funktion zur Verfgung steht
  	 * @see #doRedo()
  	 */
  	public boolean canRedo() {
  		return redoBuffer.size()>0;
  	}
  
  	/**
  	 * Fhrt (wenn mglich) einen Rckgngig-Schritt aus
  	 * @see #canUndo()
  	 */
  	public void doUndo() {
  		if (undoBuffer.size()==0) return;
  
  		redoBuffer.add(surface.clone(false,surface.getResources(),surface.getSchedules().clone(),surface.getParentSurface(),null));
  
  		final ModelSurface restoreSurface=undoBuffer.remove(undoBuffer.size()-1);
  		setSurfaceNoUndoCheck(model,restoreSurface);
  		lastSurfaceState=restoreSurface.clone(false,restoreSurface.getResources().clone(),restoreSurface.getSchedules().clone(),restoreSurface.getParentSurface(),null);
  
  		restoreSurface.setSelectedElement(null);
  		fireSelectionListener();
  		fireUndoRedoDoneListener();
  	}
  
  	/**
  	 * Fhrt (wenn mglich) einen Wiederholen-Schritt aus
  	 * @see #canUndo()
  	 */
  	public void doRedo() {
  		if (redoBuffer.size()==0) return;
  
  		undoBuffer.add(surface.clone(false,surface.getResources(),surface.getSchedules(),surface.getParentSurface(),null));
  
  		final ModelSurface restoreSurface=redoBuffer.remove(redoBuffer.size()-1);
  		setSurfaceNoUndoCheck(model,restoreSurface);
  		lastSurfaceState=restoreSurface.clone(false,restoreSurface.getResources().clone(),restoreSurface.getSchedules().clone(),restoreSurface.getParentSurface(),null);
  
  		restoreSurface.setSelectedElement(null);
  		fireSelectionListener();
  		fireUndoRedoDoneListener();
  	}
  
  	/**
  	 * Zeigt einen Dialog zur Auswahl des Rckgngig- oder Wiederholen-Schritts an.
  	 * @see #canUndo
  	 * @see #canRedo()
  	 */
  	public void doUnDoRedoByDialog() {
  		if (undoBuffer.size()==0 && redoBuffer.size()==0) return;
  
  		final UndoRedoDialog dialog=new UndoRedoDialog(this,model,undoBuffer,surface,redoBuffer);
  		if (dialog.getClosedBy()!=BaseDialog.CLOSED_BY_OK) return;
  		final int step=dialog.getSelectedStep();
  
  		if (step>0) {
  			/* Redo */
  			for (int i=0;i<step;i++) doRedo();
  			return;
  		}
  
  		if (step<0) {
  			/* Undo */
  			for (int i=0;i<-step;i++) doUndo();
  		}
  	}
  
  	/**
  	 * Stellt ein UserPaint-Objekt, welches beim Zeichnen zustzlich aufgerufen wird
  	 * @param additionalUserPaint	UserPaint-Objekt, das beim Zeichnen zustzlich aufgerufen wird; kann <code>null</code> sein, wenn nichts zustzlich aufgerufen werden soll.
  	 */
  	public void setAdditionalUserPaint(final UserPaint additionalUserPaint) {
  		this.additionalUserPaint=additionalUserPaint;
  	}
  
  	/**
  	 * Zeichnet die Surface-Elemente auf einem <code>Graphics</code>-Element ein. Hintergrund usw. werden dabei nicht gezeichnet.
  	 * @param g	Grafik-Objekt auf dem gezeichnet werden soll
  	 * @param viewArea	Darzustellender Bereich
  	 * @param showBackground	Hintergrund anzeigen?
  	 * @param showBoundingBox	Wenn Hintergrund aus: Wenigstens weien Kasten? (bei svg-Export: aus, sonst: an)
  	 * @param raster	Raster anzeigen?
  	 * @param colors	2- oder 3-elementiges Array aus Hintergrund-, Raster- und optional oberer Gradienthintergrundfarbe
  	 * @param backgroundImage	Optionales Hintergrundbild
  	 * @param backgroundImageHash	Optionaler zustzliches Hash-Wert des optionales Hintergrundbildes (siehe {@link ScaledImageCache#getHash(java.awt.Image)}
  	 * @param backgroundImageScale	Skalierungsfaktor fr das optionale Hintergrundbild
  	 * @param showSelectionFrames	Rahmen anzeigen, wenn etwas ausgewhlt ist
  	 */
  	public void paintElements(final Graphics g, final Rectangle viewArea, final ModelSurface.BackgroundImageMode showBackground, final boolean showBoundingBox, final ModelSurface.Grid raster, final Color[] colors, final BufferedImage backgroundImage, final String backgroundImageHash, final double backgroundImageScale, final boolean showSelectionFrames) {
  		if (SetupData.getSetup().antialias) ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
  
  		/* Surface zeichnet alle Elemente in Graphics-Objekt */
  		surface.drawToGraphics(g,viewArea,zoom,true,showBackground,showBoundingBox,raster,colors,backgroundImage,backgroundImageHash,backgroundImageScale,showSelectionFrames);
  	}
  
  	/**
  	 * Stellt die vertikalen Positionen fr die Infotexte,
  	 * die sich auf die drei Schaltflchen in der vertikalen
  	 * Leiste links neben der Zeichenflche beziehen, ein.
  	 * @param positions	Array aus drei Elementen mit den vertikalen Positionen fr die Infotexte oder <code>null</code>, wenn keine Infotexte angezeigt werden sollen.
  	 */
  	public void setInfoPositions(final int[] positions) {
  		if (positions==null || positions.length!=3) {
  			infoPositions=null;
  		} else {
  			infoPositions=Arrays.copyOf(positions,3);
  		}
  	}
  
  	/**
  	 * Zeichenflchen-Links
  	 * @see #paintInfo(Graphics, Rectangle)
  	 */
  	private ModelSurfaceLinks surfaceLinks;
  
  	/**
  	 * Zeichnet die zustzlichen Infotexte auf der Zeichenflche ein.
  	 * @param g	Grafikausgabeobjekt
  	 * @param viewArea	Sichtbarer Bereich
  	 * @see #paint(Graphics)
  	 */
  	private void paintInfo(final Graphics g, final Rectangle viewArea) {
  		if (infoPositions==null) {
  			linkPositions=null;
  			return;
  		}
  
  		/* Font-Einstellungen speichern */
  
  		final Font saveFont=g.getFont();
  		g.setFont(FontCache.getFontCache().getFont(FontCache.defaultFamily,0,(int)Math.round(12*GUITools.getScaleFactor())));
  
  		/* Pfeile mit Beschriftungen fr die Schaltflchen */
  
  		g.setColor(Color.GRAY);
  		((Graphics2D)g).setStroke(new BasicStroke(2));
  		final int lineH=g.getFontMetrics().getAscent()+g.getFontMetrics().getDescent();
  		final int lineYShift=g.getFontMetrics().getAscent();
  
  		for (int i=0;i<3;i++) {
  			final int y=infoPositions[i];
  			g.drawLine(20,y,65,y);
  			g.drawLine(20,y,30,y-10);
  			g.drawLine(20,y,30,y+10);
  
  			String text="";
  			Image image=null;
  			switch (i) {
  			case 0:
  				text=Language.tr("Editor.SurfaceTooltip.SurfaceInfoProperties");
  				image=Images.MODEL.getImage();
  				break;
  			case 1:
  				text=Language.tr("Editor.SurfaceTooltip.SurfaceInfoTemplates");
  				image=Images.EDIT_ADD.getImage();
  				break;
  			case 2:
  				text=Language.tr("Editor.SurfaceTooltip.SurfaceInfoAddEgde");
  				image=Images.EDIT_EDGES_ADD.getImage();
  				break;
  			}
  
  			if (image!=null) g.drawImage(image,70,y-8,null);
  
  			final String[] lines=text.split("\\n");
  			int textY=y-lineH*lines.length/2;
  			for (String line: lines) {
  				g.drawString(line,90,textY+lineYShift);
  				textY+=lineH;
  			}
  		}
  
  		/* Links */
  
  		if (surfaceLinks==null) surfaceLinks=new ModelSurfaceLinks();
  		linkPositions=surfaceLinks.drawLinks(g,20,infoPositions[2]+60);
  
  		/* Font-Einstellungen zurcksetzen */
  
  		g.setFont(saveFont);
  	}
  
  	@Override
  	public void paint(Graphics g) {
  		/* Kein Surface als Datenquelle eingetragen */
  		if (surface==null) {super.paint(g); return;}
  
  		/* Gre des Elements selbst einstellen */
  		final int surfaceSizeX=(int)Math.round(SIZE*zoom);
  		final int surfaceSizeY=(int)Math.round(SIZE*zoom);
  		final Dimension currentSize=getPreferredSize();
  		if (currentSize.width!=surfaceSizeX || currentSize.height!=surfaceSizeY) setPreferredSize(new Dimension(surfaceSizeX,surfaceSizeY));
  
  		/* Bestimmung des tatschlich dargestellten Ausschnitts */
  		final Rectangle viewArea;
  		if (getParent() instanceof JViewport) {
  			final JViewport viewport=(JViewport)getParent();
  			viewArea=viewport.getViewRect();
  		} else {
  			viewArea=new Rectangle(getSize());
  		}
  
  		/* Elemente zeichnen */
  		paintElements(g,viewArea,backgroundImageMode,true,raster,colors,(backgroundImageMode!=ModelSurface.BackgroundImageMode.OFF)?backgroundImage:null,backgroundImageHash,backgroundImageScale,true);
  
  		/* Ggf. Hinweis zu Steuerelementen einzeichnen */
  		if (viewArea.x==0 && viewArea.y==0 && surface.count()==0 && useInfoPaint) {
  			paintInfo(g,viewArea);
  		} else {
  			linkPositions=null;
  		}
  
  		/* Selektionsrahmen zeichnen */
  		if (selectBox!=null) {
  			g.setClip(viewArea.x,viewArea.y,viewArea.width,viewArea.height);
  			g.setColor(new Color(0,0,255,12));
  			g.fillRect(selectBox.x,selectBox.y,selectBox.width,selectBox.height);
  			g.setColor(Color.BLUE);
  			g.drawRect(selectBox.x,selectBox.y,selectBox.width,selectBox.height);
  		}
  
  		if (additionalUserPaint!=null) {
  			g.setClip(viewArea.x,viewArea.y,viewArea.width,viewArea.height);
  			additionalUserPaint.paint(g,zoom);
  		}
  
  		checkIfModelChanged(false);
  	}
  
  	/**
  	 * Liefert die Zeichenflche als Bilddatei.<br>
  	 * Das Bild wird unter Beibehaltung des Seitenverhltnisses
  	 * auf die angegebene Gre skaliert.
  	 * @param xSize	Gre in x-Richtung
  	 * @param ySize	Gre in y-Richtung
  	 * @return	Bild-Objekt
  	 */
  	public BufferedImage getImage(final int xSize, final int ySize) {
  		return getImageMaxFactor(xSize,ySize,-1,false);
  	}
  
  	/**
  	 * Liefert die Zeichenflche als Bilddatei.<br>
  	 * Das Bild wird unter Beibehaltung des Seitenverhltnisses
  	 * auf die angegebene Gre skaliert.
  	 * @param xSize	Gre in x-Richtung
  	 * @param ySize	Gre in y-Richtung
  	 * @return	Bild-Objekt
  	 */
  	public BufferedImage getImageWithBackground(final int xSize, final int ySize) {
  		return getImageMaxFactor(xSize,ySize,-1,true);
  	}
  
  	/**
  	 * Liefert die Zeichenflche als Bilddatei.<br>
  	 * Das Bild wird nach Mglichkeit in Originalgre ausgegeben
  	 * und ntigenfalls unter Beibehaltung des Seitenverhltnisses
  	 * auf die angegebene Gre verkleinert.
  	 * @param xSizeMax	Gre in x-Richtung
  	 * @param ySizeMax	Gre in y-Richtung
  	 * @return	Bild-Objekt
  	 */
  	public BufferedImage getImageMaxSize(final int xSizeMax, final int ySizeMax) {
  		return getImageMaxFactor(xSizeMax,ySizeMax,1,false);
  	}
  
  	/**
  	 * Liefert die Zeichenflche als Bilddatei.<br>
  	 * Das Bild wird unter Beibehaltung des Seitenverhltnisses
  	 * auf die angegebene Gre skaliert. Beim Skalieren nach oben
  	 * wird jedoch der angegebene maximale Zoomfaktor nicht berschritten.
  	 * @param xSize	Gre in x-Richtung (werden fr die x- und die y-Gre Werte kleiner oder gleich 0 angegeben, so wird die tatschliche Gre verwendet)
  	 * @param ySize	Gre in y-Richtung (werden fr die x- und die y-Gre Werte kleiner oder gleich 0 angegeben, so wird die tatschliche Gre verwendet)
  	 * @param maxZoomFactor	Maximaler Zoomfaktor (oder &lt;0, wenn keine Begrenzung nach oben erfolgen soll)
  	 * @param withBackground	Soll der Hintergrund in dem Bild dargestellt werden?
  	 * @return	Bild-Objekt
  	 */
  	public BufferedImage getImageMaxFactor(int xSize, int ySize, final double maxZoomFactor, final boolean withBackground) {
  		final Point p1=surface.getUpperLeftModelCorner();
  		final Point p2=surface.getLowerRightModelCorner();
  
  		p1.x=Math.max(0,p1.x-2);
  		p1.y=Math.max(0,p1.y-2);
  		p2.x++;
  		p2.y++;
  
  		final int xDraw=p2.x-p1.x;
  		final int yDraw=p2.y-p1.y;
  
  		if (xSize<=0 && ySize<=0) {
  			xSize=xDraw;
  			ySize=yDraw;
  		}
  
  		double imageZoom=(xDraw==1 && yDraw==1)?1:Math.min(xSize/((double)xDraw),ySize/((double)yDraw));
  		if (maxZoomFactor>0 && imageZoom>maxZoomFactor) imageZoom=maxZoomFactor;
  
  		final int xSurfaceImage=(int)Math.ceil(p2.x*imageZoom);
  		int ySurfaceImage=(int)Math.ceil(p2.y*imageZoom);
  
  		final BufferedImage preImage=new BufferedImage(10,10,BufferedImage.TYPE_4BYTE_ABGR);
  		final Graphics2D preGraphics=(Graphics2D)preImage.getGraphics();
  		final int infoFontSize=(int)FastMath.round(10*imageZoom);
  		final Font infoFont=new Font(ModelElementBox.DEFAULT_FONT_TYPE,Font.PLAIN,infoFontSize);
  		final FontMetrics infoFontMetrics=preGraphics.getFontMetrics(infoFont);
  		double totalSize=infoFontSize*(infoFontMetrics.getAscent()+infoFontMetrics.getDescent())/infoFontMetrics.getAscent();
  
  		if (ySurfaceImage>1) ySurfaceImage+=totalSize;
  
  		final BufferedImage image=new BufferedImage(xSurfaceImage,ySurfaceImage,BufferedImage.TYPE_4BYTE_ABGR);
  
  		final Graphics2D g=(Graphics2D)image.getGraphics();
  
  		if (SetupData.getSetup().useTransparencyExportFix) {
  			g.setColor(new Color(255,255,255,1)); /* Sonst zeigt die Windows-Bildanzeige (flschlich) einen schwarzen Hintergrund an. */
  		}
  		g.fillRect(0,0,xSurfaceImage+1,ySurfaceImage+1);
  		g.setClip(0,0,xSurfaceImage,ySurfaceImage);
  		g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT);
  
  		surface.drawToGraphics(g,new Rectangle(0,0,xSurfaceImage,ySurfaceImage),imageZoom,withBackground,backgroundImageMode,true,ModelSurface.Grid.OFF,null,backgroundImage,backgroundImageHash,backgroundImageScale,false);
  		if (additionalUserPaint!=null) {
  			g.setClip(new Rectangle(0,0,xSurfaceImage,ySurfaceImage));
  			additionalUserPaint.paint(g,imageZoom);
  		}
  
  		final int xRemove=(int)Math.round(p1.x*imageZoom);
  		final int yRemove=(int)Math.round(p1.y*imageZoom);
  
  		if (xRemove>0 || yRemove>0) {
  			return image.getSubimage(xRemove,yRemove,xSurfaceImage-xRemove,ySurfaceImage-yRemove);
  		} else {
  			return image;
  		}
  	}
  
  	/**
  	 * Liefert die Zeichenflche als Bilddatei.<br>
  	 * Das Bild wird unter Beibehaltung des Seitenverhltnisses
  	 * ggf. auf die angegebene Gre hochskaliert.
  	 * @param xSize	Gre in x-Richtung (werden fr die x- und die y-Gre Werte kleiner oder gleich 0 angegeben, so wird die tatschliche Gre verwendet)
  	 * @param ySize	Gre in y-Richtung (werden fr die x- und die y-Gre Werte kleiner oder gleich 0 angegeben, so wird die tatschliche Gre verwendet)
  	 * @return	Bild-Objekt
  	 */
  	public BufferedImage getImageMinSize(int xSize, int ySize) {
  		final int MAX_IMAGE_BIGGER_FACTOR=10; /* Eingestellte Exportgre maximal um diesen Faktor berschreiten */
  		final int MAX_IMAGE_SIZE=15_000; /* Maximale Gre */
  
  		final Point p1=surface.getUpperLeftModelCorner();
  		final Point p2=surface.getLowerRightModelCorner();
  
  		p1.x=Math.max(0,p1.x-2);
  		p1.y=Math.max(0,p1.y-2);
  		p2.x++;
  		p2.y++;
  
  		final int xDraw=p2.x-p1.x;
  		final int yDraw=p2.y-p1.y;
  
  		if (xSize<=0 && ySize<=0) {
  			xSize=xDraw;
  			ySize=yDraw;
  		}
  
  		double imageZoom=Math.max(xSize/((double)xDraw),ySize/((double)yDraw));
  
  		int xSurfaceImage=(int)Math.ceil(p2.x*imageZoom);
  		int ySurfaceImage=(int)Math.ceil(p2.y*imageZoom);
  
  		if (xSurfaceImage>xSize*MAX_IMAGE_BIGGER_FACTOR) {
  			ySurfaceImage=ySurfaceImage*xSize*MAX_IMAGE_BIGGER_FACTOR/xSurfaceImage;
  			xSurfaceImage=xSize*MAX_IMAGE_BIGGER_FACTOR;
  			imageZoom=((double)xSurfaceImage)/p2.x;
  		}
  
  		if (ySurfaceImage>ySize*MAX_IMAGE_BIGGER_FACTOR) {
  			xSurfaceImage=xSurfaceImage*ySize*MAX_IMAGE_BIGGER_FACTOR/ySurfaceImage;
  			ySurfaceImage=ySize*MAX_IMAGE_BIGGER_FACTOR;
  			imageZoom=((double)ySurfaceImage)/p2.y;
  		}
  
  		if (xSurfaceImage>MAX_IMAGE_SIZE) {
  			ySurfaceImage=ySurfaceImage*MAX_IMAGE_SIZE/xSurfaceImage;
  			xSurfaceImage=MAX_IMAGE_SIZE;
  			imageZoom=((double)xSurfaceImage)/p2.x;
  		}
  
  		if (ySurfaceImage>MAX_IMAGE_SIZE) {
  			xSurfaceImage=xSurfaceImage*MAX_IMAGE_SIZE/ySurfaceImage;
  			ySurfaceImage=MAX_IMAGE_SIZE;
  			imageZoom=((double)ySurfaceImage)/p2.y;
  		}
  
  		final BufferedImage preImage=new BufferedImage(100,100,BufferedImage.TYPE_INT_ARGB);
  		final Graphics2D preGraphics=(Graphics2D)preImage.getGraphics();
  		final int infoFontSize=(int)FastMath.round(10*imageZoom);
  		final Font infoFont=new Font(ModelElementBox.DEFAULT_FONT_TYPE,Font.PLAIN,infoFontSize);
  		final FontMetrics infoFontMetrics=preGraphics.getFontMetrics(infoFont);
  		double totalSize=infoFontSize*(infoFontMetrics.getAscent()+infoFontMetrics.getDescent())/infoFontMetrics.getAscent();
  
  		ySurfaceImage+=totalSize;
  
  		final BufferedImage image=new BufferedImage(xSurfaceImage,ySurfaceImage,BufferedImage.TYPE_INT_ARGB);
  		Graphics g=image.getGraphics();
  		g.setClip(0,0,xSurfaceImage,ySurfaceImage);
  		((Graphics2D)g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT);
  		surface.drawToGraphics(g,new Rectangle(0,0,xSurfaceImage,ySurfaceImage),imageZoom,false,backgroundImageMode,true,ModelSurface.Grid.OFF,null,backgroundImage,backgroundImageHash,backgroundImageScale,false);
  		if (additionalUserPaint!=null) {
  			g.setClip(new Rectangle(0,0,xSurfaceImage,ySurfaceImage));
  			additionalUserPaint.paint(g,imageZoom);
  		}
  
  		final int xRemove=(int)Math.round(p1.x*imageZoom);
  		final int yRemove=(int)Math.round(p1.y*imageZoom);
  
  		return image.getSubimage(xRemove,yRemove,xSurfaceImage-xRemove,ySurfaceImage-yRemove);
  	}
  
  	/**
  	 * Speichert die Zeichenflche als svg-Datei
  	 * @param file	Dateiname, unter der die svg gespeichert werden soll
  	 * @return	Gibt <code>true</code> zurck, wenn das Bild erfolgreich gespeichert werden konnte.
  	 */
  	public boolean saveSVGToFile(final File file) {
  		int minX=Integer.MAX_VALUE;
  		int minY=Integer.MAX_VALUE;
  		int maxX=0;
  		int maxY=0;
  		for (ModelElement element: surface.getElements()) {
  			final Point p1=element.getPosition(true);
  			final Point p2=element.getLowerRightPosition();
  			if (p1!=null) {minX=Math.min(minX,p1.x); minY=Math.min(minY,p1.y);}
  			if (p2!=null) {maxX=Math.max(maxX,p2.x); maxY=Math.max(maxY,p2.y);}
  		}
  
  		final DOMImplementation domImpl=GenericDOMImplementation.getDOMImplementation();
  		final Document document=domImpl.createDocument("http://www.w3.org/2000/svg","svg",null);
  		final SVGGraphics2D svgGenerator=new SVGGraphics2D(document);
  		svgGenerator.setSVGCanvasSize(new Dimension(maxX+minX,maxY+minY));
  
  		final Rectangle area=new Rectangle(0,0,maxX*10,maxY*10);
  		paintElements(svgGenerator,area,ModelSurface.BackgroundImageMode.OFF,false,ModelSurface.Grid.OFF,null,null,null,1.0,false);
  
  		try (FileOutputStream fileWriter=new FileOutputStream(file)) {
  			final Writer out=new OutputStreamWriter(fileWriter,StandardCharsets.UTF_8);
  			svgGenerator.stream(out,true);
  		} catch (IOException e) {
  			return false;
  		}
  
  		return true;
  	}
  
  	/**
  	 * Speichert die Zeichenflche als eps-Datei
  	 * @param file	Dateiname, unter der die eps gespeichert werden soll
  	 * @return	Gibt <code>true</code> zurck, wenn das Bild erfolgreich gespeichert werden konnte.
  	 */
  	public boolean saveEPSToFile(final File file) {
  		int minX=Integer.MAX_VALUE;
  		int minY=Integer.MAX_VALUE;
  		int maxX=0;
  		int maxY=0;
  		for (ModelElement element: surface.getElements()) {
  			final Point p1=element.getPosition(true);
  			final Point p2=element.getLowerRightPosition();
  			if (p1!=null) {minX=Math.min(minX,p1.x); minY=Math.min(minY,p1.y);}
  			if (p2!=null) {maxX=Math.max(maxX,p2.x); maxY=Math.max(maxY,p2.y);}
  		}
  
  		final Graphics2D vg2d=new VectorGraphics2D() {
  			@Override
  			public void setColor(final Color color) {
  				if (color!=null) setPaint(new GradientPaint(new Point(0,0),color,new Point(0,0),color));
  				super.setColor(color);
  			}
  		};
  
  		final Rectangle area=new Rectangle(0,0,maxX*10,maxY*10);
  		paintElements(vg2d,area,ModelSurface.BackgroundImageMode.OFF,false,ModelSurface.Grid.OFF,null,null,null,1.0,false);
  
  		final CommandSequence commands=((VectorGraphics2D)vg2d).getCommands();
  		final EPSProcessor epsProcessor=new EPSProcessor();
  		final de.erichseifert.vectorgraphics2d.Document doc=epsProcessor.getDocument(commands,new PageSize(maxX,maxY));
  		try (FileOutputStream stream=new FileOutputStream(file)) {
  			doc.writeTo(stream);
  		} catch (IOException e) {
  			return false;
  		}
  
  		return true;
  	}
  
  	/**
  	 * Fgt bei einer DOCX-Ausgabe ein Bild des Modells in die Ausgabe ein.
  	 * @param docx	DOCX-Ausgabe-Dokument
  	 * @param xSize	Gre des Bildes in x-Richtung
  	 * @param ySize	Gre des Bildes in y-Richtung
  	 * @return	Liefert im Erfolgsfall <code>true</code>
  	 */
  	private boolean addImageToDOCX(final XWPFDocument docx, final int xSize, final int ySize) {
  		final BufferedImage image=getImage(xSize,ySize);
  		try (ByteArrayOutputStream streamOut=new ByteArrayOutputStream()) {
  			try {if (!ImageIO.write(image,"png",streamOut)) return false;} catch (IOException e) {return false;}
  			if (!XWPFDocumentPictureTools.addPicture(docx,streamOut,org.apache.poi.xwpf.usermodel.Document.PICTURE_TYPE_PNG,image.getWidth(),image.getHeight())) return false;
  		} catch (IOException e) {return false;}
  		return true;
  	}
  
  	/**
  	 * Fgt bei einer DOCX-Ausgabe eine berschrift in die Ausgabe ein.
  	 * @param docx	DOCX-Ausgabe-Dokument
  	 * @param line	berschriftzeilen
  	 */
  	private void addHeadingToDOCX(final XWPFDocument docx, final String line) {
  		final XWPFParagraph p=docx.createParagraph();
  		final XWPFRun r=p.createRun();
  		r.setBold(true);
  		r.setFontSize(24);
  		r.setText(line);
  	}
  
  	/**
  	 * Speichert die Zeichenflche als Bild in einer docx-Datei
  	 * @param file	Dateiname, unter der die eps gespeichert werden soll
  	 * @param xSize	Gre des eingebetteten Bilder in horizontaler Richtung
  	 * @param ySize	Gre des eingebetteten Bilder in vertikaler Richtung
  	 * @param addDescription	Soll nur das Bild ausgegeben werden (<code>false</code>) oder sollen auch Modellbeschreibung usw. ausgegeben werden (<code>true</code>)?
  	 * @return	Gibt <code>true</code> zurck, wenn das Bild erfolgreich gespeichert werden konnte.
  	 */
  	public boolean saveDOCXToFile(final File file, final int xSize, final int ySize, final boolean addDescription) {
  		try(XWPFDocument docx=new XWPFDocument()) {
  
  			if (addDescription) {
  				/* berschrift */
  				final String name;
  				if (model.name.isBlank()) name=Language.tr("SlidesGenerator.Modell"); else name=model.name;
  				addHeadingToDOCX(docx,name+"\n"+MainFrame.PROGRAM_NAME+" "+Language.tr("SlidesGenerator.ModelType"));
  			}
  
  			/* Bild */
  			if (!addImageToDOCX(docx,xSize,ySize)) return false;
  
  			if (addDescription) {
  				/* Beschreibung */
  				if (model.description!=null && !model.description.isBlank()) {
  					final List<String> lines=new ArrayList<>(Arrays.asList(model.description.split("\\n")));
  					final String[] content=lines.stream().filter(line->!line.isBlank()).toArray(String[]::new);
  					if (content.length>0) {
  						addHeadingToDOCX(docx,Language.tr("SlidesGenerator.ModellDescription"));
  						final XWPFParagraph p=docx.createParagraph();
  						for (String line: content) {
  							p.createRun().setText(line);
  							p.createRun().addBreak();
  						}
  					}
  				}
  
  				/* Parameter */
  				addHeadingToDOCX(docx,Language.tr("SlidesGenerator.ModellParameters"));
  				final ModelDescriptionBuilderStyled description=new ModelDescriptionBuilderStyled(model);
  				description.run();
  				description.saveDOCX(docx);
  			}
  
  			try (FileOutputStream out=new FileOutputStream(file)) {docx.write(out);}
  			return true;
  		} catch (IOException e) {return false;}
  	}
  
  	/**
  	 * Speichert die Zeichenflche als Bilddatei, svg- oder pdf-Datei
  	 * @param file	Dateiname, unter der das Bild bzw. die pdf bzw. die svg-Datei gespeichert werden soll
  	 * @param format	Dateiformat
  	 * @param xSize	Gre des Bildes in x-Richtung
  	 * @param ySize	Gre des Bildes in y-Richtung
  	 * @return	Gibt <code>true</code> zurck, wenn das Bild erfolgreich gespeichert werden konnte.
  	 */
  	public boolean saveImageToFile(final File file, String format, final int xSize, final int ySize) {
  		return saveImageToFile(file,format,xSize,ySize,false);
  	}
  
  	/**
  	 * Hintergrund-Thread zum Speichern von Bildern
  	 * (muss eine globale Variable sein, da es sonst nicht erfllbare Warnungen zum AutoClose gibt)
  	 * @see #saveImageToFile(File, String, int, int, boolean)
  	 */
  	private ThreadPoolExecutor executor;
  
  	/**
  	 * Speichert die Zeichenflche als Bilddatei, svg- oder pdf-Datei
  	 * @param file	Dateiname, unter der das Bild bzw. die pdf bzw. die svg-Datei gespeichert werden soll
  	 * @param format	Dateiformat
  	 * @param xSize	Gre des Bildes in x-Richtung
  	 * @param ySize	Gre des Bildes in y-Richtung
  	 * @param forceWithBackground	Fgt, wenn hier <code>true</code> bergeben wird, beim Export als Bilddatei einen Hintergrund ein.
  	 * @return	Gibt <code>true</code> zurck, wenn das Bild erfolgreich gespeichert werden konnte.
  	 */
  	public boolean saveImageToFile(final File file, String format, final int xSize, final int ySize, final boolean forceWithBackground) {
  		final int PDF_IMAGE_SIZE=2_000;
  		final int MAX_IMAGE_BIGGER_FACTOR=10; /* Eingestellte Exportgre maximal um diesen Faktor berschreiten */
  		final int MAX_IMAGE_SIZE=10_000; /* Maximale Gre */
  
  		if (format.equalsIgnoreCase("pdf")) {
  			return ImageTools.saveImage(this,getImage(PDF_IMAGE_SIZE,PDF_IMAGE_SIZE),file);
  		}
  
  		if (format.equalsIgnoreCase("svg")) {
  			return saveSVGToFile(file);
  		}
  
  		if (format.equalsIgnoreCase("eps")) {
  			return saveEPSToFile(file);
  		}
  
  		if (format.equalsIgnoreCase("docx")) {
  			return saveDOCXToFile(file,xSize,ySize,false);
  		}
  
  		/* gif, jpeg, png, bmp */
  		final int maxXSize=Math.min(MAX_IMAGE_SIZE,MAX_IMAGE_BIGGER_FACTOR*xSize);
  		final int maxYSize=Math.min(MAX_IMAGE_SIZE,MAX_IMAGE_BIGGER_FACTOR*ySize);
  		BufferedImage image=getImageMaxFactor(maxXSize,maxYSize,10,forceWithBackground);
  		if (format.equalsIgnoreCase("jpg")) format="jpeg";
  
  		final String imageFormat=format.toLowerCase();
  
  		executor=new ThreadPoolExecutor(0,1,10,TimeUnit.SECONDS,new LinkedBlockingQueue<>(),new ThreadFactory() {
  			@Override
  			public Thread newThread(Runnable r) {
  				return new Thread(r,"BackgroundImageSaver");
  			}
  		});
  		executor.allowCoreThreadTimeOut(true);
  		executor.submit(()->{
  			if (imageFormat.equals("jpeg") || imageFormat.equals("bmp")) {
  				final BufferedImage image2=new BufferedImage(image.getWidth(),image.getHeight(),BufferedImage.TYPE_INT_RGB);
  				final Graphics2D g2=image2.createGraphics();
  				g2.setBackground(Color.WHITE);
  				g2.clearRect(0,0,image.getWidth(),image.getHeight());
  				g2.drawImage(image,0,0,null);
  				g2.dispose();
  				try {
  					ImageIO.write(image2,imageFormat,file);
  				} catch (IOException e) {}
  				return;
  			}
  
  			try {
  				ImageIO.write(image,imageFormat,file);
  			} catch (IOException e) {}
  		});
  		return true;
  	}
  
  	/**
  	 * Erzeugt einen Timer, der dafr sorgt, dass auch dann Tooltips fr die Zeichenflche
  	 * angezeigt werden knnen, wenn diese durch ein anderes modales Fenster blockiert ist.
  	 * @return	Timer zur Prfung und Anzeige von Tooltips
  	 */
  	private Timer buildTooltipTimer() {
  		final Window ownWindow=SwingUtilities.getWindowAncestor(ModelSurfacePanel.this);
  		final Timer timer=new Timer("BackgroundTooltipUpdater");
  		timer.scheduleAtFixedRate(new TimerTask() {
  			private int lastX;
  			private int lastY;
  			@Override
  			public void run() {
  				final Point pScreen=MouseInfo.getPointerInfo().getLocation();
  				if (lastX==pScreen.x && lastY==pScreen.y) return; /* Fr Tooltips muss die Maus ruhig stehen, daher keine unntigen Events senden. */
  				lastX=pScreen.x;
  				lastY=pScreen.y;
  				final Point pLocal=new Point(pScreen);
  				SwingUtilities.convertPointFromScreen(pLocal,ModelSurfacePanel.this);
  
  				/* Keinen Tooltip anzeigen, wenn anderen Fenster an der aktuellen Mausposition ist */
  				for (Window wnd: ownWindow.getOwnedWindows()) {
  					if (!(wnd instanceof JDialog)) continue;
  					if (!wnd.isDisplayable()) continue;
  					if (!wnd.isVisible()) continue;
  					final Point location=wnd.getLocationOnScreen();
  					final Dimension size=wnd.getSize();
  					if (pScreen.x>=location.x && pScreen.x<=location.x+size.width && pScreen.y>=location.y && pScreen.y<=location.y+size.height) return; /* Mauszeiger in anderem Fenster */
  				}
  
  				/* Mausbewegung an eigene Ereignisbehandlung durchreichen */
  				final MouseEvent event=new MouseEvent(ModelSurfacePanel.this,MouseEvent.MOUSE_MOVED,System.currentTimeMillis(),0,pLocal.x,pLocal.y,pScreen.x,pScreen.y,0,false,0);
  				ModelSurfacePanel.this.dispatchEvent(event);
  			}
  		},500,100);
  		return timer;
  	}
  
  	/**
  	 * Fokus-Listener, der beim Aufruf eines Element-Bearbeiten-Dialogs
  	 * aktiviert und danach wieder deaktiviert wird. Auf diese Weise
  	 * knnen der Tooltip-Timer deaktiviert und nach dem Dialog notwendige
  	 * Updates durchgefhrt werden.
  	 * @see #showElementProperties(ModelElement, int)
  	 */
  	private FocusListener focusListener;
  
  	/**
  	 * Ruft den Modelleigenschaften-Dialog einer Station auf.
  	 * @param element	Aktuelle Station
  	 * @param modifieres	Gedrckte Umschalt-Tasten
  	 */
  	public void showElementProperties(final ModelElement element, final int modifieres) {
  		if (element==null) return;
  		surface.setSelectedArea(null,zoom);
  		surface.setSelectedElement(element);
  		fireSelectionListener();
  		dragElement=null;
  		dragCopyElement=null;
  		dragStartElementPosition=null;
  		element.setModel(model);
  
  		final Timer timer=SetupData.getSetup().showBackgroundTooltips?buildTooltipTimer():null;
  
  		focusListener=new FocusAdapter() {
  			@Override
  			public void focusGained(FocusEvent e) {
  				fireStateChangeListener(); /* Die Aufrufe unten funktionieren nicht, da die Methode sofort zurckkehrt und die Dialog erst per invokeLater sichtbar werden. */
  				removeFocusListener(focusListener);
  				if (timer!=null) timer.cancel();
  			}
  		};
  		addFocusListener(focusListener);
  
  		/* Simulationsdaten */
  		if ((modifieres & InputEvent.CTRL_DOWN_MASK)!=0 && (modifieres & InputEvent.SHIFT_DOWN_MASK)!=0 && (modifieres & InputEvent.ALT_DOWN_MASK)==0) {
  			if (element.hasAnimationStatisticsData(simData)) {
  				element.showElementAnimationStatisticsData(ModelSurfacePanel.this,simData);
  				fireStateChangeListener();
  				return;
  			}
  		}
  
  		/* Untermodell bearbeiten */
  		if ((modifieres & InputEvent.CTRL_DOWN_MASK)==0 && (modifieres & InputEvent.SHIFT_DOWN_MASK)!=0 && (modifieres & InputEvent.ALT_DOWN_MASK)==0) {
  			showElementSubEditor(surface.getSelectedElement());
  			fireStateChangeListener();
  			return;
  		}
  
  		/* Elementeigenschaften whrend angehaltener Animation bearbeiten */
  		if (simData!=null && mainAnimator!=null && simData.simulator.isPaused()) {
  			final Runnable propertiesSemiEditableRunner=element.getPropertiesSemiEditable(this,clientData,sequences);
  			if (propertiesSemiEditableRunner!=null) {
  				propertiesSemiEditableRunner.run();
  				return;
  			}
  		}
  
  		/* Elementeigenschaften bearbeiten */
  		final Runnable propertiesRunner=element.getProperties(this,readOnly && !allowChangeOperationsOnReadOnly,clientData,sequences);
  		if (propertiesRunner!=null) propertiesRunner.run();
  	}
  
  	/**
  	 * Reinitialisiert die Animationsdaten in einem Element nach dem der Bearbeitendialog
  	 * whrend einer angehaltenen Animation aufgerufen wurde.<br>
  	 * Diese Methode sollte nur von {@link ModelElementBaseDialog} aus aufgerufen werden.
  	 * @param element	Element fr das die Animationsdaten reinitialisiert werden sollen.
  	 * @see ModelElementBaseDialog#storeData()
  	 */
  	public void updateElementAnimationSystem(final ElementWithAnimationDisplay element) {
  		if (simData==null) return;
  		element.initAnimation(simData);
  	}
  
  	/**
  	 * Ruft den Dialog zum Bearbeiten eines Untermodells auf.
  	 * @param element	Aktuelle Station
  	 */
  	private void showElementSubEditor(final ModelElement element) {
  		if (!(element instanceof ModelElementSub)) return;
  		surface.setSelectedArea(null,zoom);
  		surface.setSelectedElement(element);
  		fireSelectionListener();
  		dragElement=null;
  		dragCopyElement=null;
  		dragStartElementPosition=null;
  		((ModelElementSub)element).showSubEditDialog(this,readOnly && !allowChangeOperationsOnReadOnly,false);
  		fireStateChangeListener();
  	}
  
  	/**
  	 * Kopiert die ausgewhlten Elemente in die Zwischenablage.
  	 */
  	public void copyToClipboard() {
  		final Clipboard clipboard=Toolkit.getDefaultToolkit().getSystemClipboard();
  		final ModelSurfaceTransferable transferable=new ModelSurfaceTransferable(surface.getTransferData(),surface.getTransferTxt(),surface.getTransferRtf(),surface.getTransferImage());
  		clipboard.setContents(transferable,null);
  	}
  
  	/**
  	 * Kopiert das gesamte Modell als Bild in die Zwischenablage
  	 * (ggf. mit aktuellen Animationsdaten).
  	 * @param xSize	Gre des Bildes in x-Richtung
  	 * @param ySize	Gre des Bildes in y-Richtung
  	 */
  	public void copyToClipboardAsImage(final int xSize, final int ySize) {
  		final int MAX_IMAGE_SIZE=10_000; /* Maximale Gre */
  
  		final int maxXSize=Math.min(MAX_IMAGE_SIZE,xSize);
  		final int maxYSize=Math.min(MAX_IMAGE_SIZE,ySize);
  		final BufferedImage image=getImageMaxFactor(maxXSize,maxYSize,10,false);
  
  		ImageTools.copyImageToClipboard(image);
  	}
  
  	/**
  	 * Fgt die in der Zwischenablage befindlichen Elemente in die Zeichnung ein.
  	 * Befinden sich keine Elemente in der Zwischenablage, dafr aber Text, so wird dieser
  	 * als neues Text-Element eingefgt.
  	 * @return	Gibt <code>true</code> zurck, wenn Elemente oder Text erfolgreich eingefgt werden konnten.
  	 */
  	public boolean pasteFromClipboard() {
  		if (readOnly) return false;
  		final Clipboard clipboard=Toolkit.getDefaultToolkit().getSystemClipboard();
  		final Transferable transferable=clipboard.getContents(this);
  		if (transferable==null) return false;
  
  		/* Elemente einfgen */
  		if (transferable.isDataFlavorSupported(ModelSurfaceTransferable.MODEL_FLAVOR)) {
  			Object obj=null;
  			try {obj=transferable.getTransferData(ModelSurfaceTransferable.MODEL_FLAVOR);} catch (UnsupportedFlavorException | IOException e) {obj=null;}
  			if (obj instanceof ModelSurfaceTransferable) {
  				clipboardData=((ModelSurfaceTransferable)obj).getStream();
  				if (clipboardData==null) return false;
  				setMode(ClickMode.MODE_INSERT_ELEMENTS_FROM_CLIPBOARD);
  				setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
  				return true;
  			}
  		}
  
  		/* Text als neues Element einfgen */
  		if (transferable.isDataFlavorSupported(DataFlavor.stringFlavor)) {
  			Object text=null;
  			try {text=transferable.getTransferData(DataFlavor.stringFlavor);} catch (UnsupportedFlavorException | IOException e) {text=null;}
  			if (text instanceof String) {
  				clipboardText=(String)text;
  				if (clipboardText.length()>1024) clipboardText=clipboardText.substring(0,1024);
  				setMode(ClickMode.MODE_INSERT_TEXT);
  				setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
  				return true;
  			}
  		}
  
  		if (transferable.isDataFlavorSupported(DataFlavor.imageFlavor)) {
  			Object image=null;
  			try {image=transferable.getTransferData(DataFlavor.imageFlavor);} catch (UnsupportedFlavorException | IOException e) {image=null;}
  			if (image instanceof BufferedImage) {
  				clipboardImage=(BufferedImage)image;
  				setMode(ClickMode.MODE_INSERT_IMAGE);
  				setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
  			}
  		}
  
  		if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
  			Object list=null;
  			try {list=transferable.getTransferData(DataFlavor.javaFileListFlavor);} catch (UnsupportedFlavorException | IOException e) {list=null;}
  			if (list instanceof List) for (Object entry: (List<?>)list) if (entry instanceof File) {
  				dropFile((File)entry,null);
  				break;
  			}
  		}
  
  		return false;
  	}
  
  	/**
  	 * Fgt die als Stream vorliegenden Elemente in die Zeichnung ein.
  	 * @param data	Einzufgende Elemente
  	 */
  	public void pasteDirect(final ByteArrayInputStream data) {
  		if (readOnly || data==null) return;
  		clipboardData=data;
  		setMode(ClickMode.MODE_INSERT_ELEMENTS_FROM_TEMPLATE);
  		setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
  	}
  
  	/**
  	 * Fgt ein Bild als Element in die Zeichnung ein.
  	 * @param image	Einzufgendes Bild
  	 */
  	public void pasteImage(final BufferedImage image) {
  		if (readOnly || image==null) return;
  		clipboardImage=image;
  		setMode(ClickMode.MODE_INSERT_IMAGE);
  		setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
  	}
  
  	/**
  	 * Prft, ob zwischen zwei Elementen eine Verbindungkanten eingefgt werden darf.
  	 * @param element1	Mgliches Ausgangelement
  	 * @param element2	Mgliches Zielelement
  	 * @return	Liefert <code>true</code>, wenn zwischen den Elementen eine Verbindungskante eingefgt werden darf
  	 */
  	private boolean canAddEdge(final ModelElement element1, final ModelElement element2) {
  		if (element1==null || element2==null) return false;
  		if (element1==element2) return false;
  
  		if (!(element1 instanceof ModelElementPosition)) return false;
  		if (!(element2 instanceof ModelElementPosition)) return false;
  
  		final ModelElementPosition position1=(ModelElementPosition)element1;
  		final ModelElementPosition position2=(ModelElementPosition)element2;
  
  		if (!position1.canAddEdgeOut()) return false;
  		if (!position2.canAddEdgeIn()) return false;
  
  		for (ModelElement element: surface.getElements()) if (element instanceof ModelElementEdge) {
  			final ModelElementEdge edge=(ModelElementEdge)element;
  			if (edge.getConnectionStart()==position1 && edge.getConnectionEnd()==position2) return false;
  			if (edge.getConnectionStart()==position2 && edge.getConnectionEnd()==position1) return false;
  		}
  
  		return true;
  	}
  
  	/**
  	 * Listener, die benachrichtigt werden sollen, wenn die Modelleigenschaften aufgerufen werden sollen
  	 * @see #fireShowPropertiesDialog(String)
  	 */
  	private final List<ActionListener> showModelPropertiesListeners=new ArrayList<>();
  
  	/**
  	 * Es soll der Modelleigenschaftendialog angezeigt werden (wird als {@link ActionEvent#getActionCommand()} bergeben).
  	 * @see #addShowModelPropertiesListener(ActionListener)
  	 * @see #fireShowPropertiesDialog(String)
  	 */
  	public static String PROPERTIES_TYPE_PROPERTIES="showModelProperties";
  
  	/**
  	 * Es soll der Modelleigenschaftendialog mit der Ressourcen-Seite angezeigt werden (wird als {@link ActionEvent#getActionCommand()} bergeben).
  	 * @see #addShowModelPropertiesListener(ActionListener)
  	 * @see #fireShowPropertiesDialog(String)
  	 */
  	public static String PROPERTIES_TYPE_PROPERTIES_OPERATORS="showModelPropertiesResources";
  
  	/**
  	 * Es soll der Modelleigenschaftendialog mit der Transporter-Seite angezeigt werden (wird als {@link ActionEvent#getActionCommand()} bergeben).
  	 * @see #addShowModelPropertiesListener(ActionListener)
  	 * @see #fireShowPropertiesDialog(String)
  	 */
  	public static String PROPERTIES_TYPE_PROPERTIES_TRANSPORTERS="showModelPropertiesTransporters";
  
  	/**
  	 * Es soll der Modelleigenschaftendialog mit der Zeitplne-Seite angezeigt werden (wird als {@link ActionEvent#getActionCommand()} bergeben).
  	 * @see #addShowModelPropertiesListener(ActionListener)
  	 * @see #fireShowPropertiesDialog(String)
  	 */
  	public static String PROPERTIES_TYPE_PROPERTIES_SCHEDULES="showModelPropertiesSchedules";
  
  	/**
  	 * Es soll der Dialog zur Konfiguration der Hintergrundfarbe angezeigt werden (wird als {@link ActionEvent#getActionCommand()} bergeben).
  	 * @see #addShowModelPropertiesListener(ActionListener)
  	 * @see #fireShowPropertiesDialog(String)
  	 */
  	public static String PROPERTIES_TYPE_BACKGROUND="showModelBackgroundColor";
  
  	/**
  	 * Es soll das Fenster zur Auswahl des Heatmap-Modus angezeigt werden (wird als {@link ActionEvent#getActionCommand()} bergeben).
  	 * @see #addShowModelPropertiesListener(ActionListener)
  	 * @see #fireShowPropertiesDialog(String)
  	 */
  	public static String PROPERTIES_TYPE_HEATMAP_MODES="showHeadMapModeSelect";
  
  	/**
  	 * Es soll der Dialog zur Konfiguration der Ebenen anzeigt werden (wird als {@link ActionEvent#getActionCommand()} bergeben).
  	 * @see #addShowModelPropertiesListener(ActionListener)
  	 * @see #fireShowPropertiesDialog(String)
  	 */
  	public static String PROPERTIES_TYPE_LAYERS="showLayers";
  
  	/**
  	 * Fgt einen Listener hinzu, der benachrichtigt wird, wenn die Modelleigenschaften aufgerufen werden sollen
  	 * @param showModelPropertiesListener	Zu benachrichtigender Listener
  	 * @see #PROPERTIES_TYPE_PROPERTIES
  	 * @see #PROPERTIES_TYPE_PROPERTIES_OPERATORS
  	 * @see #PROPERTIES_TYPE_BACKGROUND
  	 * @see #PROPERTIES_TYPE_HEATMAP_MODES
  	 * @see #PROPERTIES_TYPE_LAYERS
  	 */
  	public void addShowModelPropertiesListener(final ActionListener showModelPropertiesListener) {
  		if (showModelPropertiesListeners.indexOf(showModelPropertiesListener)<0) showModelPropertiesListeners.add(showModelPropertiesListener);
  	}
  
  	/**
  	 * Entfernt einen Listener aus der Liste, die benachrichtigt werden sollen, wenn die Modelleigenschaften aufgerufen werden sollen
  	 * @param showModelPropertiesListener In Zukunft nicht mehr zu benachrichtigender Listener
  	 * @return	Gibt <code>true</code> zurck, wenn der Listener erfolgreich aus der Liste entfernt werden konnte
  	 */
  	public boolean removeShowModelPropertiesListener(final ActionListener showModelPropertiesListener) {
  		return showModelPropertiesListeners.remove(showModelPropertiesListener);
  	}
  
  	/**
  	 * Liste der Consumer, die benachrichtigt werden sollen, wenn die Anzahl an Bedienern in einer Gruppe gendert werden soll
  	 * @see #setResourceCount(String, int)
  	 */
  	private final transient List<BiConsumer<String,Integer>> resourceCountSetter=new ArrayList<>();
  
  	/**
  	 * Fgt einen Consumer hinzu, der benachrichtigt werden soll, wenn die Anzahl an Bedienern in einer Gruppe gendert werden soll.
  	 * @param setter	Consumer, der benachrichtigt werden soll, wenn die Anzahl an Bedienern in einer Gruppe gendert werden soll
  	 */
  	public void addResourceCountSetter(final BiConsumer<String,Integer> setter) {
  		resourceCountSetter.add(setter);
  	}
  
  	/**
  	 * Entfernt einen Consumer aus der Liste der Consumer hinzu, die benachrichtigt werden sollen, wenn die Anzahl an Bedienern in einer Gruppe gendert werden soll.
  	 * @param setter	Consumer, der nicht mehr benachrichtigt werden soll, wenn die Anzahl an Bedienern in einer Gruppe gendert werden soll
  	 * @return	Gibt <code>true</code> zurck, wenn der Consumer erfolgreich aus der Liste entfernt werden konnte
  	 */
  	public boolean removeResourceCountSetter(final BiConsumer<String,Integer> setter) {
  		return resourceCountSetter.remove(setter);
  	}
  
  	/**
  	 * Lst die Consumer, die benachrichtigt werden sollen, wenn die Anzahl an Bedienern in einer Gruppe gendert werden soll, aus.
  	 * @param name	Name der Bedienergruppe
  	 * @param count	Neue Anzahl an Bedienern in der Gruppe
  	 */
  	private void setResourceCount(final String name, final int count) {
  		for (BiConsumer<String,Integer> setter: resourceCountSetter) setter.accept(name,count);
  	}
  
  	/**
  	 * Fgt einen Menpunkt zum Einfgen einer Visualisierung zu einem Men hinzu
  	 * @param parentMenu	bergeordnetes Men
  	 * @param elements	Visualisierungselemente die durch den Menpunkt eingefgt werden sollen
  	 * @return	Liefert den bereits eingefgten Menpunkt zurck
  	 */
  	private JMenuItem addVisualizationContextMenuItem(final JMenu parentMenu, final ModelElementPosition[] elements) {
  		final ModelElementPosition element=elements[0];
  		final ModelElementPosition[] additions=Stream.of(elements).skip(1).toArray(ModelElementPosition[]::new);
  		final JMenuItem item=new JMenuItem(element.getContextMenuElementName()+" - "+element.getName());
  		item.setToolTipText(element.getToolTip());
  		final Icon icon=element.getAddElementIcon();
  		if (icon!=null) item.setIcon(icon);
  		item.addActionListener(e->startAddElementMulti(element,additions));
  		parentMenu.add(item);
  		return item;
  	}
  
  	/**
  	 * Erstellt ein Text-Element.
  	 * @param info	Anzuzeigender Text
  	 * @param fontSize	Schriftgre (Werte &le;0 fr Standardgre)
  	 * @param deltaX	x-Achsen-Verschiebung gegenber Ausgangsposition
  	 * @param deltaY	y-Achsen-Verschiebung gegenber Ausgangsposition
  	 * @return	Text-Element
  	 * @see #addVisualizationContextMenuItems(JMenu)
  	 */
  	public static ModelElementText getVisualizationPlainText(final String info, final int fontSize, final int deltaX, final int deltaY) {
  		final ModelElementText text=new ModelElementText(null,null);
  		final var p=text.getPosition(true);
  		text.setPosition(new Point(p.x+deltaX,p.y+deltaY));
  		text.setText(info);
  		if (fontSize>0) text.setTextSize(fontSize);
  
  		if (FlatLaFHelper.isDark()) EditModelDark.processElement(text,EditModelDark.ColorMode.LIGHT,EditModelDark.ColorMode.DARK);
  
  		return text;
  	}
  
  	/**
  	 * Erstellt ein Text-Element.
  	 * @param info	Anzuzeigender Text
  	 * @param deltaX	x-Achsen-Verschiebung gegenber Ausgangsposition
  	 * @param deltaY	y-Achsen-Verschiebung gegenber Ausgangsposition
  	 * @return	Text-Element
  	 * @see #addVisualizationContextMenuItems(JMenu)
  	 */
  	public static ModelElementText getVisualizationPlainText(final String info, final int deltaX, final int deltaY) {
  		return getVisualizationPlainText(info,-1,deltaX,deltaY);
  	}
  
  	/**
  	 * Erstellt ein Text-Animations-Element zur Anzeige von Simulationsdaten.
  	 * @param expression	Rechenausdruck dessen Wert ausgegeben werden soll
  	 * @return	Text-Animations-Element
  	 * @see #addVisualizationContextMenuItems(JMenu)
  	 */
  	public static ModelElementAnimationTextValue getVisualizationText(final String expression) {
  		final ModelElementAnimationTextValue text=new ModelElementAnimationTextValue(null,null);
  		text.setExpression(expression);
  
  		if (FlatLaFHelper.isDark()) EditModelDark.processElement(text,EditModelDark.ColorMode.LIGHT,EditModelDark.ColorMode.DARK);
  
  		return text;
  	}
  
  	/**
  	 * Erstellt ein Text-Animations-Element zur Anzeige von Simulationsdaten.
  	 * @param name	Name des Elements
  	 * @param expression	Rechenausdruck dessen Wert ausgegeben werden soll
  	 * @return	Text-Animations-Element
  	 * @see #addVisualizationContextMenuItems(JMenu)
  	 */
  	public static ModelElementAnimationTextValue getVisualizationText(final String name, final String expression) {
  		final ModelElementAnimationTextValue text=getVisualizationText(expression);
  		text.setName(name);
  		return text;
  	}
  
  	/**
  	 * Erstellt ein LCD-Animations-Element zur Anzeige von Simulationsdaten.
  	 * @param expression	Rechenausdruck dessen Wert ausgegeben werden soll
  	 * @return	LCD-Animations-Element
  	 * @see #addVisualizationContextMenuItems(JMenu)
  	 */
  	public static ModelElementAnimationLCD getVisualizationLCD(final String expression) {
  		final ModelElementAnimationLCD lcd=new ModelElementAnimationLCD(null,null);
  		lcd.getExpression().setExpression(expression);
  
  		if (FlatLaFHelper.isDark()) EditModelDark.processElement(lcd,EditModelDark.ColorMode.LIGHT,EditModelDark.ColorMode.DARK);
  
  		return lcd;
  	}
  
  	/**
  	 * Erstellt ein LCD-Animations-Element zur Anzeige von Simulationsdaten.
  	 * @param name	Name des Elements
  	 * @param expression	Rechenausdruck dessen Wert ausgegeben werden soll
  	 * @return	LCD-Animations-Element
  	 * @see #addVisualizationContextMenuItems(JMenu)
  	 */
  	public static ModelElementAnimationLCD getVisualizationLCD(final String name, final String expression) {
  		final ModelElementAnimationLCD lcd=getVisualizationLCD(expression);
  		lcd.setName(name);
  		return lcd;
  	}
  
  	/**
  	 * Erstellt ein Balken-Animations-Element zur Anzeige von Simulationsdaten.
  	 * @param expression	Rechenausdruck dessen Wert ausgegeben werden soll
  	 * @return	Balken-Animations-Element
  	 * @see #addVisualizationContextMenuItems(JMenu)
  	 */
  	public static ModelElementAnimationBar getVisualizationBar(final String expression) {
  		final ModelElementAnimationBar bar=new ModelElementAnimationBar(null,null);
  		bar.getExpression().setExpression(expression);
  		bar.setSize(new Dimension(25,50));
  
  		bar.setBackgroundColor(Color.WHITE);
  		bar.setGradientFillColor(new Color(230,230,250));
  		if (FlatLaFHelper.isDark()) EditModelDark.processElement(bar,EditModelDark.ColorMode.LIGHT,EditModelDark.ColorMode.DARK);
  
  		return bar;
  	}
  
  	/**
  	 * Erstellt ein Balken-Animations-Element zur Anzeige von Simulationsdaten.
  	 * @param name	Name des Elements
  	 * @param expression	Rechenausdruck dessen Wert ausgegeben werden soll
  	 * @return	Balken-Animations-Element
  	 * @see #addVisualizationContextMenuItems(JMenu)
  	 */
  	public static ModelElementAnimationBar getVisualizationBar(final String name, final String expression) {
  		final ModelElementAnimationBar bar=getVisualizationBar(expression);
  		bar.setName(name);
  		return bar;
  	}
  
  	/**
  	 * Erstellt ein Zeiger-Animations-Element zur Anzeige von Simulationsdaten.
  	 * @param expression	Rechenausdruck dessen Wert ausgegeben werden soll
  	 * @return	Zeiger-Animations-Element
  	 * @see #addVisualizationContextMenuItems(JMenu)
  	 */
  	public static ModelElementAnimationPointerMeasuring getVisualizationPointer(final String expression) {
  		final ModelElementAnimationPointerMeasuring pointer=new ModelElementAnimationPointerMeasuring(null,null);
  		pointer.getExpression().setExpression(expression);
  
  		if (FlatLaFHelper.isDark()) EditModelDark.processElement(pointer,EditModelDark.ColorMode.LIGHT,EditModelDark.ColorMode.DARK);
  
  		return pointer;
  	}
  
  	/**
  	 * Erstellt ein Zeiger-Animations-Element zur Anzeige von Simulationsdaten.
  	 * @param name	Name des Elements
  	 * @param expression	Rechenausdruck dessen Wert ausgegeben werden soll
  	 * @return	Zeiger-Animations-Element
  	 * @see #addVisualizationContextMenuItems(JMenu)
  	 */
  	public static ModelElementAnimationPointerMeasuring getVisualizationPointer(final String name, final String expression) {
  		final ModelElementAnimationPointerMeasuring pointer=getVisualizationPointer(expression);
  		pointer.setName(name);
  		return pointer;
  	}
  
  	/**
  	 * Erstellt ein Diagramm-Animations-Element zur Anzeige von Simulationsdaten.
  	 * @param data	Im Diagramm anzuzeigende Datenreihen
  	 * @return	Diagramm-Animations-Element
  	 * @see #addVisualizationContextMenuItems(JMenu)
  	 */
  	public static ModelElementAnimationLineDiagram getVisualizationChart(List<Object[]> data) {
  		final ModelElementAnimationLineDiagram chart=new ModelElementAnimationLineDiagram(null,null);
  		chart.setExpressionData(data);
  		chart.setTimeArea(3600);
  		chart.setSize(new Dimension(400,200));
  
  		chart.setBackgroundColor(Color.WHITE);
  		chart.setGradientFillColor(new Color(230,230,250));
  		if (FlatLaFHelper.isDark()) EditModelDark.processElement(chart,EditModelDark.ColorMode.LIGHT,EditModelDark.ColorMode.DARK);
  
  		return chart;
  	}
  
  	/**
  	 * Erstellt ein Diagramm-Animations-Element zur Anzeige von Simulationsdaten.
  	 * @param expressionCurrent	Rechenausdruck dessen Wert als dnne Linie ausgegeben werden soll
  	 * @param expressionAverage	Rechenausdruck dessen Wert als dicke Linie ausgegeben werden soll
  	 * @return	Diagramm-Animations-Element
  	 * @see #addVisualizationContextMenuItems(JMenu)
  	 */
  	public static ModelElementAnimationLineDiagram getVisualizationChart(final String expressionCurrent, final String expressionAverage) {
  		final List<Object[]> data=new ArrayList<>();
  		data.add(new Object[]{new AnimationExpression(expressionCurrent),Double.valueOf(0),Double.valueOf(10),Color.RED,Integer.valueOf(1)});
  		data.add(new Object[]{new AnimationExpression(expressionAverage),Double.valueOf(0),Double.valueOf(10),Color.RED,Integer.valueOf(3)});
  		final ModelElementAnimationLineDiagram chart=getVisualizationChart(data);
  		return chart;
  	}
  
  	/**
  	 * Erstellt ein Diagramm-Animations-Element zur Anzeige von Simulationsdaten.
  	 * @param name	Name des Elements
  	 * @param expressionCurrent	Rechenausdruck dessen Wert als dnne Linie ausgegeben werden soll
  	 * @param expressionAverage	Rechenausdruck dessen Wert als dicke Linie ausgegeben werden soll
  	 * @return	Diagramm-Animations-Element
  	 * @see #addVisualizationContextMenuItems(JMenu)
  	 */
  	public static  ModelElementAnimationLineDiagram getVisualizationChart(final String name, final String expressionCurrent, final String expressionAverage) {
  		final ModelElementAnimationLineDiagram chart=getVisualizationChart(expressionCurrent,expressionAverage);
  		chart.setName(name);
  		return chart;
  	}
  
  	/**
  	 * Erstellt ein Balkendiagramm-Animations-Element zur Anzeige von Simulationsdaten als Histogramm.
  	 * @return	Balkendiagramm-Animations-Element
  	 */
  	public static ModelElementAnimationBarChart getVisualizationHistogram() {
  		final ModelElementAnimationBarChart barChart=new ModelElementAnimationBarChart(null,null);
  		barChart.setMinValue(0.0);
  		barChart.setMaxValue(1.0);
  		barChart.setSize(new Dimension(400,200));
  
  		barChart.setBackgroundColor(Color.WHITE);
  		barChart.setGradientFillColor(new Color(230,230,250));
  		if (FlatLaFHelper.isDark()) EditModelDark.processElement(barChart,EditModelDark.ColorMode.LIGHT,EditModelDark.ColorMode.DARK);
  
  		return barChart;
  	}
  
  	/**
  	 * Erstellt ein Balkendiagramm-Animations-Element zur Anzeige von Simulationsdaten als Histogramm.
  	 * @param name	Name des Elements
  	 * @return	Balkendiagramm-Animations-Element
  	 */
  	public static ModelElementAnimationBarChart getVisualizationHistogram(final String name) {
  		final ModelElementAnimationBarChart barChart=getVisualizationHistogram();
  		barChart.setName(name);
  		return barChart;
  	}
  
  	/**
  	 * Zeigt einen Dialog zur Generierung der Einstellungen fr ein Histogramm-Element an.
  	 * @param parent	bergeordnetes Element (zur Ausrichtung des Dialogs)
  	 * @param functionNameSingle	Name des Rechenbefehls mit einem "%d" zur Ausgabe eines Histogramm-Einzelwertes
  	 * @param functionNameRange	Name des Rechenbefehls mit zwei "%d" zur Ausgabe eines Histogramm-Bereiches
  	 * @return	Liefert im Erfolgsfall die Daten fr das Histogramm-Element, sonst <code>null</code>
  	 */
  	public static List<Object[]> getHistogramData(final Component parent, final String functionNameSingle, final String functionNameRange) {
  		final ModelElementBoxHistogramDialog histogramDialog=new ModelElementBoxHistogramDialog(parent);
  		if (histogramDialog.getClosedBy()!=BaseDialog.CLOSED_BY_OK) return null;
  		final int bars=histogramDialog.getBarCount();
  		final int valuesPerBar=histogramDialog.getValuesPerBar();
  
  		final List<Object[]> histData=new ArrayList<>();
  		for (int i=0;i<bars-1;i++) {
  			if (valuesPerBar==1) {
  				histData.add(new Object[]{new AnimationExpression(String.format(functionNameSingle,i)),(i==0)?Color.GREEN:Color.BLUE});
  			} else {
  				final int i1=i*valuesPerBar;
  				final int i2=(i+1)*valuesPerBar-1;
  				histData.add(new Object[]{new AnimationExpression(String.format(functionNameRange,i1,i2)),Color.BLUE});
  			}
  		}
  		histData.add(new Object[]{new AnimationExpression(String.format(functionNameRange,(bars-1)*valuesPerBar,10000)),Color.RED});
  		return histData;
  	}
  
  	/**
  	 * Erstellt ein Datenpunkteanzeige-Animations-Element zur Anzeige von Simulationsdaten.
  	 * @return	Datenpunkteanzeige-Animations-Element
  	 */
  	public static ModelElementAnimationRecord getVisualizationRecord() {
  		final ModelElementAnimationRecord record=new ModelElementAnimationRecord(null,null);
  		record.setDisplayPoints(1000);
  		record.setSize(new Dimension(400,200));
  
  		record.setBackgroundColor(Color.WHITE);
  		record.setGradientFillColor(new Color(230,230,250));
  		if (FlatLaFHelper.isDark()) EditModelDark.processElement(record,EditModelDark.ColorMode.LIGHT,EditModelDark.ColorMode.DARK);
  
  		return record;
  	}
  
  	/**
  	 * Fgt mehrere Visualisierungs-Kontextmen-Eintrge zu einem Men hinzu
  	 * @param parentMenu	bergeordnetes Men
  	 */
  	private void addVisualizationContextMenuItems(final JMenu parentMenu) {
  		/* Texte */
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {getVisualizationText(Language.tr("Surface.Popup.AddVisualization.CurrentWIPTotal"),"wip()")});
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {getVisualizationText(Language.tr("Surface.Popup.AddVisualization.AverageWIPTotal"),"wip_avg()")});
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {getVisualizationText(Language.tr("Surface.Popup.AddVisualization.CurrentNQTotal"),"NQ()")});
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {getVisualizationText(Language.tr("Surface.Popup.AddVisualization.AverageNQTotal"),"NQ_avg()")});
  
  		/* LCD-Anzeigen */
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {
  				getVisualizationLCD(Language.tr("Surface.Popup.AddVisualization.CurrentWIPTotal"),"wip()"),
  				getVisualizationPlainText(Language.tr("Surface.Popup.AddVisualization.CurrentWIPTotal"),11,0,-20)
  		});
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {
  				getVisualizationLCD(Language.tr("Surface.Popup.AddVisualization.AverageWIPTotal"),"wip_avg()"),
  				getVisualizationPlainText(Language.tr("Surface.Popup.AddVisualization.AverageWIPTotal"),11,0,-20)
  		});
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {
  				getVisualizationLCD(Language.tr("Surface.Popup.AddVisualization.CurrentNQTotal"),"NQ()"),
  				getVisualizationPlainText(Language.tr("Surface.Popup.AddVisualization.CurrentNQTotal"),11,0,-20)
  		});
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {
  				getVisualizationLCD(Language.tr("Surface.Popup.AddVisualization.AverageNQTotal"),"NQ_avg()"),
  				getVisualizationPlainText(Language.tr("Surface.Popup.AddVisualization.AverageNQTotal"),11,0,-20)
  		});
  
  		/* Zeigermessinstrumente */
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {
  				getVisualizationPointer(Language.tr("Surface.Popup.AddVisualization.CurrentWIPTotal"),"wip()"),
  				getVisualizationPlainText(Language.tr("Surface.Popup.AddVisualization.CurrentWIPTotal"),11,0,50)
  		});
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {
  				getVisualizationPointer(Language.tr("Surface.Popup.AddVisualization.AverageWIPTotal"),"wip_avg()"),
  				getVisualizationPlainText(Language.tr("Surface.Popup.AddVisualization.AverageWIPTotal"),11,0,50)
  		});
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {
  				getVisualizationPointer(Language.tr("Surface.Popup.AddVisualization.CurrentNQTotal"),"NQ()"),
  				getVisualizationPlainText(Language.tr("Surface.Popup.AddVisualization.CurrentNQTotal"),11,0,50)
  		});
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {
  				getVisualizationPointer(Language.tr("Surface.Popup.AddVisualization.AverageNQTotal"),"NQ_avg()"),
  				getVisualizationPlainText(Language.tr("Surface.Popup.AddVisualization.AverageNQTotal"),11,0,50)
  		});
  
  		/* Einzelne Balken */
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {
  				getVisualizationBar(Language.tr("Surface.Popup.AddVisualization.CurrentWIPTotal"),"wip()"),
  				getVisualizationPlainText(Language.tr("Surface.Popup.AddVisualization.CurrentWIPTotal"),11,0,-20)
  		});
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {
  				getVisualizationBar(Language.tr("Surface.Popup.AddVisualization.AverageWIPTotal"),"wip_avg()"),
  				getVisualizationPlainText(Language.tr("Surface.Popup.AddVisualization.AverageWIPTotal"),11,0,-20)
  		});
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {
  				getVisualizationBar(Language.tr("Surface.Popup.AddVisualization.CurrentNQTotal"),"NQ()"),
  				getVisualizationPlainText(Language.tr("Surface.Popup.AddVisualization.CurrentNQTotal"),11,0,-20)
  		});
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {
  				getVisualizationBar(Language.tr("Surface.Popup.AddVisualization.AverageNQTotal"),"NQ_avg()"),
  				getVisualizationPlainText(Language.tr("Surface.Popup.AddVisualization.AverageNQTotal"),11,0,-20)
  		});
  
  		/* Liniendiagramme */
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {
  				getVisualizationChart(Language.tr("Surface.Popup.AddVisualization.CurrentWIPTotal"),"wip()","wip_avg()"),
  				getVisualizationPlainText(Language.tr("Surface.Popup.AddVisualization.CurrentWIPTotal"),0,-20)
  		});
  		addVisualizationContextMenuItem(parentMenu,new ModelElementPosition[] {
  				getVisualizationChart(Language.tr("Surface.Popup.AddVisualization.CurrentNQTotal"),"NQ()","NQ_avg()"),
  				getVisualizationPlainText(Language.tr("Surface.Popup.AddVisualization.CurrentNQTotal"),0,-20)
  		});
  	}
  
  	/**
  	 * Sortiert die Elemente in einem Untermen.
  	 * Evtl. vorhandene Trenner werden dabei entfernt.
  	 * @param menu	Zu sortierendes Untermen (Die Eintrge werden direkt in diesem Men sortiert, es wird kein neues Men angelegt.)
  	 */
  	public static void sortMenu(final JMenu menu) {
  		final List<JMenuItem> list=new ArrayList<>();
  		for (int i=0;i<menu.getItemCount();i++) {
  			final JMenuItem item=menu.getItem(i);
  			if (item!=null) list.add(item);
  		}
  
  		while (menu.getItemCount()>0) menu.remove(0);
  
  		list.stream().sorted((i1,i2)->String.CASE_INSENSITIVE_ORDER.compare(i1.getText(),i2.getText())).forEach(i->menu.add(i));
  	}
  
  	/**
  	 * Zeigt das Kontextmen an einem bestimmten Punkt an.
  	 * @param point	Klickpunkt an dem das Kontextmen angezeigt werden soll
  	 */
  	private void showModelContextMenu(final Point point) {
  		final JPopupMenu menu=new JPopupMenu();
  
  		JMenu subMenu;
  		JMenuItem item;
  
  		if (!readOnly) {
  			/* Slider fr Bedieneranzahl */
  			int groupCount=0;
  			for (final ModelResource resource: model.resources.getResources()) {
  				if (resource.getMode()!=ModelResource.Mode.MODE_NUMBER) continue;
  				final String name=resource.getName();
  				final Function<Integer,String> resourceNumberChange=value->{
  					final int count;
  					if (value==null) {
  						count=resource.getCount();
  					} else {
  						count=value.intValue();
  						setResourceCount(name,count);
  					}
  					return ""+count+" "+((count==1)?Language.tr("Editor.Operator.Singular"):Language.tr("Editor.Operator.Plural"));
  				};
  				menu.add(ModelElement.createContextMenuSliderValue(String.format(Language.tr("Editor.Operator"),resource.getName()),resource.getCount(),20,resourceNumberChange));
  				groupCount++;
  				if (groupCount==3) break;
  			}
  			if (groupCount>0) menu.addSeparator();
  
  			/* Visualisierung hinzufgen */
  			final JMenu sub=new JMenu(Language.tr("Surface.Popup.AddVisualization"));
  			addVisualizationContextMenuItems(sub);
  			if (sub.getItemCount()>0) {
  				sortMenu(sub);
  				sub.setIcon(Images.MODELEDITOR_ELEMENT_ADD_VISUALIZATION.getIcon());
  				menu.add(sub);
  			}
  
  			/* Notiz hinzufgen */
  			menu.add(item=new JMenuItem(Language.tr("Surface.Popup.AddNote")));
  			item.setIcon(Images.MODEL_NOTES.getIcon());
  			item.addActionListener(e->startAddElement(new ModelElementNote(model,surface)));
  		}
  
  		/* Modelleigenschaften */
  		menu.add(item=new JMenuItem(Language.tr("Main.Menu.File.ModelProperties")));
  		item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F2,InputEvent.CTRL_DOWN_MASK));
  		item.setIcon(Images.MODEL.getIcon());
  		item.addActionListener(e->fireShowPropertiesDialog(PROPERTIES_TYPE_PROPERTIES));
  
  		/* Ebenen */
  		menu.add(item=new JMenuItem(Language.tr("Main.Menu.View.Layers")));
  		item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F9,InputEvent.CTRL_DOWN_MASK+InputEvent.SHIFT_DOWN_MASK));
  		item.setIcon(Images.EDIT_LAYERS.getIcon());
  		item.addActionListener(e->fireShowPropertiesDialog(PROPERTIES_TYPE_LAYERS));
  
  		if (!readOnly) {
  			final AnimationImageSource imageSource=new AnimationImageSource();
  
  			/* Hintergrundfarbe */
  			menu.add(item=new JMenuItem(Language.tr("Main.Menu.View.BackgroundColor")));
  			item.setIcon(Images.EDIT_BACKGROUND_COLOR.getIcon());
  			item.addActionListener(e->fireShowPropertiesDialog(PROPERTIES_TYPE_BACKGROUND));
  
  			/* Heatmaptyp */
  			menu.add(item=new JMenuItem(Language.tr("Main.Menu.View.Statistics.HeatMapSelect")));
  			item.setAccelerator(KeyStroke.getKeyStroke('H',InputEvent.CTRL_DOWN_MASK+InputEvent.SHIFT_DOWN_MASK));
  			item.addActionListener(e->fireShowPropertiesDialog(PROPERTIES_TYPE_HEATMAP_MODES));
  
  			/* Parameterreihe: Anzahl an Bedienern */
  			subMenu=new JMenu(Language.tr("Surface.PopupMenu.ParameterCompare.ChangeOperatorCount"));
  			subMenu.setIcon(Images.PARAMETERSERIES.getIcon());
  			for (ModelResource resource: model.resources.getResources()) {
  				if (resource.getMode()==ModelResource.Mode.MODE_NUMBER && resource.getCount()>0) {
  					String icon=resource.getIcon();
  					if (icon==null || icon.isBlank()) icon=ModelSurfaceAnimatorBase.DEFAULT_OPERATOR_ICON_NAME;
  					subMenu.add(item=new JMenuItem(resource.getName(),new ImageIcon(imageSource.get(icon,model.animationImages,16))));
  					item.addActionListener(e->{
  						final TemplateRecord record=new TemplateRecord(TemplateMode.MODE_OPERATORS,Language.tr("Surface.PopupMenu.ParameterCompare.ChangeOperatorCount.Short"));
  						record.input.setMode(ModelChanger.Mode.MODE_RESOURCE);
  						record.input.setTag(resource.getName());
  						fireBuildParameterSeries(record);
  					});
  				}
  			}
  			if (subMenu.getItemCount()>0) {
  				if (subMenu.getItemCount()>1) {
  					menu.add(subMenu);
  				} else {
  					item=new JMenuItem(Language.tr("Surface.PopupMenu.ParameterCompare.ChangeOperatorCount")+" - "+subMenu.getItem(0).getText(),Images.PARAMETERSERIES.getIcon());
  					item.addActionListener(subMenu.getItem(0).getActionListeners()[0]);
  					menu.add(item);
  				}
  			}
  
  			/* Parameterreihe: Variablenwerte */
  			subMenu=new JMenu(Language.tr("Surface.PopupMenu.ParameterCompare.ChangeVariableValue"));
  			subMenu.setIcon(Images.PARAMETERSERIES.getIcon());
  			for (String name: model.getModelVariableNames()) {
  				subMenu.add(item=new JMenuItem(name,Images.EXPRESSION_BUILDER_VARIABLE.getIcon()));
  				item.addActionListener(e->{
  					final TemplateRecord record=new TemplateRecord(TemplateMode.MODE_VARIABLES,Language.tr("Surface.PopupMenu.ParameterCompare.ChangeVariableValue.Short")+" \""+name+"\"");
  					record.input.setMode(ModelChanger.Mode.MODE_VARIABLE);
  					record.input.setTag(name);
  					fireBuildParameterSeries(record);
  				});
  			}
  			if (subMenu.getItemCount()>0) {
  				if (subMenu.getItemCount()>1) {
  					menu.add(subMenu);
  				} else {
  					item=new JMenuItem(Language.tr("Surface.PopupMenu.ParameterCompare.ChangeVariableValue")+" - "+subMenu.getItem(0).getText(),Images.PARAMETERSERIES.getIcon());
  					item.addActionListener(subMenu.getItem(0).getActionListeners()[0]);
  					menu.add(item);
  				}
  			}
  			subMenu=new JMenu(Language.tr("Surface.PopupMenu.ParameterCompare.ChangeMapValue"));
  			subMenu.setIcon(Images.PARAMETERSERIES.getIcon());
  			for (String name: model.globalMapInitial.keySet()) {
  				subMenu.add(item=new JMenuItem(name,Images.SCRIPT_MAP.getIcon()));
  				item.addActionListener(e->{
  					final TemplateRecord record=new TemplateRecord(TemplateMode.MODE_MAP,Language.tr("Surface.PopupMenu.ParameterCompare.ChangeMapValue.Short"));
  					record.input.setMode(ModelChanger.Mode.MODE_MAP);
  					record.input.setTag(name);
  					fireBuildParameterSeries(record);
  				});
  			}
  			if (subMenu.getItemCount()>0) {
  				if (subMenu.getItemCount()>1) {
  					menu.add(subMenu);
  				} else {
  					item=new JMenuItem(Language.tr("Surface.PopupMenu.ParameterCompare.ChangeMapValue")+" - "+subMenu.getItem(0).getText(),Images.PARAMETERSERIES.getIcon());
  					item.addActionListener(subMenu.getItem(0).getActionListeners()[0]);
  					menu.add(item);
  				}
  			}
  		}
  
  		menu.show(this,point.x,point.y);
  	}
  
  	/**
  	 * Aktiviert die per {@link #addShowModelPropertiesListener(ActionListener)} registrierten Listener.
  	 * @param propertiesType	Art des Eigenschaftendialogs der geffnet werden soll
  	 * @see #showModelPropertiesListeners
  	 */
  	public void fireShowPropertiesDialog(final String propertiesType) {
  		final ActionEvent event=new ActionEvent(ModelSurfacePanel.this,AWTEvent.RESERVED_ID_MAX+1,propertiesType);
  		for (ActionListener listener: showModelPropertiesListeners) listener.actionPerformed(event);
  	}
  
  	/**
  	 * Sichtbarkeit der Elementenvorlagenleiste
  	 * @author Alexander Herzog
  	 * @see ModelSurfacePanel#addSetElementTemplatesVisibilityListener(Consumer)
  	 */
  	public enum ElementTemplatesVisibility {
  		/** Elementenvorlagenleiste anzeigen */
  		SHOW,
  		/** Elementenvorlagenleiste ausblenden */
  		HIDE,
  		/** Sichtbarkeit der Elementenvorlagenleiste umschalten */
  		TOGGLE
  	}
  
  	/**
  	 * Listener, die benachrichtigt werden sollen, wenn die Elementenvorlagenleiste ein- oder ausgeblendet werden soll
  	 * @see #setElementTemplatesVisibility(ElementTemplatesVisibility)
  	 */
  	private final List<Consumer<ElementTemplatesVisibility>> setElementTemplatesVisibilityListeners=new ArrayList<>();
  
  	/**
  	 * Fgt einen Listener hinzu, der benachrichtigt wird, wenn die Elementenvorlagenleiste ein- oder ausgeblendet werden soll
  	 * @param setElementTemplatesVisibilityListener	Zu benachrichtigender Listener
  	 */
  	public void addSetElementTemplatesVisibilityListener(final Consumer<ElementTemplatesVisibility> setElementTemplatesVisibilityListener) {
  		if (setElementTemplatesVisibilityListeners.indexOf(setElementTemplatesVisibilityListener)<0) setElementTemplatesVisibilityListeners.add(setElementTemplatesVisibilityListener);
  	}
  
  	/**
  	 * Entfernt einen Listener aus der Liste, die benachrichtigt werden sollen, wenn Elementenvorlagenleiste ein- oder ausgeblendet werden soll
  	 * @param setElementTemplatesVisibilityListener In Zukunft nicht mehr zu benachrichtigender Listener
  	 * @return	Gibt <code>true</code> zurck, wenn der Listener erfolgreich aus der Liste entfernt werden konnte
  	 */
  	public boolean removeSetElementTemplatesVisibilityListener(final Consumer<ElementTemplatesVisibility> setElementTemplatesVisibilityListener) {
  		return setElementTemplatesVisibilityListeners.remove(setElementTemplatesVisibilityListener);
  	}
  
  	/**
  	 * Listener, die benachrichtigt werden sollen, wenn die Elementenvorlagenleiste ein- oder ausgeblendet werden soll
  	 * @param visible	Soll die Elementenvorlagenleiste ein- oder ausgeblendet werden?
  	 * @see #setElementTemplatesVisibilityListeners
  	 */
  	private void setElementTemplatesVisibility(final ElementTemplatesVisibility visible) {
  		for (Consumer<ElementTemplatesVisibility> listener: setElementTemplatesVisibilityListeners) listener.accept(visible);
  	}
  
  	/**
  	 * Bewegt das momentan ausgewhlte Element um eine oder mehrere Stufen in der Zeichenflchen-Hierarchie nach vorne
  	 * @param max	Wird hier <code>true</code> bergeben, so wird das Element ganz nach vorne verschoben; ansonsten nur um eine Stufe nach vorne
  	 */
  	public void moveSelectedElementToFront(final boolean max) {
  		if (readOnly && !allowChangeOperationsOnReadOnly) return;
  		surface.moveSelectedElementToFront(max);
  	}
  
  	/**
  	 * Bewegt das momentan ausgewhlte Element um eine oder mehrere Stufen in der Zeichenflchen-Hierarchie nach hinten
  	 * @param max	Wird hier <code>true</code> bergeben, so wird das Element ganz nach hinten verschoben; ansonsten nur um eine Stufe nach hinten
  	 */
  	public void moveSelectedElementToBack(final boolean max) {
  		if (readOnly && !allowChangeOperationsOnReadOnly) return;
  		surface.moveSelectedElementToBack(max);
  	}
  
  	/**
  	 * Richtet die ausgewhlten Element an einer gemeinsamen Oberkante aus.
  	 */
  	public void alignSelectedElementsTop() {
  		if (readOnly && !allowChangeOperationsOnReadOnly) return;
  		surface.alignSelectedElementsTop();
  	}
  
  	/**
  	 * Richtet die ausgewhlten Element so aus, dass sich ihre vertikale Mitte auf derselben Hhe befindet.
  	 */
  	public void alignSelectedElementsMiddle() {
  		if (readOnly && !allowChangeOperationsOnReadOnly) return;
  		surface.alignSelectedElementsMiddle();
  	}
  
  	/**
  	 * Richtet die ausgewhlten Element an einer gemeinsamen Unterkante aus.
  	 */
  	public void alignSelectedElementsBottom() {
  		if (readOnly && !allowChangeOperationsOnReadOnly) return;
  		surface.alignSelectedElementsBottom();
  	}
  
  	/**
  	 * Richtet die ausgewhlten Element an einer gemeinsamen linken Kante aus.
  	 */
  	public void alignSelectedElementsLeft() {
  		if (readOnly && !allowChangeOperationsOnReadOnly) return;
  		surface.alignSelectedElementsLeft();
  	}
  
  	/**
  	 * Richtet die ausgewhlten Element so aus, dass sich ihre horizontale Mitte auf derselben Linie befindet.
  	 */
  	public void alignSelectedElementsCenter() {
  		if (readOnly && !allowChangeOperationsOnReadOnly) return;
  		surface.alignSelectedElementsCenter();
  	}
  
  	/**
  	 * Richtet die ausgewhlten Element an einer gemeinsamen rechten Kante aus.
  	 */
  	public void alignSelectedElementsRight() {
  		if (readOnly && !allowChangeOperationsOnReadOnly) return;
  		surface.alignSelectedElementsRight();
  	}
  
  	/**
  	 * Gibt an, ob Kanten zu neu eingefgten Elementen wenn mglich automatisch hinzugefgt werden sollen.
  	 * @return	Kanten-Hinzufge-Modus
  	 */
  	public ConnectMode getAutoConnect() {
  		return autoConnect;
  	}
  
  	/**
  	 * Stellt ein, ob Kanten zu neu eingefgten Elementen wenn mglich automatisch hinzugefgt werden sollen.
  	 * @param autoConnect	Kanten-Hinzufge-Modus
  	 */
  	public void setAutoConnect(final ConnectMode autoConnect) {
  		this.autoConnect=autoConnect;
  	}
  
  	/**
  	 * Stellt die (Animations-)Simulationsdaten ein, so dass Statistikdaten whrend der Animation
  	 * ber das Kontextmen abgerufen werden knnen.
  	 * @param simData	Simulationsdaten (kann auch <code>null</code> sein, wenn keine Daten zur Verfgung gestellt werden sollen)
  	 * @param mainAnimator	Animationssystem-Objekt (kann auch <code>null</code> sein, wenn keine Daten zur Verfgung gestellt werden sollen)
  	 */
  	public void setAnimationSimulationData(final SimulationData simData, final ModelSurfaceAnimator mainAnimator) {
  		this.simData=simData;
  		this.mainAnimator=mainAnimator;
  	}
  
  	/**
  	 * Listener, die benachrichtigt werden sollen, wenn Elemente ausgewhlt werden
  	 * @see #fireSelectionListener()
  	 */
  	private transient List<ActionListener> selectionListeners=new ArrayList<>();
  
  	/**
  	 * Fgt einen Listener hinzu, der benachrichtigt wird, wenn Elemente ausgewhlt werden
  	 * @param selectionListener	Zu benachrichtigender Listener
  	 */
  	public void addSelectionListener(final ActionListener selectionListener) {
  		if (selectionListeners.indexOf(selectionListener)<0) selectionListeners.add(selectionListener);
  	}
  
  	/**
  	 * Entfernt einen Listener aus der Liste der im Falle einer Auswahl von Elementen zu benachrichtigenden Listener
  	 * @param selectionListener	In Zukunft nicht mehr zu benachrichtigender Listener
  	 * @return	Gibt <code>true</code> zurck, wenn der Listener erfolgreich aus der Liste entfernt werden konnte
  	 */
  	public boolean removeSelectionListener(final ActionListener selectionListener) {
  		return selectionListeners.remove(selectionListener);
  	}
  
  	/**
  	 * Lst die Listener, die benachrichtigt werden sollen, wenn Elemente ausgewhlt werden, aus.
  	 * @see #selectionListeners
  	 */
  	private void fireSelectionListener() {
  		final ActionEvent event=new ActionEvent(this,AWTEvent.RESERVED_ID_MAX+1,"selectionchanged");
  		for (ActionListener listener: selectionListeners) listener.actionPerformed(event);
  	}
  
  	/**
  	 * Listener die auf Klicks auf Zeichenflchen-Links reagieren sollen
  	 * @see #fireLinkListener(ui.modeleditor.ModelSurfaceLinks.Link)
  	 */
  	private transient List<Consumer<ModelSurfaceLinks.Link>> linkListeners=new ArrayList<>();
  
  	/**
  	 * Fgt einen Listener hinzu, der benachrichtigt wird, wenn ein Zeichenflchen-Link angeklickt wird
  	 * @param linkListener	Zu benachrichtigender Listener
  	 */
  
  	public void addLinkListener(final Consumer<ModelSurfaceLinks.Link> linkListener) {
  		if (linkListeners.indexOf(linkListener)<0) linkListeners.add(linkListener);
  	}
  
  	/**
  	 * Entfernt einen Listener aus der Liste der im Falle eins Klicks auf einen Zeichenflchen-Link zu benachrichtigenden Listener
  	 * @param linkListener	In Zukunft nicht mehr zu benachrichtigender Listener
  	 * @return	Gibt <code>true</code> zurck, wenn der Listener erfolgreich aus der Liste entfernt werden konnte
  	 */
  	public boolean removeLinkListener(final Consumer<ModelSurfaceLinks.Link> linkListener) {
  		return linkListeners.remove(linkListener);
  	}
  
  	/**
  	 * Lst die Listener, die auf Klicks auf Zeichenflchen-Links reagieren sollen, aus.
  	 * @param link	Nummer des Links
  	 * @see #linkListeners
  	 */
  	private void fireLinkListener(final ModelSurfaceLinks.Link link) {
  		for (Consumer<ModelSurfaceLinks.Link> linkListener: linkListeners) linkListener.accept(link);
  	}
  
  	/**
  	 * Liefert das aktuelle selektierte Element
  	 * @return	Aktuell selektiertes Element oder <code>null</code>, falls nichts selektiert ist.
  	 */
  	public ModelElement getSelectedElement() {
  		return surface.getSelectedElement();
  	}
  
  	/**
  	 * Liefert eine Liste der Elemente, die ber eine Bereichsselektion markiert sind.
  	 * @param addRegularSelectedElement	Auch die normal ausgewhlten Objekte hinzufgen?
  	 * @return	Liste der markierten Elemente (ist nie <code>null</code>, kann aber leer sein)
  	 */
  	public List<ModelElement> getSelectedArea(final boolean addRegularSelectedElement) {
  		return surface.getSelectedArea(addRegularSelectedElement);
  	}
  
  	/**
  	 * Liefert den Bereich der markierten Elemente.
  	 * @return	Bereich der markierten Elemente oder <code>null</code>, wenn keine Elemente markiert sind.
  	 */
  	public Rectangle getSelectedArea() {
  		int minX=Integer.MAX_VALUE;
  		int minY=Integer.MAX_VALUE;
  		int maxX=-Integer.MAX_VALUE;
  		int maxY=-Integer.MAX_VALUE;
  
  		for (ModelElement element: surface.getElements()) if ((element.isSelected() || element.isSelectedArea()) && (element instanceof ModelElementPosition)) {
  			final ModelElementPosition pos=((ModelElementPosition)element);
  			final Point p1=pos.getPosition(true);
  			final Point p2=pos.getLowerRightPosition();
  			if (p1.x<minX) minX=p1.x;
  			if (p1.y<minY) minY=p1.y;
  			if (p2.x>maxX) maxX=p2.x;
  			if (p2.y>maxY) maxY=p2.y;
  		}
  
  		if (minX==Integer.MAX_VALUE) return null;
  
  		return new Rectangle(minX,minY,maxX-minX,maxY-minY);
  	}
  
  	/**
  	 * Liefert die momentane Hintergrund- und Rasterfarbe
  	 * @return	2-elementiges Array aus Hintergrund- und Rasterfarbe
  	 */
  	public Color[] getColors() {
  		return colors;
  	}
  
  	/**
  	 * Stellt die Hintergrund- und Rasterfarbe ein
  	 * @param colors	2-elementiges Array aus neuer Hintergrund- und Rasterfarbe
  	 */
  	public void setColors(final Color[] colors) {
  		if (colors==null || colors.length<2 || colors[0]==null || colors[1]==null) return;
  		if (colors.length==2 || colors[2]==null) {
  			this.colors=Arrays.copyOf(colors,2);
  		} else {
  			this.colors=Arrays.copyOf(colors,3);
  		}
  		repaint();
  	}
  
  	/**
  	 * Stellt das aktuelle Hintergrundbild ein.
  	 * @param image	Hintergrundbild (kann <code>null</code> sein)
  	 * @param scale	Skalierung fr das Hintergrundbild (muss grer als 0 sein)
  	 * @param mode	Soll das Hintergrundbild, sofern der <code>image</code>-Parameter ungleich <code>null</code> ist, berhaupt gezeichnet werden?
  	 */
  	public void setBackgroundImage(final BufferedImage image, final double scale, final ModelSurface.BackgroundImageMode mode) {
  		if (image==null) {
  			backgroundImage=null;
  			backgroundImageHash=null;
  		} else {
  			backgroundImage=ScaledImageCache.copyImage(image);
  			backgroundImageHash=ScaledImageCache.getHash(backgroundImage);
  		}
  		backgroundImageMode=mode;
  		backgroundImageScale=Math.max(0.1,scale);
  	}
  
  	/**
  	 * Liefert das aktuell eingestellte Hintergrundbild.
  	 * @return	Aktuell eingestelltes Hintergrundbild (kann <code>null</code> sein)
  	 * @see #setBackgroundImage(BufferedImage, double, ModelSurface.BackgroundImageMode)
  	 * @see #getBackgroundImageScale()
  	 */
  	public BufferedImage getBackgroundImage() {
  		return backgroundImage;
  	}
  
  	/**
  	 * Liefert den Hash-Wert des aktuellen Hintergrundbildes
  	 * (kann <code>null</code> sein, wenn kein Hintergrundbild eingestellt ist)
  	 * @return	Hash-Wert des aktuellen Hintergrundbildes
  	 * @see #setBackgroundImage(BufferedImage, double, ModelSurface.BackgroundImageMode)
  	 */
  	public String getBackgroundImageHash() {
  		return backgroundImageHash;
  	}
  
  	/**
  	 * Liefert die aktuelle Skalierung fr das Hintergrundbild.
  	 * @return	Aktuelle Skalierung fr das Hintergrundbild
  	 * @see #setBackgroundImage(BufferedImage, double, ModelSurface.BackgroundImageMode)
  	 * @see #getBackgroundImage()
  	 */
  	public double getBackgroundImageScale() {
  		return backgroundImageScale;
  	}
  
  	/**
  	 * Liefert die Einstellung, wie das Hintergrundbild gezeichnet werden soll.
  	 * @return	Wie soll das Hintergrundbild gezeichnet werden?
  	 * @see ModelSurface.BackgroundImageMode
  	 */
  	public ModelSurface.BackgroundImageMode getBackgroundImageMode() {
  		return backgroundImageMode;
  	}
  
  	/**
  	 * Fgt eine Tabellen-basierte Kundenquelle zu dem Modell hinzu
  	 * @param file	Tabellendatei
  	 * @param position	Einfgeposition
  	 * @return	Liefert true, wenn die Quelle erfolgreich hinzugefgt werden konnte
  	 */
  	private boolean addTableFileBasedSource(final File file, final Point position) {
  		final ModelElementSourceTable source=new ModelElementSourceTable(model,surface);
  		source.setInputFile(file.toString());
  		source.setPosition(new Point((int)Math.round(position.x/zoom),(int)Math.round(position.y/zoom)));
  		surface.add(source);
  		return true;
  	}
  
  	/**
  	 * Fgt eine Tabellen-basierte Eingabe-Station zu dem Modell hinzu
  	 * @param file	Tabellendatei
  	 * @param position	Einfgeposition
  	 * @return	Liefert true, wenn die Eingabe-Station erfolgreich hinzugefgt werden konnte
  	 */
  	private boolean addTableFileBasedInput(final File file, final Point position) {
  		final ModelElementInput input=new ModelElementInput(model,surface);
  		input.setInputFile(file.toString());
  		input.setPosition(new Point((int)Math.round(position.x/zoom),(int)Math.round(position.y/zoom)));
  		surface.add(input);
  		return true;
  	}
  
  	/**
  	 * Erzeugt eine Tabellen-Quelle oder ein Eingabeelement mit der angegebenen Datei als Quelle an der angegebenen Position
  	 * @param file	Datei, die als Datenquelle fr das Element verwendet werden soll
  	 * @param position	Maus-Position bezogen auf dieses Objekt (d.h. nicht Zeichenflchen-Punkt)
  	 * @return	Gibt <code>true</code> zurck, wenn das Tabellen-basierende-Element hinzugefgt werden konnte
  	 */
  	public boolean addTableFileBasedElement(final File file, final Point position) {
  		final int select=MsgBox.options(this,Language.tr("Editor.DragDropTableLoad.Title"),Language.tr("Editor.DragDropTableLoad.Info"),new String[]{Language.tr("Editor.DragDropTableLoad.OptionSource.Title"),Language.tr("Editor.DragDropTableLoad.OptionInput.Title")},new String[]{Language.tr("Editor.DragDropTableLoad.OptionSource.Info"),Language.tr("Editor.DragDropTableLoad.OptionInput.Info")});
  
  		switch (select) {
  		case 0: return addTableFileBasedSource(file,position);
  		case 1: return addTableFileBasedInput(file,position);
  		default: return false;
  		}
  	}
  
  	/**
  	 * Erzeugt ein Bild-Element auf der Zeichenflche
  	 * @param image	Bild, das in dem Bild-Element angezeigt werden soll
  	 * @param position	Maus-Position bezogen auf dieses Objekt (d.h. nicht Zeichenflchen-Punkt)
  	 * @return	Gibt <code>true</code> zurck, wenn das Bild-Element hinzugefgt werden konnte
  	 */
  	public boolean addImageElement(final BufferedImage image, final Point position) {
  		final ModelElementImage element=new ModelElementImage(model,surface);
  		element.setImage(image);
  		element.setPosition(new Point((int)Math.round(position.x/zoom),(int)Math.round(position.y/zoom)));
  		surface.add(element);
  		return true;
  	}
  
  	/**
  	 * Befindet sich ein Punkt im dem Bereich eines Infotext-Links?
  	 * @param point	Zu prfender Punkt
  	 * @return	Liefert den Infotext-Link oder <code>null</code>, wenn sich der Punkt nicht im Bereich eines Infotextes befindet
  	 * @see #linkPositions
  	 */
  	private ModelSurfaceLinks.Link emptyScreenLinkPosition(final Point point) {
  		final Rectangle viewArea;
  		if (getParent() instanceof JViewport) {
  			final JViewport viewport=(JViewport)getParent();
  			viewArea=viewport.getViewRect();
  		} else {
  			viewArea=new Rectangle(getSize());
  		}
  
  		if (viewArea.x==0 && viewArea.y==0 && surface.count()==0 && useInfoPaint && linkPositions!=null) {
  			for (int i=0;i<linkPositions.length;i++) if (linkPositions[i]!=null && linkPositions[i].contains(point)) {
  				if (surfaceLinks==null) return null;
  				return surfaceLinks.getLink(i);
  			}
  		}
  
  		return null;
  	}
  
  	/**
  	 * Zeigt das QuickFix-Popupmen fr ein Element an (wenn es Fehler gibt, sonst passiert nichts)
  	 * @param element	Element fr dass das QuickFix-Popupmen angezeigt werden soll
  	 */
  	private void showQuickFixPopup(final ModelElementPosition element) {
  		final JPopupMenu popupMenu=element.getQuickFixPopupMenu();
  		if (popupMenu==null) return;
  
  		final Point point=element.getMiddlePosition(true);
  		popupMenu.show(this,(int)Math.round(point.x*zoom),(int)Math.round(point.y*zoom));
  	}
  
  	/**
  	 * Schaltet einige Funktionen zum Bearbeiten auch whrend des Read-Only-Modus frei.<br>
  	 * Erlaubt wird das Bearbeiten von Elementen sowie das Verschieben.
  	 * Das Lschen von Elementen und das Erstellen neuer Elemente bleibt aber verboten.
  	 */
  	public void allowEditorDialogs() {
  		allowChangeOperationsOnReadOnly=true;
  	}
  
  	/**
  	 * Zeigt an, ob gerade eine Operation, die evtl. mehrere Elemente umfasst, luft und daher Statusausgaben usw. zunchst nicht verarbeitet werden sollen.
  	 * Alle Funktionen, die hier zu einer Rckgabe von <code>true</code> fhren, stellen sicher, dass nach Abschluss der Operation
  	 * noch eine weitere Benachrichtigung mit bei Rckgabewert <code>false</code> hier ausgefhrt wird.
  	 * @return	Gibt <code>true</code> zurck, wenn gerade eine umfangreichere Operation ausgefhrt wird.
  	 */
  	public boolean isOperationRunning() {
  		return operationRunning;
  	}
  
  	/**
  	 * Liefert eine Beschreibung fr eine Station zur Anzeige in einem Tooltip.
  	 * @param element	Element fr das die Beschreibung generiert werden soll
  	 * @return	Beschreibung
  	 */
  	private String getTooltipDescription(final ModelElementBox element) {
  		if (element==null || !SetupData.getSetup().showStationDescription) return "";
  
  		final ModelDescriptionBuilderSingleStation simpleDescriptionBuilder=new ModelDescriptionBuilderSingleStation(model,ModelDescriptionBuilderSingleStation.Mode.HTML,25);
  		element.buildDescription(simpleDescriptionBuilder);
  		simpleDescriptionBuilder.done();
  		String s=simpleDescriptionBuilder.getDescription();
  		return s;
  	}
  
  	/**
  	 * Liefert, sofern hinterlegt, einen zustzlichen Tooltip-Text fr ein Element bzw. eine Kante.
  	 * @param element	Element fr das der zustzliche Tooltip-Text abgerufen werden soll
  	 * @return	Zustzlicher Tooltip-Text (kann <code>null</code> sein, wenn kein zustzlicher Text zur Verfgung steht)
  	 * @see #additionalTooltipGetter
  	 * @see #setAdditionalTooltipGetter(Function)
  	 */
  	private String getAdditionalTooltip(final ModelElement element) {
  		if (additionalTooltipGetter==null) return null;
  		return additionalTooltipGetter.apply(element);
  	}
  
  	/**
  	 * Stellt ein Callback ein, das zu einem Element weitere Tooltip-Daten (aus der Statistik) liefert
  	 * @param additionalTooltipGetter	Callback, das weitere Tooltip-Daten liefert. (Dass Callback kann <code>null</code> sein und auch die zurckgelieferten Tooltip-Daten knnen jederzeit <code>null</code> sein)
  	 */
  	public void setAdditionalTooltipGetter(final Function<ModelElement,String> additionalTooltipGetter) {
  		this.additionalTooltipGetter=additionalTooltipGetter;
  	}
  
  	/**
  	 * Callback zur Ermittlung der HeatMap-Intensitt fr ein Element
  	 * (kann <code>null</code> sein)
  	 */
  	private Function<ModelElement,Double> heatMapIntensityGetter;
  
  	/**
  	 * Stellt das Callback zur Ermittlung der HeatMap-Intensitt fr ein Element ein.
  	 * @param heatMapIntensityGetter	Callback zur Ermittlung der HeatMap-Intensitt fr ein Element (<code>null</code>, wenn keine HeatMap gezeichnet werden soll)
  	 */
  	public void setHeatMapIntensityGetter(final Function<ModelElement,Double> heatMapIntensityGetter) {
  		this.heatMapIntensityGetter=heatMapIntensityGetter;
  		if (surface!=null) surface.setHeatMapIntensityGetter(heatMapIntensityGetter);
  	}
  
  	/**
  	 * Lscht das aktuelle Heatmap-Bild,
  	 * so dass die Heatmap beim nchsten Zeichnen neu angelegt wird.<br>
  	 * Dies ist ntig, wenn die Heatmap-Einstellungen verndert wurden.
  	 */
  	public void clearHeatMapCache() {
  		if (surface!=null) surface.clearHeatMapCache();
  	}
  
  	/**
  	 * Listener, die benachrichtigt werden sollen, wenn der Nutzer per Kontextmen die Erstellung einer Parameterreihe auslst
  	 * @see #fireBuildParameterSeries(ui.parameterseries.ParameterCompareTemplatesDialog.TemplateRecord)
  	 */
  	private final transient Set<Consumer<ParameterCompareTemplatesDialog.TemplateRecord>> buildParameterSeriesListeners=new HashSet<>();
  
  	/**
  	 * Fgt einen Listener zu der Liste der Listener hinzu, die benachrichtigt werden sollen, wenn der Nutzer per Kontextmen die Erstellung einer Parameterreihe auslst.
  	 * @param listener	Zustzlicher zu benachrichtigender Listener
  	 * @return	Gibt an, ob der Listener zu der Liste der zu benachrichtigenden Listener hinzugefgt werden konnte
  	 */
  	public boolean addBuildParameterSeriesListener(final Consumer<ParameterCompareTemplatesDialog.TemplateRecord> listener) {
  		return buildParameterSeriesListeners.add(listener);
  	}
  
  	/**
  	 * Entfernt einen Listener aus der Liste der Listener, die benachrichtigt werden sollen, wenn der Nutzer per Kontextmen die Erstellung einer Parameterreihe auslst.
  	 * @param listener	Nicht mehr zu benachrichtigender Listener
  	 * @return	Gibt an, ob der Listener aus der Liste der zu benachrichtigenden Listener entfernt werden konnte
  	 */
  	public boolean removeBuildParameterSeriesListener(final Consumer<ParameterCompareTemplatesDialog.TemplateRecord> listener) {
  		return buildParameterSeriesListeners.remove(listener);
  	}
  
  	/**
  	 * Lst eine Benachrichtigung der Listener, die benachrichtigt werden sollen, wenn der Nutzer per Kontextmen die Erstellung einer Parameterreihe auslst, aus.
  	 * @param template	Vorlage fr die Parameterreihe
  	 */
  	private void fireBuildParameterSeries(final ParameterCompareTemplatesDialog.TemplateRecord template) {
  		buildParameterSeriesListeners.stream().forEach(listener->listener.accept(template));
  	}
  
  	/**
  	 * Reagiert auf Mausklicks auf der Zeichenflche
  	 */
  	private class ModelSurfacePanelMouseListener implements MouseListener {
  		/**
  		 * Konstruktor der Klasse
  		 */
  		public ModelSurfacePanelMouseListener() {
  			/*
  			 * Wird nur bentigt, um einen JavaDoc-Kommentar fr diesen (impliziten) Konstruktor
  			 * setzen zu knnen, damit der JavaDoc-Compiler keine Warnung mehr ausgibt.
  			 */
  		}
  
  		@Override
  		public void mouseClicked(MouseEvent e) {
  		}
  
  		@Override
  		public void mousePressed(MouseEvent e) {
  			requestFocus();
  			requestFocusInWindow();
  
  			/* Linksklick: Fly-out-zoom wieder zurcknehmen */
  			if (mode==ClickMode.MODE_NORMAL && SwingUtilities.isLeftMouseButton(e) && flyOutOriginalZoom>0) {
  				flyInZoom(e.getPoint());
  				return;
  			}
  
  			/* Linksklick: Drag-Start / Auswahl */
  			if (mode==ClickMode.MODE_NORMAL && SwingUtilities.isLeftMouseButton(e)) {
  				dragStartMousePosition=e.getPoint();
  				if (surface==null) return;
  				ModelElement element=surface.getElementAtPosition(e.getPoint(),zoom);
  				if (element!=null && !surface.isVisibleOnLayer(element)) element=null;
  				if (element==null) {
  					final ModelSurfaceLinks.Link link=emptyScreenLinkPosition(e.getPoint());
  					if (link!=null) {
  						fireLinkListener(link);
  					} else {
  						operationRunning=true;
  						try {
  							surface.setSelectedArea(null,zoom);
  							surface.setSelectedElement(null);
  						} finally {
  							operationRunning=false;
  							fireSelectionListener();
  						}
  						dragElement=null;
  					}
  				} else {
  					if (e.isShiftDown() && surface.getSelectedElement()!=null && surface.getSelectedElement()!=element) {
  						surface.getSelectedElement().setSelectedArea(true);
  						surface.setSelectedElement(element);
  						fireSelectionListener();
  						dragElement=element;
  					} else {
  						dragElement=element;
  						if (!readOnly || allowChangeOperationsOnReadOnly) dragStartElementBorderPointNr=element.getBorderPointNr(new Point((int)Math.round(e.getPoint().x/zoom),(int)Math.round(e.getPoint().y/zoom)));
  
  						if (!e.isShiftDown() && !element.isSelected() && !element.isSelectedArea()) {
  							surface.setSelectedArea(null,zoom);
  						}
  						if (!e.isShiftDown() && !element.isSelected() && element.isSelectedArea()) {
  							if (getSelectedElement()!=null && !getSelectedElement().isSelectedArea()) getSelectedElement().setSelectedArea(true);
  						}
  
  						surface.setSelectedElement(dragElement);
  						fireSelectionListener();
  						if (!readOnly || allowChangeOperationsOnReadOnly) {
  							if (dragStartElementBorderPointNr>=0) {
  								dragStartElementPosition=dragElement.getBorderPointPosition(dragStartElementBorderPointNr);
  							} else {
  								dragStartElementPosition=dragElement.getPosition(false);
  							}
  						}
  					}
  				}
  				fireStateChangeListener();
  				/* hier kein return, sonst funktioniert Doppelklick (s.u.) nicht mehr. */
  			}
  
  			/*
  			 *  Manchmal ist ein Klick zum Einfgen eines Elements auf die Zeichenflche ein Doppelklick,
  			 *  daher erst versuchen als Einfge-Klick zu interpretieren und dann erst Doppelklick-Prfung.
  			 */
  
  			/* Linksklick: Element hinzufgen */
  			if (SwingUtilities.isLeftMouseButton(e) && mode==ClickMode.MODE_ADD_ELEMENT && modeAddElement!=null) {
  				if (surface==null || readOnly) return;
  				final ModelElement element=modeAddElement.clone(model,surface);
  				final var p=new Point((int)Math.round(e.getX()/zoom),(int)Math.round(e.getY()/zoom));
  				element.setPosition(p);
  				final var pBase=modeAddElement.getPosition(true);
  				surface.add(element);
  				if (modeAddElementMore!=null) {
  					for (var el1: modeAddElementMore) {
  						final ModelElement el2=el1.clone(model,surface);
  						final var pEl=el1.getPosition(true);
  						el2.setPosition(new Point(pEl.x-pBase.x+p.x,pEl.y-pBase.y+p.y));
  						surface.add(el2);
  					}
  					modeAddElementMore=null;
  				}
  				if (modeAddElementSource!=null) {
  					if (modeAddElementSource.canAddEdgeOut() && (element instanceof ModelElementBox) && ((ModelElementBox)element).canAddEdgeIn()) {
  						final ModelElementEdge edge=new ModelElementEdge(model,surface,modeAddElementSource,element);
  						modeAddElementSource.addEdgeOut(edge);
  						((ModelElementBox)element).addEdgeIn(edge);
  						surface.add(edge);
  					}
  					modeAddElementSource=null;
  				}
  				setMode(ClickMode.MODE_NORMAL);
  				fireStateChangeListener();
  				e.consume();
  				return;
  			}
  
  			/* Doppelklick: Element bearbeiten */
  			if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount()==2) {
  				setMode(ClickMode.MODE_NORMAL);
  				if (surface!=null) showElementProperties(surface.getElementAtPosition(e.getPoint(),zoom),e.getModifiersEx());
  				e.consume();
  				return;
  			}
  
  			/* Linksklick: Erstes Element fr Kante gewhlt */
  			if (SwingUtilities.isLeftMouseButton(e) && mode==ClickMode.MODE_ADD_EDGE_STEP1) {
  				if (surface==null || readOnly) return;
  				final ModelElement element=surface.getElementAtPosition(e.getPoint(),zoom);
  				boolean ok=false;
  				if (element instanceof ModelElementPosition) ok=((ModelElementPosition)element).canAddEdgeOut();
  				if (ok) {
  					modeAddElement=element;
  					setMode(ClickMode.MODE_ADD_EDGE_STEP2);
  				}
  				e.consume();
  				return;
  			}
  
  			/* Linksklick oder mittlere Maustaste: Zweites Element fr Kante gewhlt -> Kante hinzufgen */
  			if ((SwingUtilities.isLeftMouseButton(e) || SwingUtilities.isMiddleMouseButton(e)) && mode==ClickMode.MODE_ADD_EDGE_STEP2 && modeAddElement!=null) {
  				if (surface==null || readOnly) return;
  				final ModelElement element=surface.getElementAtPosition(e.getPoint(),zoom);
  				if (canAddEdge(modeAddElement,element)) {
  					ModelElementEdge edge=new ModelElementEdge(model,surface,modeAddElement,element);
  					if (((ModelElementPosition)modeAddElement).addEdgeOut(edge)) {
  						if (((ModelElementPosition)element).addEdgeIn(edge)) {
  							surface.add(edge);
  						} else {
  							modeAddElement.removeConnectionNotify(edge);
  						}
  					}
  					setMode(ClickMode.MODE_ADD_EDGE_STEP1);
  					checkIfModelChanged(true);
  					fireStateChangeListener();
  				}
  				e.consume();
  				return;
  			}
  
  			/* Mittlere Maustaste: Erstes Element fr Kante whlen */
  			if (SwingUtilities.isMiddleMouseButton(e) && (mode==ClickMode.MODE_NORMAL || mode==ClickMode.MODE_ADD_EDGE_STEP1)) {
  				if (surface==null || readOnly) return;
  				final ModelElement element=surface.getElementAtPosition(e.getPoint(),zoom);
  				boolean ok=false;
  				if (element instanceof ModelElementPosition) ok=((ModelElementPosition)element).canAddEdgeOut();
  				if (ok) {
  					modeAddElement=element;
  					setMode(ClickMode.MODE_ADD_EDGE_STEP2);
  				} else {
  					switch (mode) {
  					case MODE_NORMAL: setMode(ClickMode.MODE_ADD_EDGE_STEP1); break;
  					case MODE_ADD_EDGE_STEP1: setMode(ClickMode.MODE_NORMAL); break;
  					default: /* Tritt nicht auf, ob in der if-Anweisung werden nur die beiden anderen case-Flle berhaupt zugelassen. */ break;
  					}
  				}
  				e.consume();
  				return;
  			}
  
  			/* Linksklick: Elemente aus Zwischenablage einfgen */
  			if (SwingUtilities.isLeftMouseButton(e) && (mode==ClickMode.MODE_INSERT_ELEMENTS_FROM_CLIPBOARD || mode==ClickMode.MODE_INSERT_ELEMENTS_FROM_TEMPLATE) && clipboardData!=null) {
  				surface.setSelectedElement(null);
  				surface.setSelectedArea(null,zoom);
  				surface.setTransferData(clipboardData,e.getPoint(),zoom);
  				clipboardData=null;
  				setMode(ClickMode.MODE_NORMAL);
  				fireStateChangeListener();
  				e.consume();
  				return;
  			}
  
  			/* Linksklick: Text aus Zwischenablage einfgen */
  			if (SwingUtilities.isLeftMouseButton(e) && mode==ClickMode.MODE_INSERT_TEXT && clipboardText!=null) {
  				surface.setSelectedElement(null);
  				surface.setSelectedArea(null,zoom);
  				final ModelElementText text=new ModelElementText(model,surface);
  				text.setText(clipboardText);
  				text.setPosition(new Point((int)Math.round(e.getPoint().x/zoom),(int)Math.round(e.getPoint().y/zoom)));
  				surface.add(text);
  				clipboardText=null;
  				setMode(ClickMode.MODE_NORMAL);
  				fireStateChangeListener();
  				e.consume();
  				return;
  			}
  
  			/* Linksklick: Bild aus Zwischenablage einfgen */
  			if (SwingUtilities.isLeftMouseButton(e) && mode==ClickMode.MODE_INSERT_IMAGE && clipboardImage!=null) {
  				surface.setSelectedElement(null);
  				surface.setSelectedArea(null,zoom);
  				final ModelElementImage image=new ModelElementImage(model,surface);
  				image.setImage(clipboardImage);
  				image.setPosition(new Point((int)Math.round(e.getPoint().x/zoom),(int)Math.round(e.getPoint().y/zoom)));
  				surface.add(image);
  				clipboardImage=null;
  				setMode(ClickMode.MODE_NORMAL);
  				fireStateChangeListener();
  				e.consume();
  				return;
  			}
  
  			/* Rechtsklick: Kontextmen */
  			if (SwingUtilities.isRightMouseButton(e)) {
  				setMode(ClickMode.MODE_NORMAL);
  				if (surface==null) return;
  				setToolTipText(null);
  				final ModelElement element=surface.getElementAtPosition(e.getPoint(),zoom);
  				surface.setSelectedArea(null,zoom);
  				if (element==null) {
  					if (showEditModelProperties) showModelContextMenu(e.getPoint());
  				} else {
  					surface.setSelectedElement(element);
  					fireStateChangeListener();
  					element.setModel(model);
  					element.showContextMenu(ModelSurfacePanel.this,e.getPoint(),readOnly,allowChangeOperationsOnReadOnly,(e.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK)==InputEvent.SHIFT_DOWN_MASK,ModelSurfacePanel.this,template->fireBuildParameterSeries(template),clientData,sequences,simData,mainAnimator);
  				}
  				fireSelectionListener();
  				e.consume();
  			}
  		}
  
  		@Override
  		public void mouseReleased(MouseEvent e) {
  			setCursor(Cursor.getDefaultCursor());
  			if (mode==ClickMode.MODE_NORMAL && SwingUtilities.isLeftMouseButton(e)) {
  				if (!e.isShiftDown() && dragStartMousePosition!=null && Math.abs(dragStartMousePosition.x-e.getX())+Math.abs(dragStartMousePosition.y-e.getY())==0) {
  					operationRunning=true;
  					try {
  						surface.setSelectedArea(null,1.0);
  					} finally {
  						operationRunning=false;
  						fireSelectionListener();
  					}
  					final ModelElement element=surface.getElementAtPosition(e.getPoint(),zoom);
  					if (element!=null) {
  						element.setSelected(true);
  						if (element instanceof ElementAnimationClickable) {
  							final Point click=e.getPoint();
  							final Point vertex=element.getPosition(true);
  							final int x=(int)Math.round(click.x/zoom)-vertex.x;
  							final int y=(int)Math.round(click.y/zoom)-vertex.y;
  							((ElementAnimationClickable)element).clicked(x,y,simData);
  							if (simData!=null && simData.simulator.isPaused()) {
  								mainAnimator.updateSurfaceAnimationDisplayElements(simData,false,true);
  							}
  						}
  					}
  				}
  
  				if (dragElement!=null && dragCopyElement!=null) {
  					surface.setSelectedArea(null,zoom);
  					surface.smartRename(dragCopyElement);
  					surface.setSelectedElement(dragCopyElement);
  				}
  				if (selectBox!=null) {
  					surface.setSelectedArea(selectBox,zoom);
  					fireStateChangeListener();
  				}
  				fireSelectionListener();
  				dragStartMousePosition=null;
  				dragStartElementBorderPointNr=-1;
  				dragStartElementPosition=null;
  				dragCopyElement=null;
  				selectBox=null;
  				repaint();
  			}
  
  			dragElement=null;
  			fireStateChangeListener(); /* Weil wir dies whrend des Drag&Drop explizit unterdrcken. */
  		}
  
  		@Override
  		public void mouseEntered(MouseEvent e) {
  			/*
  			Wrde die QuickAccess-Eingabezeile ganz erheblich stren:
  			requestFocus();
  			requestFocusInWindow();
  			 */
  		}
  
  		@Override
  		public void mouseExited(MouseEvent e) {
  		}
  	}
  
  	/**
  	 * Reagiert auf Mausbewegungen (Drag&amp;drop und Infos fr Tooltips beim
  	 * berfahren von Elementen usw.) auf der Zeichenflche
  	 */
  	private class ModelSurfacePanelMouseMotionListener implements MouseMotionListener {
  		/**
  		 * Konstruktor der Klasse
  		 */
  		public ModelSurfacePanelMouseMotionListener() {
  			/*
  			 * Wird nur bentigt, um einen JavaDoc-Kommentar fr diesen (impliziten) Konstruktor
  			 * setzen zu knnen, damit der JavaDoc-Compiler keine Warnung mehr ausgibt.
  			 */
  		}
  
  		@Override
  		public void mouseDragged(MouseEvent e) {
  			if ((readOnly && !allowChangeOperationsOnReadOnly) || dragStartMousePosition==null) return;
  			if (dragElement==null) {
  				/* Rahmen aufziehen */
  				if (readOnly) return;
  				int x=Math.min(dragStartMousePosition.x,e.getPoint().x);
  				int y=Math.min(dragStartMousePosition.y,e.getPoint().y);
  				int w=Math.abs(dragStartMousePosition.x-e.getPoint().x);
  				int h=Math.abs(dragStartMousePosition.y-e.getPoint().y);
  				selectBox=new Rectangle(x,y,w,h);
  				repaint();
  			} else {
  				if (!dragElement.isUserMoveable() || dragStartElementPosition==null) return;
  
  				GlassInfo.info(getParent(),Language.tr("Editor.SurfaceTooltip.Glass.DragElement"),500);
  
  				if ((e.isControlDown() && !e.isAltGraphDown()) && dragElement.canCopy() && !readOnly) {
  					/* Kopie platzieren */
  					final Point dragPos=dragElement.getPosition(false);
  					if (dragPos.x!=dragStartElementPosition.x || dragPos.y!=dragStartElementPosition.y) {
  						/* Aufrumen: Zurckversetzen */
  						dragElement.setPosition(dragStartElementPosition);
  						final Point subDelta=new Point(dragStartElementPosition.x-dragPos.x,dragStartElementPosition.y-dragPos.y);
  						for (ModelElement element : surface.getSelectedArea(true)) if (element instanceof ModelElementPosition && element!=dragElement) {
  							final Point point=((ModelElementPosition)element).getPosition(false);
  							point.translate(subDelta.x,subDelta.y);
  							((ModelElementPosition)element).setPosition(point);
  						}
  					}
  					if (dragCopyElement==null) {
  						/* Kopie anlegen */
  						dragCopyElement=dragElement.clone(model,surface);
  						dragCopyElement.setId(surface.getNextFreeId());
  						surface.add(dragCopyElement);
  					}
  					/* Element platzieren */
  					Point delta=new Point(e.getPoint());
  					delta.translate(-dragStartMousePosition.x,-dragStartMousePosition.y);
  					Point point=new Point(dragStartElementPosition);
  					point.translate((int)Math.round(delta.x/zoom),(int)Math.round(delta.y/zoom));
  					if (!e.isShiftDown()) {
  						point.x=5*(int)Math.round(point.x/5.0);
  						point.y=5*(int)Math.round(point.y/5.0);
  					}
  					if (e.isAltDown() || e.isAltGraphDown()) {
  						final int xDelta=Math.abs(dragStartElementPosition.x-point.x);
  						final int yDelta=Math.abs(dragStartElementPosition.y-point.y);
  						if (xDelta>yDelta) point.y=dragStartElementPosition.y; else point.x=dragStartElementPosition.x;
  					}
  					dragCopyElement.setPosition(point);
  				} else {
  					/* Elemente verschieben */
  					setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
  					if (dragCopyElement!=null) {
  						/* Aufrumen: Kopie entfernen */
  						surface.remove(dragCopyElement);
  						dragCopyElement=null;
  					}
  					Point delta=new Point(e.getPoint());
  					delta.translate(-dragStartMousePosition.x,-dragStartMousePosition.y);
  					Point point=new Point(dragStartElementPosition);
  					point.translate((int)Math.round(delta.x/zoom),(int)Math.round(delta.y/zoom));
  					Point subDelta;
  					if (dragStartElementBorderPointNr>=0) {
  						subDelta=dragElement.getBorderPointPosition(dragStartElementBorderPointNr);
  					} else {
  						subDelta=dragElement.getPosition(false);
  					}
  					if (!e.isShiftDown()) {
  						point.x=5*(int)Math.round(point.x/5.0);
  						point.y=5*(int)Math.round(point.y/5.0);
  					}
  					if (e.isAltDown() || e.isAltGraphDown()) {
  						final int xDelta=Math.abs(dragStartElementPosition.x-point.x);
  						final int yDelta=Math.abs(dragStartElementPosition.y-point.y);
  						if (xDelta>yDelta) point.y=dragStartElementPosition.y; else point.x=dragStartElementPosition.x;
  					}
  					subDelta.x=point.x-subDelta.x;
  					subDelta.y=point.y-subDelta.y;
  					operationRunning=true;
  					try {
  						if (dragStartElementBorderPointNr>=0) {
  							dragElement.setBorderPointPosition(dragStartElementBorderPointNr,point);
  						} else {
  							if (point.x<0) point.x=0;
  							if (point.y<0) point.y=0;
  							dragElement.setPosition(point);
  							for (ModelElement element : surface.getSelectedArea(true)) if (element instanceof ModelElementPosition && element!=dragElement) {
  								point=((ModelElementPosition)element).getPosition(false);
  								point.translate(subDelta.x,subDelta.y);
  								if (point.x<0) point.x=0;
  								if (point.y<0) point.y=0;
  								((ModelElementPosition)element).setPosition(point);
  							}
  						}
  					} finally {
  						operationRunning=false;
  						fireStateChangeListener();
  					}
  				}
  			}
  		}
  
  		@Override
  		public void mouseMoved(MouseEvent e) {
  			boolean ok;
  			ModelElement element;
  			String info;
  			String tooltip;
  			if (selectBox!=null) {
  				/* Wenn beim Loslassen der Maustaste irgendwie das Entfernen des Selektionsrahmens unterlassen wurde. */
  				selectBox=null;
  				repaint();
  			}
  			switch (mode) {
  			case MODE_NORMAL:
  				element=surface.getElementAtPosition(e.getPoint(),zoom);
  				if (element!=null && !surface.isVisibleOnLayer(element)) element=null;
  				if (element==null) {
  					setToolTipText(null);
  					final boolean handCursor=(emptyScreenLinkPosition(e.getPoint())!=null);
  					setCursor(handCursor?Cursor.getPredefinedCursor(Cursor.HAND_CURSOR):Cursor.getDefaultCursor());
  				} else {
  					tooltip=null;
  					if (element instanceof ModelElementPosition) tooltip=((ModelElementPosition)element).getToolTip();
  					if (tooltip==null) tooltip=element.getContextMenuElementName();
  					String description="";
  					if (element instanceof ModelElementBox) {
  						final String additional=getAdditionalTooltip(element);
  						if (additional!=null && !additional.isBlank()) tooltip=tooltip+"<br>"+additional;
  						description=getTooltipDescription((ModelElementBox)element);
  						if (description==null) description="";
  						if (!description.isEmpty()) description="<hr>"+description;
  					}
  					if (element instanceof ModelElementEdge) {
  						ModelElement el;
  						final ModelElementEdge edge=(ModelElementEdge)element;
  						final StringBuilder descriptionBuilder=new StringBuilder();
  						descriptionBuilder.append("<hr>\n<p>");
  						descriptionBuilder.append(Language.tr("Surface.Connection.Tooltip.From"));
  						descriptionBuilder.append(": ");
  						el=edge.getConnectionStart();
  						descriptionBuilder.append(el.getContextMenuElementName());
  						if (!el.getName().isEmpty()) {
  							descriptionBuilder.append(" (");
  							descriptionBuilder.append(el.getName());
  							descriptionBuilder.append(")");
  						}
  						descriptionBuilder.append(", id=");
  						descriptionBuilder.append(el.getId());
  
  						descriptionBuilder.append("\n<br>");
  						descriptionBuilder.append(Language.tr("Surface.Connection.Tooltip.To"));
  						descriptionBuilder.append(": ");
  						el=edge.getConnectionEnd();
  						descriptionBuilder.append(el.getContextMenuElementName());
  						if (!el.getName().isEmpty()) {
  							descriptionBuilder.append(" (");
  							descriptionBuilder.append(el.getName());
  							descriptionBuilder.append(")");
  						}
  						descriptionBuilder.append(", id=");
  						descriptionBuilder.append(el.getId());
  
  						final String additional=getAdditionalTooltip(element);
  						if (additional!=null && !additional.isBlank()) tooltip=tooltip+"<br>"+additional;
  
  						descriptionBuilder.append("<br>&nbsp;\n</p>");
  						description=descriptionBuilder.toString();
  					}
  					setToolTipText("<html>"+tooltip+"<br><b>id="+element.getId()+"</b>"+description+"</html>");
  					final int borderPoint=element.getBorderPointNr(new Point((int)Math.round(e.getPoint().x/zoom),(int)Math.round(e.getPoint().y/zoom)));
  					switch (borderPoint) {
  					case 0: setCursor(Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR)); break;
  					case 1: setCursor(Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR)); break;
  					case 2: setCursor(Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR)); break;
  					case 3: setCursor(Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR)); break;
  					default: setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); break;
  					}
  				}
  				break;
  			case MODE_ADD_ELEMENT:
  				setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
  				setToolTipText(null);
  				break;
  			case MODE_ADD_EDGE_STEP1:
  				ok=false;
  				element=surface.getElementAtPosition(e.getPoint(),zoom);
  				if (element instanceof ModelElementPosition) ok=((ModelElementPosition)element).canAddEdgeOut();
  				setCursor(ok?Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR):cursorNotAllowed);
  				tooltip=null;
  				if (element instanceof ModelElementPosition)  tooltip=((ModelElementPosition)element).getToolTip();
  				if (tooltip==null) tooltip=(element==null)?"":element.getContextMenuElementName();
  				info=(element==null)?"":(tooltip+" (id="+element.getId()+")<br>");
  				if (ok) setToolTipText("<html>"+info+"<span style=\"color: green;\"><b>"+Language.tr("Editor.SurfaceTooltip.InsertEdge1")+"</b></span></html>"); else setToolTipText("<html>"+info+"<b><span style=\"color: red\">"+Language.tr("Editor.SurfaceTooltip.NotEdge1")+"</b></span></html>");
  
  				break;
  			case MODE_ADD_EDGE_STEP2:
  				ok=false;
  				element=surface.getElementAtPosition(e.getPoint(),zoom);
  				if (element instanceof ModelElementPosition) ok=((ModelElementPosition)element).canAddEdgeIn();
  				setCursor(ok?Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR):cursorNotAllowed);
  				tooltip=null;
  				if (element instanceof ModelElementPosition)  tooltip=((ModelElementPosition)element).getToolTip();
  				if (tooltip==null) tooltip=(element==null)?"":element.getContextMenuElementName();
  				info=(element==null)?"":(tooltip+" (id="+element.getId()+")<br>");
  				if (ok) setToolTipText("<html>"+info+"<span style=\"color: green;\"><b>"+Language.tr("Editor.SurfaceTooltip.InsertEdge2")+"</b></span></html>"); else setToolTipText("<html>"+info+"<b><span style=\"color: red\">"+Language.tr("Editor.SurfaceTooltip.NotEdge2")+"</b></span></html>");
  				break;
  			case MODE_INSERT_ELEMENTS_FROM_CLIPBOARD:
  				setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
  				setToolTipText(Language.tr("Editor.SurfaceTooltip.PasteFromClipboard"));
  				break;
  			case MODE_INSERT_ELEMENTS_FROM_TEMPLATE:
  				setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
  				setToolTipText(Language.tr("Editor.SurfaceTooltip.PasteFromTemplate"));
  				break;
  			case MODE_INSERT_IMAGE:
  				setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
  				setToolTipText(Language.tr("Editor.SurfaceTooltip.PasteFromClipboard.Image"));
  				break;
  			case MODE_INSERT_TEXT:
  				setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
  				setToolTipText(Language.tr("Editor.SurfaceTooltip.PasteFromClipboard.Text"));
  				break;
  			}
  		}
  	}
  
  	/**
  	 * Reagiert auf Mausrad-Bewegungen auf der Zeichenflche
  	 */
  	private class ModelSurfacePanelMouseWheelListener implements MouseWheelListener {
  		/**
  		 * Konstruktor der Klasse
  		 */
  		public ModelSurfacePanelMouseWheelListener() {
  			/*
  			 * Wird nur bentigt, um einen JavaDoc-Kommentar fr diesen (impliziten) Konstruktor
  			 * setzen zu knnen, damit der JavaDoc-Compiler keine Warnung mehr ausgibt.
  			 */
  		}
  
  		@Override
  		public void mouseWheelMoved(MouseWheelEvent e) {
  			if (e.isControlDown() && !e.isAltGraphDown()) {
  				if (Math.abs(e.getWheelRotation())==1) {
  					/* Rad */
  					setZoom(Math.max(ZOOM_MIN,Math.min(ZOOM_MAX,zoom-ZOOM_STEP*e.getWheelRotation())),true,e.getX(),e.getY(),true);
  				}
  			} else {
  				if (getParent() instanceof JViewport) {
  					JViewport viewport=(JViewport)getParent();
  					Point point=viewport.getViewPosition();
  					if (e.isAltDown() || e.isAltGraphDown()) {
  						if (Math.abs(e.getWheelRotation())==1 && !e.isShiftDown()) {
  							/* Rad */
  							point.x+=viewport.getWidth()/WHEEL_SCROLL_FRACTION*e.getWheelRotation();
  							if (point.x<0) point.x=0;
  						} else {
  							/* Rad-Klick */
  							point.y+=viewport.getWidth()/WHEEL_SCROLL_FRACTION*Math.signum(e.getWheelRotation());
  							if (point.y<0) point.y=0;
  						}
  					} else {
  						if (Math.abs(e.getWheelRotation())==1 && !e.isShiftDown()) {
  							/* Rad */
  							point.y+=viewport.getHeight()/WHEEL_SCROLL_FRACTION*e.getWheelRotation();
  							if (point.y<0) point.y=0;
  						} else {
  							/* Rad-Klick */
  							point.x+=viewport.getWidth()/WHEEL_SCROLL_FRACTION*Math.signum(e.getWheelRotation());
  							if (point.x<0) point.x=0;
  						}
  					}
  					viewport.setViewPosition(point);
  				}
  			}
  			e.consume();
  			repaint();
  
  			GlassInfo.info(getParent(),Language.tr("Editor.SurfaceTooltip.Glass.MouseWheel"),500);
  		}
  	}
  
  	/**
  	 * Reagiert auf Tastendrcken auf der Zeichenflche
  	 */
  	private class ModelSurfacePanelKeyListener implements KeyListener {
  		/**
  		 * Konstruktor der Klasse
  		 */
  		public ModelSurfacePanelKeyListener() {
  			/*
  			 * Wird nur bentigt, um einen JavaDoc-Kommentar fr diesen (impliziten) Konstruktor
  			 * setzen zu knnen, damit der JavaDoc-Compiler keine Warnung mehr ausgibt.
  			 */
  		}
  
  		@Override
  		public void keyTyped(KeyEvent e) {
  		}
  
  		@Override
  		public void keyPressed(KeyEvent e) {
  			if (e.getKeyCode()==KeyEvent.VK_CONTEXT_MENU) {
  				final ModelElement element=surface.getSelectedElement();
  				if (element!=null) {
  					surface.setSelectedArea(null,zoom);
  					fireStateChangeListener();
  					fireSelectionListener();
  					Point point=element.getPosition(false);
  					point.x=(int)Math.round(point.x*zoom);
  					point.y=(int)Math.round(point.y*zoom);
  					point.translate(10,10);
  					element.setModel(model);
  					element.showContextMenu(ModelSurfacePanel.this,point,readOnly,allowChangeOperationsOnReadOnly,(e.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK)==InputEvent.SHIFT_DOWN_MASK,ModelSurfacePanel.this,template->fireBuildParameterSeries(template),clientData,sequences,simData,mainAnimator);
  				} else {
  					if (showEditModelProperties) {
  						Container c=getParent();
  						if (c!=null) {
  							c=c.getParent();
  							if (c!=null) showModelContextMenu(new Point(c.getWidth()/2,c.getHeight()/2));
  						}
  					}
  				}
  				e.consume();
  				return;
  			}
  
  			if (e.getKeyCode()==KeyEvent.VK_DELETE && !e.isControlDown() && !e.isShiftDown() && !e.isAltDown()) {
  				if (!readOnly) deleteSelectedElements();
  				fireStateChangeListener();
  				e.consume();
  				return;
  			}
  
  			if (e.getKeyCode()==KeyEvent.VK_DELETE && !e.isControlDown() && e.isShiftDown() && !e.isAltDown()) {
  				if (!readOnly) deleteSelectedElementAndCloseGap();
  				fireStateChangeListener();
  				e.consume();
  				return;
  			}
  
  			if (e.getKeyCode()==KeyEvent.VK_ENTER && e.isControlDown() && !e.isShiftDown() && !e.isAltDown()) {
  				showElementProperties(surface.getSelectedElement(),0);
  				fireStateChangeListener();
  				e.consume();
  				return;
  			}
  
  			if (e.getKeyCode()==KeyEvent.VK_ENTER && e.isControlDown() && e.isShiftDown() && !e.isAltDown()) {
  				final ModelElement element=surface.getSelectedElement();
  				if (element!=null && element.hasAnimationStatisticsData(simData)) element.showElementAnimationStatisticsData(ModelSurfacePanel.this,simData);
  				fireStateChangeListener();
  				e.consume();
  				return;
  			}
  
  			if (e.getKeyCode()==KeyEvent.VK_ENTER && !e.isControlDown() && e.isShiftDown() && !e.isAltDown()) {
  				showElementSubEditor(surface.getSelectedElement());
  				fireStateChangeListener();
  				e.consume();
  				return;
  			}
  
  			if ((e.getKeyCode()==KeyEvent.VK_LEFT || e.getKeyCode()==KeyEvent.VK_RIGHT || e.getKeyCode()==KeyEvent.VK_UP || e.getKeyCode()==KeyEvent.VK_DOWN) && e.isAltDown() && !e.isControlDown()) {
  				ModelElement element=surface.getSelectedElement();
  				if (element==null) {
  					List<ModelElement> list=surface.getSelectedArea(true);
  					if (list!=null && list.size()>0) element=list.get(0);
  				}
  
  				if (element instanceof ModelElementPosition && !(readOnly && !allowChangeOperationsOnReadOnly)) {
  					Point delta;
  					switch (e.getKeyCode()) {
  					case KeyEvent.VK_LEFT: delta=new Point(-1,0); break;
  					case KeyEvent.VK_RIGHT: delta=new Point(1,0); break;
  					case KeyEvent.VK_UP: delta=new Point(0,-1); break;
  					case KeyEvent.VK_DOWN: delta=new Point(0,1); break;
  					default: delta=new Point(1,0); break;
  					}
  					if (!e.isShiftDown()) {delta.x*=5; delta.y*=5;}
  					final Point point=element.getPosition(false);
  					final Point pointOrig=new Point(point);
  					point.x=Math.max(point.x+delta.x,0);
  					point.y=Math.max(point.y+delta.y,0);
  					if (!e.isShiftDown()) {
  						point.x=5*(int)Math.round(point.x/5.0);
  						point.y=5*(int)Math.round(point.y/5.0);
  					}
  					element.setPosition(point);
  					final int xShift=point.x-pointOrig.x;
  					final int yShift=point.y-pointOrig.y;
  					for (ModelElement areaElement: surface.getSelectedArea(true)) if (areaElement!=element && areaElement instanceof ModelElementPosition) {
  						final Point p=areaElement.getPosition(false);
  						p.translate(xShift,yShift);
  						areaElement.setPosition(p);
  					}
  					repaint();
  					fireStateChangeListener();
  					e.consume();
  					return;
  				}
  			}
  
  			if ((e.getKeyCode()==KeyEvent.VK_C || e.getKeyCode()==KeyEvent.VK_INSERT) && e.isControlDown() && !e.isAltDown() && !e.isShiftDown()) {
  				copyToClipboard();
  				e.consume();
  				return;
  			}
  
  			if (e.getKeyCode()==KeyEvent.VK_V && e.isControlDown() && !e.isAltDown() && !e.isShiftDown() && !readOnly) {
  				pasteFromClipboard();
  				e.consume();
  				return;
  			}
  
  			if (e.getKeyCode()==KeyEvent.VK_INSERT && !e.isControlDown() && !e.isAltDown() && e.isShiftDown() && !readOnly) {
  				pasteFromClipboard();
  				e.consume();
  				return;
  			}
  
  			if (e.getKeyCode()==KeyEvent.VK_PAGE_UP) {
  				moveSelectedElementToFront(e.isControlDown());
  				e.consume();
  				return;
  			}
  
  			if (e.getKeyCode()==KeyEvent.VK_PAGE_DOWN) {
  				moveSelectedElementToBack(e.isControlDown());
  				e.consume();
  				return;
  			}
  
  			if (e.getKeyCode()==KeyEvent.VK_ESCAPE && mode==ClickMode.MODE_ADD_EDGE_STEP2) {
  				setMode(ClickMode.MODE_ADD_EDGE_STEP1);
  				e.consume();
  				return;
  			}
  
  			if (e.getKeyCode()==KeyEvent.VK_ESCAPE && mode==ClickMode.MODE_ADD_EDGE_STEP1) {
  				setMode(ClickMode.MODE_NORMAL);
  				e.consume();
  				return;
  			}
  
  			if (e.getKeyCode()==KeyEvent.VK_ESCAPE && mode==ClickMode.MODE_NORMAL && !readOnly) {
  				setElementTemplatesVisibility(ElementTemplatesVisibility.TOGGLE);
  				e.consume();
  				return;
  			}
  
  			if (e.getKeyCode()==KeyEvent.VK_1 && e.isControlDown() && !readOnly) {
  				ModelElement element=surface.getSelectedElement();
  				if (element==null) {
  					List<ModelElement> list=surface.getSelectedArea(true);
  					if (list!=null && list.size()>0) element=list.get(0);
  				}
  				if (element instanceof ModelElementPosition) {
  					showQuickFixPopup((ModelElementPosition)element);
  					e.consume();
  				}
  			}
  		}
  
  		@Override
  		public void keyReleased(KeyEvent e) {
  		}
  	}
  }