/*
* Copyright 2015 Igor Maznitsa.
*
* 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 com.igormaznitsa.mindmap.swing.panel.ui;
import com.igormaznitsa.mindmap.swing.panel.MindMapPanelConfig;
import com.igormaznitsa.mindmap.model.Topic;
import static com.igormaznitsa.mindmap.swing.panel.StandardTopicAttribute.*;
import com.igormaznitsa.mindmap.swing.panel.utils.Utils;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.text.JTextComponent;
import com.igormaznitsa.mindmap.swing.panel.ui.gfx.MMGraphics;
import static com.igormaznitsa.meta.common.utils.Assertions.assertNotNull;
public abstract class AbstractElement {
@Nonnull
protected final Topic model;
@Nonnull
protected final TextBlock textBlock;
@Nonnull
protected final IconBlock extrasIconBlock;
@Nonnull
protected final VisualAttributeImageBlock visualAttributeImageBlock;
protected final Rectangle2D bounds = new Rectangle2D.Double();
protected final Dimension2D blockSize = new Dimension();
protected Color fillColor;
protected Color textColor;
protected Color borderColor;
@Nonnull
public String getText() {
return this.model.getText();
}
public void setText(@Nonnull final String text) {
this.model.setText(text);
this.textBlock.updateText(text);
}
protected AbstractElement(@Nonnull final AbstractElement orig) {
this.model = orig.model;
this.textBlock = new TextBlock(orig.textBlock);
this.extrasIconBlock = new IconBlock(orig.extrasIconBlock);
this.visualAttributeImageBlock = new VisualAttributeImageBlock(orig.visualAttributeImageBlock);
this.bounds.setRect(orig.bounds);
this.blockSize.setSize(orig.blockSize);
this.fillColor = orig.fillColor;
this.textColor = orig.textColor;
this.borderColor = orig.borderColor;
}
public AbstractElement(@Nonnull final Topic model) {
this.model = model;
this.textBlock = new TextBlock(this.model.getText(), TextAlign.CENTER);
this.textBlock.setTextAlign(TextAlign.findForName(model.getAttribute("align"))); //NOI18N
this.extrasIconBlock = new IconBlock(model);
this.visualAttributeImageBlock = new VisualAttributeImageBlock(model);
updateColorAttributeFromModel();
}
public final void updateColorAttributeFromModel() {
this.borderColor = Utils.html2color(this.model.getAttribute(ATTR_BORDER_COLOR.getText()), false);
this.textColor = Utils.html2color(this.model.getAttribute(ATTR_TEXT_COLOR.getText()), false);
this.fillColor = Utils.html2color(this.model.getAttribute(ATTR_FILL_COLOR.getText()), false);
}
@Nullable
public AbstractElement getParent() {
final Topic parent = this.model.getParent();
return parent == null ? null : (AbstractElement) parent.getPayload();
}
@Nonnull
public Topic getModel() {
return this.model;
}
@Nonnull
public TextAlign getTextAlign() {
return this.textBlock.getTextAlign();
}
public void setTextAlign(@Nonnull final TextAlign textAlign) {
this.textBlock.setTextAlign(textAlign);
this.model.setAttribute("align", this.textBlock.getTextAlign().name()); //NOI18N
}
public void updateElementBounds(@Nonnull final MMGraphics gfx, @Nonnull final MindMapPanelConfig cfg) {
this.visualAttributeImageBlock.updateSize(gfx, cfg);
this.textBlock.updateSize(gfx, cfg);
this.extrasIconBlock.updateSize(gfx, cfg);
final double scaledHorzBlockGap = cfg.getScale() * cfg.getHorizontalBlockGap();
double width = 0.0d;
if (this.visualAttributeImageBlock.mayHaveContent()) {
width += this.visualAttributeImageBlock.getBounds().getWidth() + scaledHorzBlockGap;
}
width += this.textBlock.getBounds().getWidth();
if (this.extrasIconBlock.hasContent()) {
width += this.extrasIconBlock.getBounds().getWidth() + scaledHorzBlockGap;
}
this.bounds.setRect(0d, 0d, width, Math.max(this.visualAttributeImageBlock.getBounds().getHeight(),Math.max(this.textBlock.getBounds().getHeight(), this.extrasIconBlock.getBounds().getHeight())));
}
public void updateBlockSize(@Nonnull final MindMapPanelConfig cfg) {
this.calcBlockSize(cfg, this.blockSize, false);
}
@Nonnull
public Dimension2D getBlockSize() {
return this.blockSize;
}
public void moveTo(final double x, final double y) {
this.bounds.setFrame(x, y, this.bounds.getWidth(), this.bounds.getHeight());
}
public void moveWholeTreeBranchCoordinates(final double deltaX, final double deltaY) {
moveTo(this.bounds.getX() + deltaX, this.bounds.getY() + deltaY);
for (final Topic t : this.model.getChildren()) {
final AbstractElement el = (AbstractElement) t.getPayload();
if (el != null) {
el.moveWholeTreeBranchCoordinates(deltaX, deltaY);
}
}
}
@Nonnull
public Rectangle2D getBounds() {
return this.bounds;
}
public final void doPaint(@Nonnull final MMGraphics g, @Nonnull final MindMapPanelConfig cfg, final boolean drawCollapsator) {
final MMGraphics gfx = g.copy();
try {
if (this.hasChildren() && !isCollapsed()) {
doPaintConnectors(g, isLeftDirection(), cfg);
}
final Rectangle clip = g.getClipBounds();
if (clip == null) {
gfx.translate(this.bounds.getX(), this.bounds.getY());
drawComponent(gfx, cfg, drawCollapsator);
} else if (clip.intersects(this.bounds)) {
gfx.translate(this.bounds.getX(), this.bounds.getY());
drawComponent(gfx, cfg, drawCollapsator);
}
} finally {
gfx.dispose();
}
}
public void doPaintConnectors(@Nonnull final MMGraphics g, final boolean leftDirection, @Nonnull final MindMapPanelConfig cfg) {
final Rectangle2D source = this.bounds;
for (final Topic t : this.model.getChildren()) {
drawConnector(g, source, (assertNotNull((AbstractElement) t.getPayload())).getBounds(), leftDirection, cfg);
}
}
public boolean hasChildren() {
return this.model.hasChildren();
}
@Nonnull
public JTextComponent fillByTextAndFont(@Nonnull final JTextComponent compo) {
this.textBlock.fillByTextAndFont(compo);
return compo;
}
public abstract void drawComponent(@Nonnull MMGraphics g, @Nonnull MindMapPanelConfig cfg, boolean drawCollapsator);
public abstract void drawConnector(@Nonnull MMGraphics g, @Nonnull Rectangle2D source, @Nonnull Rectangle2D destination, boolean leftDirection, @Nonnull MindMapPanelConfig cfg);
public abstract boolean isMoveable();
public abstract boolean isCollapsed();
public void alignElementAndChildren(@Nonnull MindMapPanelConfig cfg, boolean leftSide, double centerX, double centerY){
final double textMargin = cfg.getScale() * cfg.getTextMargins();
final double centralBlockLineY = textMargin + Math.max(this.visualAttributeImageBlock.getBounds().getHeight(), Math.max(this.textBlock.getBounds().getHeight(), this.extrasIconBlock.getBounds().getHeight())) / 2;
final double scaledHorzBlockGap = cfg.getScale() * cfg.getHorizontalBlockGap();
double offset = textMargin;
if (this.visualAttributeImageBlock.mayHaveContent()) {
this.visualAttributeImageBlock.setCoordOffset(offset, centralBlockLineY - this.visualAttributeImageBlock.getBounds().getHeight() / 2);
offset += this.visualAttributeImageBlock.getBounds().getWidth() + scaledHorzBlockGap;
}
this.textBlock.setCoordOffset(offset, centralBlockLineY - this.textBlock.getBounds().getHeight() / 2);
offset += this.textBlock.getBounds().getWidth() + scaledHorzBlockGap;
if (this.extrasIconBlock.hasContent()) {
this.extrasIconBlock.setCoordOffset(offset, centralBlockLineY - this.extrasIconBlock.getBounds().getHeight() / 2);
}
}
@Nonnull
public abstract Dimension2D calcBlockSize(@Nonnull MindMapPanelConfig cfg, @Nonnull Dimension2D size, boolean childrenOnly);
public abstract boolean hasDirection();
@Nonnull
public ElementPart findPartForPoint(@Nonnull final Point point) {
ElementPart result = ElementPart.NONE;
if (this.bounds.contains(point)) {
final double offX = point.getX() - this.bounds.getX();
final double offY = point.getY() - this.bounds.getY();
result = ElementPart.AREA;
if (this.visualAttributeImageBlock.getBounds().contains(offX, offY)) {
result = ElementPart.VISUAL_ATTRIBUTES;
} else {
if (this.textBlock.getBounds().contains(offX, offY)) {
result = ElementPart.TEXT;
} else if (this.extrasIconBlock.getBounds().contains(offX, offY)) {
result = ElementPart.ICONS;
}
}
}
return result;
}
@Nullable
public Topic findTopicBeforePoint(@Nonnull final MindMapPanelConfig cfg, @Nonnull final Point point) {
Topic result = null;
if (this.hasChildren()) {
if (this.isCollapsed()) {
return this.getModel().getLast();
} else {
double py = point.getY();
final double vertInset = cfg.getOtherLevelVerticalInset() * cfg.getScale();
Topic prev = null;
for (final Topic t : this.model.getChildren()) {
final AbstractElement el = assertNotNull((AbstractElement) t.getPayload());
final double childStartBlockY = el.calcBlockY();
final double childEndBlockY = childStartBlockY + el.getBlockSize().getHeight() + vertInset;
if (py < childEndBlockY) {
result = py < el.getBounds().getCenterY() ? prev : t;
break;
} else if (this.model.isLastChild(t)) {
result = t;
break;
}
prev = t;
}
}
}
return result;
}
protected double calcBlockY() {
return this.bounds.getY() - (this.blockSize.getHeight() - this.bounds.getHeight()) / 2;
}
protected double calcBlockX() {
return this.bounds.getX() - (this.isLeftDirection() ? this.blockSize.getWidth() - this.bounds.getWidth() : 0.0d);
}
@Nullable
public AbstractElement findNearestOpenedTopicToPoint(@Nullable final AbstractElement elementToIgnore, @Nonnull final Point point) {
return findNearestTopic(elementToIgnore, Double.MAX_VALUE, point);
}
@Nullable
private AbstractElement findNearestTopic(@Nullable final AbstractElement elementToIgnore, double maxDistance, @Nonnull final Point point) {
AbstractElement result = null;
if (elementToIgnore != this) {
final double dist = calcAverageDistanceToPoint(point);
if (dist < maxDistance) {
maxDistance = dist;
result = this;
}
}
if (!this.isCollapsed()) {
for (final Topic t : this.model.getChildren()) {
final AbstractElement element = t.getPayload() == null ? null : (AbstractElement) t.getPayload();
if (element != null) {
final AbstractElement nearestChild = element.findNearestTopic(elementToIgnore, maxDistance, point);
if (nearestChild != null) {
maxDistance = nearestChild.calcAverageDistanceToPoint(point);
result = nearestChild;
}
}
}
}
return result;
}
public double calcAverageDistanceToPoint(@Nonnull final Point point) {
final double d1 = point.distance(this.bounds.getX(), this.bounds.getY());
final double d2 = point.distance(this.bounds.getMaxX(), this.bounds.getY());
final double d3 = point.distance(this.bounds.getX(), this.bounds.getMaxY());
final double d4 = point.distance(this.bounds.getMaxX(), this.bounds.getMaxY());
return (d1 + d2 + d3 + d4) / (this.bounds.contains(point) ? 8.0d : 4.0d);
}
@Nullable
public AbstractElement findTopicBlockForPoint(@Nullable final Point point) {
AbstractElement result = null;
if (point != null) {
final double px = point.getX();
final double py = point.getY();
if (px >= calcBlockX() && py >= calcBlockY() && px < this.bounds.getX() + this.blockSize.getWidth() && py < this.bounds.getY() + this.blockSize.getHeight()) {
if (this.isCollapsed()) {
result = this;
} else {
AbstractElement foundChild = null;
for (final Topic t : this.model.getChildren()) {
final AbstractElement theElement = (AbstractElement) t.getPayload();
if (theElement != null) {
foundChild = theElement.findTopicBlockForPoint(point);
if (foundChild != null) {
break;
}
}
}
result = foundChild == null ? this : foundChild;
}
}
}
return result;
}
@Nullable
public AbstractElement findForPoint(@Nullable final Point point) {
AbstractElement result = null;
if (point != null) {
if (this.bounds.contains(point)) {
result = this;
} else {
for (final Topic t : this.model.getChildren()) {
final AbstractElement w = (AbstractElement) t.getPayload();
result = w == null ? null : w.findForPoint(point);
if (result != null) {
break;
}
}
}
}
return result;
}
public boolean isLeftDirection() {
return false;
}
@Nonnull
public TextBlock getTextBlock() {
return this.textBlock;
}
@Nonnull
public IconBlock getIconBlock() {
return this.extrasIconBlock;
}
@Nonnull
public VisualAttributeImageBlock getVisualAttributeImageBlock() {
return this.visualAttributeImageBlock;
}
public boolean collapseOrExpandAllChildren(final boolean collapse) {
boolean result = false;
if (this instanceof AbstractCollapsableElement) {
final AbstractCollapsableElement el = (AbstractCollapsableElement) this;
if (collapse) {
if (!el.isCollapsed()) {
el.setCollapse(true);
result = true;
}
} else if (el.isCollapsed()) {
el.setCollapse(false);
result = true;
}
}
if (this.hasChildren()) {
for (final Topic t : this.model.getChildren()) {
final AbstractElement e = (AbstractElement) t.getPayload();
if (e != null) {
result |= e.collapseOrExpandAllChildren(collapse);
}
}
}
return result;
}
@Nonnull
public abstract Color getBackgroundColor(@Nonnull MindMapPanelConfig config);
@Nonnull
public abstract Color getTextColor(@Nonnull MindMapPanelConfig config);
@Nonnull
public Color getBorderColor(@Nonnull final MindMapPanelConfig config) {
final Color dflt = this.borderColor == null ? config.getElementBorderColor() : this.borderColor;
return dflt;
}
@Nonnull
public abstract AbstractElement makeCopy();
}