// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.mappaint.styleelement;
import java.awt.Color;
import java.awt.Rectangle;
import java.util.Objects;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.visitor.paint.MapPaintSettings;
import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer;
import org.openstreetmap.josm.gui.mappaint.Cascade;
import org.openstreetmap.josm.gui.mappaint.Environment;
import org.openstreetmap.josm.gui.mappaint.Keyword;
import org.openstreetmap.josm.gui.mappaint.MultiCascade;
import org.openstreetmap.josm.tools.CheckParameterUtil;
/**
* Text style attached to a style with a bounding box, like an icon or a symbol.
*/
public class BoxTextElement extends StyleElement {
/**
* MapCSS text-anchor-horizontal
*/
public enum HorizontalTextAlignment { LEFT, CENTER, RIGHT }
/**
* MapCSS text-anchor-vertical
*/
public enum VerticalTextAlignment { ABOVE, TOP, CENTER, BOTTOM, BELOW }
/**
* Something that provides us with a {@link BoxProviderResult}
* @since 10600 (functional interface)
*/
@FunctionalInterface
public interface BoxProvider {
/**
* Compute and get the {@link BoxProviderResult}. The temporary flag is set if the result of the computation may change in the future.
* @return The result of the computation.
*/
BoxProviderResult get();
}
/**
* A box rectangle with a flag if it is temporary.
*/
public static class BoxProviderResult {
private final Rectangle box;
private final boolean temporary;
public BoxProviderResult(Rectangle box, boolean temporary) {
this.box = box;
this.temporary = temporary;
}
/**
* Returns the box.
* @return the box
*/
public Rectangle getBox() {
return box;
}
/**
* Determines if the box can change in future calls of the {@link BoxProvider#get()} method
* @return {@code true} if the box can change in future calls of the {@code BoxProvider#get()} method
*/
public boolean isTemporary() {
return temporary;
}
}
/**
* A {@link BoxProvider} that always returns the same non-temporary rectangle
*/
public static class SimpleBoxProvider implements BoxProvider {
private final Rectangle box;
/**
* Constructs a new {@code SimpleBoxProvider}.
* @param box the box
*/
public SimpleBoxProvider(Rectangle box) {
this.box = box;
}
@Override
public BoxProviderResult get() {
return new BoxProviderResult(box, false);
}
@Override
public int hashCode() {
return Objects.hash(box);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
SimpleBoxProvider that = (SimpleBoxProvider) obj;
return Objects.equals(box, that.box);
}
}
/**
* The default style a simple node should use for it's text
*/
public static final BoxTextElement SIMPLE_NODE_TEXT_ELEMSTYLE;
static {
MultiCascade mc = new MultiCascade();
Cascade c = mc.getOrCreateCascade("default");
c.put(TEXT, Keyword.AUTO);
Node n = new Node();
n.put("name", "dummy");
SIMPLE_NODE_TEXT_ELEMSTYLE = create(new Environment(n, mc, "default", null), NodeElement.SIMPLE_NODE_ELEMSTYLE.getBoxProvider());
if (SIMPLE_NODE_TEXT_ELEMSTYLE == null) throw new AssertionError();
}
/**
* Caches the default text color from the preferences.
*
* FIXME: the cache isn't updated if the user changes the preference during a JOSM
* session. There should be preference listener updating this cache.
*/
private static volatile Color defaultTextColorCache;
/**
* The text this element should display.
*/
public TextLabel text;
/**
* The {@link HorizontalTextAlignment} for this text.
*/
public HorizontalTextAlignment hAlign;
/**
* The {@link VerticalTextAlignment} for this text.
*/
public VerticalTextAlignment vAlign;
protected BoxProvider boxProvider;
/**
* Create a new {@link BoxTextElement}
* @param c The current cascade
* @param text The text to display
* @param boxProvider The box provider to use
* @param hAlign The {@link HorizontalTextAlignment}
* @param vAlign The {@link VerticalTextAlignment}
*/
public BoxTextElement(Cascade c, TextLabel text, BoxProvider boxProvider,
HorizontalTextAlignment hAlign, VerticalTextAlignment vAlign) {
super(c, 5f);
CheckParameterUtil.ensureParameterNotNull(text);
CheckParameterUtil.ensureParameterNotNull(hAlign);
CheckParameterUtil.ensureParameterNotNull(vAlign);
this.text = text;
this.boxProvider = boxProvider;
this.hAlign = hAlign;
this.vAlign = vAlign;
}
/**
* Create a new {@link BoxTextElement} with a boxprovider and a box.
* @param env The MapCSS environment
* @param boxProvider The box provider.
* @return A new {@link BoxTextElement} or <code>null</code> if the creation failed.
*/
public static BoxTextElement create(Environment env, BoxProvider boxProvider) {
initDefaultParameters();
TextLabel text = TextLabel.create(env, defaultTextColorCache, false);
if (text == null) return null;
// Skip any primitives that don't have text to draw. (Styles are recreated for any tag change.)
// The concrete text to render is not cached in this object, but computed for each
// repaint. This way, one BoxTextElement object can be used by multiple primitives (to save memory).
if (text.labelCompositionStrategy.compose(env.osm) == null) return null;
Cascade c = env.mc.getCascade(env.layer);
HorizontalTextAlignment hAlign;
switch (c.get(TEXT_ANCHOR_HORIZONTAL, Keyword.RIGHT, Keyword.class).val) {
case "left":
hAlign = HorizontalTextAlignment.LEFT;
break;
case "center":
hAlign = HorizontalTextAlignment.CENTER;
break;
case "right":
default:
hAlign = HorizontalTextAlignment.RIGHT;
}
VerticalTextAlignment vAlign;
switch (c.get(TEXT_ANCHOR_VERTICAL, Keyword.BOTTOM, Keyword.class).val) {
case "above":
vAlign = VerticalTextAlignment.ABOVE;
break;
case "top":
vAlign = VerticalTextAlignment.TOP;
break;
case "center":
vAlign = VerticalTextAlignment.CENTER;
break;
case "below":
vAlign = VerticalTextAlignment.BELOW;
break;
case "bottom":
default:
vAlign = VerticalTextAlignment.BOTTOM;
}
return new BoxTextElement(c, text, boxProvider, hAlign, vAlign);
}
/**
* Get the box in which the content should be drawn.
* @return The box.
*/
public Rectangle getBox() {
return boxProvider.get().getBox();
}
private static void initDefaultParameters() {
if (defaultTextColorCache != null) return;
defaultTextColorCache = PaintColors.TEXT.get();
}
@Override
public void paintPrimitive(OsmPrimitive osm, MapPaintSettings settings, StyledMapRenderer painter,
boolean selected, boolean outermember, boolean member) {
if (osm instanceof Node) {
painter.drawBoxText((Node) osm, this);
}
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
if (!super.equals(obj)) return false;
BoxTextElement that = (BoxTextElement) obj;
return hAlign == that.hAlign &&
vAlign == that.vAlign &&
Objects.equals(text, that.text) &&
Objects.equals(boxProvider, that.boxProvider);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), text, boxProvider, hAlign, vAlign);
}
@Override
public String toString() {
return "BoxTextElement{" + super.toString() + ' ' + text.toStringImpl()
+ " box=" + getBox() + " hAlign=" + hAlign + " vAlign=" + vAlign + '}';
}
}