SpriteLayoutProperties.java

/*
 * SmartSprites Project
 *
 * Copyright (C) 2007-2009, Stanisław Osiński.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * - Redistributions of  source code must  retain the above  copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice, this
 *   list of conditions and the following  disclaimer in  the documentation  and/or
 *   other materials provided with the distribution.
 *
 * - Neither the name of the SmartSprites Project nor the names of its contributors
 *   may  be used  to endorse  or  promote  products derived   from  this  software
 *   without specific prior written permission.
 *
 * - We kindly request that you include in the end-user documentation provided with
 *   the redistribution and/or in the software itself an acknowledgement equivalent
 *   to  the  following: "This product includes software developed by the SmartSprites
 *   Project."
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"  AND
 * ANY EXPRESS OR  IMPLIED WARRANTIES, INCLUDING,  BUT NOT LIMITED  TO, THE IMPLIED
 * WARRANTIES  OF  MERCHANTABILITY  AND  FITNESS  FOR  A  PARTICULAR  PURPOSE   ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE  FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL,  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL  DAMAGES
 * (INCLUDING, BUT  NOT LIMITED  TO, PROCUREMENT  OF SUBSTITUTE  GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS;  OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND  ON
 * ANY  THEORY  OF  LIABILITY,  WHETHER  IN  CONTRACT,  STRICT  LIABILITY,  OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE)  ARISING IN ANY WAY  OUT OF THE USE  OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.carrot2.labs.smartsprites;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;

import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.carrot2.labs.smartsprites.SpriteImageDirective.SpriteImageLayout;
import org.carrot2.labs.smartsprites.css.CssProperty;
import org.carrot2.labs.smartsprites.css.CssSyntaxUtils;
import org.carrot2.labs.smartsprites.message.Message.MessageType;
import org.carrot2.labs.smartsprites.message.MessageLog;

/**
 * Represents common sprite layout properties that can be used both in {@link SpriteImageDirective} and
 * {@link SpriteReferenceDirective}.
 */
public class SpriteLayoutProperties {

    /** The Constant PROPERTY_SPRITE_ALIGNMENT. */
    public static final String PROPERTY_SPRITE_ALIGNMENT = "sprite-alignment";

    /** The Constant PROPERTY_SPRITE_MARGIN_BOTTOM. */
    public static final String PROPERTY_SPRITE_MARGIN_BOTTOM = "sprite-margin-bottom";

    /** The Constant PROPERTY_SPRITE_MARGIN_TOP. */
    public static final String PROPERTY_SPRITE_MARGIN_TOP = "sprite-margin-top";

    /** The Constant PROPERTY_SPRITE_MARGIN_RIGHT. */
    public static final String PROPERTY_SPRITE_MARGIN_RIGHT = "sprite-margin-right";

    /** The Constant PROPERTY_SPRITE_MARGIN_LEFT. */
    public static final String PROPERTY_SPRITE_MARGIN_LEFT = "sprite-margin-left";

    /** Allowed properties of this directive. */
    static final Set<String> ALLOWED_PROPERTIES = ImmutableSet.of(PROPERTY_SPRITE_ALIGNMENT,
            PROPERTY_SPRITE_MARGIN_LEFT, PROPERTY_SPRITE_MARGIN_RIGHT, PROPERTY_SPRITE_MARGIN_TOP,
            PROPERTY_SPRITE_MARGIN_BOTTOM);

    /**
     * Alignment of the individual image within the sprite image.
     */
    public enum SpriteAlignment {
        /**
         * To the left edge of a vertical sprite.
         */
        LEFT,

        /**
         * To the right edge of a vertical sprite.
         */
        RIGHT,

        /**
         * To the top edge of a horizontal sprite.
         */
        TOP,

        /**
         * To the bottom edge of a horizontal sprite.
         */
        BOTTOM,

        /**
         * Repeated across the full width/ height of the sprite image.
         */
        REPEAT,

        /** To the center of a vertical or horizontal sprite. */
        CENTER;

        /** The value. */
        private final String value;

        /**
         * Instantiates a new sprite alignment.
         */
        SpriteAlignment() {
            this.value = name().toLowerCase(Locale.ENGLISH);
        }

        @Override
        public String toString() {
            return value;
        }

        /**
         * Gets the value.
         *
         * @param value
         *            the value
         *
         * @return the value
         */
        public static SpriteAlignment getValue(String value) {
            return valueOf(value.toUpperCase(Locale.ENGLISH));
        }

        /**
         * Values as string.
         *
         * @return the string
         */
        public static String valuesAsString() {
            final String list = Lists.newArrayList(values()).toString();
            return list.substring(1, list.length() - 1);
        }
    }

    /** Alignment of this individual image. */
    public final SpriteAlignment alignment;

    /** Left margin of the individual image. */
    public final int marginLeft;

    /** Right margin of the individual image. */
    public final int marginRight;

    /** Top margin of the individual image. */
    public final int marginTop;

    /** Bottom margin of the individual image. */
    public final int marginBottom;

    /**
     * Instantiates a new sprite layout properties.
     *
     * @param alignment
     *            the alignment
     * @param marginLeft
     *            the margin left
     * @param marginRight
     *            the margin right
     * @param marginTop
     *            the margin top
     * @param marginBottom
     *            the margin bottom
     */
    public SpriteLayoutProperties(SpriteAlignment alignment, int marginLeft, int marginRight, int marginTop,
            int marginBottom) {
        this.alignment = alignment;
        this.marginLeft = marginLeft;
        this.marginRight = marginRight;
        this.marginTop = marginTop;
        this.marginBottom = marginBottom;
    }

    /**
     * Creates an instance with default values.
     *
     * @param layout
     *            the layout
     */
    SpriteLayoutProperties(SpriteImageLayout layout) {
        this(getDefaultAlignment(layout), 0, 0, 0, 0);
    }

    /**
     * Parses a {@link SpriteLayoutProperties} from the provided {@link String} logging messages to the provided
     * {@link MessageLog}.
     *
     * @param directiveString
     *            the directive string
     * @param spriteImageLayout
     *            the sprite image layout
     * @param messageCollector
     *            the message collector
     *
     * @return the sprite layout properties
     */
    public static SpriteLayoutProperties parse(String directiveString, SpriteImageLayout spriteImageLayout,
            MessageLog messageCollector) {
        return parse(directiveString, spriteImageLayout, new SpriteLayoutProperties(spriteImageLayout),
                messageCollector);
    }

    /**
     * Parses a {@link SpriteLayoutProperties} from the provided {@link String}, using the provided defaults and logging
     * messages to the provided {@link MessageLog}.
     *
     * @param directiveString
     *            the directive string
     * @param spriteImageLayout
     *            the sprite image layout
     * @param defaults
     *            the defaults
     * @param messageCollector
     *            the message collector
     *
     * @return the sprite layout properties
     */
    public static SpriteLayoutProperties parse(String directiveString, SpriteImageLayout spriteImageLayout,
            SpriteLayoutProperties defaults, MessageLog messageCollector) {
        final Map<String, CssProperty> rules = CssSyntaxUtils
                .propertiesAsMap(CssSyntaxUtils.extractRules(directiveString, messageCollector));

        // We don't check for allowed properties here. The check, including
        // sprite layout properties will be done when parsing the directive
        // that embeds sprite layout properties.

        // Alignment is optional
        SpriteAlignment alignment;
        if (CssSyntaxUtils.hasNonBlankValue(rules, PROPERTY_SPRITE_ALIGNMENT)) {
            final String alignmentValue = rules.get(PROPERTY_SPRITE_ALIGNMENT).value;
            try {
                alignment = correctAlignment(spriteImageLayout, SpriteAlignment.getValue(alignmentValue),
                        messageCollector);
            } catch (final IllegalArgumentException e) {
                messageCollector.warning(MessageType.UNSUPPORTED_ALIGNMENT, alignmentValue);
                alignment = getDefaultAlignment(spriteImageLayout);
            }
        } else {
            alignment = defaults.alignment;
        }

        // Parse margins
        final int marginLeft = getMargin(PROPERTY_SPRITE_MARGIN_LEFT, rules, defaults.marginLeft, messageCollector);
        final int marginRight = getMargin(PROPERTY_SPRITE_MARGIN_RIGHT, rules, defaults.marginRight, messageCollector);
        final int marginTop = getMargin(PROPERTY_SPRITE_MARGIN_TOP, rules, defaults.marginTop, messageCollector);
        final int marginBottom = getMargin(PROPERTY_SPRITE_MARGIN_BOTTOM, rules, defaults.marginBottom,
                messageCollector);

        return new SpriteLayoutProperties(alignment, marginLeft, marginRight, marginTop, marginBottom);
    }

    /**
     * Corrects sprite alignment if necessary based on the layout of the enclosing sprite image.
     *
     * @param spriteImageLayout
     *            the sprite image layout
     * @param alignment
     *            the alignment
     * @param messageCollector
     *            the message collector
     *
     * @return the sprite alignment
     */
    private static SpriteAlignment correctAlignment(SpriteImageLayout spriteImageLayout, SpriteAlignment alignment,
            MessageLog messageCollector) {
        if (spriteImageLayout.equals(SpriteImageLayout.HORIZONTAL)) {
            if (alignment.equals(SpriteAlignment.LEFT) || alignment.equals(SpriteAlignment.RIGHT)) {
                messageCollector.warning(MessageType.ONLY_TOP_OR_BOTTOM_ALIGNMENT_ALLOWED, alignment.value);
                return SpriteAlignment.TOP;
            }
        } else if (alignment.equals(SpriteAlignment.TOP) || alignment.equals(SpriteAlignment.BOTTOM)) {
            messageCollector.warning(MessageType.ONLY_LEFT_OR_RIGHT_ALIGNMENT_ALLOWED, alignment.value);
            return SpriteAlignment.LEFT;
        }

        return alignment;
    }

    /**
     * Returns default alignment for given sprite image directive.
     *
     * @param spriteImageLayout
     *            the sprite image layout
     *
     * @return the default alignment
     */
    private static SpriteAlignment getDefaultAlignment(SpriteImageLayout spriteImageLayout) {
        if (spriteImageLayout.equals(SpriteImageLayout.HORIZONTAL)) {
            return SpriteAlignment.TOP;
        }
        return SpriteAlignment.LEFT;
    }

    /**
     * Parses margin value.
     *
     * @param marginRule
     *            the margin rule
     * @param rules
     *            the rules
     * @param defaultMargin
     *            the default margin
     * @param messageLog
     *            the message log
     *
     * @return the margin
     */
    private static int getMargin(String marginRule, Map<String, CssProperty> rules, int defaultMargin,
            MessageLog messageLog) {
        if (!CssSyntaxUtils.hasNonBlankValue(rules, marginRule)) {
            return defaultMargin;
        }
        final String rawMarginValue = rules.get(marginRule).value;
        String marginValue = rawMarginValue;
        if (marginValue.toLowerCase(Locale.ENGLISH).endsWith("px")) {
            marginValue = marginValue.substring(0, marginValue.length() - 2);
        }
        try {
            int marginIntValue = Integer.parseInt(marginValue);
            if (marginIntValue < 0) {
                messageLog.warning(MessageType.IGNORING_NEGATIVE_MARGIN_VALUE, marginRule);
                marginIntValue = 0;
            }

            return marginIntValue;
        } catch (final NumberFormatException e) {
            messageLog.warning(MessageType.CANNOT_PARSE_MARGIN_VALUE, rawMarginValue);
            return 0;
        }
    }
}