package nodebox.node; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import nodebox.graphics.Color; import nodebox.graphics.Point; import nodebox.util.StringUtils; import java.util.ArrayList; import java.util.List; import java.util.Locale; import static com.google.common.base.Preconditions.*; public final class Port { public static final String TYPE_INT = "int"; public static final String TYPE_FLOAT = "float"; public static final String TYPE_STRING = "string"; public static final String TYPE_BOOLEAN = "boolean"; public static final String TYPE_POINT = "point"; public static final String TYPE_COLOR = "color"; public static final String TYPE_LIST = "list"; public static final String TYPE_GEOMETRY = "geometry"; public static final String TYPE_CONTEXT = "context"; public static final String TYPE_STATE = "state"; public enum Attribute {NAME, TYPE, LABEL, CHILD_REFERENCE, WIDGET, RANGE, VALUE, DESCRIPTION, MINIMUM_VALUE, MAXIMUM_VALUE, MENU_ITEMS} /** * The UI control for this port. This defines how the port is represented in the user interface. */ public enum Widget { NONE, ANGLE, COLOR, DATA, FILE, FLOAT, FONT, GRADIENT, IMAGE, INT, MENU, SEED, STRING, TEXT, PASSWORD, TOGGLE, POINT } public enum Direction { INPUT, OUTPUT } public enum Range { VALUE, LIST } public static final Range DEFAULT_RANGE = Range.VALUE; public static final ImmutableMap<String, Object> DEFAULT_VALUES; public static final ImmutableSet<String> STANDARD_TYPES; public static final ImmutableMap<String, ImmutableList<Port.Widget>> WIDGET_MAPPING; static { ImmutableMap.Builder<String, Object> b = ImmutableMap.builder(); b.put(TYPE_INT, 0L); b.put(TYPE_FLOAT, 0.0); b.put(TYPE_BOOLEAN, false); b.put(TYPE_STRING, ""); b.put(TYPE_POINT, Point.ZERO); b.put(TYPE_COLOR, Color.BLACK); DEFAULT_VALUES = b.build(); STANDARD_TYPES = ImmutableSet.of(TYPE_INT, TYPE_FLOAT, TYPE_BOOLEAN, TYPE_STRING, TYPE_POINT, TYPE_COLOR); ImmutableMap.Builder<String, ImmutableList<Port.Widget>> w = ImmutableMap.builder(); w.put(TYPE_INT, ImmutableList.of(Widget.INT, Widget.SEED)); w.put(TYPE_FLOAT, ImmutableList.of(Widget.ANGLE, Widget.FLOAT)); w.put(TYPE_BOOLEAN, ImmutableList.of(Widget.TOGGLE)); w.put(TYPE_STRING, ImmutableList.of(Widget.DATA, Widget.FILE, Widget.FONT, Widget.IMAGE, Widget.MENU, Widget.STRING, Widget.TEXT)); w.put(TYPE_POINT, ImmutableList.of(Widget.POINT)); w.put(TYPE_COLOR, ImmutableList.of(Widget.COLOR)); WIDGET_MAPPING = w.build(); } private final String name; private final String type; private final String label; private final String description; private final String childReference; private final Widget widget; private final Range range; private final Object value; private final Double minimumValue; private final Double maximumValue; private final ImmutableList<MenuItem> menuItems; private final transient int hashCode; public static Port intPort(String name, long value) { return intPort(name, value, null, null); } public static Port intPort(String name, long value, Integer minimumValue, Integer maximumValue) { checkNotNull(value, "Value cannot be null."); return new Port(name, TYPE_INT, value, minimumValue != null ? minimumValue.doubleValue() : null, maximumValue != null ? maximumValue.doubleValue() : null); } public static Port floatPort(String name, double value) { return floatPort(name, value, null, null); } public static Port floatPort(String name, double value, Double minimumValue, Double maximumValue) { checkNotNull(value, "Value cannot be null."); return new Port(name, TYPE_FLOAT, value, minimumValue, maximumValue); } public static Port booleanPort(String name, boolean value) { checkNotNull(value, "Value cannot be null."); return new Port(name, TYPE_BOOLEAN, value); } public static Port stringPort(String name, String value) { checkNotNull(value, "Value cannot be null."); return new Port(name, TYPE_STRING, value); } public static Port stringPort(String name, String value, Iterable<MenuItem> menuItems) { checkNotNull(value, "Value cannot be null."); return new Port(name, TYPE_STRING, value, menuItems); } public static Port pointPort(String name, Point value) { checkNotNull(value, "Value cannot be null."); return new Port(name, TYPE_POINT, value); } public static Port colorPort(String name, Color value) { checkNotNull(value, "Value cannot be null."); return new Port(name, TYPE_COLOR, value); } public static Port customPort(String name, String type) { checkNotNull(type, "Type cannot be null."); return new Port(name, type, null); } public static Port publishedPort(Node childNode, Port childPort, String publishedName) { checkNotNull(childNode); checkNotNull(childPort); String childReference = buildChildReference(childNode, childPort); return new Port(publishedName, childPort.getType(), "", childReference, childPort.getWidget(), childPort.getRange(), childPort.getValue(), childPort.getDescription(), childPort.getMinimumValue(), childPort.getMaximumValue(), childPort.getMenuItems()); } /** * Parse the type and create the appropriate Port. Use the default value appropriate for the port type. * * @param name The port name. * @param type The port type. * @return A new Port. */ public static Port portForType(String name, String type) { checkNotNull(type, "Type cannot be null."); // If the type is not found in the default values, get() returns null, which is what we need for custom types. return new Port(name, type, "", null, defaultWidgetForType(type), DEFAULT_RANGE, DEFAULT_VALUES.get(type), "", null, null, ImmutableList.<MenuItem>of()); } /** * Create a new Port with the given value as a string parsed to the correct format. * * @param name The port name. * @param type The port type. * @param stringValue The port value as a string, e.g. "32.5". * @return A new Port. */ public static Port parsedPort(String name, String type, String stringValue) { return parsedPort(name, type, "", "", DEFAULT_RANGE.toString().toLowerCase(Locale.US), stringValue, "", null, null, ImmutableList.<MenuItem>of()); } /** * Create a new Port with the given value as a string parsed to the correct format. * * @param name The port name. * @param type The port type. * @param valueString The port value as a string, e.g. "32.5". * @param minString The minimum value as a string. * @param maxString The maximum value as a string. * @param menuItems The list of menu items. * @return A new Port. */ public static Port parsedPort(String name, String type, String label, String widgetString, String rangeString, String valueString, String description, String minString, String maxString, ImmutableList<MenuItem> menuItems) { checkNotNull(name, "Name cannot be null."); checkNotNull(type, "Type cannot be null."); if (STANDARD_TYPES.contains(type)) { Object value; if (valueString == null) { value = DEFAULT_VALUES.get(type); checkNotNull(value); } else { if (type.equals("int")) { value = Long.valueOf(valueString); } else if (type.equals("float")) { value = Double.valueOf(valueString); } else if (type.equals("string")) { value = valueString; } else if (type.equals("boolean")) { value = Boolean.valueOf(valueString); } else if (type.equals("point")) { value = Point.valueOf(valueString); } else if (type.equals("color")) { value = Color.valueOf(valueString); } else { throw new AssertionError("Unknown type " + type); } } Widget widget; if (widgetString != null && !widgetString.isEmpty()) { widget = parseWidget(widgetString); } else { widget = Widget.NONE; } Range range = rangeString != null ? parseRange(rangeString) : DEFAULT_RANGE; Double minimumValue = null; Double maximumValue = null; if (minString != null) minimumValue = Double.valueOf(minString); if (maxString != null) maximumValue = Double.valueOf(maxString); return new Port(name, type, label, null, widget, range, value, description, minimumValue, maximumValue, menuItems); } else { return Port.customPort(name, type); } } public static Widget defaultWidgetForType(String type) { checkNotNull(type, "Type cannot be null."); if (type.equals(TYPE_INT)) { return Widget.INT; } else if (type.equals(TYPE_FLOAT)) { return Widget.FLOAT; } else if (type.equals(TYPE_STRING)) { return Widget.STRING; } else if (type.equals(TYPE_BOOLEAN)) { return Widget.TOGGLE; } else if (type.equals(TYPE_POINT)) { return Widget.POINT; } else if (type.equals(TYPE_COLOR)) { return Widget.COLOR; } else { return Widget.NONE; } } private Port(String name, String type, Object value) { this(name, type, "", null, defaultWidgetForType(type), DEFAULT_RANGE, value, "", null, null, ImmutableList.<MenuItem>of()); } private Port(String name, String type, Object value, Double minimumValue, Double maximumValue) { this(name, type, "", null, defaultWidgetForType(type), DEFAULT_RANGE, value, "", minimumValue, maximumValue, ImmutableList.<MenuItem>of()); } private Port(String name, String type, Object value, Iterable<MenuItem> menuItems) { this(name, type, "", null, defaultWidgetForType(type), DEFAULT_RANGE, value, "", null, null, menuItems); } private Port(String name, String type, String label, String childReference, Widget widget, Range range, Object value, String description, Double minimumValue, Double maximumValue, Iterable<MenuItem> menuItems) { checkNotNull(name, "Name cannot be null."); checkNotNull(type, "Type cannot be null."); checkNotNull(menuItems, "Menu items cannot be null."); this.name = name; this.type = type; this.label = label; this.childReference = childReference; this.widget = widget; this.range = range; this.minimumValue = minimumValue; this.maximumValue = maximumValue; this.value = clampValue(value); this.description = description; this.menuItems = ImmutableList.copyOf(menuItems); this.hashCode = Objects.hashCode(name, type, value); } public String getName() { return name; } public String getLabel() { return label; } public String getDisplayLabel() { if (label != null && ! label.isEmpty()) return label; return StringUtils.humanizeName(name); } public String getType() { return type; } public String getDescription() { return description; } public boolean isPublishedPort() { return childReference != null; } public String getChildReference() { return childReference; } public String getChildNodeName() { return childReference == null ? null : childReference.split("\\.")[0]; } public String getChildPortName() { return childReference == null ? null : childReference.split("\\.")[1]; } public Node getChildNode(Node network) { return network.getChild(getChildNodeName()); } public Port getChildPort(Node network) { Node child = network.getChild(getChildNodeName()); return child.getInput(getChildPortName()); } public Range getRange() { return range; } public boolean hasValueRange() { return range.equals(Range.VALUE); } public boolean hasListRange() { return range.equals(Range.LIST); } public Double getMinimumValue() { return minimumValue; } public Double getMaximumValue() { return maximumValue; } public boolean hasMenu() { return !menuItems.isEmpty(); } public List<MenuItem> getMenuItems() { return menuItems; } /** * Check if the Port type is a standard type, meaning it can be persisted, and its value can be accessed. * * @return true if this is a standard type. */ public boolean isStandardType() { return STANDARD_TYPES.contains(type); } /** * Check if the Port type is a custom type. * * @return true if this is a custom type. */ public boolean isCustomType() { return !isStandardType(); } /** * Return the value stored in the port as a long. * <ul> * <li>Integers are returned as-is.</li> * <li>Floats are rounded using Math.round().</li> * <li>Other types return 0.</li> * </ul> * * @return The value as a long or 0 if the value cannot be converted. */ public long intValue() { checkValueType(); if (type.equals(TYPE_INT)) { return (Long) value; } else if (type.equals(TYPE_FLOAT)) { return Math.round((Double) value); } else { return 0L; } } /** * Return the value stored in the port as a Float. * <ul> * <li>Integers are converted to Floats.</li> * <li>Floats are returned as-is.</li> * <li>Other types return 0f.</li> * </ul> * * @return The value as a Float or 0f if the value cannot be converted. */ public double floatValue() { checkValueType(); if (type.equals(TYPE_INT)) { return ((Long) value).doubleValue(); } else if (type.equals(TYPE_FLOAT)) { return (Double) value; } else { return 0.0; } } /** * Return the value stored in the port as a String. * <p/> * This conversion simply uses String.valueOf(), which does the right thing. * * @return The value as a String or "null" if the value is null. (for custom types) * @see String#valueOf(Object) */ public String stringValue() { checkValueType(); return String.valueOf(value); } /** * Return the value stored in the port as a boolean. * <p/> * If the port has a different type, false is returned. * * @return The value as a Float or 0f if the value cannot be converted. */ public boolean booleanValue() { checkValueType(); if (type.equals(TYPE_BOOLEAN)) { return (Boolean) value; } else { return false; } } /** * Return the value stored in the port as a Port. * <p/> * If the port has a different type, Point.ZERO is returned. * * @return The value as a Point or Point.ZERO if the value is of an incorrect type. */ public Point pointValue() { checkValueType(); if (type.equals(TYPE_POINT)) { return (Point) value; } else { return Point.ZERO; } } public Color colorValue() { checkValueType(); if (type.equals(TYPE_COLOR)) { return (Color) value; } else { return Color.BLACK; } } /** * Return the value stored in the port as an Object. * <p/> * If this is a port with a custom type, this method returns null. * * @return The value as an Object or null. */ public Object getValue() { checkValueType(); return value; } //// Shim implementations of methods //// public boolean hasExpression() { return false; } public String getExpression() { return ""; } public boolean isEnabled() { return true; } public Widget getWidget() { return widget; } public boolean isFileWidget() { return widget == Widget.FILE || widget == Widget.IMAGE; } //// Mutation methods //// public Port withLabel(String label) { return new Port(getName(), getType(), label, getChildReference(), getWidget(), getRange(), getValue(), getDescription(), getMinimumValue(), getMaximumValue(), getMenuItems()); } public Port withDescription(String description) { return new Port(getName(), getType(), getLabel(), getChildReference(), getWidget(), getRange(), getValue(), description, getMinimumValue(), getMaximumValue(), getMenuItems()); } public Port withChildReference(Node childNode, Port childPort) { checkNotNull(childNode); checkNotNull(childPort); String childReference = buildChildReference(childNode, childPort); return new Port(getName(), getType(), this.label, childReference, getWidget(), getRange(), getValue(), getDescription(), getMinimumValue(), getMaximumValue(), getMenuItems()); } private static String buildChildReference(Node childNode, Port childPort) { checkNotNull(childNode); checkNotNull(childPort); return String.format("%s.%s", childNode.getName(), childPort.getName()); } /** * Return a new Port with the value set to the given value. * * @param value The new value. This must be of the correct type. * @return The new Port. * @throws IllegalStateException If you're trying to change the value of a standard type, or you give the wrong value. */ public Port withValue(Object value) { checkState(isStandardType(), "You can only change the value of a standard type."); checkArgument(correctValueForType(value), "Value '%s' is not correct for %s port.", value, getType()); return new Port(getName(), getType(), getLabel(), getChildReference(), getWidget(), getRange(), clampValue(convertValue(getType(), value)), getDescription(), getMinimumValue(), getMaximumValue(), getMenuItems()); } /** * Return a new Port with the widget set to the given widget value. * * @param widget The new widget. * @return The new Port. */ public Port withWidget(Widget widget) { return new Port(getName(), getType(), getLabel(), getChildReference(), widget, getRange(), getValue(), getDescription(), getMinimumValue(), getMaximumValue(), getMenuItems()); } /** * Return a new Port with the range set to the given range value. * * @param range The new range. * @return The new Port. */ public Port withRange(Range range) { return new Port(getName(), getType(), getLabel(), getChildReference(), getWidget(), range, getValue(), getDescription(), getMinimumValue(), getMaximumValue(), getMenuItems()); } /** * Convert integers to longs and floats to doubles. All other values are passed through as-is. * * @param type The expected type. * @param value The original value. * @return The converted value. */ private Object convertValue(String type, Object value) { if (value instanceof Integer) { checkArgument(type.equals(TYPE_INT)); return (long) ((Integer) value); } else if (value instanceof Float) { checkArgument(type.equals(TYPE_FLOAT)); return (double) ((Float) value); } else { return value; } } /** * Convert integers to longs and floats to doubles. All other values are passed through as-is. * * @param value The original value. * @return The converted value. */ public Object clampValue(Object value) { if (getType().equals(TYPE_FLOAT)) { return clamp((Double) value); } else if (getType().equals(TYPE_INT)) { return (long) clamp(((Long) value).doubleValue()); } else { return value; } } private double clamp(double v) { if (minimumValue != null && v < minimumValue) { return minimumValue; } else if (maximumValue != null && v > maximumValue) { return maximumValue; } else { return v; } } private void checkValueType() { checkState(correctValueForType(this.value), "The internal value %s is not a %s.", value, type); } private boolean correctValueForType(Object value) { if (type.equals(TYPE_INT)) { return value instanceof Long || value instanceof Integer; } else if (type.equals(TYPE_FLOAT)) { return value instanceof Double || value instanceof Float; } else if (type.equals(TYPE_STRING)) { return value instanceof String; } else if (type.equals(TYPE_BOOLEAN)) { return value instanceof Boolean; } else if (type.equals(TYPE_POINT)) { return value instanceof Point; } else if (type.equals(TYPE_COLOR)) { return value instanceof Color; } else { // The value of a custom type should always be null. return value == null; } } public Object getAttributeValue(Attribute attribute) { if (attribute == Attribute.NAME) { return getName(); } else if (attribute == Attribute.TYPE) { return getType(); } else if (attribute == Attribute.LABEL) { return getLabel(); } else if (attribute == Attribute.DESCRIPTION) { return getDescription(); } else if (attribute == Attribute.CHILD_REFERENCE) { return getChildReference(); } else if (attribute == Attribute.WIDGET) { return getWidget(); } else if (attribute == Attribute.RANGE) { return getRange(); } else if (attribute == Attribute.MINIMUM_VALUE) { return getMinimumValue(); } else if (attribute == Attribute.MAXIMUM_VALUE) { return getMaximumValue(); } else if (attribute == Attribute.MENU_ITEMS) { return getMenuItems(); } else { throw new AssertionError("Unknown port attribute " + attribute); } } public static Object parseValue(String type, String valueString) { if (type.equals("int")) { return Long.valueOf(valueString); } else if (type.equals("float")) { return Double.valueOf(valueString); } else if (type.equals("string")) { return valueString; } else if (type.equals("boolean")) { return Boolean.valueOf(valueString); } else if (type.equals("point")) { return Point.valueOf(valueString); } else if (type.equals("color")) { return Color.valueOf(valueString); } else { throw new AssertionError("Unknown type " + type); } } private static Widget parseWidget(String valueString) { return Widget.valueOf(valueString.toUpperCase(Locale.US)); } private static Range parseRange(String valueString) { if (valueString.equals("value")) return Range.VALUE; else if (valueString.equals("list")) return Range.LIST; else throw new AssertionError("Unknown range " + valueString); } public Port withMinimumValue(Double minimumValue) { checkArgument(type.equals(Port.TYPE_INT) || type.equals(Port.TYPE_FLOAT), "You can only set a minimum value on int or float ports, not %s", this); return new Port(getName(), getType(), getLabel(), getChildReference(), getWidget(), getRange(), getValue(), getDescription(), minimumValue, getMaximumValue(), getMenuItems()); } public Port withMaximumValue(Double maximumValue) { checkArgument(type.equals(Port.TYPE_INT) || type.equals(Port.TYPE_FLOAT), "You can only set a maximum value on int or float ports, not %s", this); return new Port(getName(), getType(), getLabel(), getChildReference(), getWidget(), getRange(), getValue(), getDescription(), getMinimumValue(), maximumValue, getMenuItems()); } public Port withMenuItems(Iterable<MenuItem> items) { checkNotNull(items); checkArgument(type.equals(Port.TYPE_STRING), "You can only use menu items on string ports, not %s", this); return new Port(getName(), getType(), getLabel(), getChildReference(), getWidget(), getRange(), getValue(), getDescription(), getMinimumValue(), getMaximumValue(), items); } public Port withMenuItemAdded(String key, String label) { ImmutableList.Builder<MenuItem> b = ImmutableList.builder(); b.addAll(menuItems); b.add(new MenuItem(key, label)); return withMenuItems(b.build()); } public Port withMenuItemRemoved(MenuItem menuItem) { ImmutableList.Builder<MenuItem> b = ImmutableList.builder(); for (MenuItem item : menuItems) { if (item.equals(menuItem)) { // Do nothing } else { b.add(item); } } return withMenuItems(b.build()); } public Port withMenuItemMovedUp(int index) { checkArgument(0 < index && index < menuItems.size()); return withMenuItemMoved(index, index - 1); } public Port withMenuItemMovedDown(int index) { checkArgument(0 <= index && index < menuItems.size() - 1); return withMenuItemMoved(index, index + 1); } private Port withMenuItemMoved(int fromIndex, int toIndex) { List<MenuItem> items = new ArrayList<MenuItem>(0); items.addAll(menuItems); MenuItem item = items.get(fromIndex); items.remove(item); items.add(toIndex, item); return withMenuItems(ImmutableList.copyOf(items)); } public Port withMenuItemChanged(int index, String key, String label) { checkArgument(0 <= index && index < menuItems.size()); List<MenuItem> items = new ArrayList<MenuItem>(0); items.addAll(menuItems); items.set(index, new MenuItem(key, label)); return withMenuItems(ImmutableList.copyOf(items)); } public Port withParsedAttribute(Attribute attribute, String valueString) { checkNotNull(valueString); String name = this.name; String type = this.type; String label = this.label; String childReference = this.childReference; Widget widget = this.widget; Range range = this.range; Object value = this.value; String description = this.description; Double minimumValue = this.minimumValue; Double maximumValue = this.maximumValue; switch (attribute) { case LABEL: label = valueString; break; case DESCRIPTION: description = valueString; break; case CHILD_REFERENCE: childReference = valueString; break; case VALUE: checkArgument(STANDARD_TYPES.contains(type), "Port %s: you can only set the value for one of the standard types, not %s (value=%s)", name, type, valueString); value = parseValue(type, valueString); break; case WIDGET: widget = parseWidget(valueString); break; case RANGE: range = parseRange(valueString); break; case MINIMUM_VALUE: minimumValue = Double.valueOf(valueString); break; case MAXIMUM_VALUE: maximumValue = Double.valueOf(valueString); break; default: throw new AssertionError("You cannot use withParsedAttribute with attribute " + attribute); } return new Port(name, type, label, childReference, widget, range, value, description, minimumValue, maximumValue, getMenuItems()); } //// Object overrides //// @Override public int hashCode() { return hashCode; } @Override public boolean equals(Object o) { if (!(o instanceof Port)) return false; final Port other = (Port) o; return Objects.equal(name, other.name) && Objects.equal(type, other.type) && Objects.equal(label, other.label) && Objects.equal(value, other.value) && Objects.equal(description, other.description) && Objects.equal(range, other.range); } @Override public String toString() { return String.format("<Port %s (%s): %s>", name, type, value); } }