Subversion Repositories mkgmap

Rev

Rev 2188 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * Copyright (C) 2011.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */

package uk.me.parabola.imgfmt.app.typ;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import uk.me.parabola.imgfmt.app.BitWriter;
import uk.me.parabola.imgfmt.app.ImgFileWriter;
import uk.me.parabola.imgfmt.app.Writeable;

/**
 * Holds colour information for elements in the typ file.
 *
 * The Colour information can relate to a bitmap or solid shapes.
 *
 * @author Steve Ratcliffe
 */

public class ColourInfo implements Writeable, AlphaAdder {
        private static final int S_NIGHT = 1;
        private static final int S_DAY_TRANSPARENT = 0x2;
        private static final int S_NIGHT_TRANSPARENT = 0x4;
        private static final int S_HAS_BITMAP = 0x8;

        private int numberOfColours;
        private int numberOfSolidColours;

        private boolean hasBitmap;
        private boolean hasBorder;
        private final List<RgbWithTag> colours = new ArrayList<RgbWithTag>();
        private final Map<String, Integer> indexMap = new HashMap<String, Integer>();

        private char width;
        private char height;
        private char charsPerPixel;

        private boolean simple = true;
        private char colourMode;

        /**
         * Add a colour for this element.
         * @param tag The xpm tag that represents the colour.
         * @param rgb The actual colour.
         */

        public void addColour(String tag, Rgb rgb) {
                RgbWithTag cwt = new RgbWithTag(tag, rgb);
                colours.add(cwt);
        }

        /**
         * Add a transparent colour. Convenience routine.
         */

        public void addTransparent(String colourTag) {
                addColour(colourTag, new Rgb(0, 0, 0, 0));
        }

        public void setHasBitmap(boolean hasBitmap) {
                this.hasBitmap = hasBitmap;
        }

        /**
         * The colour scheme in use. This is a bitmask that has the following bits:
         * 0 - Has night colour
         * 1 - day background colour is transparent
         * 2 - night background colour is transparent
         * 3 - has bitmap
         *
         * If there is no night colour, then set the night background colour bit to be the same as
         * the day one.
         *
         * @return The colour scheme bitmask. The term colour scheme is historical, it doesn't really
         * describe it.
         */

        public int getColourScheme() {
                if (numberOfColours == 0)
                        numberOfColours = colours.size();
               
                int scheme = 0;
                if (hasBitmap)
                        scheme |= S_HAS_BITMAP;

                if (numberOfColours == 4)
                        scheme |= S_NIGHT;

                if (!hasBitmap && !hasBorder && numberOfColours == 2)
                        scheme |= S_NIGHT | S_DAY_TRANSPARENT | S_NIGHT_TRANSPARENT;
               
                if (numberOfColours < 2 || colours.get(1).isTransparent())
                        scheme |= S_DAY_TRANSPARENT;
                if (numberOfColours == 4 && (colours.get(3).isTransparent()))
                        scheme |= S_NIGHT_TRANSPARENT;

                if ((scheme & S_NIGHT) == 0)
                        if ((scheme & S_DAY_TRANSPARENT) != 0)
                                scheme |= S_NIGHT_TRANSPARENT;

                return scheme;
        }

        /**
         * Get the number of bits per pixel that will be used in the written bitmap.
         *
         * This depends on the colour mode and number of colours to be represented.
         */

        public int getBitsPerPixel() {
                if (simple)
                        return 1;

                // number of colours includes the transparent pixel in colormode=0x10 so this
                // works for all colour modes.
                int nc = numberOfColours;
                if (nc == 0)
                        return 24;
                else if (nc < 2)
                        return 1;
                else if (nc < 4)
                        return 2;
                else if (nc < 16)
                        return 4;
                else
                        return 8;
        }

        /**
         * Write out the colours only.
         */

        public void write(ImgFileWriter writer) {
                if (colourMode == 0x20) {
                        writeColours20(writer);
                } else {
                        for (Rgb rgb : colours) {
                                if (!rgb.isTransparent())
                                        rgb.write(writer, (byte) 0x10);
                        }
                }
        }

        /**
         * Write out the colours in the colormode=x20 case.
         */

        private void writeColours20(ImgFileWriter writer) {
                BitWriter bw = new BitWriter();
                for (Rgb rgb : colours) {
                        bw.putn(rgb.getB(), 8);
                        bw.putn(rgb.getG(), 8);
                        bw.putn(rgb.getR(), 8);

                        int alpha = 0xff - rgb.getA();
                        alpha = alphaRound4(alpha);

                        bw.putn(alpha, 4);
                }
                writer.put(bw.getBytes(), 0, bw.getLength());
        }

        /**
         * Round alpha value to four bits.
         * @param alpha The original alpha value eg 0xf0.
         * @return Rounded alpha to four bits eg 0xe.
         */

        static int alphaRound4(int alpha) {
                int top = (alpha >> 4) & 0xf;
                int low = alpha & 0xf;

                int diff = low-top;
                if (diff > 8)
                        top++;
                else if (diff < -8)
                        top--;
                return top;
        }

        public int getIndex(String tag) {
                Integer ind = indexMap.get(tag);

                // If this is a simple bitmap (for line or polygon), then the foreground colour is
                // first and so has index 0, but we want the foreground to have index 1, so reverse.
                if (simple)
                        ind = ~ind;

                return ind;
        }

        public void setWidth(int width) {
                this.width = (char) width;
        }

        public void setHeight(int height) {
                this.height = (char) height;
        }

        public void setNumberOfColours(int numberOfColours) {
                this.numberOfColours = numberOfColours;
        }

        public void setCharsPerPixel(int charsPerPixel) {
                this.charsPerPixel = (char) (charsPerPixel == 0 ? 1 : charsPerPixel);
        }

        public int getNumberOfColours() {
                return numberOfColours;
        }

        public int getNumberOfSColoursForCM() {
                if (colourMode == 0x10)
                        return numberOfSolidColours;
                else
                        return numberOfColours;
        }

        public int getCharsPerPixel() {
                return charsPerPixel;
        }

        public int getHeight() {
                return height;
        }

        public int getWidth() {
                return width;
        }

        public int getColourMode() {
                return colourMode;
        }

        public void setColourMode(int colourMode) {
                this.colourMode = (char) colourMode;
        }

        public void setSimple(boolean simple) {
                this.simple = simple;
        }

        public void setHasBorder(boolean hasBorder) {
                this.hasBorder = hasBorder;
        }

        /**
         * Replace the last pixel with a pixel with the same colour components and the given
         * alpha.
         *
         * This is used when the alpha value is specified separately to the colour values in the
         * input file.
         * @param alpha The alpha value to be added to the pixel. This is a real alpha, not a transparency.
         */

        public void addAlpha(int alpha) {
                int last = colours.size();
                RgbWithTag rgb = colours.get(last - 1);
                rgb = new RgbWithTag(rgb, alpha);
                colours.set(last - 1, rgb);
        }

        /**
         * Analyse the colour pallet and normalise it.
         *
         * Try to work out what is required from the supplied colour pallet and set the colour mode
         * and rearrange transparent pixels if necessary to be in the proper place.
         *
         * At the end we build the index from colour tag to pixel index.
         *
         * @param simple If this is a line or polygon.
         * @return A string describing the validation failure.
         */

        public String analyseColours(boolean simple) {
                setSimple(simple);
               
                if (simple) {
                        // There can be up to four colours, no partial transparency, and a max of one transparent pixel
                        // in each of the day/night sections.

                        if (numberOfColours > 4)
                                return ("Too many colours for a line or polygon");
                        if (numberOfColours == 0)
                                return "Line or polygon cannot have zero colours";

                        // Putting the transparent pixel first is common, so reverse if found
                        if (colours.get(0).isTransparent()) {
                                if (numberOfColours < 2)
                                        return "Only colour cannot be transparent for line or polygon";
                                swapColour(0, 1);
                        }
                        if (numberOfColours > 2 && colours.get(2).isTransparent()) {
                                if (numberOfColours < 4)
                                        return "Only colour cannot be transparent for line or polygon";
                                swapColour(2, 3);
                        }

                        // There can only be one transparent pixel per colour pair
                        if (numberOfColours > 1 && colours.get(0).isTransparent())
                                return "Both day foreground and background are transparent";
                        if (numberOfColours > 3 && colours.get(2).isTransparent())
                                return "Both night foreground and background are transparent";

                } else {
                        int transIndex = 0; // index of last transparent pixel, only used when there is only one
                        int nTrans = 0; // completely transparent
                        int nAlpha = 0; // partially transparent
                        int count = 0;  // total number of colours
                        for (RgbWithTag rgb : colours) {
                                if (rgb.isTransparent()) {
                                        nTrans++;
                                        transIndex = count;
                                }

                                if (rgb.getA() != 0xff && rgb.getA() != 0)
                                        nAlpha++;
                                count++;
                        }

                        if (nAlpha > 0 || (count > 0 && count == nTrans)) {
                                // If there is any partial transparency we need colour mode 0x20
                                // Also if there is only one pixel and it is transparent, since otherwise there would be zero
                                // solid colours and that is a special case used to indicate a true colour pixmap.
                                colourMode = 0x20;

                        } else if (nTrans == 1) {
                                colourMode = 0x10;

                                // Ensure the transparent pixel is at the end
                                RgbWithTag rgb = colours.remove(transIndex);
                                colours.add(rgb);
                        }
                }

                int count = 0;
                for (RgbWithTag rgb : colours) {
                        indexMap.put(rgb.getTag(), count++);
                        if (!rgb.isTransparent())
                                numberOfSolidColours++;
                }

                return null;
        }

        private void swapColour(int c1, int c2) {
                RgbWithTag tmp = colours.get(c1);
                colours.set(c1, colours.get(c2));
                colours.set(c2, tmp);
        }
}