public class AdvancedSymbolController extends com.esri.militaryapps.controller.AdvancedSymbolController implements MessageControllerListener {
    
    public static final String SPOT_REPORT_LAYER_NAME = "Spot Reports";

    private final MapController mapController;
    private final MessageGroupLayer groupLayer;
    private final GraphicsLayer spotReportLayer;
    private final Symbol spotReportSymbol;
    private final MessageController messageController;
    private final File symDictDir;
    private final AppConfigController appConfigController;

    /**
     * Creates a new AdvancedSymbolController.
     * @param mapController the application's MapController.
     * @param spotReportIcon the spot report icon image.
     */
    public AdvancedSymbolController(
            MapController mapController,
            BufferedImage spotReportIcon,
            MessageController messageController,
            AppConfigController appConfigController) {
        super(mapController);
        this.mapController = mapController;
        this.appConfigController = appConfigController;
        
        spotReportLayer = new GraphicsLayer();
        spotReportLayer.setName(SPOT_REPORT_LAYER_NAME);
        mapController.addLayer(spotReportLayer, false);
        
        groupLayer = new MessageGroupLayer(SymbolDictionary.DictionaryType.Mil2525C);
        mapController.addLayer(groupLayer, false);
        
        spotReportSymbol = new PictureMarkerSymbol(spotReportIcon);
        
        setShowLabels(appConfigController.isShowMessageLabels());
        
        this.messageController = messageController;
        symDictDir = new File(ArcGISRuntime.getRuntimeBinariesDir(), "../../resources/symbols/mil2525c");
    }

    @Override
    protected void toggleLabels() {
        for (Layer layer : groupLayer.getLayers()) {
            GraphicsLayer graphicsLayer = (GraphicsLayer) layer;
            if (graphicsLayer.getRenderer() instanceof DictionaryRenderer) {
                DictionaryRenderer dictionaryRenderer = (DictionaryRenderer) graphicsLayer.getRenderer();
                dictionaryRenderer.setLabelsVisible(isShowLabels());
                graphicsLayer.setRenderer(dictionaryRenderer);
            }
        }
    }
    
    @Override
    protected Integer displaySpotReport(double x, double y, final int wkid, Integer graphicId, Geomessage geomessage) {
        try {
            Geometry pt = new Point(x, y);
            if (null != mapController.getSpatialReference() && wkid != mapController.getSpatialReference().getID()) {
                pt = GeometryEngine.project(pt, SpatialReference.create(wkid), mapController.getSpatialReference());
            }
            if (null != graphicId) {
                spotReportLayer.updateGraphic(graphicId, pt);
                spotReportLayer.updateGraphic(graphicId, geomessage.getProperties());
            } else {
                Graphic graphic = new Graphic(pt, spotReportSymbol, geomessage.getProperties());
                graphicId = spotReportLayer.addGraphic(graphic);
                
            }
            return graphicId;
        } catch (NumberFormatException nfe) {
            Logger.getLogger(getClass().getName()).log(Level.SEVERE, "Could not parse spot report", nfe);
            return null;
        }
    }
    
    @Override
    protected String translateColorString(String geomessageColorString) {
        if ("1".equals(geomessageColorString)) {
            geomessageColorString = "red";
        } else if ("2".equals(geomessageColorString)) {
            geomessageColorString = "green";
        } else if ("3".equals(geomessageColorString)) {
            geomessageColorString = "blue";
        } else if ("4".equals(geomessageColorString)) {
            geomessageColorString = "yellow";
        }
        return geomessageColorString;
    }
    
    /**
     * Gets the symbol image of size 100x100 for the specified symbol name or SIC.
     * @param symbolNameOrId a symbol name or SIC.
     * @return the symbol image for the specified symbol name or SIC.
     */
    public BufferedImage getSymbolImage(String symbolNameOrId) {
        return getSymbolImage(symbolNameOrId, 100, 100);
    }
        
    /**
     * Gets the symbol image for the specified symbol name or SIC.
     * @param symbolNameOrId a symbol name or SIC.
     * @param width the width (in pixels) of the generated image.
     * @param height the height (in pixels) of the generated image.
     * @return the symbol image for the specified symbol name or SIC.
     */
    public BufferedImage getSymbolImage(String symbolNameOrId, int width, int height) {
        return groupLayer.getMessageProcessor().getSymbolDictionary().getSymbolImage(symbolNameOrId, width, height);
    }
    
    /**
     * Searches for symbols having a keyword matching (fully or partially) the provided
     * search string.
     * @param searchString The search string.
     * @return A list of symbols matching the provided search string.
     */
    public List<SymbolProperties> findSymbols(String searchString) throws IOException {
        ArrayList<String> keywords = new ArrayList<String>();
        StringTokenizer tok = new StringTokenizer(searchString, Utilities.MIL_2525C_WHITESPACE_CHARS);
        while (tok.hasMoreTokens()) {
            keywords.add(tok.nextToken());
        }
        return groupLayer.getMessageProcessor().getSymbolDictionary().findSymbols(keywords);
    }
    
    /**
     * Returns a list of symbol categories.
     * @return a list of symbol categories.
     */
    public List<String> getCategories() {
        return groupLayer.getMessageProcessor().getSymbolDictionary().getFilters().get("Category");
    }
    
    /**
     * Returns a list of symbols in a category.
     * @param category the category name.
     * @return a list of symbols in a category.
     */
    public List<SymbolProperties> getSymbolsInCategory(String category) throws IOException {
        HashMap<String, List<String>> filters = new HashMap<String, List<String>>(1); 
        ArrayList<String> categories = new ArrayList<String>(1);
        categories.add(category);
        filters.put("Category", categories);
        return groupLayer.getMessageProcessor().getSymbolDictionary().findSymbols(filters);
    }
    
    @Override
    protected boolean processMessage(Geomessage geomessage) {
        //Filter out messages that we just sent
        if (null != geomessage.getId() && geomessage.getId().equals(appConfigController.getUniqueId())) {
            return false;
        }
        
        String action = (String) geomessage.getProperty(Geomessage.ACTION_FIELD_NAME);
        Message message;
        if ("select".equalsIgnoreCase(action)) {
            message = MessageHelper.createSelectMessage(DictionaryType.Mil2525C,
                    geomessage.getId(),
                    (String) geomessage.getProperty(Geomessage.TYPE_FIELD_NAME),
                    true);
        } else if ("un-select".equalsIgnoreCase(action)) {
            message = MessageHelper.createSelectMessage(DictionaryType.Mil2525C,
                    geomessage.getId(),
                    (String) geomessage.getProperty(Geomessage.TYPE_FIELD_NAME),
                    false);
        } else if ("remove".equalsIgnoreCase(action)) {
            message = MessageHelper.createRemoveMessage(DictionaryType.Mil2525C,
                    geomessage.getId(),
                    (String) geomessage.getProperty(Geomessage.TYPE_FIELD_NAME));
        } else {
            ArrayList<Point> points = new ArrayList<Point>();
            String pointsString = (String) geomessage.getProperty(Geomessage.CONTROL_POINTS_FIELD_NAME);
            if (null != pointsString) {
                StringTokenizer tok = new StringTokenizer(pointsString, ";");
                while (tok.hasMoreTokens()) {
                    StringTokenizer tok2 = new StringTokenizer(tok.nextToken(), ",");
                    try {
                        points.add(new Point(Double.parseDouble(tok2.nextToken()), Double.parseDouble(tok2.nextToken())));
                    } catch (Throwable t) {
                        Logger.getLogger(getClass().getName()).warning("Couldn't parse point from '" + pointsString + "'");
                    }
                }
            }
            message = MessageHelper.createUpdateMessage(DictionaryType.Mil2525C,
                    geomessage.getId(),
                    (String) geomessage.getProperty(Geomessage.TYPE_FIELD_NAME),
                    points);
            message.setProperties(geomessage.getProperties());
            message.setID(geomessage.getId());
        }
        
        try {
            return _processMessage(message);
        } catch (RuntimeException re) {
            //This is probably a message type that the MessageProcessor type doesn't support
            Logger.getLogger(getClass().getName()).log(Level.FINER, "Couldn't process message: " + re.getMessage() + "\n"
                    + "\tIt is possible that this MessageProcessor doesn't handle messages of type "
                    + message.getProperty(MessageHelper.MESSAGE_2525C_TYPE_PROPERTY_NAME) + ".");
            return false;
        }
    }
    
    private boolean _processMessage(Message message) {
        final int layerCount = groupLayer.getLayers().length;
        
        /**
         * Workaround: ArcGIS Runtime 10.2.4 requires a chem light message to have
         * a "sic" field.
         */
        if (ChemLightController.REPORT_TYPE.equals(message.getProperty(MessageHelper.MESSAGE_2525C_TYPE_PROPERTY_NAME))
                && null == message.getProperty(Geomessage.SIC_FIELD_NAME)) {
            String sic = appConfigController.getSic();
            if (null == sic) {
                sic = "SFGPU----------";
            }
            message.setProperty(Geomessage.SIC_FIELD_NAME, sic);
        }
        
        boolean success = groupLayer.getMessageProcessor().processMessage(message);
        if (layerCount < groupLayer.getLayers().length) {
            toggleLabels();
        }
        return success;
    }
    
    @Override
    protected boolean processHighlightMessage(String geomessageId, String messageType, boolean highlight) {
        Message message = MessageHelper.createSelectMessage(DictionaryType.Mil2525C, geomessageId, messageType, highlight);
        return _processMessage(message);
    }

    @Override
    public String[] getMessageTypesSupported() {
        return groupLayer.getMessageProcessor().getMessageTypesSupported();
    }

    @Override
    public String getActionPropertyName() {
        return MessageHelper.MESSAGE_ACTION_PROPERTY_NAME;
    }

    public IdentifyResultList identify(float screenX, float screenY, int tolerance) {
        IdentifyResultList results = new IdentifyResultList();
        Layer[] layers = groupLayer.getLayers();
        List<Layer> layerList = new ArrayList<Layer>(Arrays.asList(layers));
        layerList.add(spotReportLayer);
        for (Layer layer : layerList) {
            if (layer instanceof GraphicsLayer) {
                IdentifyResultList theseResults = Mil2525CMessageLayer.identify((GraphicsLayer) layer, screenX, screenY, tolerance);
                for (int i = 0; i < theseResults.size(); i++) {
                    results.add(theseResults.get(i), layer);
                }
            }
        }
        return results;
    }
    
    /**
     * Identifies at most one Graphic in the specified layer within the specified tolerance.
     * @param layerName the layer name.
     * @param screenX the X value in pixels.
     * @param screenY the Y value in pixels.
     * @param tolerance the tolerance in pixels.
     * @return the Graphic in the specified layer within the specified tolerance that is closest
     *         to the point specified by screenX and screenY, or null if no such Graphic exists.
     */
    public Graphic identifyOneGraphic(String layerName, float screenX, float screenY, int tolerance) {
        Layer layer = SPOT_REPORT_LAYER_NAME.equals(layerName) ? spotReportLayer : groupLayer.getLayer(layerName);
        if (null != layer && layer instanceof GraphicsLayer) {
            GraphicsLayer gl = (GraphicsLayer) layer;
            int[] graphicIds = gl.getGraphicIDs(screenX, screenY, tolerance, 1);
            if (0 < graphicIds.length) {
                return gl.getGraphic(graphicIds[0]);
            }
        }
        return null;
    }

    @Override
    protected void processRemoveGeomessage(String geomessageId, String messageType) {
        Message message = MessageHelper.createRemoveMessage(DictionaryType.Mil2525C, geomessageId, messageType);
        _processMessage(message);
    }
    
    @Override
    protected void removeSpotReportGraphic(int graphicId) {
        spotReportLayer.removeGraphic(graphicId);
    }

    public void geomessageReceived(Geomessage geomessage) {
        processGeomessage(geomessage);
    }

    public void datagramReceived(String contents) {
        
    }
    
    @Override
    public void clearLayer(String layerName, boolean sendRemoveMessageForOwnMessages) {
        if (SPOT_REPORT_LAYER_NAME.equals(layerName)) {
            int[] graphicIds = spotReportLayer.getGraphicIDs();
            loopAndRemove(graphicIds, spotReportLayer, sendRemoveMessageForOwnMessages, true);
        }
        Layer layer = groupLayer.getLayer(layerName);
        if (layer instanceof GraphicsLayer) {
            GraphicsLayer graphicsLayer = (GraphicsLayer) layer;
            int[] graphicIds = graphicsLayer.getGraphicIDs();
            loopAndRemove(graphicIds, graphicsLayer, sendRemoveMessageForOwnMessages, false);
        }
    }
    
    @Override
    public String[] getMessageLayerNames() {
        Layer[] layers = groupLayer.getLayers();
        String[] names = new String[layers.length + 1];
        for (int i = 0; i < layers.length; i++) {
            names[i] = layers[i].getName();
        }
        names[layers.length] = SPOT_REPORT_LAYER_NAME;
        return names;
    }
    
    @Override
    public String getMessageLayerName(String messageType) {
        if (null == messageType) {
            return null;
        }
        
        if (SpotReportController.REPORT_TYPE.equals(messageType)) {
            return SPOT_REPORT_LAYER_NAME;
        }
        
        File messageTypesDir = new File(symDictDir, "messagetypes");
        File[] files = messageTypesDir.listFiles(new FilenameFilter() {
            
            @Override
            public boolean accept(File dir, String filename) {
                return null != filename && filename.toLowerCase().endsWith(".json");
            }
        });
        for (File file : files) {
            BufferedReader in = null;
            try {
                in = new BufferedReader(new FileReader(file));
                StringBuffer sb = new StringBuffer();
                String line = null;
                while (null != (line = in.readLine())) {
                    sb.append(line);
                }
                JSONObject obj = new JSONObject(sb.toString());
                if (messageType.equals(obj.getString("type"))) {
                    return obj.getString("layerName");
                }
            } catch (Throwable t) {
                Logger.getLogger(getClass().getName()).log(Level.SEVERE, "Could not read and parse " + file.getAbsolutePath(), t);
            } finally {
                if (null != in) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        Logger.getLogger(getClass().getName()).log(Level.SEVERE, "Could not close file", e);
                    }
                }
            }
        }
        
        return null;
    }
    
    private void loopAndRemove(int[] graphicIds, GraphicsLayer graphicsLayer, boolean sendRemoveMessageForOwnMessages, boolean removeGraphics) {
        if (null != graphicIds) {
            for (int graphicId : graphicIds) {
                Graphic graphic = graphicsLayer.getGraphic(graphicId);
                removeGeomessage(graphic, sendRemoveMessageForOwnMessages);
                if (removeGraphics) {
                    graphicsLayer.removeGraphic(graphicId);
                }
            }
        }
    }
    
    private void removeGeomessage(Graphic graphic, boolean sendRemoveMessageForOwnMessages) {
        final String geomessageId = (String) graphic.getAttributeValue(Geomessage.ID_FIELD_NAME);
        final String geomessageType = (String) graphic.getAttributeValue(Geomessage.TYPE_FIELD_NAME);
        String uniqueDesignation = (String) graphic.getAttributeValue("uniquedesignation");
        if (sendRemoveMessageForOwnMessages && null != uniqueDesignation && uniqueDesignation.equals(messageController.getSenderUsername())) {
            new Thread() {
                public void run() {
                    try {
                        sendRemoveMessage(messageController, geomessageId, geomessageType);
                    } catch (Throwable t) {
                        Logger.getLogger(getClass().getName()).log(Level.SEVERE, "Couldn't send REMOVE message", t);
                    }
                }
            }.start();
        } else {
            processRemoveGeomessage(geomessageId, geomessageType);
        }
    }

} public class AppConfigController {

    private static final String KEY_USERNAME = AppConfigController.class.getSimpleName() + "username";
    private static final String KEY_VEHICLE_TYPE = AppConfigController.class.getSimpleName() + "vehicleType";
    private static final String KEY_UNIQUE_ID = AppConfigController.class.getSimpleName() + "uniqueId";
    private static final String KEY_SIC = AppConfigController.class.getSimpleName() + "sic";
    private static final String KEY_PORT = AppConfigController.class.getSimpleName() + "port";
    private static final String KEY_POSITION_MESSAGE_INTERVAL = AppConfigController.class.getSimpleName() + "positionMessageInterval";
    private static final String KEY_VEHICLE_STATUS_MESSAGE_INTERVAL = AppConfigController.class.getSimpleName() + "vehicleStatusMessageInterval";
    private static final String KEY_GPS_TYPE = AppConfigController.class.getSimpleName() + "gpsType";
    private static final String KEY_GPX = AppConfigController.class.getSimpleName() + "gpx";
    private static final String KEY_SPEED_MULTIPLIER = AppConfigController.class.getSimpleName() + "speedMultiplier";
    private static final String KEY_MPK_CHOOSER_DIR = AppConfigController.class.getSimpleName() + "mpkFileChooserDirectory";
    private static final String KEY_GPX_CHOOSER_DIR = AppConfigController.class.getSimpleName() + "gpxFileChooserDirectory";
    private static final String KEY_SHOW_MESSAGE_LABELS = AppConfigController.class.getSimpleName() + "showMessageLabels";
    private static final String KEY_DECORATED = AppConfigController.class.getSimpleName() + "decorated";
    private static final String KEY_SHOW_MGRS_GRID = AppConfigController.class.getSimpleName() + "showMgrsGrid";
    private static final String KEY_SHOW_LOCAL_TIME_ZONE = AppConfigController.class.getSimpleName() + "showLocalTimeZone";
    private static final String KEY_MGRS_COORDINATE_NOTATION = AppConfigController.class.getSimpleName() + "useMgrs";
    private static final String KEY_HEADING_UNITS = AppConfigController.class.getSimpleName() + "headingUnits";
    private static final String KEY_GEOMESSAGE_VERSION = AppConfigController.class.getSimpleName() + "geomessageVersion";

    private boolean gpsTypeDirty = false;

    /**
     * @param messageController the messageController to set
     */
    public void setMessageController(MessageController messageController) {
        this.messageController = messageController;
        if (null != messageController && -1 != getPort()) {
            messageController.setPort(getPort());
        }
    }
    
    public MessageController getMessageController() {
        return messageController;
    }

    private class AppConfigHandler extends DefaultHandler {

        private String username = null;
        private String vehicleType = null;
        private String uniqueId = null;
        private String sic = null;
        private int port = -1;
        private int positionMessageInterval = -1;
        private int vehicleStatusMessageInterval = -1;
        private LocationMode gpsType = LocationMode.SIMULATOR;
        private String gpx = null;
        private double speedMultiplier = -1;
        private int headingUnits = AngularUnit.Code.DEGREE;
        private String geomessageVersion = "1.1";
        
        private boolean readingUser = false;
        private boolean readingCode = false;
        private boolean readingMessaging = false;
        private boolean readingPort = false;
        private boolean readingPositionMessageInterval = false;
        private boolean readingVehicleStatusMessageInterval = false;
        private boolean readingGps = false;

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            if ("user".equalsIgnoreCase(qName)) {
                readingUser = true;
                username = attributes.getValue("name");
                vehicleType = attributes.getValue("type");
                uniqueId = attributes.getValue("id");
            } else if (readingUser) {
                if ("code".equalsIgnoreCase(qName)) {
                    readingCode = true;
                }
            } else if ("messaging".equalsIgnoreCase(qName)) {
                readingMessaging = true;
            } else if (readingMessaging) {
                if ("port".equalsIgnoreCase(qName)) {
                    readingPort = true;
                } else if ("interval".equalsIgnoreCase(qName) || "positionmessageinterval".equalsIgnoreCase(qName)) {
                    //Vehicle Commander 1.0 used "interval" instead of "positionmessageinterval"; accept either one
                    readingPositionMessageInterval = true;
                } else if ("vehiclestatusmessageinterval".equalsIgnoreCase(qName)) {
                    readingVehicleStatusMessageInterval = true;
                }
            } else if ("gps".equalsIgnoreCase(qName)) {
                readingGps = true;
                gpsType = "onboard".equalsIgnoreCase(attributes.getValue("type"))
                        ? LocationMode.LOCATION_SERVICE : LocationMode.SIMULATOR;
                gpx = attributes.getValue("gpx");
                String speedMultiplierString = attributes.getValue("speedMultiplier");
                if (null != speedMultiplierString) {
                    try {
                        speedMultiplier = Double.parseDouble(speedMultiplierString);
                    } catch (Throwable t) {}
                }
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            String value = new String(ch, start, length);
            if (readingCode) {
                sic = value;
            } else {
                try {
                    int intValue = Integer.parseInt(value);
                    if (readingPort) {
                        port = intValue;
                    } else if (readingPositionMessageInterval) {
                        positionMessageInterval = intValue;
                    } else if (readingVehicleStatusMessageInterval) {
                        vehicleStatusMessageInterval = intValue;
                    }
                } catch (NumberFormatException nfe) {

                }
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if ("user".equalsIgnoreCase(qName)) {
                readingUser = false;
            } else if ("code".equalsIgnoreCase(qName)) {
                readingCode = false;
            } else if ("messaging".equalsIgnoreCase(qName)) {
                readingMessaging = false;
            } else if ("port".equalsIgnoreCase(qName)) {
                readingPort = false;
            } else if ("interval".equalsIgnoreCase(qName) || "positionmessageinterval".equalsIgnoreCase(qName)) {
                //Vehicle Commander 1.0 used "interval" instead of "positionmessageinterval"; accept either one
                readingPositionMessageInterval = false;
            } else if ("vehiclestatusmessageinterval".equalsIgnoreCase(qName)) {
                readingVehicleStatusMessageInterval = false;
            } else if ("gps".equalsIgnoreCase(qName)) {
                readingGps = false;
            }
        }
        
    }

    private final Preferences preferences;
    private final Set<AppConfigListener> listeners = new HashSet<AppConfigListener>();

    private LocationController locationController;
    private MessageController messageController;

    /**
     * Creates a new AppConfigController. This constructor first reads the user's
     * settings from the system. Then, if appconfig.xml is present in the working
     * directory, any settings not present in the user profile will be read from
     * appconfig.xml.
     */
    public AppConfigController() {
        preferences = Preferences.userNodeForPackage(getClass());
        
        try {
            resetFromAppConfigFile(false);
        } catch (ParserConfigurationException ex) {
            Logger.getLogger(AppConfigController.class.getName()).log(Level.SEVERE, null, ex);
        } catch (SAXException ex) {
            Logger.getLogger(AppConfigController.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(AppConfigController.class.getName()).log(Level.SEVERE, null, ex);
        } catch (URISyntaxException ex) {
            Logger.getLogger(AppConfigController.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public void addListener(AppConfigListener listener) {
        listeners.add(listener);
    }

    /**
     * Returns the file that will be read when resetting application configuration
     * settings. This file may or may not actually exist.
     * @return the file that will be read when resetting application configuration
     *         settings.
     */
    private URI getAppConfigFileUri() throws URISyntaxException {
        File configFile = new File("./appconfig.xml");
        if (configFile.exists()) {
            return configFile.toURI();
        } else {
            return getClass().getResource("/com/esri/vehiclecommander/resources/appconfig.xml").toURI();
        }
    }

    /**
     * Resets application configuration settings by reading appconfig.xml found
     * in the working directory.
     * @param overwriteExistingSettings true if settings that are present should
     *                                  be overwritten; false if settings that
     *                                  are present should not be overwritten
     */
    public final void resetFromAppConfigFile(boolean overwriteExistingSettings) throws ParserConfigurationException, SAXException, IOException, URISyntaxException {
        if (overwriteExistingSettings || null == getUsername() || null == getUniqueId()
                || null == getVehicleType()
                || null == getGpsType() || null == getGpx() || 0 >= getSpeedMultiplier()
                || null == getSic() || -1 == getPort() || -1 == getPositionMessageInterval()
                || -1 == getVehicleStatusMessageInterval()) {
            AppConfigHandler handler = new AppConfigHandler();
            SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
            parser.parse(getAppConfigFileUri().toString(), handler);
            if (overwriteExistingSettings || null == getUsername()) {
                setUsername(handler.username);
            }
            if (overwriteExistingSettings || null == getVehicleType()) {
                setVehicleType(handler.vehicleType);
            }
            if (overwriteExistingSettings || null == getUniqueId()) {
                setUniqueId(handler.uniqueId);
            }
            if (overwriteExistingSettings || null == getSic()) {
                setSic(handler.sic);
            }
            if (overwriteExistingSettings || -1 == getPort()) {
                setPort(handler.port);
            }
            if (overwriteExistingSettings || -1 == getPositionMessageInterval()) {
                setPositionMessageInterval(handler.positionMessageInterval);
            }
            if (overwriteExistingSettings || -1 == getVehicleStatusMessageInterval()) {
                setVehicleStatusMessageInterval(handler.vehicleStatusMessageInterval);
            }
            if (overwriteExistingSettings || null == getGpsType()) {
                setLocationMode(handler.gpsType, false);
            }
            if (overwriteExistingSettings || null == getGpx()) {
                setGpx(handler.gpx, false);
            }
            if (overwriteExistingSettings || -1 == getSpeedMultiplier()) {
                setSpeedMultiplier(handler.speedMultiplier);
            }
            if (overwriteExistingSettings || null == getGeomessageVersion()) {
                setGeomessageVersion(handler.geomessageVersion);
            }
            resetLocationController();
        }
    }

    private void setPreference(String key, String value) {
        if (null != key) {
            if (null == value) {
                preferences.remove(key);
            } else {
                preferences.put(key, value);
            }
        }
    }

    private void setPreference(String key, int value) {
        preferences.putInt(key, value);
    }

    private void setPreference(String key, double value) {
        preferences.putDouble(key, value);
    }

    private void setPreference(String key, boolean value) {
        preferences.putBoolean(key, value);
    }

    /**
     * Saves the specified username to the application configuration settings.
     * @param username the username, or null to erase the setting.
     */
    public void setUsername(String username) {
        setPreference(KEY_USERNAME, username);
    }

    /**
     * Returns the stored username, or null if no username has been set.
     * @return the stored username, or null if no username has been set.
     */
    public final String getUsername() {
        return preferences.get(KEY_USERNAME, null);
    }

    /**
     * Saves the specified vehicle type to the application configuration settings.
     * @param vehicleType the vehicle type, or null to erase the setting.
     */
    public void setVehicleType(String vehicleType) {
        setPreference(KEY_VEHICLE_TYPE, vehicleType);
    }

    /**
     * Returns the stored vehicle type, or null if no vehicle type has been set.
     * @return the stored vehicle type, or null if no vehicle type has been set.
     */
    public final String getVehicleType() {
        return preferences.get(KEY_VEHICLE_TYPE, null);
    }

    /**
     * Saves the specified unique ID to the application configuration settings.
     * Normally this is a UUID/GUID.
     * @param uniqueId the unique ID. Unique ID cannot be null, so if uniqueId parameter
     *                 is null, the unique ID will be set to a new random GUID.
     */
    public void setUniqueId(String uniqueId) {
        if (null == uniqueId) {
            uniqueId = UUID.randomUUID().toString();
        }
        setPreference(KEY_UNIQUE_ID, uniqueId);

    }

    /**
     * Returns the stored unique ID. If no ID has been set, a new ID will be generated.
     * @return the stored unique ID. If no ID has been set, a new ID will be generated.
     */
    public final String getUniqueId() {
        String uniqueId = preferences.get(KEY_UNIQUE_ID, null);
        if (null == uniqueId) {
            uniqueId = UUID.randomUUID().toString();
            setUniqueId(uniqueId);
        }
        return uniqueId;
    }

    /**
     * Saves the specified symbol ID code to the application configuration settings.
     * @param sic the symbol ID code, or null to erase the setting.
     */
    public void setSic(String sic) {
        setPreference(KEY_SIC, sic);
    }

    /**
     * Returns the stored symbol ID code, or null if no symbol ID code has been set.
     * @return the stored symbol ID code, or null if no symbol ID code has been set.
     */
    public final String getSic() {
        return preferences.get(KEY_SIC, null);
    }

    /**
     * Saves the specified UDP port number for messaging to the application configuration settings.
     * @param port the messaging port number.
     * @throws IllegalArgumentException if port is less than 0 or more than 0xFFFF.
     */
    public void setPort(int port) throws IllegalArgumentException {
        if (port < 0 || port > 0xFFFF) {
            //Invalid port number; do nothing
            Logger.getLogger(AppConfigController.class.getName()).log(Level.WARNING, "Invalid port number: {0}", port);
        } else {
            setPreference(KEY_PORT, port);
            if (null != messageController) {
                messageController.setPort(port);
            }
        }
    }

    /**
     * Returns the stored UDP port number for messaging, or -1 if no port number has been set.
     * @return the stored UDP port number for messaging, or -1 if no port number has been set.
     */
    public final int getPort() {
        return preferences.getInt(KEY_PORT, -1);
    }

    /**
     * Saves the specified position message interval, in milliseconds, to the application configuration settings.
     * @param positionMessageInterval the position message interval, in milliseconds.
     */
    public void setPositionMessageInterval(int positionMessageInterval) {
        setPreference(KEY_POSITION_MESSAGE_INTERVAL, positionMessageInterval);
    }

    /**
     * Returns the stored position message interval, in milliseconds, or -1 if no messaging interval has been set.
     * @return the stored position message interval, in milliseconds, or -1 if no messaging interval has been set.
     */
    public final int getPositionMessageInterval() {
        return preferences.getInt(KEY_POSITION_MESSAGE_INTERVAL, -1);
    }

    /**
     * Saves the specified vehicle status message interval, in milliseconds, to the application configuration settings.
     * @param vehicleStatusMessageInterval the vehicle status message interval, in milliseconds.
     */
    public void setVehicleStatusMessageInterval(int vehicleStatusMessageInterval) {
        setPreference(KEY_VEHICLE_STATUS_MESSAGE_INTERVAL, vehicleStatusMessageInterval);
    }

    /**
     * Returns the stored vehicle status message interval, in milliseconds, or -1 if no messaging interval has been set.
     * @return the stored vehicle status message interval, in milliseconds, or -1 if no messaging interval has been set.
     */
    public final int getVehicleStatusMessageInterval() {
        return preferences.getInt(KEY_VEHICLE_STATUS_MESSAGE_INTERVAL, -1);
    }

    /**
     * Saves the GPS type to the application configuration settings.
     * @deprecated use setLocationMode instead.
     * @param locationMode the location mode.
     */
    public void setGpsType(LocationMode locationMode) throws ParserConfigurationException, SAXException, IOException {
        setLocationMode(locationMode);
    }

    /**
     * Saves the location mode to the application configuration settings.
     * @param locationMode the location mode.
     */
    public void setLocationMode(LocationMode locationMode) throws ParserConfigurationException, SAXException, IOException {
        setLocationMode(locationMode, true);
    }
    
    private void setLocationMode(LocationMode locationMode, boolean resetNow) throws ParserConfigurationException, SAXException, IOException {
        LocationMode oldMode = getLocationMode();
        setPreference(KEY_GPS_TYPE, locationMode.toString());

        if (null == oldMode || !oldMode.equals(locationMode)) {
            gpsTypeDirty = true;
            if (resetNow) {
                resetLocationController();
            }
        }
    }

    private void resetLocationController() throws ParserConfigurationException, SAXException, IOException {
        if (null != locationController) {
            locationController.pause();
            locationController.reset();
            if (getLocationMode().equals(LocationMode.SIMULATOR)) {
                locationController.setGpxFile(null == getGpx() ? null : new File(getGpx()));
            }
            locationController.setMode(getLocationMode(), false);
            gpsTypeDirty = false;
            locationController.start();
        }
    }

    /**
     * Returns the GPS type.
     * @return the GPS type.
     * @deprecated use getLocationMode() instead.
     */
    public LocationMode getGpsType() {
        return getLocationMode();
    }
    
    public LocationMode getLocationMode() {
        String name = preferences.get(KEY_GPS_TYPE, null);
        if (null == name) {
            return null;
        } else {
            try {
                return LocationMode.valueOf(name);
            } catch (Throwable t) {
                return null;
            }
        }
    }

    /**
     * Saves the GPX file to be used for simulated GPS to the application configuration settings.
     * @param gpx the GPX file to be used for simulated GPS.
     */
    public void setGpx(String gpx) throws ParserConfigurationException, SAXException, IOException {
        setGpx(gpx, true);
    }

    private void setGpx(String gpx, boolean resetNow) throws ParserConfigurationException, SAXException, IOException {
        String oldGpx = getGpx();
        setPreference(KEY_GPX, gpx);
        if (resetNow) {
            if (gpsTypeDirty) {
                resetLocationController();
            } else if (null == oldGpx) {
                if (null != gpx) {
                    resetLocationController();
                }
            } else if (!oldGpx.equals(gpx)) {
                resetLocationController();
            }
        }
    }

    /**
     * Returns the GPX file to be used for simulated GPS.
     * @return the GPX file to be used for simulated GPS.
     */
    public String getGpx() {
        return preferences.get(KEY_GPX, null);
    }

    /**
     * Saves the simulated GPS speed multiplier to the application configuration settings.
     * @param multiplier the simulated GPS speed multiplier.
     */
    public void setSpeedMultiplier(double multiplier) {
        setPreference(KEY_SPEED_MULTIPLIER, multiplier);
        if (null != locationController) {
            locationController.setSpeedMultiplier(multiplier);
        }
    }

    /**
     * Returns the simulated GPS speed multiplier, or -1 if no speed multiplier has been set.
     * @return the simulated GPS speed multiplier, or -1 if no speed multiplier has been set.
     */
    public final double getSpeedMultiplier() {
        return preferences.getDouble(KEY_SPEED_MULTIPLIER, -1);
    }

    /**
     * Saves the current directory for the map package file chooser to the application
     * configuration settings.
     * @param the current directory for the map package file chooser.
     */
    public void setMPKFileChooserCurrentDirectory(String dir) {
        setPreference(KEY_MPK_CHOOSER_DIR, dir);
    }

    /**
     * Returns the stored current directory for the map package file chooser, or
     * null if no directory has been set.
     * @return the stored current directory for the map package file chooser, or
     *         null if no directory has been set.
     */
    public String getMPKFileChooserCurrentDirectory() {
        return preferences.get(KEY_MPK_CHOOSER_DIR, null);
    }

    /**
     * Saves the current directory for the GPX file chooser to the application
     * configuration settings.
     * @param the current directory for the GPX file chooser.
     */
    public void setGPXFileChooserCurrentDirectory(String dir) {
        setPreference(KEY_GPX_CHOOSER_DIR, dir);
    }

    /**
     * Returns the stored current directory for the GPX file chooser, or
     * null if no directory has been set.
     * @return the stored current directory for the GPX file chooser, or
     *         null if no directory has been set.
     */
    public String getGPXFileChooserCurrentDirectory() {
        return preferences.get(KEY_GPX_CHOOSER_DIR, null);
    }

    /**
     * Gets the application's GPSController.
     * @return the gpsController
     * @deprecated use getLocationController() instead.
     */
    public LocationController getGpsController() {
        return getLocationController();
    }
    
    public LocationController getLocationController() {
        return locationController;
    }

    /**
     * Gives this AppConfigController a reference to the application's GPSController.
     * @param gpsController the gpsController to set
     * @deprecated use setLocationController(LocationController) instead.
     */
    public void setGpsController(LocationController locationController) {
        setLocationController(locationController);
    }
    
    public void setLocationController(LocationController locationController) {
        this.locationController = locationController;
    }

    /**
     * Returns true if the application should show labels for new message features.
     * @return true if the application should show labels for new message features.
     */
    public boolean isShowMessageLabels() {
        return preferences.getBoolean(KEY_SHOW_MESSAGE_LABELS, true);
    }

    /**
     * Tells the application whether it should show labels for new message features.
     * @param showMessageLabels true if the application should show labels for new message features.
     */
    public void setShowMessageLabels(final boolean showMessageLabels) {
        final boolean oldValue = isShowMessageLabels();
        setPreference(KEY_SHOW_MESSAGE_LABELS, showMessageLabels);
        if (oldValue != showMessageLabels) {
            synchronized (listeners) {
                for (final AppConfigListener listener : listeners) {
                    new Thread() {

                        @Override
                        public void run() {
                            listener.showMessageLabelsChanged(showMessageLabels);
                        }
                        
                    }.start();                    
                }
            }
        }
    }

    /**
     * Returns true if the application should be decorated (title bar, resizable, etc.).
     * @return true if the application should be decorated (title bar, resizable, etc.).
     */
    public boolean isDecorated() {
        return preferences.getBoolean(KEY_DECORATED, true);
    }

    /**
     * Tells the application whether it should be decorated (title bar, resizable, etc.).
     * @param decorated true if the application should be decorated (title bar, resizable, etc.).
     */
    public void setDecorated(final boolean decorated) {
        boolean oldDecorated = isDecorated();
        setPreference(KEY_DECORATED, decorated);
        if (decorated != oldDecorated) {
            for (final AppConfigListener listener : listeners) {
                listener.decoratedChanged(decorated);
            }
        }
    }
    
    /**
     * Returns true if the application should show an MGRS grid on the map.
     * @return true if the application should show an MGRS grid on the map.
     */
    public boolean isShowMgrsGrid() {
        return preferences.getBoolean(KEY_SHOW_MGRS_GRID, false);
    }

    /**
     * Tells the application whether it should show an MGRS grid on the map.
     * @param showMessageLabels true if the application should show an MGRS grid on the map.
     */
    public void setShowMgrsGrid(final boolean showMgrsGrid) {
        setPreference(KEY_SHOW_MGRS_GRID, showMgrsGrid);
    }
    
    /**
     * Returns true if the application should display the time in the machine's
     * time zone.
     * @return true if the application should display the time in the machine's
     *         time zone.
     */
    public boolean isShowLocalTimeZone() {
        return preferences.getBoolean(KEY_SHOW_LOCAL_TIME_ZONE, false);
    }

    /**
     * Tells the application whether it should display the time in the machine's
     * time zone.
     * @param showLocalTimeZone true if the application should display the time
     *                          in the machine's time zone.
     */
    public void setShowLocalTimeZone(boolean showLocalTimeZone) {
        setPreference(KEY_SHOW_LOCAL_TIME_ZONE, showLocalTimeZone);
    }
    
    /**
     * Returns true if the application should display the current GPS location in
     * MGRS coordinates.
     * @return true if the application should display the current GPS location in
     * MGRS coordinates and false if the application should display the current
     * GPS location in longitude/latitude.
     */
    public boolean isShowMgrs() {
        return preferences.getBoolean(KEY_MGRS_COORDINATE_NOTATION, true);
    }
    
    /**
     * Tells the application whether to use MGRS or longitude/latitude coordinate
     * notation for coordinate display.
     * @param useMgrs true for MGRS and false for longitude/latitude.
     */
    public void setShowMgrs(boolean showMgrs) {
        setPreference(KEY_MGRS_COORDINATE_NOTATION, showMgrs);
    }
    
    /**
     * Tells the application the units in which to display the GPS heading.
     * @param an AngularUnit.Code constant representing the units in which to display
     *        the GPS heading.
     * @see AngularUnit.Code
     */
    public void setHeadingUnits(int headingUnits) {
        setPreference(KEY_HEADING_UNITS, headingUnits);
    }

    /**
     * Returns the units in which to display the GPS heading.
     * @return an AngularUnit.Code constant representing the units in which to display
     *         the GPS heading. The default is degrees.
     */
    public int getHeadingUnits() {
        return preferences.getInt(KEY_HEADING_UNITS, AngularUnit.Code.DEGREE);
    }
    
    /**
     * Tells the application the Geomessage version to use for outgoing messages.
     * @param geomessageVersion the Geomessage version.
     */
    public void setGeomessageVersion(String geomessageVersion) {
        setPreference(KEY_GEOMESSAGE_VERSION, geomessageVersion);
    }
    
    /**
     * Returns the Geomessage version that the application is using for outgoing messages.
     * @return the Geomessage version that the application is using for outgoing messages.
     */
    public String getGeomessageVersion() {
        return preferences.get(KEY_GEOMESSAGE_VERSION, "1.1");
    }

} public interface AppConfigListener {

    /**
     * Called when the "decorated" preference changes. A decorated application is
     * one with a title bar that can be moved, resized, maximized, etc.
     * @param isDecorated true if the preference was changed to decorated, or false
     *                    if the preference was changed to undecorated.
     */
    public void decoratedChanged(boolean isDecorated);
    
    /**
     * Called when the "showMessageLabels" preference changes. This preference
     * indicates whether labels will appear with military symbols.
     */
    public void showMessageLabelsChanged(boolean showMessageLabels);

}
 public abstract class GPAdapter implements GPListener {

    public void gpDisbled() {
        
    }

    public void gpEnabled() {
        
    }
    
    public void gpStarted() {
        
    }

    public void gpEnded(ArcGISDynamicMapServiceLayer resultLayer) {
        
    }
    
}
 public interface IdentifyListener {

    /**
     * Called when a complete identify operation finishes, which may include the
     * completion of more than one IdentifyTask.
     * @param identifyPoint the point that was used for the identify operation,
     *                      for display purposes. Can be null.
     * @param results the combined results from all identify tasks.
     * @param resultToLayer a map of results to the layer from which each result comes.
     */
    public void identifyComplete(Point identifyPoint, IdentifiedItem[] results, Map<IdentifiedItem, Layer> resultToLayer);

}
 public class MapController extends com.esri.militaryapps.controller.MapController {

    /**
     * Constant representing a mouse click event, to be used with the trackAsync
     * method.
     * @see #trackAsync(com.esri.vehiclecommander.MapOverlayListener, short)
     */
    public static final short EVENT_MOUSE_CLICKED = 1;

    /**
     * Constant representing a mouse drag event, to be used with the trackAsync
     * method.
     * @see #trackAsync(com.esri.vehiclecommander.MapOverlayListener, short)
     */
    public static final short EVENT_MOUSE_DRAGGED = 2;

    /**
     * Constant representing a mouse press event, to be used with the trackAsync
     * method.
     * @see #trackAsync(com.esri.vehiclecommander.MapOverlayListener, short)
     */
    public static final short EVENT_MOUSE_PRESSED = 4;

    /**
     * Constant representing a mouse drag event, to be used with the trackAsync
     * method.
     * @see #trackAsync(com.esri.vehiclecommander.MapOverlayListener, short)
     */
    public static final short EVENT_MOUSE_RELEASED = 8;

    private final JMap map;
    private final List<Layer> overlayLayers = new ArrayList<Layer>();
//    private final List<MapControllerListener> listeners = new ArrayList<MapControllerListener>();
    private final HashSet<CallbackListener<IdentifyResult[]>> identifyListeners = new HashSet<CallbackListener<IdentifyResult[]>>();
    private final ArrayList<IdentifiedItem> allResults = new ArrayList<IdentifiedItem>();
    /**
     * Maps each result to the layer from which it came.
     */
    private final Map<IdentifiedItem, Layer> resultToLayer = new HashMap<IdentifiedItem, Layer>();
    /**
     * Maps layers--probably map service layers--to corresponding feature layers,
     * where individual feature layers are identified by their IDs.
     */
    private final Map<Layer, Map<Integer, ArcGISFeatureLayer>> layerToFeatureLayer = new HashMap<Layer, Map<Integer, ArcGISFeatureLayer>>();
    private final IdentifyListener identifyListener;
    private final Object lastLocationLock = new Object(); 
    private final ChemLightController chemLightController;
    
    private AdvancedSymbolController symbolController;
    private MapOverlay trackOverlay = null;
    private boolean autoPan = false;
    private Point lastLocation = null;
    private PopupDialog chemLightPopupDialog = null;

    /**
     * Constructs a MapController with a JMap
     * @param map The JMap to be controlled by this MapController
     * @param identifyListener the application's identify listener.
     */
    public MapController(
            JMap map,
            IdentifyListener identifyListener,
            AppConfigController appConfig,
            ChemLightController chemLightController) {
        this.chemLightController = chemLightController;
        map.addMapEventListener(new MapEventListenerAdapter() {

            @Override
            public void mapReady(MapEvent event) {
                fireMapReady();
            }

        });
        this.map = map;
        
        setAutoPan(autoPan);
        setGridVisible(appConfig.isShowMgrsGrid());
        map.getGrid().setType(Grid.GridType.MGRS);
        this.identifyListener = identifyListener;
        activateIdentify();
    }

    @Override
    protected LocationController createLocationController() {
        try {
            return new LocationController(this, LocationController.LocationMode.SIMULATOR, autoPan);
        } catch (Exception ex) {
            Logger.getLogger(MapController.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
    }

    @Override
    public void zoom(double factor) {
        map.zoom(factor);
    }

    @Override
    protected void _zoomToScale(double scale, double centerPointX, double centerPointY) {
        map.zoomToScale(scale, new Point(centerPointX, centerPointY));
    }

    @Override
    public int getWidth() {
        return map.getWidth();
    }

    @Override
    public int getHeight() {
        return map.getHeight();
    }

    @Override
    public void panTo(double centerX, double centerY) {
        map.panTo(new Point(centerX, centerY));
    }
    
    /**
     * Pans the map to a new center point, if a valid MGRS string is provided.
     * @param newCenterMgrs the map's new center point, as an MGRS string.
     * @return if the string was valid, the point to which the map was panned; null otherwise
     */
    public Point panTo(String newCenterMgrs) {
        newCenterMgrs = Utilities.convertToValidMgrs(newCenterMgrs,
                pointToMgrs(map.getExtent().getCenter(), map.getSpatialReference()));
        if (null != newCenterMgrs) {
            Point pt = mgrsToPoint(newCenterMgrs);
            if (null != pt) {
                map.panTo(pt);
                return pt;
            } else {
                Logger.getLogger(getClass().getName()).warning("MGRS string " + newCenterMgrs + " could not be converted to a point");
                return null;
            }
        } else {
            return null;
        }
    }

    @Override
    public double[] toMapPoint(int screenX, int screenY) {
        Point pt = toMapPointObject(screenX, screenY);
        if (null == pt) {
            return null;
        } else {
            return new double[] { pt.getX(), pt.getY() };
        }
    }
    
    /**
     * Returns a Point object for the specified screen coordinates.
     * @param screenX the screen X-coordinate.
     * @param screenY the screen Y-coordinate.
     * @return a Point object for the specified screen coordinates.
     */
    public Point toMapPointObject(int screenX, int screenY) {
        return map.toMapPoint(screenX, screenY);
    }

    @Override
    public void setGridVisible(boolean visible) {
        map.getGrid().setVisibility(visible);
    }

    @Override
    public boolean isGridVisible() {
        return map.getGrid().getVisibility();
    }

    @Override
    public void setAutoPan(boolean autoPan) {
        if (autoPan) {
            synchronized (lastLocationLock) {
                if (null != lastLocation) {
                    map.panTo(lastLocation);
                }
            }
        }
        this.autoPan = autoPan;
    }

    @Override
    public boolean isAutoPan() {
        return autoPan;
    }

    @Override
    public String pointToMgrs(double x, double y, int wkid) {
        Point pt = new Point(x, y);
        try {
            SpatialReference sr = SpatialReference.create(wkid);
            return pointToMgrs(pt, sr);
        } catch (Throwable t) {
            Logger.getLogger(MapController.class.getName()).log(Level.SEVERE, null, t);
            return null;
        }
    }
    
    /**
     * Converts a point to an MGRS string.
     * @param pt the point to be converted to an MGRS string.
     * @param sr the point's spatial reference.
     * @return the MGRS representation of the point.
     */
    public String pointToMgrs(Point pt, SpatialReference sr) {
        return CoordinateConversion.pointToMgrs(pt, sr, CoordinateConversion.MGRSConversionMode.AUTO, 5, false, false);
    }
    
    /**
     * Converts an MGRS string to a map point.
     * @param mgrsString the MGRS string to convert to a map point.
     * @return a map point in the coordinate system of the map.
     */
    public Point mgrsToPoint(String mgrsString) {
        SpatialReference sr = map.getSpatialReference();
        if (null == sr) {
            //Assume Web Mercator (3857)
            sr = SpatialReference.create(3857);
        }
        return CoordinateConversion.mgrsToPoint(mgrsString, sr, CoordinateConversion.MGRSConversionMode.AUTO);
    }

    @Override
    public double[] projectPoint(double x, double y, int fromWkid, int toWkid) {
        Point pt = (Point) GeometryEngine.project(new Point(x, y), SpatialReference.create(fromWkid), SpatialReference.create(toWkid));
        return new double[] { pt.getX(), pt.getY() };
    }

    public void onLocationChanged(Location location) {
        if (null != location) {
            final Point mapPoint = GeometryEngine.project(location.getLongitude(), location.getLatitude(), getSpatialReference());
            new Thread() {
                public void run() {
                    synchronized (lastLocationLock) {
                        lastLocation = mapPoint;
                    }
                };
            }.start();
            
            if (isAutoPan()) {
                map.panTo(mapPoint);
                
                switch (getLocationController().getNavigationMode()) {
                    case NORTH_UP:
                        setRotation(0);
                        break;

                    case TRACK_UP:
                        setRotation(location.getHeading());
                        break;
                        
                    case WAYPOINT_UP:
                        Graphic waypoint = ((LocationController) getLocationController()).getSelectedWaypoint();
                        if (null != waypoint) {
                            Point waypointLonLat = (Point) GeometryEngine.project(
                                    waypoint.getGeometry(),
                                    null == waypoint.getSpatialReference() ? getSpatialReference() : waypoint.getSpatialReference(),
                                    Utilities.WGS84);
                            setRotation(Utilities.calculateBearingDegrees(location.getLongitude(), location.getLatitude(), waypointLonLat.getX(), waypointLonLat.getY()));
                        }
                }
            }
        }
    }

    public void onStateChanged(LocationProvider.LocationProviderState state) {
        
    }

    /**
     * Sets the map's rotation, in degrees.
     * @param degrees the new map rotation.
     */
    @Override
    public void setRotation(double degrees) {
        map.setRotation(Utilities.fixAngleDegrees(degrees, -180, 180));
    }

    /**
     * Gets the map's rotation, in degrees.
     */
    @Override
    public double getRotation() {
        return map.getRotation();
    }

    /**
     * Adds a layer to the map.
     * @param layer the layer to add.
     * @param isOverlay true if the layer is an overlay that can be turned on and
     *                  off, and false otherwise.
     */
    public void addLayer(Layer layer, boolean isOverlay) {
        synchronized (map) {
            if (0 == map.getLayers().size()) {
                addLayer(0, layer, isOverlay);
            } else {
                //Don't add it on top of graphics layers or GPSLayer at the top
                for (int i = map.getLayers().size() - 1; i >= 0; i--) {
                    if (!(map.getLayers().get(i) instanceof GraphicsLayer)
                            && !(map.getLayers().get(i) instanceof GPSLayer)) {
                        addLayer(i + 1, layer, isOverlay);
                        i = 0;
                    }
                }
            }
        }
        fireLayersChanged(isOverlay);
    }

    /**
     * Adds a layer to the map at a certain index in the layer list, adjusting the
     * index to place the layer below (i.e. lesser index than) all GPSLayer instances.
     * @param layerIndex the index in the layer list. If greater than the current
     *                   length, the layer will be added to the end of
     *                   the list.
     * @param layer the layer to add.
     * @param isOverlay true if the layer is an overlay that can be turned on and
     *                  off, and false otherwise.
     */
    public void addLayer(int layerIndex, Layer layer, boolean isOverlay) {
        addLayer(layerIndex, layer, isOverlay, true);
    }

    /**
     * Adds a layer to the map at a certain index in the layer list.
     * @param layerIndex the index in the layer list. If greater than the current
     *                   length, the layer will be added to the end of
     *                   the list.
     * @param layer the layer to add.
     * @param isOverlay true if the layer is an overlay that can be turned on and
     *                  off, and false otherwise.
     * @param placeBelowGps true if the layer should go below any instance of GPSLayer;
     *                  false if instances of GPSLayer should be ignored and this
     *                  layer should just be placed at layerIndex.
     */
    public synchronized void addLayer(int layerIndex, Layer layer, boolean isOverlay, boolean placeBelowGps) {
        if (layerIndex > map.getLayers().size()) {
            layerIndex = map.getLayers().size();
        }
        if (placeBelowGps) {
            for (int index = 0; index < layerIndex; index++) {
                if (map.getLayers().get(index) instanceof GPSLayer) {
                    layerIndex = index;//This 1) changes layerIndex and 2) breaks out of the for loop
                }
            }
        }
        map.getLayers().add(layerIndex, layer);
        if (isOverlay) {
            for (int mapIndex = 0, listIndex = 0; mapIndex < map.getLayers().size(); mapIndex++) {
                if (layerIndex == mapIndex || overlayLayers.size() <= listIndex) {
                    overlayLayers.add(listIndex, layer);
                    mapIndex = map.getLayers().size();
                } else {
                    if (overlayLayers.get(listIndex) == map.getLayers().get(mapIndex)) {
                        listIndex++;
                    }
                }
            }
        }
        fireLayersChanged(isOverlay);
    }

    /**
     * Adds a Collection of layers to the map.
     * @param layers the Collection of layers to add.
     * @param isOverlay true if the layers are overlays that can be turned on and
     *                  off, and false otherwise.
     */
    public void addLayers(Collection<Layer> layers, boolean isOverlay) {
        synchronized (map) {
            map.getLayers().addAll(layers);
        }
        if (isOverlay) {
            overlayLayers.addAll(layers);
        }
        fireLayersChanged(isOverlay);
    }
    
    /**
     * Moves the layer to a new location in the layer list. If the layer is not already
     * in the map, nothing happens.
     * @param layer the layer to move.
     * @param newIndex the new index of the layer. This index will be adjusted to
     *                 be at least 0 (bottom layer) and at most the number of layers
     *                 (top layer), so you could pass Integer.MAX_VALUE to move
     *                 the layer to the top of the map.
     */
    public void moveLayer(Layer layer, int newIndex) {
        if (newIndex < 0) {
            newIndex = 0;
        }
        synchronized (map) {
            LayerList layers = map.getLayers();
            if (layers.remove(layer)) {
                if (newIndex > layers.size()) {
                    newIndex = layers.size();
                }
                layers.add(newIndex, layer);
            }
        }
    }

    /**
     * Removes a layer from the map if it is present.
     * @param layer the layer to remove.
     * @return the removed layer's former index, or -1 if the layer was not present.
     */
    public int removeLayer(Layer layer) {
        int layerIndex = -1;
        synchronized (map) {
            layerIndex = map.getLayers().indexOf(layer);
            map.getLayers().remove(layer);
        }
        boolean isOverlay = overlayLayers.remove(layer);
        fireLayersChanged(isOverlay);
        layerToFeatureLayer.remove(layer);
        
        return layerIndex;
    }

    /**
     * Removes all layers from the map.
     */
    public void removeAllLayers() {
        synchronized (map) {
            map.getLayers().clear();
        }
        overlayLayers.clear();
        resultToLayer.clear();
        layerToFeatureLayer.clear();
    }

    /**
     * Returns a list of overlay layers in the map.
     * @return a list of overlay layers in the map.
     */
    public List<Layer> getOverlayLayers() {
        return overlayLayers;
    }

    /**
     * Returns true if the map contains the specified layer and false otherwise.
     * @param layer the layer to check.
     * @return true if the map contains the specified layer and false otherwise.
     */
    public boolean hasLayer(Layer layer) {
        if (null != map) {
            return map.getLayers().contains(layer);
        } else {
            return false;
        }
    }

    /**
     * Track MapOverlay events, most often used to allow the user to interact with
     * the map control and get the user's mouse events. The provided MapOverlayListener
     * receives an event for each MapOverlay event specified in the mask. To stop
     * tracking points, call cancelTrackAsync.
     * @param listener the listener that receives click events.
     * @param eventsMask A bitmask equal to the sum of EVENT_ constants corresponding
     *                   to the events you want to track. For example, to track just
     *                   mouse clicks, pass EVENT_MOUSE_CLICKED. To track mouse
     *                   clicks and mouse drags, pass EVENT_MOUSE_CLICKED + EVENT_MOUSE_DRAGGED.
     */
    public void trackAsync(final MapOverlayListener listener, final short eventsMask) {
        if (null != trackOverlay) {
            map.removeMapOverlay(trackOverlay);
        }
        trackOverlay = new MapOverlay() {

            private static final long serialVersionUID = 7396150864102685407L;

            @Override
            public void onMouseClicked(MouseEvent event) {
                if (0 != (eventsMask & EVENT_MOUSE_CLICKED)) {
                    listener.mouseClicked(event);
                } else {
                    super.onMouseClicked(event);
                }
            }

            @Override
            public void onMouseDragged(MouseEvent event) {
                if (0 != (eventsMask & EVENT_MOUSE_DRAGGED)) {
                    listener.mouseDragged(event);
                } else {
                    super.onMouseDragged(event);
                }
            }

            @Override
            public void onMousePressed(MouseEvent event) {
                if (0 != (eventsMask & EVENT_MOUSE_PRESSED)) {
                    listener.mousePressed(event);
                } else {
                    super.onMousePressed(event);
                }
            }

            @Override
            public void onMouseReleased(MouseEvent event) {
                if (0 != (eventsMask & EVENT_MOUSE_RELEASED)) {
                    listener.mouseReleased(event);
                } else {
                    super.onMouseReleased(event);
                }
            }

        };
        map.addMapOverlay(trackOverlay);
        trackOverlay.setActive(true);
    }

    /**
     * Stops tracking MapOverlay events. Call trackAsync first, and when you no
     * longer need events, call this method.
     */
    public void cancelTrackAsync() {
        if (null != trackOverlay) {
            trackOverlay.setActive(false);
            map.removeMapOverlay(trackOverlay);
        }
        activateIdentify();
    }
    
    private void processResults(IdentifyResultList results) {
        for (int i = 0; i < results.size(); i++) {
            IdentifiedItem result = results.get(i);
            synchronized (allResults) {
                allResults.add(result);
                resultToLayer.put(result, results.getLayer(result));
            }
        }
    }

    private void activateIdentify() {
        trackAsync(new MapOverlayAdapter() {

            @Override
            public void mouseClicked(MouseEvent event) {
                synchronized (identifyListeners) {
                    identifyListeners.removeAll(identifyListeners);
                }
                synchronized (allResults) {
                    allResults.clear();
                    resultToLayer.clear();
                }
                Graphic poppedUpChemLight = symbolController.identifyOneGraphic(
                        symbolController.getMessageLayerName(ChemLightController.REPORT_TYPE),
                        event.getX(), event.getY(), 5);
                if (null != poppedUpChemLight) {
                    ChemLightJPanel panel = new ChemLightJPanel(
                            chemLightController,
                            poppedUpChemLight,
                            null != poppedUpChemLight.getSpatialReference() ?
                            poppedUpChemLight.getSpatialReference() : getSpatialReference());
                    Dimension panelPreferredSize = panel.getPreferredSize();
                    chemLightPopupDialog = map.createPopup(new JComponent[] { panel }, poppedUpChemLight);
                    Component[] components = chemLightPopupDialog.getContentPane().getComponents();
                    for (Component c : components) {
                        if (!(c instanceof ChemLightJPanel)) {
                            c.setVisible(false);
                        }
                    }
                    chemLightPopupDialog.setPreferredSize(panelPreferredSize);
                    chemLightPopupDialog.pack();
                    panel.setComponentToHide(chemLightPopupDialog);
                    chemLightPopupDialog.setVisible(true);
                } else {
                    if (null != chemLightPopupDialog) {
                        chemLightPopupDialog.setVisible(false);
                        chemLightPopupDialog = null;
                    }
                    final Point mapPoint = map.toMapPoint(event.getX(), event.getY());
                    IdentifyResultList results = symbolController.identify(event.getX(), event.getY(), 5);
                    processResults(results);

                    List<Layer> overlayLayers = getOverlayLayers();
                    for (final Layer layer : overlayLayers) {
                        if (layer instanceof Mil2525CMessageLayer) {
                            processResults(((Mil2525CMessageLayer) layer).identify(event.getX(), event.getY(), 5));
                        }
                        if (null != layer.getUrl()) {
                            IdentifyTask task = new IdentifyTask(layer.getUrl());
                            IdentifyParameters params = new IdentifyParameters();
                            params.setSpatialReference(map.getSpatialReference());
                            params.setGeometry(mapPoint);
                            params.setMapExtent(map.getExtent());
                            params.setMapWidth(map.getWidth());
                            params.setMapHeight(map.getHeight());
                            //TODO there might be a better way to get the DPI
                            params.setDPI(96);
                            CallbackListener<IdentifyResult[]> identifyListener = new CallbackListener<IdentifyResult[]>() {

                                public void onCallback(IdentifyResult[] results) {
                                    synchronized (identifyListeners) {
                                        if (identifyListeners.contains(this)) {
                                            synchronized (results) {
                                                for (IdentifyResult result : results) {
                                                    /**
                                                     * Feature results have a non-null value, and
                                                     * raster results have a null value. For now,
                                                     * only allow feature results by checking for
                                                     * null.
                                                     */
                                                    if (null != result.getValue()) {
                                                        IdentifiedItem item = new IdentifiedItem(
                                                                result.getGeometry(),
                                                                result.getLayerId(),
                                                                result.getAttributes(),
                                                                result.getValue());
                                                        allResults.add(item);
                                                        resultToLayer.put(item, layer);
                                                    }
                                                }
                                            }

                                            removeListenerAndCheckResult();
                                        }
                                    }
                                }

                                public void onError(Throwable e) {
                                    Logger.getLogger(MapController.class.getName()).log(Level.WARNING, null, e);
                                    removeListenerAndCheckResult();
                                }

                                private void removeListenerAndCheckResult() {
                                    synchronized (identifyListeners) {
                                        identifyListeners.remove(this);
                                        if (identifyListeners.isEmpty()) {
                                            fireIdentifyComplete(mapPoint);
                                        }
                                    }
                                }

                            };
                            synchronized (identifyListeners) {
                                identifyListeners.add(identifyListener);
                                task.execute(params, identifyListener);
                            }
                        }
                    }
                    synchronized (identifyListeners) {
                        if (identifyListeners.isEmpty()) {
                            fireIdentifyComplete(mapPoint);
                        }
                    }
                }
            }

        }, EVENT_MOUSE_CLICKED);
    }
    
    private void fireIdentifyComplete(Point mapPoint) {
        identifyListener.identifyComplete(
                mapPoint,
                allResults.toArray(new IdentifiedItem[allResults.size()]),
                resultToLayer
                );
    }

    /**
     * Returns the map's spatial reference.
     * @return the map's spatial reference.
     */
    public SpatialReference getSpatialReference() {
        return map.getSpatialReference();
    }

    /**
     * Opens a map package and adds it to the map. The map package should be a .mpk
     * file that was created in ArcGIS 10.1 or newer and was enabled for ArcGIS
     * Runtime.
     * @param mpkFile the .mpk file to open and add to the map as a layer.
     */
    public void openMapPackage(final File mpkFile) {
        final LocalServer localServer = LocalServer.getInstance();
        startLocalServer(new ServerLifetimeListener() {

            public void serverLifetimeInitialized(ServerLifetimeEvent e) {
                if (LocalServerStatus.INITIALIZED == localServer.getStatus()) {                    
                    //A dummy layer to display "Loading..." in the TOC
                    final GraphicsLayer dummyLayer = new GraphicsLayer();
                    
                    //The feature service that will provide the real map layer and a feature layer
                    final LocalFeatureService featureService = new LocalFeatureService(mpkFile.getAbsolutePath());
                    featureService.addLocalServiceStartCompleteListener(new LocalServiceStartCompleteListener() {

                        public void localServiceStartComplete(LocalServiceStartCompleteEvent e) {
                            //Create map layer and add to the map.
                            ArcGISDynamicMapServiceLayer mapLayer;
                            if (null == featureService.getUrlMapService()) {
                                mapLayer = new ArcGISLocalDynamicMapServiceLayer(mpkFile.getAbsolutePath());
                            } else {
                                mapLayer = new ArcGISDynamicMapServiceLayer(featureService.getUrlMapService());
                            }
                            addLayer(mapLayer, true);
                            removeLayer(dummyLayer);
                            
                            //Create corresponding feature layer, but don't add it to the map.
                            for (LayerDetails featureLayerDetails : featureService.getFeatureLayers()) {
                                ArcGISFeatureLayer featureLayer = new ArcGISFeatureLayer(featureLayerDetails.getUrl());
                                featureLayer.initializeAsync();
                                saveFeatureLayer(mapLayer, featureLayerDetails.getId(), featureLayer);
                            }
                        }
                    });
                    
                    //Add the dummy layer to get "Loading..." into the TOC
                    dummyLayer.setName("Loading...");
                    addLayer(dummyLayer, true);
                    
                    //Start the feature service, which will call the listener above
                    featureService.startAsync();
                }
            }

            public void serverLifetimeShutdown(ServerLifetimeEvent e) {

            }
        });
    }
    
    /**
     * Maps a layer to a corresponding feature layer, which will enable attachments
     * to be retrieved for the layer. A layer-featureLayerId pair uniquely identifies
     * a feature layer.
     * @param layer the layer whose attachments need to be retrieved.
     * @param featureLayerId the feature layer's ID.
     * @param featureLayer the feature layer that has the attachments.
     */
    public void saveFeatureLayer(Layer layer, int featureLayerId, ArcGISFeatureLayer featureLayer) {
        Map<Integer, ArcGISFeatureLayer> map = layerToFeatureLayer.get(layer);
        if (null == map) {
            map = new HashMap<Integer, ArcGISFeatureLayer>();
            layerToFeatureLayer.put(layer, map);
        }
        map.put(featureLayerId, featureLayer);
    }
    
    /**
     * Returns the feature layer associated with the specified layer (if any). For
     * example, a map layer may have an associated feature layer, and the feature
     * layer can be used to get attachments for identified features.
     * @param layer the layer whose associated feature layer is to be returned.
     * @para featureLayerId the ID of the feature layer to return.
     * @return the feature layer associated with the specified layer (if any).
     */
    public ArcGISFeatureLayer getFeatureLayer(Layer layer, int featureLayerId) {
        Map<Integer, ArcGISFeatureLayer> featureLayers = layerToFeatureLayer.get(layer);
        if (null == featureLayers) {
            return null;
        } else {
            return featureLayers.get(featureLayerId);
        }
    }

    private void startLocalServer(ServerLifetimeListener... listeners) {
        final LocalServer localServer = LocalServer.getInstance();

        for (ServerLifetimeListener listener : listeners) {
            localServer.addServerLifetimeListener(listener);
        }
        if (localServer.isInitialized()) {
            for (ServerLifetimeListener listener : listeners) {
                listener.serverLifetimeInitialized(new ServerLifetimeEvent(localServer, ServerLifetimeEvent.SERVERLIFETIME_INITIALIZED));
            }
        } else {
            localServer.initializeAsync();
        }
    }

    /**
     * Enables or disables keyboard navigation on the map. Keyboard navigation includes
     * using the keyboard's arrow keys to pan the map.
     * @param enabled true to enable keyboard navigation, and false to disable it.
     */
    public void setKeyboardEnabled(boolean enabled) {
        map.setKeyboardEnabled(enabled);
    }
    
    public void setAdvancedSymbolController(AdvancedSymbolController symbolController) {
        this.symbolController = symbolController;
    }
    
}
 public class RouteController extends GPController {
    
    private static final SimpleMarkerSymbol SYMBOL_WAYPOINT =
            new SimpleMarkerSymbol(new Color(0, 0, 255, 180), 20, SimpleMarkerSymbol.Style.CIRCLE);
    private static final SimpleMarkerSymbol SYMBOL_WAYPOINT_SELECTED =
            new SimpleMarkerSymbol(new Color(0, 0, 255, 180), 30, SimpleMarkerSymbol.Style.CIRCLE);
    private static final SimpleLineSymbol SYMBOL_ROUTE =
            new SimpleLineSymbol(new Color(0, 0, 255, 180), 5);
    
    private final ArrayDeque<Integer> graphicsAdded = new ArrayDeque<Integer>();
    private final ArrayDeque<Integer> waypointGraphicIds = new ArrayDeque<Integer>();
    private final ArrayList<RouteListener> routeListeners = new ArrayList<RouteListener>();
    
    private Point lastRoutePoint = null;
    private Polyline currentRouteLine = null;
    private int currentRouteLineId = -1;
    private int nextWaypointNumber = 1;
    private Integer selectedWaypointGraphicId = null;
    
    /**
     * Instantiates the RouteController.
     * @param mapController the MapController.
     */
    public RouteController(MapController mapController) {
        super(mapController, "Route");
        setOverlayLayer(true);
    }
    
    /**
     * Adds a RouteListener to this controller.
     * @param listener the listener to add.
     */
    public void addRouteListener(RouteListener listener) {
        synchronized (routeListeners) {
            routeListeners.add(listener);
        }
    }
    
    /**
     * Adds a waypoint to the route layer.
     * @param point the location of the new waypoint.
     */
    public void addWaypoint(Point point) {
        HashMap<String, Object> attributes = new HashMap<String, Object>();
        attributes.put("name", "Waypoint " + (nextWaypointNumber++));
        Graphic graphic = new Graphic(point, SYMBOL_WAYPOINT, attributes, 1);
        int graphicId = addGraphic(graphic);
        graphicsAdded.push(graphicId);
        waypointGraphicIds.push(graphicId);
        synchronized (routeListeners) {
            for (RouteListener listener : routeListeners) {
                listener.waypointAdded(graphic, graphicId);
            }
        }
    }
    
    /**
     * Adds a point to the current route line, and displays the line.
     * @param point the point to add to the current route line.
     */
    public void addPointToRouteLine(Point point) {
        if (null == currentRouteLine) {
            currentRouteLine = new Polyline();
        } else {
            Line segment = new Line();
            segment.setStart(lastRoutePoint);
            segment.setEnd(point);
            currentRouteLine.addSegment(segment, true);
            if (-1 == currentRouteLineId) {
                currentRouteLineId = addGraphic(new Graphic(currentRouteLine, SYMBOL_ROUTE));
                graphicsAdded.push(currentRouteLineId);
            } else {
                updateGraphic(currentRouteLineId, currentRouteLine);
            }
        }
        
        lastRoutePoint = point;
    }
    
    /**
     * Ends the current route line, so that the next call to addPointToRouteLine
     * will start a new route line.
     */
    public void endRouteLine() {
        currentRouteLine = null;
        currentRouteLineId = -1;
    }
    
    /**
     * Sets the currently selected waypoint graphid ID.
     * @param graphicId the currently selected waypoint graphid ID.
     */
    public void setSelectedWaypoint(Integer graphicId) {
        if (null != selectedWaypointGraphicId) {
            Graphic graphic = getGraphic(selectedWaypointGraphicId);
            updateGraphic(selectedWaypointGraphicId, SYMBOL_WAYPOINT);
        }
        selectedWaypointGraphicId = graphicId;
        Graphic graphic = getGraphic(graphicId);
        fireWaypointSelected(graphic);
        if (null != graphic) {
            updateGraphic(graphicId, SYMBOL_WAYPOINT_SELECTED);
        }
    }
    
    private void fireWaypointSelected(Graphic waypoint) {
        synchronized (routeListeners) {
            for (RouteListener listener : routeListeners) {
                listener.waypointSelected(waypoint);
            }
        }
    }
    
    private void fireWaypointRemoved(int graphicUid) {
        synchronized (routeListeners) {
            for (RouteListener listener : routeListeners) {
                listener.waypointRemoved(graphicUid);
            }
        }
    }
    
    /**
     * Removes all route lines and waypoints from the route layer.
     */
    public void clear() {
        synchronized (waypointGraphicIds) {
            for (int id : waypointGraphicIds) {
                fireWaypointRemoved(id);
            }
        }
        clearGraphics();
        graphicsAdded.clear();
        waypointGraphicIds.clear();
        nextWaypointNumber = 1;
    }
    
    /**
     * Removes the last waypoint or route line added to the route layer.
     */
    public void undo() {
        if (!graphicsAdded.isEmpty()) {
            int graphicId = graphicsAdded.pop();
            if (graphicId == waypointGraphicIds.peek()) {
                int id = waypointGraphicIds.pop();
                fireWaypointRemoved(id);
                nextWaypointNumber--;
            }
            removeGraphic(graphicId);
        }
    }
    
    /**
     * Returns a copy of the list of waypoints.
     * @return a copy of the list of waypoints.
     */
    public List<Graphic> getWaypoints() {
        ArrayList<Graphic> waypointsList = new ArrayList<Graphic>();
        Iterator<Integer> iterator = waypointGraphicIds.descendingIterator();
        while (iterator.hasNext()) {
            int id = iterator.next();
            Graphic graphic = getGraphic(id);
            waypointsList.add(graphic);
        }
        
        return waypointsList;
    }
    
}
 public class VehicleStatusController implements LocationListener {
    
    private final AppConfigController appConfig;
    private final Timer timer;

    private MessageController messageController;
    private final Point currentPoint = new Point();
    
    /**
     * Instantiates the controller and starts sending vehicle status updates, assuming
     * the app config's GPSController is providing location information.
     * @param appConfig the application configuration.
     * @param messageController the MessageController.
     */
    public VehicleStatusController(AppConfigController appConfig, MessageController messageController) {
        this.appConfig = appConfig;
        this.messageController = messageController;
        appConfig.getLocationController().addListener(this);
        timer = new Timer(appConfig.getVehicleStatusMessageInterval(), new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                timer.setDelay(VehicleStatusController.this.appConfig.getVehicleStatusMessageInterval());
                try {
                    sendVehicleStatusReport();
                } catch (XMLStreamException ex) {
                    Logger.getLogger(VehicleStatusController.class.getName()).log(Level.SEVERE, null, ex);
                } catch (IOException ex) {
                    Logger.getLogger(VehicleStatusController.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        });
        timer.start();
    }

    @Override
    protected void finalize() throws Throwable {
        timer.stop();
    }
    
    private void sendVehicleStatusReport() throws XMLStreamException, IOException {
        boolean sendMessage = false;
        double x = 0;
        double y = 0;
        synchronized (currentPoint) {
            if (null != currentPoint && !currentPoint.isEmpty()) {
                sendMessage = true;
                x = currentPoint.getX();
                y = currentPoint.getY();
            }
        }
        if (sendMessage) {
            StringWriter xmlStringWriter = new StringWriter();
            XMLStreamWriter xmlStreamWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(xmlStringWriter);
            xmlStreamWriter.writeStartDocument();
            xmlStreamWriter.writeStartElement("geomessages");
            xmlStreamWriter.writeStartElement("geomessage");
            xmlStreamWriter.writeAttribute("v", Utilities.GEOMESSAGE_VERSION);

            Utilities.writeTextElement(xmlStreamWriter,
                    MessageHelper.MESSAGE_2525C_TYPE_PROPERTY_NAME, "sysmsg");
            Utilities.writeTextElement(xmlStreamWriter,
                    MessageHelper.MESSAGE_ACTION_PROPERTY_NAME, "UPDATE");
            Utilities.writeTextElement(xmlStreamWriter,
                    MessageHelper.MESSAGE_ID_PROPERTY_NAME, UUID.randomUUID().toString());
            Utilities.writeTextElement(xmlStreamWriter,
                    MessageHelper.MESSAGE_WKID_PROPERTY_NAME, Integer.toString(Utilities.WGS84.getID()));
            Utilities.writeTextElement(xmlStreamWriter,
                    MessageHelper.MESSAGE_2525C_CONTROL_POINTS_PROPERTY_NAME,
                    x + "," + y);
            Utilities.writeTextElement(xmlStreamWriter, "uniquedesignation", appConfig.getUsername());
            Utilities.writeTextElement(xmlStreamWriter, "type", appConfig.getVehicleType());
            String dateString = Utilities.DATE_FORMAT_GEOMESSAGE.format(new Date());
            Utilities.writeTextElement(xmlStreamWriter, "datetimevalid", dateString);
            Utilities.writeTextElement(xmlStreamWriter, "fuel_state", "100");
            Utilities.writeTextElement(xmlStreamWriter, 
                    "1.0".equals(appConfig.getGeomessageVersion()) ? "sys_msg" : "system_msgs",
                    "Operational");
            for (int i = 1; i <= 4; i++) {
                /**
                 * Status coded values:
                 * 1 = Operational
                 * 2 = Advisory
                 * 3 = Critical
                 * 4 = Inoperable
                 */
                Utilities.writeTextElement(xmlStreamWriter, "sys_status_" + i, "1");
            }

            xmlStreamWriter.writeEndElement(); // geomessage
            xmlStreamWriter.writeEndElement(); // geomessages
            xmlStreamWriter.writeEndDocument();
            xmlStreamWriter.flush();
            String messageText = xmlStringWriter.toString();
            messageController.sendMessage(messageText.getBytes());
        }
    }

    public void onLocationChanged(Location location) {
        if (null != location) {
            synchronized (currentPoint) {
                currentPoint.setXY(location.getLongitude(), location.getLatitude());
            }
        }
    }

    public void onStateChanged(LocationProvider.LocationProviderState state) {
        
    }
    
} public class ViewshedController extends GPController {
    
    private static final SimpleFillSymbol VIEWSHED_BOUNDS_SYMBOL = new SimpleFillSymbol(
            new Color(255, 255, 255, 0),
            new SimpleLineSymbol(Color.RED, 3));
    private static final SimpleMarkerSymbol VIEWSHED_CENTER_SYMBOL =
            new SimpleMarkerSymbol(Color.RED, 10, SimpleMarkerSymbol.Style.X);
    
    private int viewshedBoundsGraphicId = -1;
    private int viewshedCenterGraphicId = -1;
    private String elevationPath = "C:/defensetemplates/vehiclecommander/GP/Elevation/Elevation.gdb/Elevation";
    private double observerHeight = 2.0;
    private String observerParamName = "Observer";
    private String observerHeightParamName = "ObserverHeight";
    private String radiusParamName = "Radius";
    private String elevationParamName = "Elevation";

    /**
     * Creates a new ViewshedController, which adds a graphics layer to the map.
     * @param mapController the MapController on which viewsheds will be displayed.
     */
    public ViewshedController(MapController mapController) {
        super(mapController, "Viewshed");
    }
    
    /**
     * Displays a temporary graphic on the map representing the bounds of the viewshed
     * that will be calculated if the given points are used.
     * @param viewshedCenter the center of the proposed viewshed (i.e. the observer).
     * @param viewshedOuter the outer point of the proposed viewshed's boundaries.
     * @return the distance between the two points.
     * @see #showViewshedBoundsGraphic(com.esri.core.geometry.Point, double)
     * @see #removeViewshedBoundsGraphic()
     */
    public double showViewshedBoundsGraphic(Point viewshedCenter, Point viewshedOuter) {
        double radius = GeometryEngine.distance(
                viewshedCenter,
                viewshedOuter,
                mapController.getSpatialReference());
        showViewshedBoundsGraphic(viewshedCenter, radius);
        return radius;
    }
    
    /**
     * Displays a temporary graphic on the map representing the bounds of the viewshed
     * that will be calculated if the given points are used.
     * @param viewshedCenter the center of the proposed viewshed (i.e. the observer).
     * @param radius the radius of the proposed viewshed.
     * @see #showViewshedBoundsGraphic(com.esri.core.geometry.Point, com.esri.core.geometry.Point)
     * @see #removeViewshedBoundsGraphic()
     */
    public void showViewshedBoundsGraphic(Point viewshedCenter, double radius) {
        Polygon buffer = GeometryEngine.buffer(
                viewshedCenter,
                mapController.getSpatialReference(),
                radius,
                mapController.getSpatialReference().getUnit());
        if (0 > viewshedBoundsGraphicId) {
            viewshedBoundsGraphicId = addGraphic(
                    new Graphic(buffer, VIEWSHED_BOUNDS_SYMBOL));
        } else {
            updateGraphic(viewshedBoundsGraphicId, buffer);
        }
    }
    
    /**
     * Removes the viewshed bounds graphic from the map.
     * @see #showViewshedBoundsGraphic(com.esri.core.geometry.Point, com.esri.core.geometry.Point)
     * @see #showViewshedBoundsGraphic(com.esri.core.geometry.Point, double)
     */
    public void removeViewshedBoundsGraphic() {
        removeGraphic(viewshedBoundsGraphicId);
        viewshedBoundsGraphicId = -1;
    }
    
    /**
     * Displays a graphic on the map representing the center of the viewshed.
     * @param viewshedCenter the center of the viewshed.
     * @see #removeViewshedCenterGraphic()
     */
    public void showViewshedCenterGraphic(Point viewshedCenter) {
        if (0 > viewshedCenterGraphicId) {
            viewshedCenterGraphicId = addGraphic(new Graphic(viewshedCenter, VIEWSHED_CENTER_SYMBOL));
        } else {
            updateGraphic(viewshedCenterGraphicId, viewshedCenter);
        }
    }
    
    /**
     * Removes the graphic on the map representing the center of the viewshed.
     * @see #showViewshedCenterGraphic(com.esri.core.geometry.Point)
     */
    public void removeViewshedCenterGraphic() {
        removeGraphic(viewshedCenterGraphicId);
        viewshedCenterGraphicId = -1;
    }
    
    /**
     * Calculates the viewshed based on the given parameters.
     * @param centerPoint the center of the viewshed.
     * @param radius the viewshed radius, in map units.
     */
    public void calculateViewshed(Point centerPoint, double radius) {
        try {
            fireGPStarted();
            ArrayList<GPParameter> parameters = new ArrayList<GPParameter>(); 

            Graphic centerGraphic = new Graphic(centerPoint, VIEWSHED_CENTER_SYMBOL);
            GPFeatureRecordSetLayer pointParam = new GPFeatureRecordSetLayer(observerParamName); 
            pointParam.setGeometryType(Geometry.Type.POINT); 
            pointParam.setSpatialReference(mapController.getSpatialReference()); 
            pointParam.addGraphic(centerGraphic); 

            GPLinearUnit radiusParam = new GPLinearUnit(radiusParamName); 
            radiusParam.setUnits("esri" + mapController.getSpatialReference().getUnit().toString()); 
            radiusParam.setDistance(radius); 

            GPDouble heightParam = new GPDouble(observerHeightParamName);
            heightParam.setValue(observerHeight);

            GPString rasterParam = new GPString(elevationParamName);
            rasterParam.setValue(elevationPath);

            parameters.add(pointParam); 
            parameters.add(radiusParam);
            parameters.add(heightParam);
//            parameters.add(rasterParam);//TODO add this back in if/when we want to pass a raster as a parameter
            getGeoprocessor().submitJobAsync(parameters, new CallbackListener<GPJobResource>() {

                public void onCallback(GPJobResource gpJobResource) {
                    //Display the raster as an overlay
                    JobStatus jobStatus = gpJobResource.getJobStatus();
                    ArcGISDynamicMapServiceLayer layer = null;
                    switch (jobStatus) {
                        case SUCCEEDED: {
                            layer = new ArcGISDynamicMapServiceLayer(getGeoprocessor().getUrl(), gpJobResource);
                            //Don't break
                        }

                        case CANCELLED:
                        case DELETED:
                        case FAILED:
                        case TIMED_OUT: {
                            fireGPEnded(layer);
                            break;
                        }
                        
                        default: {
                            try {
                                Thread.sleep(1000 / 24);
                            } catch (InterruptedException ex) {
                                Logger.getLogger(ViewshedController.class.getName()).log(Level.SEVERE, null, ex);
                            }
                            getGeoprocessor().getJobStatusAsync(this);
                        }
                    }
                }

                public void onError(Throwable e) {
                    fireGPEnded(null);
                    Logger.getLogger(ViewshedController.class.getName()).log(Level.SEVERE, null, e);
                }
            });
        } catch (Throwable t) {
            fireGPEnded(null);
            Logger.getLogger(ViewshedController.class.getName()).log(Level.SEVERE, null, t);
        }
    }

    /**
     * Gets the path to the elevation raster being used for viewshed analysis.
     * @return the elevation path.
     */
    public String getElevationPath() {
        return elevationPath;
    }

    /**
     * Sets the path to the elevation raster being used for viewshed analysis.
     * @param elevationPath the elevation path to set.
     */
    public void setElevationPath(String elevationPath) {
        this.elevationPath = elevationPath;
    }

    /**
     * @return the observerParamName
     */
    public String getObserverParamName() {
        return observerParamName;
    }

    /**
     * @param observerParamName the observerParamName to set
     */
    public void setObserverParamName(String observerParamName) {
        this.observerParamName = observerParamName;
    }

    /**
     * @return the observerHeightParamName
     */
    public String getObserverHeightParamName() {
        return observerHeightParamName;
    }

    /**
     * @param observerHeightParamName the observerHeightParamName to set
     */
    public void setObserverHeightParamName(String observerHeightParamName) {
        this.observerHeightParamName = observerHeightParamName;
    }

    /**
     * @return the radiusParamName
     */
    public String getRadiusParamName() {
        return radiusParamName;
    }

    /**
     * @param radiusParamName the radiusParamName to set
     */
    public void setRadiusParamName(String radiusParamName) {
        this.radiusParamName = radiusParamName;
    }

    /**
     * @return the elevationParamName
     */
    public String getElevationParamName() {
        return elevationParamName;
    }

    /**
     * @param elevationParamName the elevationParamName to set
     */
    public void setElevationParamName(String elevationParamName) {
        this.elevationParamName = elevationParamName;
    }

    /**
     * @return the observerHeight
     */
    public double getObserverHeight() {
        return observerHeight;
    }

    /**
     * @param observerHeight the observerHeight to set
     */
    public void setObserverHeight(double observerHeight) {
        this.observerHeight = observerHeight;
    }
    
}
 public class IdentifiedItem {
    
    private final Geometry geometry;
    private final int layerId;
    private final Map<String, Object> attributes;
    private final Object value;
    
    public IdentifiedItem(Geometry geometry, int layerId, Map<String, Object> attributes, Object value) {
        this.geometry = geometry;
        this.layerId = layerId;
        this.attributes = attributes;
        this.value = value;
    }

    /**
     * @return the geometry
     */
    public Geometry getGeometry() {
        return geometry;
    }

    /**
     * @return the layerId
     */
    public int getLayerId() {
        return layerId;
    }

    /**
     * @return the attributes
     */
    public Map<String, Object> getAttributes() {
        return attributes;
    }

    /**
     * @return the value
     */
    public Object getValue() {
        return value;
    }
    
}
 public class MapConfig {

    private List<BasemapLayer> layers = new ArrayList<BasemapLayer>();
    private final List<Map<String, String>> toolbarItems;
    
    /**
     * Instantiates a MapConfig with an empty list of toolbar items.
     */
    public MapConfig() {
        this.toolbarItems = new ArrayList<Map<String, String>>();
    }

    /**
     * Instantiates a MapConfig with a list of toolbar items.
     * @param toolbarItems the toolbar items. Each item in the list is a map of
     *                     key-value pairs used to instantiate a toolbar item.
     */
    public MapConfig(List<Map<String, String>> toolbarItems) {
        this.toolbarItems = toolbarItems;
    }

    /**
     * Returns the basemap layers contained by this MapConfig.
     * @return The basemap layers contained by this MapConfig.
     */
    public List<BasemapLayer> getBasemapLayers() {
        return layers;
    }

    /**
     * Sets this MapConfig's basemap layers.
     * @param layers the basemap layers to be stored by this MapConfig
     */
    public void setBasemapLayers(List<BasemapLayer> layers) {
        this.layers = layers;
    }
    
    /**
     * Returns the toolbar items as a list of key-value pairs.
     * @return the toolbar items.
     */
    public List<Map<String, String>> getToolbarItems() {
        return toolbarItems;
    }

}
 public class MapConfigReader {
    
    private static class MapConfigHandler extends DefaultHandler {

        private final MapController mapController;
        private final AppConfigController appConfig;
        private boolean addedLayersToMap = false;
        private final Object addedLayersToMapLock = new Object();

        private List<Layer> nonBasemapLayers = new ArrayList<Layer>();
        private BasemapLayerList basemapLayers = new BasemapLayerList();
        private Double x = null;
        private Double y = null;
        private Double scale = null;
        private Double rotation = null;
        private String servicePath = null;
        private String elevation = null;
        private String taskName = null;
        private Double observerHeight = null;
        private String observerParamName = "Observer";
        private String observerHeightParamName = "ObserverHeight";
        private String radiusParamName = "Radius";
        private String elevationParamName = "Elevation";
        private final List<Map<String, String>> toolbarItems = new ArrayList<Map<String, String>>();

        private boolean readingMapconfig = false;
        private boolean readingLayers = false;
        private boolean readingLayer = false;
        private boolean readingDatasetpath = false;
        private boolean readingInitialextent = false;
        private boolean readingAnchor = false;
        private boolean readingScale = false;
        private boolean readingRotation = false;
        private boolean readingX = false;
        private boolean readingY = false;
        private boolean readingViewshed = false;
        private boolean readingServicepath = false;
        private boolean readingElevation = false;
        private boolean readingObserverHeight = false;
        private boolean readingTaskName = false;
        private boolean readingObserverParamName = false;
        private boolean readingObserverHeightParamName = false;
        private boolean readingRadiusParamName = false;
        private boolean readingElevationParamName = false;
        private boolean readingExtensions = false;
        private boolean readingToolbarItem = false;

        private String currentLayerType = null;
        private boolean currentLayerVisible = false;
        private String currentLayerName = null;
        private boolean currentLayerBasemap = false;
        private String currentLayerThumbnail = null;

        public MapConfigHandler(MapController mapController, AppConfigController appConfig) {
            this.mapController = mapController;
            this.appConfig = appConfig;
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            if ("mapconfig".equalsIgnoreCase(qName)) {
                readingMapconfig = true;
            } else if ("layers".equalsIgnoreCase(qName) && readingMapconfig) {
                readingLayers = true;
            } else if ("layer".equalsIgnoreCase(qName) && readingLayers) {
                readingLayer = true;
                currentLayerType = attributes.getValue("type");
                currentLayerVisible = "true".equalsIgnoreCase(attributes.getValue("visible"));
                currentLayerName = attributes.getValue("name");
                currentLayerBasemap = "true".equalsIgnoreCase(attributes.getValue("basemap"));
                currentLayerThumbnail = attributes.getValue("thumbnail");
            } else if (("datasetpath".equalsIgnoreCase(qName) || "url".equalsIgnoreCase(qName)) && readingLayer) {
                readingDatasetpath = true;
            } else if ("initialextent".equalsIgnoreCase(qName) && readingMapconfig) {
                readingInitialextent = true;
            } else if ("anchor".equalsIgnoreCase(qName) && readingInitialextent) {
                readingAnchor = true;
            } else if ("x".equalsIgnoreCase(qName) && readingAnchor) {
                readingX = true;
            } else if ("y".equalsIgnoreCase(qName) && readingAnchor) {
                readingY = true;
            } else if ("scale".equalsIgnoreCase(qName) && readingInitialextent) {
                readingScale = true;
            } else if ("rotation".equalsIgnoreCase(qName) && readingInitialextent) {
                readingRotation = true;
            } else if ("viewshed".equalsIgnoreCase(qName) && readingMapconfig) {
                readingViewshed = true;
            } else if ("servicepath".equalsIgnoreCase(qName) && readingViewshed) {
                readingServicepath = true;
            } else if ("elevation".equalsIgnoreCase(qName) && readingViewshed) {
                readingElevation = true;
            } else if ("observerheight".equalsIgnoreCase(qName) && readingViewshed) {
                readingObserverHeight = true;
            } else if ("taskname".equalsIgnoreCase(qName) && readingViewshed) {
                readingTaskName = true;
            } else if ("observerparamname".equalsIgnoreCase(qName) && readingViewshed) {
                readingObserverParamName = true;
            } else if ("observerheightparamname".equalsIgnoreCase(qName) && readingViewshed) {
                readingObserverHeightParamName = true;
            } else if ("radiusparamname".equalsIgnoreCase(qName) && readingViewshed) {
                readingRadiusParamName = true;
            } else if ("elevationparamname".equalsIgnoreCase(qName) && readingViewshed) {
                readingElevationParamName = true;
            } else if ("extensions".equalsIgnoreCase(qName)) {
                readingExtensions = true;
            } else if (readingExtensions && "toolbaritem".equalsIgnoreCase(qName)) {
                readingToolbarItem = true;
                HashMap<String, String> toolbarItem = new HashMap<String, String>(attributes.getLength() + 1, 1f);
                for (int i = 0; i < attributes.getLength(); i++) {
                    toolbarItem.put(attributes.getQName(i), attributes.getValue(i));
                }
                toolbarItems.add(toolbarItem);
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            String string = new String(ch, start, length);            
            if (readingDatasetpath) {
            	
            	if (!(new File(string).exists())) 
            	{
            		if (!string.contains("http")) // Web Address
            		{
            			System.out.println("ERROR: Dataset File or path does not exist: " + string);
                		currentLayerType = "INVALID_DATASET_PATH";
            		}
            	}
            	
                Layer layer = null;
                if ("TiledCacheLayer".equals(currentLayerType)) {
                    layer = new ArcGISLocalTiledLayer(string);
                } else if ("TiledMapServiceLayer".equals(currentLayerType)) {
                    layer = new ArcGISTiledMapServiceLayer(string);
                } else if ("LocalDynamicMapLayer".equals(currentLayerType)) {
                    /**
                     * Open it as a feature layer so we can get attachments, but
                     * add the map layer to the map.
                     */
                    final boolean isBasemapLayer = currentLayerBasemap;
                    final String layerName = currentLayerName;
                    
                    //Dummy layer for TOC
                    final GraphicsLayer dummyLayer = new GraphicsLayer();
                    dummyLayer.setName("Loading...");
                    layer = dummyLayer;
                    
                    //The feature service that will provide the real map layer and a feature layer
                    final LocalFeatureService featureService = new LocalFeatureService(string);
                    featureService.addLocalServiceStartCompleteListener(new LocalServiceStartCompleteListener() {
                        
                        private final boolean layerIsVisible = currentLayerVisible;

                        public void localServiceStartComplete(LocalServiceStartCompleteEvent e) {
                            ArcGISDynamicMapServiceLayer mapLayer = new ArcGISDynamicMapServiceLayer(featureService.getUrlMapService());
                            mapLayer.setVisible(layerIsVisible);
                            mapLayer.setName(layerName);
                            for (LayerDetails featureLayerDetails : featureService.getFeatureLayers()) {
                                ArcGISFeatureLayer featureLayer = new ArcGISFeatureLayer(featureLayerDetails.getUrl());
                                featureLayer.initializeAsync();
                                mapController.saveFeatureLayer(mapLayer, featureLayerDetails.getId(), featureLayer);
                            }
                            synchronized (addedLayersToMapLock) {
                                List<Layer> layerList = isBasemapLayer ? basemapLayers.getLayers() : nonBasemapLayers;
                                layerList.add(layerList.indexOf(dummyLayer), mapLayer);
                                layerList.remove(dummyLayer);
                                if (addedLayersToMap) {
                                    int layerIndex = mapController.removeLayer(dummyLayer);
                                    if (-1 < layerIndex) {
                                        mapController.addLayer(layerIndex, mapLayer, !isBasemapLayer);
                                    } else {
                                        mapController.addLayer(mapLayer, !isBasemapLayer);
                                    }
                                }
                            }
                        }
                        
                    });
                    featureService.startAsync();
                    
                } else if ("DynamicMapServiceLayer".equals(currentLayerType)) {
                    layer = new ArcGISDynamicMapServiceLayer(string);
                } else if ("Mil2525CMessageLayer".equals(currentLayerType)) {
                    try {
                        layer = new Mil2525CMessageLayer(string, currentLayerName, mapController, appConfig);
                    } catch (IOException ex) {
                        Logger.getLogger(MapConfigReader.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (ParserConfigurationException ex) {
                        Logger.getLogger(MapConfigReader.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
                if (null != layer && null != currentLayerName) {
                    layer.setName(currentLayerName);
                    layer.setVisible(currentLayerVisible);
                    if (currentLayerBasemap) {
                        basemapLayers.add(new BasemapLayer(layer, currentLayerThumbnail));
                    } else {
                        nonBasemapLayers.add(layer);
                    }
                }
            } else if (readingX) {
                try {
                    x = Double.parseDouble(string);
                } catch (NumberFormatException nfe) {

                }
            } else if (readingY) {
                try {
                    y = Double.parseDouble(string);
                } catch (NumberFormatException nfe) {

                }
            } else if (readingScale) {
                try {
                    scale = Double.parseDouble(string);
                } catch (NumberFormatException nfe) {

                }
            } else if (readingRotation) {
                try {
                    rotation = Double.parseDouble(string);
                } catch (NumberFormatException nfe) {

                }
            } else if (readingServicepath) {
                servicePath = string;
            } else if (readingElevation) {
                elevation = string;
            } else if (readingTaskName) {
                taskName = string;
            } else if (readingObserverHeight) {
                try {
                    observerHeight = Double.parseDouble(string);
                } catch (NumberFormatException nfe) {

                }
            } else if (readingObserverParamName) {
                observerParamName = string;
            } else if (readingObserverHeightParamName) {
                observerHeightParamName = string;
            } else if (readingRadiusParamName) {
                radiusParamName = string;
            } else if (readingElevationParamName) {
                elevationParamName = string;
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if ("mapconfig".equalsIgnoreCase(qName)) {
                readingMapconfig = false;
            } else if ("layers".equalsIgnoreCase(qName) && readingMapconfig) {
                readingLayers = false;
            } else if ("layer".equalsIgnoreCase(qName) && readingLayers) {
                readingLayer = false;
            } else if (("datasetpath".equalsIgnoreCase(qName) || "url".equalsIgnoreCase(qName)) && readingLayer) {
                readingDatasetpath = false;
            } else if ("initialextent".equalsIgnoreCase(qName) && readingMapconfig) {
                readingInitialextent = false;
            } else if ("anchor".equalsIgnoreCase(qName) && readingInitialextent) {
                readingAnchor = false;
            } else if ("x".equalsIgnoreCase(qName) && readingAnchor) {
                readingX = false;
            } else if ("y".equalsIgnoreCase(qName) && readingAnchor) {
                readingY = false;
            } else if ("scale".equalsIgnoreCase(qName) && readingInitialextent) {
                readingScale = false;
            } else if ("rotation".equalsIgnoreCase(qName) && readingInitialextent) {
                readingRotation = false;
            } else if ("viewshed".equalsIgnoreCase(qName) && readingMapconfig) {
                readingViewshed = false;
            } else if ("servicepath".equalsIgnoreCase(qName) && readingViewshed) {
                readingServicepath = false;
            } else if ("elevation".equalsIgnoreCase(qName) && readingViewshed) {
                readingElevation = false;
            } else if ("observerheight".equalsIgnoreCase(qName) && readingViewshed) {
                readingObserverHeight = false;
            } else if ("taskname".equalsIgnoreCase(qName) && readingViewshed) {
                readingTaskName = false;
            } else if ("observerparamname".equalsIgnoreCase(qName) && readingViewshed) {
                readingObserverParamName = false;
            } else if ("observerheightparamname".equalsIgnoreCase(qName) && readingViewshed) {
                readingObserverHeightParamName = false;
            } else if ("radiusparamname".equalsIgnoreCase(qName) && readingViewshed) {
                readingRadiusParamName = false;
            } else if ("elevationparamname".equalsIgnoreCase(qName) && readingViewshed) {
                readingElevationParamName = false;
            } else if ("extensions".equalsIgnoreCase(qName)) {
                readingExtensions = false;
            } else if (readingExtensions && "toolbaritem".equalsIgnoreCase(qName)) {
                readingToolbarItem = false;
            }
        }
    }

    /**
     * Reads a map configuration XML file and applies its contents to the MapController's map.
     * @param mapConfigFile the map configuration XML file.
     * @param mapController the MapController to whose map this map configuration will apply.
     * @param appConfig the application's configuration controller.
     * @throws IOException
     * @throws ParserConfigurationException
     * @throws SAXException
     */
    public static MapConfig readMapConfig(
            File mapConfigFile,
            MapController mapController,
            AppConfigController appConfig,
            ViewshedController viewshedController) throws IOException, ParserConfigurationException, SAXException {
        MapConfigHandler handler = new MapConfigHandler(mapController, appConfig);
        SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
        parser.parse(mapConfigFile, handler);

        MapConfig mapConfig = new MapConfig(handler.toolbarItems);

        synchronized (handler.addedLayersToMapLock) {
            //Add layers to map
            mapController.addLayers(handler.basemapLayers.getLayers(), false);
            mapController.addLayers(handler.nonBasemapLayers, true);

            //Record the layers in the MapConfig object
            mapConfig.setBasemapLayers(handler.basemapLayers);
            
            handler.addedLayersToMap = true;
        }

        if (null != handler.x && null != handler.y && null != handler.scale) {
            mapController.zoomToScale(handler.scale, handler.x, handler.y);
        }

        if (null != handler.rotation) {
            mapController.setRotation(handler.rotation);
        }
        
        if (null != viewshedController && null != handler.servicePath) {
            viewshedController.setElevationPath(handler.elevation);
            if (null != handler.observerHeight) {
                viewshedController.setObserverHeight(handler.observerHeight);
            }
            viewshedController.setTaskName(handler.taskName);
            viewshedController.setObserverParamName(handler.observerParamName);
            viewshedController.setObserverHeightParamName(handler.observerHeightParamName);
            viewshedController.setRadiusParamName(handler.radiusParamName);
            viewshedController.setElevationParamName(handler.elevationParamName);
            viewshedController.setServicePath(handler.servicePath);
        }

        return mapConfig;
    }
    
}
 public class Utilities extends com.esri.militaryapps.util.Utilities {

    private static final double FIVE_PI_OVER_TWO = 5.0 * Math.PI / 2.0;
    private static final double TWO_PI = 2.0 * Math.PI;
    
    /**
     * The version string of this app. Update the value of this string when a new
     * release is created.
     */
    public static final String APP_VERSION = "4.1.1";

    /**
     * The geomessage version being sent by this application.
     */
    public static final String GEOMESSAGE_VERSION = "1.0";

    /**
     * A DateFormat object for datetimevalid timestamps.
     */
    public static final SimpleDateFormat DATE_FORMAT_GEOMESSAGE =
            new SimpleDateFormat("yyyy-MM-dd' 'HH:mm:ss");
    static {
        DATE_FORMAT_GEOMESSAGE.setTimeZone(TimeZone.getTimeZone("UTC"));
    }

    /**
     * A DateFormat object for military date/time in Zulu time.
     */
    public static final SimpleDateFormat DATE_FORMAT_MILITARY_ZULU =
            new SimpleDateFormat("ddHHmmss'Z 'MMM' 'yy");
    static {
        DATE_FORMAT_MILITARY_ZULU.setTimeZone(TimeZone.getTimeZone("UTC"));
    }
    
    /**
     * A DateFormat object for military date/time in local time.
     */
    public static final SimpleDateFormat DATE_FORMAT_MILITARY_LOCAL =
            new SimpleDateFormat("ddHHmmss'J 'MMM' 'yy");
    static {
        //Check system time zone and adjust local format accordingly
        new Timer(1000 / 24, new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                //Force re-read of OS time zone
                System.getProperties().remove("user.timezone");
                TimeZone.setDefault(null);
                
                //Adjust local format
                TimeZone tz = TimeZone.getDefault();
                DATE_FORMAT_MILITARY_LOCAL.setTimeZone(tz);
                DATE_FORMAT_MILITARY_LOCAL.applyPattern("ddHHmmss'" + getMilitaryTimeZoneCharacter(tz) + " 'MMM' 'yy");
            }
        }).start();
    }
    private static final int MILLISECONDS_IN_HOUR = 60 * 60 * 1000;
    private static char getMilitaryTimeZoneCharacter(TimeZone tz) {
        int offset = tz.getOffset(System.currentTimeMillis());
        //If it's not a whole number of hours, just return 'J'
        int offsetHours = offset / MILLISECONDS_IN_HOUR;
        if (0 != offset % MILLISECONDS_IN_HOUR || 12 < offsetHours || -12 > offsetHours) {
            return 'J';
        } else {
            if (0 == offsetHours) {
                return 'Z';
            } else if (0 < offsetHours) {
                char c = (char) ('A' + offsetHours - 1);
                if ('J' <= c) {
                    c += 1;
                }
                return c;
            } else {
                return (char) ('N' - offsetHours - 1);
            }
        }
    }

    /**
     * The WGS 1984 geographic coordinate system.
     */
    public static final SpatialReference WGS84 = SpatialReference.create(4326);

    /**
     * All non-alphanumeric ASCII characters except '-' and '*', contained in a
     * single String.
     */
    public static final String MIL_2525C_WHITESPACE_CHARS;
    static {
        StringBuilder sb = new StringBuilder();
        for (char c = 0; c < 128; c++) {
            if ((c < '0' || c > '9') && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z')
                    && c != '-' && c != '*') {
                sb.append(c);
            }
        }
        MIL_2525C_WHITESPACE_CHARS = sb.toString();
    }
    
    private static final HashMap<Integer, AngularUnit> ANGULAR_UNITS =
            new HashMap<Integer, AngularUnit>();

    /**
     * Private constructor because Utilities is not meant to be instantiated.
     */
    private Utilities() {
    }

    /**
     * Closes the application specified.
     * @param frame the application to close.
     */
    public static void closeApplication(Frame frame) {
        if (null != frame) {
            WindowEvent wev = new WindowEvent(frame, WindowEvent.WINDOW_CLOSING);
            Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(wev);
        }
    }

    /**
     * Converts a trigonometric angle to a compass heading. In trigonometry, 0
     * radians is east, pi / 2 is north, pi is west, and 3 * pi / 2 is south. In
     * compass headings, 0 radians is north, pi / 2 is east, pi is south, and
     * 3 * pi / 2 is west.
     * @param trigHeading the trigonometric heading, in radians.
     * @return the compass heading, in radians.
     */
    public static double toCompassHeadingRadians(double trigHeadingRadians) {
        double compassHeading = FIVE_PI_OVER_TWO - trigHeadingRadians;
        if (TWO_PI < compassHeading) {
            compassHeading -= TWO_PI;
        }
        else if(0.0 > compassHeading) {
            compassHeading += TWO_PI;
        }
        return compassHeading;
    }

    /**
     * Calculates the compass bearing from one point to another and returns the
     * result in degrees.
     * @param fromLocationLatLon the location from which the bearing is to be calculated,
     *                           in latitude/longitude.
     * @param toLocationLatLon the location to which the bearing is to be calculated,
     *                   in latitude/longitude.
     * @return the compass bearing from one point to another, in degrees.
     */
    public static double calculateBearingDegrees(Point fromLocationLatLon, Point toLocationLatLon) {
        return Utilities.calculateBearingDegrees(fromLocationLatLon.getX(), fromLocationLatLon.getY(), toLocationLatLon.getX(), toLocationLatLon.getY());
    }

    /**
     * Normalizes an angle in degrees to fall between specified minimum and maximum
     * values.
     * @param angle the angle to normalize.
     * @param min the minimum value.
     * @param max the maximum value.
     * @return an angle equivalent to the input angle, normalized to fall between
     *         the specified minimum and maximum values.
     */
    public static double fixAngleDegrees(double angle, double min, double max) {
        while (angle < min) {
            angle += 360;
        }
        while (angle > max) {
            angle -= 360;
        }
        return angle;
    }

    /**
     * Convenience method for writing an XML text element. For example, if you call
     * the method like this:<br/>
     * <br/>
     * <code>Utilities.writeTextElement(writer, "lastName", "Lockwood");</code><br/>
     * <br/>
     * The writer will write it like this:<br/>
     * <br/>
     * <code>&lt;lastName&gt;Lockwood&lt;/lastName&gt;
     * @param writer the XML writer.
     * @param key the name of the element.
     * @param value the string within the element.
     * @throws XMLStreamException
     */
    public static void writeTextElement(XMLStreamWriter writer, String key, String value) throws XMLStreamException {
        writer.writeStartElement(key);
        writer.writeCharacters(value);
        writer.writeEndElement();
    }

    /**
     * Displays a JOptionPane message dialog with the specified error message and
     * a header related to GPS.
     * @param errorMessage the error message to display.
     */
    public static void showGPSErrorMessage(String errorMessage) {
        JOptionPane.showMessageDialog(null, errorMessage, "Problem Connecting to GPS", JOptionPane.ERROR_MESSAGE);
    }

    /**
     * Returns an instance of an AngularUnit object for the specified WKID. This
     * method means you don't have to recreate equivalent AngularUnit objects. Note
     * that the AngularUnit returned may have an abbreviation customized by this
     * method.
     * @param wkid the angular unit WKID, which can be obtained from AngularUnit.Code.
     * @return an instance of an AngularUnit object for the specified WKID.
     */
    public static AngularUnit getAngularUnit(final int wkid) {
        AngularUnit unit = ANGULAR_UNITS.get(wkid);
        if (null == unit) {
            unit = new AngularUnit(wkid) {

                @Override
                public String getAbbreviation() {
                    switch (wkid) {
                        case AngularUnit.Code.DEGREE: {
                            return "\u00B0";
                        }
                        case AngularUnit.Code.MIL_6400: {
                            return "\u20A5";
                        }
                    }
                    return super.getAbbreviation();
                }
                
            };
            ANGULAR_UNITS.put(wkid, unit);
        }
        return unit;
    }
    
    /**
     * Converts the string to a best-guess valid MGRS string, if possible.<br/>
     * <br/>
     * This method checks only the pattern, not the coordinate itself. For example,
     * 60CVS1234567890 is valid, but 60CVR1234567890 is not, because zone 60C has
     * a VS square but not a VR square. This method considers both of those strings
     * to be valid, because both of them match the pattern.<br/>
     * <br/>
     * This method will check and try to correct at least the following:
     * <ul>
     *     <li>Digits before zone A, B, Y, Z (correction: omit the numbers)</li>
     *     <li>More than two digits before zone letter (no correction)</li>
     *     <li>Grid zone number higher than 60 (no correction available)</li>
     *     <li>100,000-meter square with more than two letters (no correction)</li>
     *     <li>100,000-meter square with fewer than two letters (no correction available)</li>
     *     <li>Odd number of easting/northing digits (correction: add a zero to
     *         the end of the northing)</li>
     * </ul>
     * TODO this method might go away when fromMilitaryGrid handles bad strings gracefully.
     * @param mgrs the MGRS string.
     * @param referenceLocation a reference location for calculating a missing grid
     *        zone identifier. If mgrs does not include a grid zone identifier,
     *        this parameter's grid zone identifier will be prepended to mgrs.
     *        This parameter can be null if mgrs contains a grid zone identifier.
     * @param referenceSR the spatial reference of the reference location. This
     *        parameter can be null if mgrs contains a grid zone identifier.
     * @return the string itself; a best guess at a valid equivalent of the string;
     *         or null if the string is known to be invalid and cannot be converted.
     */
    public static String convertToValidMgrs(String mgrs, Point referenceLocation, SpatialReference referenceSR) {
        if (null == mgrs) {
            return null;
        }
        
        //Remove non-alphanumeric
        mgrs = mgrs.replaceAll("[^a-zA-Z0-9]", "").toUpperCase();

        //Check for MGRS without grid zone identifier and add it if necessary
        Matcher gzlessMatcher = Pattern.compile("[A-Z]{2}[0-9]*").matcher(mgrs);
        if (null != referenceLocation && null != referenceSR && gzlessMatcher.matches()) {
            String referenceMgrs = CoordinateConversion.pointToMgrs(referenceLocation,
                    referenceSR, CoordinateConversion.MGRSConversionMode.AUTO, 5,
                    false, false);
            if (null != referenceMgrs) {
                Matcher gzMatcher = Pattern.compile("[0-9]{0,2}[A-Z]").matcher(referenceMgrs);
                if (gzMatcher.find() && 0 == gzMatcher.start()) {
                    mgrs = referenceMgrs.substring(0, gzMatcher.end()) + mgrs;
                }
            }
        }
        
        /**
         * A good MGRS string looks like this:
         * <grid zone ID><2 letters><even number of digits>
         * A grid zone ID looks like this:
         * <1-2 digits><letter C-X>
         * or
         * <letter A, B, Y, or Z>
         * That means every MGRS string looks like this:
         * <0-2 digits><3 letters><even number of digits>
         */
        Pattern pattern = Pattern.compile("[A-Z]+");
        Matcher matcher = pattern.matcher(mgrs);
        if (!matcher.find()) {
            //There are no letters; nothing we can do
            return null;
        }
        Matcher polarMatcher = Pattern.compile("[ABYZ][A-Z]{2}[0-9]*").matcher(mgrs);
        if (0 == matcher.start()) {
            //This string starts with letters; make sure it's polar
            if (!polarMatcher.matches()) {
                return null;
            }
        } else {
            //If the first letter is A, B, Y, or Z, omit the leading digits
            char firstLetter = mgrs.charAt(matcher.start());
            if ('A' == firstLetter || 'B' == firstLetter || 'Y' == firstLetter || 'Z' == firstLetter) {
                mgrs = mgrs.substring(matcher.start());
                if (!polarMatcher.matches()) {
                    return null;
                }
            } else {
                Matcher nonPolarMatcher = Pattern.compile("[0-9]{1,2}[C-X][A-Z]{2}[0-9]*").matcher(mgrs);
                if (!nonPolarMatcher.matches()) {
                    return null;
                }
                //This string starts with numbers; see what they are
                int gridZoneNumber = Integer.parseInt(mgrs.substring(0, matcher.start()));
                if (0 > gridZoneNumber || 60 < gridZoneNumber) {
                    return null;
                }
            }
        }
        
        //Last thing: return null if there's an odd number of easting/northing digits
        Matcher threeLetters = Pattern.compile("[A-Z]{3}").matcher(mgrs);
        threeLetters.find();
        if (threeLetters.end() < mgrs.length()) {
            String eastingNorthing = mgrs.substring(threeLetters.end());
            if (1 == eastingNorthing.length() % 2) {
                return null;
            }
        }
        
        return mgrs;
    }
    
    /**
     * Rescales an icon.
     * @param src the original icon.
     * @param newMinSize the minimum size of the new icon. The width and height of
     *                   the returned icon will be at least the width and height
     *                   respectively of newMinSize.
     * @param an ImageObserver.
     * @return the rescaled icon.
     */
    public static ImageIcon rescale(ImageIcon src, Dimension newMinSize, ImageObserver observer) {
        double widthRatio =  newMinSize.getWidth() / (double) src.getIconWidth();
        double heightRatio = newMinSize.getHeight() / (double) src.getIconHeight();
        double scale = widthRatio > heightRatio ? widthRatio : heightRatio;
        
        int w = (int) Math.round(scale * src.getIconWidth());
        int h = (int) Math.round(scale * src.getIconHeight());
        BufferedImage dst = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2 = dst.createGraphics();
        g2.drawImage(src.getImage(), 0, 0, w, h, observer);
        g2.dispose();
        return new ImageIcon(dst);
    }
    
    /**
     * Loads a JAR file so the application can access its classes.
     * @param jarPath the JAR file.
     * @throws Exception if the JAR file cannot be loaded.
     */
    public static void loadJar(String jarPath) throws Exception {
        File f = new File(jarPath);
        URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
        Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
        method.setAccessible(true);
        method.invoke(sysloader, new Object[]{f.toURI().toURL()});
    }
    
    /**
     * Translates the given color to a color string used by the ArcGIS for the Military
     * GeoEvent Processor schema.
     * @param color the color.
     * @return a color string used by the ArcGIS for the Military GeoEvent Processor
     *         schema.
     */
    public static String getAFMGeoEventColorString(Color color) {
        if (null == color) {
            return null;
        } else if (Color.RED.equals(color)) {
            return "1";
        } else if (Color.YELLOW.equals(color)) {
            return "4";
        } else if (Color.GREEN.equals(color)) {
            return "2";
        } else if (Color.BLUE.equals(color)) {
            return "3";
        } else {
            /**
             * ArcGIS Runtime does not currently support custom chem light colors.
             * But we can send a hex string in case some client can use it.
             */
            return "#" + Integer.toHexString(color.getRed()) + Integer.toHexString(color.getGreen()) + Integer.toHexString(color.getBlue());
        }
    }
    
}
import com.esri.core.geometry.AngularUnit;
import com.esri.core.geometry.GeometryEngine;
import com.esri.core.geometry.Point;
import com.esri.core.geometry.SpatialReference;
import com.esri.map.Layer;
import com.esri.map.MapOverlay;
import com.esri.militaryapps.controller.ChemLightController;
import com.esri.militaryapps.controller.LocationController;
import com.esri.militaryapps.controller.LocationListener;
import com.esri.militaryapps.controller.MessageController;
import com.esri.militaryapps.controller.PositionReportController;
import com.esri.militaryapps.model.Location;
import com.esri.militaryapps.model.LocationProvider;
import com.esri.militaryapps.model.NavigationMode;
import com.esri.runtime.ArcGISRuntime;
import com.esri.vehiclecommander.controller.AdvancedSymbolController;
import com.esri.vehiclecommander.controller.AppConfigController;
import com.esri.vehiclecommander.controller.AppConfigListener;
import com.esri.vehiclecommander.controller.GPAdapter;
import com.esri.vehiclecommander.controller.IdentifyListener;
import com.esri.vehiclecommander.controller.MapController;
import com.esri.vehiclecommander.controller.RouteController;
import com.esri.vehiclecommander.controller.VehicleStatusController;
import com.esri.vehiclecommander.controller.ViewshedController;
import com.esri.vehiclecommander.model.IdentifiedItem;
import com.esri.vehiclecommander.model.MapConfig;
import com.esri.vehiclecommander.model.MapConfigReader;
import com.esri.vehiclecommander.util.Utilities;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JLayeredPane;
import javax.swing.JOptionPane;
import javax.swing.JToggleButton;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;

                        }
                        addToolbarButton(button);
                    } else if (JButton.class.isAssignableFrom(targetClass)) {
                        Constructor<?> ctor = targetClass.getDeclaredConstructor();
                        JButton button = (JButton) ctor.newInstance();
                        addToolbarButton(button);
                    }
                }
            } catch (Exception ex) {
                Logger.getLogger(VehicleCommanderJFrame.class.getName()).log(Level.SEVERE, null, ex);
                continue;
            }
        }
 
        LocationController locationController = null;
        try {
            locationController = mapController.getLocationController();
            locationController.setMode(appConfigController.getLocationMode(), false);
            double speedMultiplier = appConfigController.getSpeedMultiplier();
            if (0 < speedMultiplier) {
                locationController.setSpeedMultiplier(speedMultiplier);
            }
            locationController.addListener(identifyPanel);
            if (locationController instanceof com.esri.vehiclecommander.controller.LocationController) {
                appConfigController.setLocationController((com.esri.vehiclecommander.controller.LocationController) locationController);
            }
            locationController.addListener(new LocationListener() {

                public void onLocationChanged(Location location) {
                    if (null != location) {
                        updatePosition(GeometryEngine.project(location.getLongitude(), location.getLatitude(), mapController.getSpatialReference()), location.getHeading());
                    }
                }

                public void onStateChanged(LocationProvider.LocationProviderState state) {

                }
            });
            locationController.start();
        } catch (Throwable t) {
            Logger.getLogger(VehicleCommanderJFrame.class.getName()).log(Level.SEVERE, null, t);
            Utilities.showGPSErrorMessage(t.getMessage());
        }
        
        messageController.addListener(symbolController);
        messageController.startReceiving();
        
        positionReportController = new PositionReportController(
                mapController.getLocationController(),
                messageController,
                appConfigController.getUsername(),
                appConfigController.getVehicleType(),
                appConfigController.getUniqueId(),
                appConfigController.getSic());
        positionReportController.setPeriod(appConfigController.getPositionMessageInterval());
        positionReportController.setEnabled(true);
        
        vehicleStatusController = new VehicleStatusController(appConfigController, messageController);

        //Key listener for application-wide key events
        ApplicationKeyListener keyListener = new ApplicationKeyListener(this, mapController);
        addKeyListener(keyListener);
        map.addKeyListener(keyListener);

        mainMenu = new MainMenuJPanel(this, mapController, appConfigController,
                symbolController, routeController, positionReportController);
        routeController.addRouteListener(mainMenu);
        mainMenu.setVisible(false);
        getLayeredPane().add(mainMenu, JLayeredPane.MODAL_LAYER);
        mainMenu.addComponentListener(new ComponentAdapter() {

            @Override
            public void componentHidden(ComponentEvent e) {
                jToggleButton_mainMenu.setSelected(false);
            }
        });
        
        viewshedPanel = new ViewshedJPanel(this, mapController, viewshedController);
        viewshedPanel.setVisible(false);
        ((ComponentShowingButton) jToggleButton_viewshed).setComponentParent(getLayeredPane());
        ((ComponentShowingButton) jToggleButton_viewshed).setComponent(viewshedPanel);

        routePanel = new RouteJPanel(this, mapController, routeController);
        routePanel.setVisible(false);
        ((ComponentShowingButton) jToggleButton_route).setComponentParent(getLayeredPane());
        ((ComponentShowingButton) jToggleButton_route).setComponent(routePanel);
        
        stopFollowMeOverlay = new MapOverlay() {

            @Override
            public void onMouseDragged(MouseEvent event) {
                cancelFollowMe();
                super.onMouseDragged(event);
            }
            
        };
        map.addMapOverlay(stopFollowMeOverlay);
        
        updateTimeDisplayTimer = new Timer(1000 / 24, new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                DateFormat dateFormat = appConfigController.isShowLocalTimeZone() ?
                        Utilities.DATE_FORMAT_MILITARY_LOCAL :
                        Utilities.DATE_FORMAT_MILITARY_ZULU;
                jLabel_time.setText(dateFormat.format(new Date()));
            }
        });
        updateTimeDisplayTimer.start();
    }

    /**
     * Initializes or resets the map configuration, according to the contents of
     * the mapconfig.xml file that provided to the constructor, or ./mapconfig.xml
     * if none was provided to the constructor.
     */
    public final void resetMapConfig() {
        mapController.removeAllLayers();

        //Read XML config file
        try {

            mapConfig = MapConfigReader.readMapConfig(
