Subversion Repositories mkgmap

Rev

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

/*
 * Copyright (C) 2006 Steve Ratcliffe
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License 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.
 *
 *
 * Author: Steve Ratcliffe
 * Create date: 12-Dec-2006
 */

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

import java.text.DecimalFormat;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import uk.me.parabola.imgfmt.app.Label;
import uk.me.parabola.imgfmt.app.lbl.LBLFile;
import uk.me.parabola.log.Logger;

/*
Add support for extended type attributes.

These are nearly all for marine objects. Attribute values are supplied
as tags with a mkgmap:xt- prefix. These tags are supported:

mkgmap:xt-depth
mkgmap:xt-height
  value is distance with optional units suffix (ft or m)
  applicable to points with types 0x0103xx and 0x0104xx and
  lines with types 0x0103xx and 0x010105-0x010107 and areas
  with types 0x0103xx.

mkgmap:xt-style
  value is 16 bit integer that specifies colour (lower 8 bits)
  and line style (upper 8 bits) - applicable to lines with types
  0x0104xx, 0x0105xx, 0x0106xx and points with type 0x010500 (colour
  only).

mkgmap:xt-colour
  value is one of:
        red
        green
        yellow
        white
        black
        black-yellow
        white-red
        black-red
        white-green
        red-yellow
        red-green
        orange
        black-yellow-black
        yellow-black
        yellow-black-yellow
        red-white
        green-red-green
        red-green-red
        black-red-black
        yellow-red-yellow
        green-red
        black-white
        white-orange
        orange-white
        green-white
  applicable to points with type 0x0102xx (buoys) and it specifies the
  foundation colour of the buoy.

mkgmap:xt-type
  value is one of:
        fixed
        isophase
        flashing
        group flashing
        composite group flashing
        occulting
        group occulting
        composite group occulting
        long flashing
        group long flashing
        (a morse code letter)
        quick
        group quick
        group quick and long flashing
        interrupted quick
        very quick
        group very quick
        group very quick and long flashing
        interrupted very quick
        ultra quick
        interrupted ultra quick
        fixed and occulting
        fixed and group occulting
        fixed and isophase
        fixed and flashing
        fixed and group flashing
        fixed and long flashing
        alternating
        alternating occulting
        alternating flashing
        alternating group flashing
  applicable to points with types 0x0101xx (lights) and 0x0102xx (buoys)
  and it specifies the type of light

mkgmap:xt-light
  one or more light definitions (separated by ;: or /) - each light
  definition is of the form colour,range,angle where colour is one
  of:
        unlit
        red
        green
        white
        blue
        yellow
        violet
        amber
  range is an optional number that specifies the visible range in nm -
  angle is the start angle for the light (only makes sense when more than
  one light is defined.

mkgmap:xt-period
  value is one or more period values (in seconds) (separated by commas) -
  applicable to point types 0x0101xx (lights) and 0x0102xx (buoys).

mkgmap:xt-racon
  when set to yes/true/1 specifies object has a racon - applicable to
  point types 0x0101xx (lights).

mkgmap:xt-height-above-foundation
mkgmap:xt-height-above-datum
  value is distance with optional units (m or ft) - applicable to
  point types 0x0101xx (lights).

mkgmap:xt-leading-angle
  value is a number - applicable to point types 0x0101xx (lights).

mkgmap:xt-note
mkgmap:xt-int-desig
mkgmap:xt-local-desig
  values are strings that are encoded as labels - applicable to
  points of type 0x01xx (lights) and 0x0102xx (buoys).

mkgmap:xt-facilities
  value is a bitmask of facilities available:
    0x000001 boat ramp
    0x000002 drinking water
    0x000004 restrooms
    0x000008 picnic area
    0x000010 campground
    0x000020 marina
    0x000040 fuel
    0x000080 marine supply
    0x000100 bait and tackle
    0x000200 groceries
    0x000400 restaurant
    0x000800 water/electric hook-up
    0x001000 boat/motor rental
    0x002000 guide service
    0x004000 lodging
    0x008000 dump station
    0x010000 handicap accessible
  applicable only to points of type 0x010903 (facility)
 */


public class ExtTypeAttributes {

        private static final Logger log = Logger.getLogger(ExtTypeAttributes.class);

        private final Map<String, String> attributes;
        private final String objectName;

        private byte[] extraBytes;

        private Label note;
        private Label intDesig;
        private Label localDesig;

        private Byte morseLetter;

        private static final int DISTANCE_FLAG_METRIC_INDEX = 0;
        private static final int DISTANCE_FLAG_TENTHS_INDEX = 1;

        private static final byte FLAGS0_RACON_BIT       = (1);
        private static final byte FLAGS0_NOTE_BIT        = (1 << 1);
        private static final byte FLAGS0_LOCAL_DESIG_BIT = (1 << 2);
        private static final byte FLAGS0_INT_DESIG_BIT   = (1 << 3);
        private static final int[] ZERO_INT_ARRAY = new int[0];

        public ExtTypeAttributes(Map<String, String> attributes, String objectName) {
                this.attributes = attributes;
                this.objectName = objectName;
        }

        public void processLabels(LBLFile lbl) {
                if(note == null) {
                        String ns = attributes.get("note");
                        if(ns != null)
                                note = lbl.newLabel(ns, false);
                }
                if(intDesig == null) {
                        String ids = attributes.get("int-desig");
                        if(ids != null)
                                intDesig = lbl.newLabel(ids, false);
                }
                if(localDesig == null) {
                        String lds = attributes.get("local-desig");
                        if(lds != null)
                                localDesig = lbl.newLabel(lds, false);
                }
        }

        protected byte[] getExtTypeExtraBytes(MapObject mapObject) {
                try {
                        return encodeExtraBytes(mapObject);
                } catch (Exception e) {
                        log.error(objectName + " (" + e + ")");
                        return null;
                }
        }

        private byte[] encodeExtraBytes(MapObject mapObject) {

                // if we get called again, just return same result as before
                if(extraBytes != null)
                        return extraBytes;

                // first see if a string of raw hex digits has been supplied
                // if so, use that and ignore everything else
                String eb = attributes.get("extra-bytes");
                if(eb != null) {
                        extraBytes = new byte[(eb.length() + 1) / 2];
                        for(int i = 0; i < eb.length(); ++i) {
                                int d = Integer.parseInt(eb.substring(i, i + 1), 16);
                                extraBytes[i / 2] |= (byte)(d << (4 * (1 - (i & 1))));
                        }
                        return extraBytes;
                }

                int type0to15 = mapObject.getType() & 0xffff;
                int type8to15 = type0to15 & 0xff00;

                // seamark:light:character = type [[(group)] colour period 's' height 'm' range 'M']
                if(attributes.get("seamark:light:character") != null) {
                        String[] parts = attributes.get("seamark:light:character").split(" ");
                        String group = attributes.get("seamark:light:group");

                        if(parts.length > 0) {
                                String lt = null;
                                int i = 1;
                                if(parts[0].startsWith("Fl"))
                                        lt = "flashing";
                                else if(parts[0].startsWith("F"))
                                        lt = "fixed";
                                else if(parts[0].startsWith("LFL"))
                                        lt = "long flashing";
                                else if(parts[0].startsWith("Q"))
                                        lt = "quick";
                                else if(parts[0].startsWith("VQ"))
                                        lt = "very quick";
                                else if(parts[0].startsWith("UQ"))
                                        lt = "ultra quick";
                                else if(parts[0].startsWith("Iso"))
                                        lt = "isophase";
                                else if(parts[0].startsWith("Oc"))
                                        lt = "occulting";
                                else if(parts[0].startsWith("IQ"))
                                        lt = "interrupted quick";
                                else if(parts[0].startsWith("IVQ "))
                                        lt = "interrupted very quick";
                                else if(parts[0].startsWith("IUQ"))
                                        lt = "interrupted ultra quick";
                                else if(parts[0].startsWith("AI"))
                                        lt = "alternating";
                                else if(parts[0].startsWith("Mo")) {
                                        if(parts[0].indexOf('(') == 2)
                                                lt = parts[0].substring(3, 4);
                                        else if(parts.length > 1 && parts[i].startsWith("(")) {
                                                lt = parts[i].substring(1, 2);
                                                ++i;
                                        }
                                }

                                String group2 = null;
                                if(!parts[0].startsWith("Mo") && parts[0].indexOf('(') > 0) {
                                        group2 = parts[0].substring(parts[0].indexOf('(') + 1, parts[0].length() - 1);
                                }
                                else if(i < parts.length && parts[i].startsWith("(")) {
                                        // should be (group)
                                        group2 = parts[i].substring(1, parts[i].length() - 1);
                                        ++i;
                                }

                                if(group2 != null) {
                                        if(group != null && !group.equals(group2))
                                                log.warn("Inconsistent light description - seamark:light:group = '" + group + "' but seamark:light:character contains '" + parts[0] + "'");
                                        else
                                                group = group2;
                                }

                                if(lt != null) {
                                        if(group != null) {
                                                attributes.put("group", group); // FIXME - implement?
                                                lt = "group " + lt;
                                                if(group.split("\\+").length > 1)
                                                        lt = "composite " + lt;
                                        }
                                        attributes.put("type", lt);
                                }

                                String light = null;
                                if(i < parts.length) {
                                        // colour
                                        String c = parts[i].toUpperCase();
                                        if(c.startsWith("W"))
                                                light = "white";
                                        else if(c.startsWith("R"))
                                                light = "red";
                                        else if(c.startsWith("G"))
                                                light = "green";
                                        else if(c.startsWith("Y"))
                                                light = "yellow";
                                        ++i;
                                } else {
                                        light = attributes.get("light");
                                }

                                String period = null;
                                if((i + 1) < parts.length && "s".equals(parts[i+1])) {
                                        // period
                                        period = parts[i];
                                        i += 2;
                                }

                                String height = null;
                                if((i + 1) < parts.length && "m".equals(parts[i+1])) {
                                        // height
                                        height = parts[i];
                                        i += 2;
                                }

                                String range = null;
                                if((i + 1) < parts.length && "M".equals(parts[i+1])) {
                                        // range
                                        range = parts[i];
                                        i += 2;
                                } else {
                                        range = attributes.get("seamark:light:range");
                                }

                                if(light != null) {
                                        if(range != null)
                                                light += "," + range;
                                        attributes.put("light", light);
                                        if(period != null)
                                                attributes.put("period", period);
                                        if(height != null)
                                                attributes.put("height-above-datum", height + "m");
                                }
                        }
                }

                // seamark:light:# = colour ':' sectorStart ':' sectorEnd ':' range
                if(attributes.get("seamark:light:1") != null) {
                        // when multiple lights are defined, they must be ordered
                        // by increasing sector start angle
                        class SeamarkLight implements Comparable<SeamarkLight> {
                                String colour;
                                int sectorStart;
                                int sectorEnd;
                                int range;
                                SeamarkLight(String desc) {
                                        String[] parts = desc.split(":");
                                        if(parts.length == 4) {
                                                colour = parts[0];
                                                if ("shore".equalsIgnoreCase(parts[1]) || "shore".equalsIgnoreCase(parts[2])) {
                                                        log.error(objectName + ": shore is no valid sector bound, please annotate a numeric value");
                                                } else {
                                                        sectorStart = Double.valueOf(parts[1]).intValue();
                                                        while(sectorStart >= 360)
                                                                sectorStart -= 360;
                                                        sectorEnd = Double.valueOf(parts[2]).intValue();
                                                        while(sectorEnd >= 360)
                                                                sectorEnd -= 360;
                                                        range = Double.valueOf(parts[3]).intValue();
                                                }
                                        }
                                }
                                public int compareTo(SeamarkLight other) {
                                        if (sectorStart == other.sectorStart) return 0;
                                        else return sectorStart > other.sectorStart? 1: -1;
                                }
                        }
                        List<SeamarkLight> lights = new ArrayList<>();
                        // create a SeamarkLight for each light
                        for(int n = 1; n <= 100; ++n) {
                                String desc = attributes.get("seamark:light:" + n);
                                if(desc != null)
                                        lights.add(new SeamarkLight(desc));
                                else
                                        break;
                        }
                        // sort the lights by increasing sector start angle
                        lights.sort(null);
                        // generate the descriptor string - each light is
                        // specified as color,range,sectorStartAngle
                        StringBuilder light = null;
                        for(int i = 0; i < lights.size(); ++i) {
                                SeamarkLight sml = lights.get(i);
                                if(light == null)
                                        light = new StringBuilder();
                                else
                                        light.append('/');
                                light.append(sml.colour).append(',').append(sml.range).append(',').append(sml.sectorStart);
                                if((i + 1) < lights.size()) {
                                        if(sml.sectorEnd != lights.get(i + 1).sectorStart) {
                                                // gap between lit sectors
                                                light.append("/unlit,0,").append(sml.sectorEnd);
                                        }
                                }
                                else if(sml.sectorEnd != lights.get(0).sectorStart) {
                                        // gap to end
                                        light.append("/unlit,0,").append(sml.sectorEnd);
                                }
                        }
                        if(light != null) {
                                attributes.put("light", light.toString());
                                if(attributes.get("seamark:light:character") == null)
                                        attributes.put("type", "fixed");
                        }
                }

                String sequence = attributes.get("seamark:light:sequence");

                if(sequence != null) {
                        StringBuilder periods = new StringBuilder();
                        StringBuilder eclipse = new StringBuilder();
                        for(String p : sequence.split("[+,]")) {
                                if (p.startsWith("(") && p.endsWith(")")) {
                                        // phases of eclipse are enclosed in (), remove them
                                        p = p.substring(1, p.length()-1);
                                        if(eclipse.length() > 0)
                                                eclipse.append(",");
                                        eclipse.append(Double.parseDouble(p));
                                } else {
                                        if(periods.length() > 0)
                                                periods.append(",");
                                        periods.append(Double.parseDouble(p));
                                }
                        }
                        attributes.put("period", periods.toString());
                        attributes.put("eclipse", eclipse.toString());
                }

                if(mapObject instanceof Point) {

                        Light[] lights = parseLights(attributes.get("light"));
                        int[] periods = parsePeriods(attributes.get("period"));
                        int[] eclipse = parsePeriods(attributes.get("eclipse"));
                        if (!(periods.length == eclipse.length || 1 == periods.length))
                                log.error(objectName + ": number of light and eclipse phases has to be equal");
                       
                        if(type8to15 == 0x0100) { // lights
                                byte flags0 = 0;
                                int lightType = lightType("");
                                if(meansYes(attributes.get("racon")))
                                        flags0 |= FLAGS0_RACON_BIT;
                                int nob = 6;
                                if(note != null) {
                                        nob += 3;
                                        flags0 |= FLAGS0_NOTE_BIT;
                                }
                                if(intDesig != null) {
                                        nob += 3;
                                        flags0 |= FLAGS0_INT_DESIG_BIT;
                                }
                                if(localDesig != null) {
                                        nob += 3;
                                        flags0 |= FLAGS0_LOCAL_DESIG_BIT;
                                }
                                byte flags1 = 0;
                                if(lights.length > 1) {
                                        for(Light l : lights)
                                                nob += (l.colour != 0)? 3 : 2;
                                        flags1 |= 0x08; // multiple lights
                                }
                                if(0 != eclipse.length) {
                                        for(int p : periods) {
                                                while(p > 0x3f) {
                                                        ++nob;
                                                        p -= 0x3f;
                                                }
                                                ++nob;
                                        }
                                        for(int p : eclipse) {
                                                while(p > 0x3f) {
                                                        ++nob;
                                                        p -= 0x3f;
                                                }
                                                ++nob;
                                        }
                                        flags1 |= 0x01; // further record present?
                                }
                                else if(morseLetter != null)
                                        flags1 |= 0x01; // further record present?
                                byte lightsDef = 0x22;
                                String hafs = attributes.get("height-above-foundation");
                                Integer hafi = null;
                                if(hafs != null) {
                                        boolean[] hafsDistFlags = new boolean[2];
                                        hafi = parseDistance(hafs, hafsDistFlags);
                                        if(hafsDistFlags[DISTANCE_FLAG_TENTHS_INDEX])
                                                hafi /= 10;
                                        nob += (hafi > 255)? 2 : 1;
                                        if(hafi > 255)
                                                lightsDef |= 0x80;
                                        else
                                                lightsDef |= 0x40;
                                        if(!hafsDistFlags[DISTANCE_FLAG_METRIC_INDEX])
                                                lightsDef &= ~0x20;
                                }
                                String hads = attributes.get("height-above-datum");
                                Integer hadi = null;
                                if(hads != null) {
                                        boolean[] hadsDistFlags = new boolean[2];
                                        hadi = parseDistance(hads, hadsDistFlags);
                                        if(hadsDistFlags[DISTANCE_FLAG_TENTHS_INDEX])
                                                hadi /= 10;
                                        nob += (hadi > 255)? 2 : 1;
                                        if(hadi > 255)
                                                lightsDef |= 0x08;
                                        else
                                                lightsDef |= 0x04;
                                        if(!hadsDistFlags[DISTANCE_FLAG_METRIC_INDEX])
                                                lightsDef &= ~0x02;
                                }
                                String las = attributes.get("leading-angle");
                            Integer leadingAngle = null;
                                if(las != null) {
                                        leadingAngle = (int)(Double.parseDouble(las.trim()) * 10);
                                        nob += 2;
                                        flags1 |= 0x02; // leading angle present
                                }
                                int period = 0;
                                for(int p : periods)
                                        period += p;
                                for(int p : eclipse)
                                        period += p;
                                if(period > 255)
                                        lightType |= 0x40; // 9th bit of period
                                else if(period > 511) {
                                        period = 511;
                                        log.warn("Can't encode periods greater than 51.1 seconds for lights");
                                }

                                extraBytes = new byte[nob + 2];
                                int i = 0;
                                extraBytes[i++] = (byte)(0xe0 | flags0);
                                extraBytes[i++] = (byte)((nob << 1) | 1); // bit0 always set?
                                extraBytes[i++] = (byte)(0x80 | lightType);
                                extraBytes[i++] = flags1;
                                extraBytes[i++] = lightsDef;
                                if(hafi != null) {
                                        extraBytes[i++] = (byte)(int)hafi;
                                        if(hafi > 255)
                                                extraBytes[i++] = (byte)(hafi >> 8);
                                }
                                if(hadi != null) {
                                        extraBytes[i++] = (byte)(int)hadi;
                                        if(hadi > 255)
                                                extraBytes[i++] = (byte)(hadi >> 8);
                                }
                                extraBytes[i++] = (byte)period;
                                if(note != null) {
                                        int off = note.getOffset();
                                        extraBytes[i++] = (byte)off;
                                        extraBytes[i++] = (byte)(off >> 8);
                                        extraBytes[i++] = (byte)(off >> 16);
                                }
                                if(localDesig != null) {
                                        int off = localDesig.getOffset();
                                        extraBytes[i++] = (byte)off;
                                        extraBytes[i++] = (byte)(off >> 8);
                                        extraBytes[i++] = (byte)(off >> 16);
                                }
                                if(intDesig != null) {
                                        int off = intDesig.getOffset();
                                        extraBytes[i++] = (byte)off;
                                        extraBytes[i++] = (byte)(off >> 8);
                                        extraBytes[i++] = (byte)(off >> 16);
                                }
                                if(leadingAngle != null) {
                                        extraBytes[i++] = (byte)(int)leadingAngle;
                                        extraBytes[i++] = (byte)(leadingAngle >> 8);
                                }
                                if(lights.length > 1) {
                                        for(int l = 0; l < lights.length; ++l) {
                                                int val = (lights[l].colour << 12) | (int)(lights[l].angle * 10);
                                                if((l + 1) == lights.length)
                                                        val |= 0x8000;
                                                extraBytes[i++] = (byte)val;
                                                extraBytes[i++] = (byte)(val >> 8);
                                                if(lights[l].colour != 0)
                                                        extraBytes[i++] = (byte)lights[l].range;
                                        }
                                }
                                else {
                                        int lc = 0;
                                        int lr = 0;
                                        if(lights.length > 0) {
                                                lc = lights[0].colour;
                                                lr = (int)lights[0].range & 0x1f;
                                        }
                                        extraBytes[i++] = (byte)((lc << 5) | lr);
                                }
                                if(periods.length > 1) {
                                        extraBytes[i++] = (byte)(0x80 + periods.length);
                                        // first all lights
                                        for (int p : periods) {
                                                while(p > 0x3f) {
                                                        extraBytes[i++] = (byte)0x3f;
                                                        p -= 0x3f;
                                                }
                                                extraBytes[i++] = (byte)p;
                                        }
                                        // second all pause
                                        for (int p : eclipse) {
                                                while(p > 0x3f) {
                                                        extraBytes[i++] = (byte)0x3f;
                                                        p -= 0x3f;
                                                }
                                                extraBytes[i++] = (byte)p;
                                        }
                                }
                                else if(morseLetter != null)
                                        extraBytes[i++] = morseLetter;
                                else
                                        extraBytes[i++] = 0x01; // terminator?

                                return extraBytes;
                        }
                        else if(type8to15 == 0x0200) { // buoys
                                byte flags0 = 0;
                                int lt = lightType("");
                                if(meansYes(attributes.get("racon"))) {
                                        // this doesn't get reported on mapsource
                                        // maybe racons aren't supported for buoys?
                                        flags0 |= FLAGS0_RACON_BIT;
                                }
                                int nob = 4;
                                if(note != null) {
                                        nob += 3;
                                        flags0 |= FLAGS0_NOTE_BIT;
                                }
                                if(intDesig != null) {
                                        nob += 3;
                                        flags0 |= FLAGS0_INT_DESIG_BIT;
                                }
                                if(localDesig != null) {
                                        nob += 3;
                                        flags0 |= FLAGS0_LOCAL_DESIG_BIT;
                                }
                                if(periods.length > 0)
                                        ++nob;          // for total period
                                byte flags1 = 0;
                                if(periods.length > 1) {
                                        for(int p : periods) {
                                                while(p > 0x3f) {
                                                        ++nob;
                                                        p -= 0x3f;
                                                }
                                                ++nob;
                                        }
                                        flags1 |= 0x02; // further record present?
                                }
                                else if(morseLetter != null)
                                        flags1 |= 0x02; // further record present?
                                extraBytes = new byte[nob + 2];
                                int i = 0;
                                extraBytes[i++] = (byte)(0xe0 | flags0);
                                extraBytes[i++] = (byte)((nob << 1) | 1); // bit0 always set?
                                int lc = 0;
                                if(lights.length > 0) {
                                        lc = lights[0].colour;
                                }
                                extraBytes[i++] = (byte)((lc << 6) | colour(""));
                                flags1 |= (byte)((lc >> 2) & 1); // bit 0 is MSB of light colour
                                extraBytes[i++] = flags1;
                                if(note != null) {
                                        int off = note.getOffset();
                                        extraBytes[i++] = (byte)off;
                                        extraBytes[i++] = (byte)(off >> 8);
                                        extraBytes[i++] = (byte)(off >> 16);
                                }
                                if(localDesig != null) {
                                        int off = localDesig.getOffset();
                                        extraBytes[i++] = (byte)off;
                                        extraBytes[i++] = (byte)(off >> 8);
                                        extraBytes[i++] = (byte)(off >> 16);
                                }
                                if(intDesig != null) {
                                        int off = intDesig.getOffset();
                                        extraBytes[i++] = (byte)off;
                                        extraBytes[i++] = (byte)(off >> 8);
                                        extraBytes[i++] = (byte)(off >> 16);
                                }
                                int period = 0;
                                for(int p : periods)
                                        period += p;
                                if(period > 255) {
                                        period = 255;
                                        log.warn("Can't encode periods greater than 25.5 seconds for buoy lights");
                                }
                                byte flags2 = 0;
                                if(periods.length > 0)
                                        flags2 |= (byte)0x80;
                                extraBytes[i++] = (byte)(flags2 | lt);
                                if(periods.length > 0) {
                                        extraBytes[i++] = (byte)period;
                                        if(periods.length > 1) {
                                                if(periods.length > 2)
                                                        extraBytes[i++] = (byte)0x82;
                                                else
                                                        extraBytes[i++] = (byte)0x81;
                                                for(int p : periods) {
                                                        while(p > 0x3f) {
                                                                extraBytes[i++] = (byte)0x3f;
                                                                p -= 0x3f;
                                                        }
                                                        extraBytes[i++] = (byte)p;
                                                }
                                        }
                                        else
                                                extraBytes[i++] = 0x01; // terminator?
                                }
                                else if(morseLetter != null)
                                        extraBytes[i++] = morseLetter;
                                else
                                        extraBytes[i++] = 0x01; // terminator?

                                return extraBytes;
                        }
                        else if(type8to15 == 0x0300 || // things with depth/height
                                        type8to15 == 0x0400) { // obstructions
                                String ds = attributes.get("depth");
                                if(ds == null)
                                        ds = attributes.get("height");
                                if(ds != null) {
                                        boolean[] distFlags = new boolean[2];
                                        Integer di = parseDistance(ds, distFlags);
                                        if(di != null) {
                                                if(di > 255) {
                                                        extraBytes = new byte[3];
                                                        extraBytes[0] = (byte)0xa0;
                                                }
                                                else {
                                                        extraBytes = new byte[2];
                                                        extraBytes[0] = (byte)0x80;
                                                }
                                                if(distFlags[DISTANCE_FLAG_METRIC_INDEX])
                                                        extraBytes[0] |= 0x10;
                                                if(distFlags[DISTANCE_FLAG_TENTHS_INDEX])
                                                        extraBytes[0] |= 0x08;
                                                if(type8to15 == 0x0400) { // obstructions
                                                        extraBytes[0] |= position();
                                                }
                                                extraBytes[1] = (byte)(int)di;
                                                if(di > 255)
                                                        extraBytes[2] = (byte)(di >> 8);

                                                return extraBytes;
                                        }
                                }
                        }
                        else if(type8to15 == 0x0500) { // label
                                String ss = attributes.get("style");
                                if(ss != null) {
                                        int style = Integer.decode(ss.trim());
                                        // format is 0xCC (CC = colour)
                                        extraBytes = new byte[1];
                                        extraBytes[0] = (byte)(style & 0xf);
                                        return extraBytes;
                                }
                        }
                        else if(type0to15 == 0x0903) { // facility
                                String fs = attributes.get("facilities");
                                if(fs != null) {
                                        int facilities = Integer.decode(fs.trim());
                                        extraBytes = new byte[3];
                                        extraBytes[0] = (byte)(0xa0 | (facilities & 0x1f));
                                        extraBytes[1] = (byte)(facilities >> 5);
                                        extraBytes[2] = (byte)(((facilities >> 13) & 0x07) |
                                                                                   ((facilities >> 12) & 0x18));
                                        return extraBytes;
                                }
                        }
                }
                else if(mapObject instanceof Polyline) {

                        if(type8to15 == 0x0300 ||       // depth areas
                           (!(mapObject instanceof Polygon) &&
                                (type0to15 == 0x0105 || // contour line
                                 type0to15 == 0x0106 || // overhead cable
                                 type0to15 == 0x0107))) { // bridge
                                String ds = attributes.get("depth");
                                if(ds == null)
                                        ds = attributes.get("height");
                                if(ds != null) {
                                        boolean[] distFlags = new boolean[2];
                                        Integer di = parseDistance(ds, distFlags);
                                        if(di != null) {
                                                if(di > 255) {
                                                        extraBytes = new byte[3];
                                                        extraBytes[0] = (byte)0xa0;
                                                }
                                                else {
                                                        extraBytes = new byte[2];
                                                        extraBytes[0] = (byte)0x80;
                                                }
                                                if(distFlags[DISTANCE_FLAG_METRIC_INDEX])
                                                        extraBytes[0] |= 0x10;
                                                if(distFlags[DISTANCE_FLAG_TENTHS_INDEX])
                                                        extraBytes[0] |= 0x08;
                                                if(type8to15 == 0x04) { // obstructions
                                                        extraBytes[0] |= position();
                                                }
                                                extraBytes[1] = (byte)(int)di;
                                                if(di > 255)
                                                        extraBytes[2] = (byte)(di >> 8);

                                                return extraBytes;
                                        }
                                }
                        }
                        else if(!(mapObject instanceof Polygon) &&
                                        (type8to15 == 0x0400 || // various lines
                                         type8to15 == 0x0500 ||
                                         type8to15 == 0x0600)) {
                                String ss = attributes.get("style");
                                if(ss != null) {
                                        int style = Integer.decode(ss.trim());
                                        if ((style & 0xff00) == 0) {
                                                // format is 0xCC (CC = colour)
                                                extraBytes = new byte[1];
                                                extraBytes[0] = (byte) (style & 0xf);
                                        } else {
                                                // format is 0xSSCC (SS = style, CC = colour)
                                                extraBytes = new byte[2];
                                                extraBytes[0] = (byte) (0x80 | (style & 0xf));
                                                extraBytes[1] = (byte) (((style >> 9) & 0x18) | ((style >> 8) & 0x3));
                                        }
                                        return extraBytes;
                                }
                        }
                }

                return null;
        }

        private static boolean meansYes(String s) {
                if(s == null)
                        return false;
                s = s.toLowerCase();
                return ("yes".startsWith(s) || "true".startsWith(s) || "1".equals(s));
        }

        private static Integer parseDistance(String ds, boolean[] flags) {
                ParsePosition pp = new ParsePosition(0);
                Number dn = new DecimalFormat().parse(ds, pp);
                if(dn != null) {
                        double dd = dn.doubleValue();
                        int di = dn.intValue();
                        flags[DISTANCE_FLAG_METRIC_INDEX] = true;
                        flags[DISTANCE_FLAG_TENTHS_INDEX] = false;
                        if("ft".equalsIgnoreCase(ds.substring(pp.getIndex()).trim()))
                                flags[DISTANCE_FLAG_METRIC_INDEX] = false;
                        if((double)di != dd) {
                                // number has fractional part
                                di = (int)(dd * 10);
                                flags[DISTANCE_FLAG_TENTHS_INDEX] = true;
                        }

                        return di;
                }

                return null;
        }

        private int colour(String prefix) {

                String c = attributes.get(prefix + "colour");
                if(c == null)
                        c = attributes.get(prefix + "color");
                if(c == null)
                        return 0;

                c = c.trim();

                if(Character.isDigit(c.charAt(0))) {
                        return Integer.decode(c);
                }

                String[] colours = {
                        "",
                        "red",
                        "green",
                        "yellow",
                        "white",
                        "black",
                        "black-yellow",
                        "white-red",
                        "black-red",
                        "white-green",
                        "red-yellow",
                        "red-green",
                        "orange",
                        "black-yellow-black",
                        "yellow-black",
                        "yellow-black-yellow",
                        "red-white",
                        "green-red-green",
                        "red-green-red",
                        "black-red-black",
                        "yellow-red-yellow",
                        "green-red",
                        "black-white",
                        "white-orange",
                        "orange-white",
                        "green-white"
                };

                c = c.toLowerCase().replaceAll("[;, ]+", "-");

                for(int i = 0; i < colours.length; ++i)
                        if(colours[i].equals(c))
                                return i;

                return 0;
        }

        private int lightType(String prefix) {
                String lt = attributes.get(prefix + "type");
                if(lt == null)
                        return 0;

                lt = lt.trim();

                if(Character.isDigit(lt.charAt(0))) {
                        return Integer.decode(lt);
                }

                if(lt.length() == 1) {
                        morseLetter = (byte)lt.charAt(0);
                        return 0x0b;
                }

                String[] types = {
                        "",
                        "fixed",
                        "isophase",
                        "flashing",
                        "group flashing",
                        "composite group flashing",
                        "occulting",
                        "group occulting",
                        "composite group occulting",
                        "long flashing",
                        "group long flashing",
                        "morse code letter",
                        "quick",
                        "group quick",
                        "group quick and long flashing",
                        "interrupted quick",
                        "very quick",
                        "group very quick",
                        "group very quick and long flashing",
                        "interrupted very quick",
                        "ultra quick",
                        "interrupted ultra quick",
                        "fixed and occulting",
                        "fixed and group occulting",
                        "fixed and isophase",
                        "fixed and flashing",
                        "fixed and group flashing",
                        "fixed and long flashing",
                        "alternating",
                        "alternating occulting",
                        "alternating flashing",
                        "alternating group flashing"
                };

                lt = lt.toLowerCase();

                for(int i = 0; i < types.length; ++i)
                        if(types[i].equals(lt))
                                return i;

                return 0;
        }

        private int position() {
                String ps = attributes.get("position");
                if(ps == null)
                        return 0;

                ps = ps.trim();

                if(Character.isDigit(ps.charAt(0))) {
                        return Integer.decode(ps);
                }

                String[] positions = {
                        "unknown",
                        "",
                        "doubtful",
                        "existence doubtful",
                        "approximate",
                        "reported"
                };

                ps = ps.toLowerCase();

                for(int i = 0; i < positions.length; ++i)
                        if(positions[i].equals(ps))
                                return i;

                return 0;
        }

        private static int[] parsePeriods(String ps) {
                if(ps == null)
                        return ZERO_INT_ARRAY;
                String [] psa = ps.split(",");
                int [] periods = new int[psa.length];
                for(int i = 0; i < psa.length; ++i)
                        periods[i] = (int)(Double.parseDouble(psa[i].trim()) * 10);
                return periods;
        }

        private Light[] parseLights(String ls) {
                if(ls == null)
                        return new Light[0];
                ls = ls.trim();
                String[] defs = new String[0];
                if(ls.startsWith("(")) {
                        // handle polish syntax "(c,r,a),(c,r,a)..."
                        List<String> out = new ArrayList<>();
                        int start = 0;
                        // start should be on the '(' at the start of the loop
                        while(start < ls.length()) {
                                int end = ++start;
                                while(end < ls.length() && ls.charAt(end) != ')')
                                        ++end;
                                out.add(ls.substring(start, end));
                                start = end + 1;
                                while(start < ls.length() && ls.charAt(start) != '(')
                                        ++start;
                        }
                        defs = out.toArray(defs);
                }
                else {
                        // handle our "simple" syntax
                        defs = ls.split("[:;/]");
                }
                Light[] lights = new Light[defs.length];
                for(int i = 0; i < defs.length; ++i) {
                        String def = defs[i].trim();
                        if(def.length() > 0)
                                lights[i] = new Light(def);
                }
                return lights;
        }

        class Light {
                private int    colour;
                private double range;
                private double angle;

                final String[] colours = {
                        "unlit",
                        "red",
                        "green",
                        "white",
                        "blue",
                        "yellow",
                        "violet",
                        "amber"
                };

                public Light(String desc) {
                        String[] parts = desc.split(",");
                        if(parts.length > 0) {
                                String lc = parts[0].trim().toLowerCase();
                                if(Character.isDigit(lc.charAt(0))) {
                                        colour = Integer.decode(lc);
                                }
                                else {
                                        for(int i = 0; i < colours.length; ++i) {
                                                if(colours[i].equals(lc)) {
                                                        colour = i;
                                                        break;
                                                }
                                        }
                                }
                        }
                        if(parts.length > 1 && colour != 0)
                                range = Double.parseDouble(parts[1]);
                        if(parts.length > 2)
                                angle = Double.parseDouble(parts[2]);
                }

                public String toString() {
                        return "(" + colours[colour] + "," + range + "," + angle + ")";
                }
        }
}