/*
* 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 com.igormaznitsa.mindmap.swing.panel.StandardTopicAttribute;
import com.igormaznitsa.mindmap.swing.panel.utils.MindMapUtils;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.geom.Dimension2D;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.awt.Color;
import com.igormaznitsa.mindmap.swing.panel.ui.gfx.StrokeType;
import com.igormaznitsa.mindmap.swing.panel.ui.gfx.MMGraphics;
import static com.igormaznitsa.meta.common.utils.Assertions.assertNotNull;
public abstract class AbstractCollapsableElement extends AbstractElement {
protected final Rectangle2D collapsatorZone = new Rectangle2D.Double();
protected AbstractCollapsableElement(@Nonnull final AbstractCollapsableElement element) {
super(element);
this.collapsatorZone.setRect(element.collapsatorZone);
}
public AbstractCollapsableElement(@Nonnull final Topic model) {
super(model);
}
protected void drawCollapsator(@Nonnull final MMGraphics g, @Nonnull final MindMapPanelConfig cfg, final boolean collapsed) {
final int x = (int) Math.round(collapsatorZone.getX());
final int y = (int) Math.round(collapsatorZone.getY());
final int w = (int) Math.round(collapsatorZone.getWidth());
final int h = (int) Math.round(collapsatorZone.getHeight());
final int DELTA = (int) Math.round(cfg.getCollapsatorSize() * 0.3d * cfg.getScale());
g.setStroke(cfg.safeScaleFloatValue(cfg.getCollapsatorBorderWidth(), 0.1f),StrokeType.SOLID);
final Color linecolor = cfg.getCollapsatorBorderColor();
g.drawOval(x, y, w, h, linecolor, cfg.getCollapsatorBackgroundColor());
g.drawLine(x + DELTA, y + h / 2, x + w - DELTA, y + h / 2, linecolor);
if (collapsed) {
g.drawLine(x + w / 2, y + DELTA, x + w / 2, y + h - DELTA, linecolor);
}
}
@Override
@Nullable
public ElementPart findPartForPoint(@Nonnull final Point point) {
ElementPart result = super.findPartForPoint(point);
if (result == ElementPart.NONE) {
if (this.hasChildren()) {
if (this.collapsatorZone.contains(point.getX() - this.bounds.getX(), point.getY() - this.bounds.getY())) {
result = ElementPart.COLLAPSATOR;
}
}
}
return result;
}
@Override
public boolean isCollapsed() {
return MindMapUtils.isCollapsed(this.model);
}
public void setCollapse(final boolean collapseElementFlag) {
MindMapUtils.setCollapsed(this.model, collapseElementFlag);
}
public void collapseAllFirstLevelChildren() {
for (final Topic t : this.model.getChildren()) {
MindMapUtils.setCollapsed(t, true);
}
}
@Override
public boolean isLeftDirection() {
return isLeftSidedTopic(this.model);
}
public void setLeftDirection(final boolean leftSide) {
makeTopicLeftSided(this.model, leftSide);
}
public static boolean isLeftSidedTopic(@Nonnull final Topic t) {
return "true".equals(t.getAttribute(StandardTopicAttribute.ATTR_LEFTSIDE.getText()));
}
public static void makeTopicLeftSided(@Nonnull final Topic topic, final boolean left) {
if (left) {
topic.setAttribute(StandardTopicAttribute.ATTR_LEFTSIDE.getText(), "true");//NOI18N
} else {
topic.setAttribute(StandardTopicAttribute.ATTR_LEFTSIDE.getText(), null);
}
}
@Override
@Nonnull
public Dimension2D calcBlockSize(@Nonnull final MindMapPanelConfig cfg, @Nullable final Dimension2D size, final boolean childrenOnly) {
final Dimension2D result = size == null ? new Dimension() : size;
final double scaledVInset = cfg.getScale() * cfg.getOtherLevelVerticalInset();
final double scaledHInset = cfg.getScale() * cfg.getOtherLevelHorizontalInset();
double width = childrenOnly ? 0.0d : this.bounds.getWidth();
double height = childrenOnly ? 0.0d : this.bounds.getHeight();
if (this.hasChildren()) {
if (!this.isCollapsed()) {
width += childrenOnly ? 0.0d : scaledHInset;
final double baseWidth = childrenOnly ? 0.0d : width;
double childrenHeight = 0.0d;
boolean notFirstChiild = false;
for (final Topic t : this.model.getChildren()) {
if (notFirstChiild) {
childrenHeight += scaledVInset;
} else {
notFirstChiild = true;
}
((AbstractElement) assertNotNull(t.getPayload())).calcBlockSize(cfg, result, false);
width = Math.max(baseWidth + result.getWidth(), width);
childrenHeight += result.getHeight();
}
height = Math.max(height, childrenHeight);
} else if (!childrenOnly) {
width += cfg.getCollapsatorSize() * cfg.getScale();
}
}
result.setSize(width, height);
return result;
}
@Override
public void alignElementAndChildren(@Nonnull final MindMapPanelConfig cfg, final boolean leftSide, final double leftX, final double topY) {
super.alignElementAndChildren(cfg, leftSide, leftX, topY);
final double horzInset = cfg.getOtherLevelHorizontalInset() * cfg.getScale();
double childrenX;
final double COLLAPSATORSIZE = cfg.getCollapsatorSize() * cfg.getScale();
final double COLLAPSATORDISTANCE = cfg.getCollapsatorSize() * 0.1d * cfg.getScale();
final double collapsatorX;
if (leftSide) {
childrenX = leftX + this.blockSize.getWidth() - this.bounds.getWidth();
this.moveTo(childrenX, topY + (this.blockSize.getHeight() - this.bounds.getHeight()) / 2);
childrenX -= horzInset;
collapsatorX = -COLLAPSATORSIZE - COLLAPSATORDISTANCE;
} else {
childrenX = leftX;
this.moveTo(childrenX, topY + (this.blockSize.getHeight() - this.bounds.getHeight()) / 2);
childrenX += this.bounds.getWidth() + horzInset;
collapsatorX = this.bounds.getWidth() + COLLAPSATORDISTANCE;
}
this.collapsatorZone.setRect(collapsatorX, (this.bounds.getHeight() - COLLAPSATORSIZE) / 2, COLLAPSATORSIZE, COLLAPSATORSIZE);
if (!this.isCollapsed()) {
final double vertInset = cfg.getOtherLevelVerticalInset() * cfg.getScale();
final Dimension2D childBlockSize = calcBlockSize(cfg, null, true);
double currentY = topY + (this.blockSize.getHeight() - childBlockSize.getHeight()) / 2.0d;
boolean notFirstChild = false;
for (final Topic t : this.model.getChildren()) {
if (notFirstChild) {
currentY += vertInset;
} else {
notFirstChild = true;
}
final AbstractElement w = (AbstractElement) assertNotNull(t.getPayload());
w.alignElementAndChildren(cfg, leftSide, leftSide ? childrenX - w.getBlockSize().getWidth() : childrenX, currentY);
currentY += w.getBlockSize().getHeight();
}
}
}
@Override
public void doPaintConnectors(@Nonnull final MMGraphics g, final boolean leftDirection, @Nonnull final MindMapPanelConfig cfg) {
final Rectangle2D source = new Rectangle2D.Double(this.bounds.getX() + this.collapsatorZone.getX(), this.bounds.getY() + this.collapsatorZone.getY(), this.collapsatorZone.getWidth(), this.collapsatorZone.getHeight());
final boolean lefDir = isLeftDirection();
for (final Topic t : this.model.getChildren()) {
this.drawConnector(g, source, (assertNotNull((AbstractElement) t.getPayload())).getBounds(), lefDir, cfg);
}
}
@Override
public void drawConnector(@Nonnull final MMGraphics g, @Nonnull final Rectangle2D source, @Nonnull final Rectangle2D destination, final boolean leftDirection, @Nonnull final MindMapPanelConfig cfg) {
g.setStroke(cfg.safeScaleFloatValue(cfg.getConnectorWidth(), 0.1f),StrokeType.SOLID);
final double dy = Math.abs(destination.getCenterY() - source.getCenterY());
if (dy < (16.0d * cfg.getScale())) {
g.drawLine((int) source.getCenterX(), (int) source.getCenterY(), (int) destination.getCenterX(), (int) source.getCenterY(), cfg.getConnectorColor());
} else {
final Path2D path = new Path2D.Double();
path.moveTo(source.getCenterX(), source.getCenterY());
if (leftDirection) {
final double dx = source.getCenterX() - destination.getMaxX();
path.lineTo((source.getCenterX() - dx / 2), source.getCenterY());
path.lineTo((source.getCenterX() - dx / 2), destination.getCenterY());
path.lineTo(destination.getCenterX(), destination.getCenterY());
} else {
final double dx = destination.getX() - source.getCenterX();
path.lineTo((source.getCenterX() + dx / 2), source.getCenterY());
path.lineTo((source.getCenterX() + dx / 2), destination.getCenterY());
path.lineTo(destination.getCenterX(), destination.getCenterY());
}
g.draw(path,cfg.getConnectorColor(),null);
}
}
@Override
@Nullable
public AbstractElement findForPoint(@Nullable final Point point) {
AbstractElement result = null;
if (point != null) {
if (this.bounds.contains(point.getX(), point.getY()) || this.collapsatorZone.contains(point.getX() - this.bounds.getX(), point.getY() - this.bounds.getY())) {
result = this;
} else if (!isCollapsed()) {
final double topZoneY = this.bounds.getY() - (this.blockSize.getHeight() - this.bounds.getHeight()) / 2;
final double topZoneX = isLeftDirection() ? this.bounds.getMaxX() - this.blockSize.getWidth() : this.bounds.getX();
if (point.getX() >= topZoneX && point.getY() >= topZoneY && point.getX() < (this.blockSize.getWidth() + topZoneX) && point.getY() < (this.blockSize.getHeight() + topZoneY)) {
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;
}
@Override
public void updateElementBounds(@Nonnull final MMGraphics gfx, @Nonnull final MindMapPanelConfig cfg) {
super.updateElementBounds(gfx, cfg);
final double marginOffset = ((cfg.getTextMargins() + cfg.getElementBorderWidth()) * 2.0d) * cfg.getScale();
this.bounds.setRect(this.bounds.getX(), this.bounds.getY(), this.bounds.getWidth() + marginOffset, this.bounds.getHeight() + marginOffset);
}
@Nonnull
public Rectangle2D getCollapsatorArea() {
return this.collapsatorZone;
}
@Override
public boolean hasDirection() {
return true;
}
}