package usda.weru.gis.gui;

import csip.Client;
import java.awt.Color;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import org.apache.log4j.Logger;
import org.codehaus.jettison.json.JSONObject;
import org.geotools.data.DataStore;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.feature.DefaultFeatureCollection;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.map.FeatureLayer;
import org.geotools.map.Layer;
import org.geotools.map.MapContent;
import org.geotools.map.MapLayerListEvent;
import org.geotools.map.MapLayerListListener;
import org.geotools.ows.ServiceException;
import org.geotools.ows.wms.xml.Attribution;
import org.geotools.ows.wmts.WebMapTileServer;
import org.geotools.ows.wmts.model.TileMatrixSet;
import org.geotools.ows.wmts.model.WMTSCapabilities;
import org.geotools.ows.wmts.model.WMTSLayer;
import org.geotools.ows.wmts.model.WMTSServiceType;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.styling.Style;
import org.geotools.tile.TileService;
import org.geotools.tile.util.TileLayer;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LinearRing;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.Name;
import org.opengis.geometry.primitive.Point;
import systems.uom.common.USCustomary;
import usda.weru.gis.GISData;
import usda.weru.gis.GISUtil;
import usda.weru.gis.MapData;
import usda.weru.gis.gui.geotools.ows.wmts.client.WMTSTileServiceWeps;
import usda.weru.gis.latlong.LatLong;
import usda.weru.util.About;
import static usda.weru.util.About.getWepsCacheDirTiles;
import usda.weru.util.ConfigData;
import usda.weru.weps.location.CligenDataModel;
import usda.weru.weps.location.Station;
import usda.weru.weps.location.WindgenDataModel;

/**
 *
 * @author mark
 */
public class MapViewerLayersContent implements MapLayerListListener, PropertyChangeListener {
    
    public static enum WmtsTypes {
        imageryNationalmapLayer,
        imageryArcgisLayer,
        imageryArcgisNatgeoLayer,
//        wmtsImageryEarthdata,
        topoArcgisLayer,
        streetsArcgisLayer,
        transportOvlArcgisLayer,
    }

    public class LayerDataContent {
        public LayerDataContent (Layer l, MapContent c, TileService s, double[] scales, LayerCfgData d) {
            layer = l;
            mapContent = c;
            service = s;
            newScaleList = scales;
            cacheLoadthread = null;
            layerCfgData = d;
        }       
        public Layer layer;
        public MapContent mapContent;
        public TileService service;
        double[] newScaleList;
        public Thread cacheLoadthread;
        public LayerCfgData layerCfgData;
    };
    
    public static final String prismDataLayerName = "Cached Prism Data";
    public static final String WepsLayerOpacityKey = "WepsLayerOpacity";
    public static final String WepsLayerZorderKey = "WepsLayerOrder";
    public static final String WepsCopyrightKey = "WepsCopyrightOrder";
    public static final String layersOutlineName = "outline";
    
    ArrayList <LayerDataContent> layerData;
    
    private static final Logger logger = Logger.getLogger(MapViewer.class);
    MapViewer viewer;
    LayerTableModel layerModel;
    
    protected DefaultFeatureCollection prismLayerCollection;
    protected FeatureLayer prismLayer;
    
    
    public MapViewerLayersContent (MapViewer viewer, LayerTableModel layerModel) {
        super ();
        this.viewer = viewer;
        this.layerModel = layerModel;
        
        layerData = new ArrayList<>();
        prismLayerCollection = null;
        prismLayer = null;
        
        initContent();
        
        ConfigData.getDefault().addPropertyChangeListener(this);
    }
    
    private void initContent () {
       
        createWmtsContent(WmtsTypes.imageryNationalmapLayer);
        createWmtsContent(WmtsTypes.imageryArcgisLayer);
        createWmtsContent(WmtsTypes.imageryArcgisNatgeoLayer);        
        createWmtsContent(WmtsTypes.topoArcgisLayer);        
        createWmtsContent(WmtsTypes.streetsArcgisLayer);
        createWmtsContent(WmtsTypes.transportOvlArcgisLayer);
        
        GISData data = GISData.getInstance();

        //add shape layers
        for (Name name : data.getNames()) {
            DataStore store = data.dataStore(name);
            ArrayList<FeatureLayer> toAdd = getLayers(store);
            for (FeatureLayer l : toAdd) {
                // these will get updated from cfg in updateLayerFromConfig()
                l.setVisible(true);
                l.setTitle(name.toString());
                addLayer(l);
            }                   
        }

        addLayer(createWindgenStationLayer());
        addLayer(createCligenStationLayer());
        addLayer(createPrismDataLocationLayer());
        addLayer(createCrLmodSoilDataLocationLayer());
        
        // layer order is reversed: 0 lowest, n highest
        // we set it initially 0 highest, n lowest
        // so comparators below are reversed.
        layerModel.layerList.sort(new Comparator<Layer> () {
            @Override
            public int compare(Layer o1, Layer o2) {
                int z1 = (int) o1.getUserData().get(WepsLayerZorderKey);
                int z2 = (int) o2.getUserData().get(WepsLayerZorderKey);
                if (z1 > z2) return -1;
                if (z1 < z2) return 1;
                return 0;
            }
        });
        layerData.sort(new Comparator<LayerDataContent> () {
            @Override
            public int compare(LayerDataContent o1, LayerDataContent o2) {
                int z1 = (int) o1.layer.getUserData().get(WepsLayerZorderKey);
                int z2 = (int) o2.layer.getUserData().get(WepsLayerZorderKey);
                if (z1 > z2) return -1;
                if (z1 < z2) return 1;
                return 0;
            }
        });
    }
    
    protected void addLayer(Layer layer) {
        addLayer (layer, null, null);
    }
    
    protected void addLayer(Layer layer, TileService s, double[] scaleList) {
        
        // Renderer uses this also, cannot overlap
        synchronized (MapRenderLayer.RENDER_LOCK) {
            UpdateConfigReturn r = updateLayerFromConfig (layer);
            layer = r.layer;
            if (layer != null) {
                MapContent content = new MapContent();
                content.addMapLayerListListener((MapRenderLayer) viewer.getMapController());
                content.addMapLayerListListener(layerModel);
                content.addMapLayerListListener(this);
                content.addLayer(layer);
                layerData.add(new LayerDataContent(layer, content, s, scaleList, r.cfgData));
            }
        }
    }
    
    class UpdateConfigReturn {
        Layer layer;
        LayerCfgData cfgData;
        UpdateConfigReturn (Layer l, LayerCfgData d) {
            layer = l;
            cfgData = d;
        }
    }
    
    protected UpdateConfigReturn updateLayerFromConfig (Layer layer) {
        LayerCfgData layerCfgData = new LayerCfgData ();
        
        String name = layer.getTitle();
        
        // don't update Location: leave it as is
        if (!name.startsWith("Current Location")) {
            if (name.endsWith(".shp")) {
                name = name.substring(0, name.length() - 4);
            }
            String configBase = null;
            int zOrder = 0;

            layerCfgData = getLayerCfgData (name);

            zOrder = layerCfgData.zOrder;
            configBase = layerCfgData.cfgBase;
            name = layerCfgData.newName;
            
            String en = ConfigData.getStringParm(configBase+ConfigData.layerEnabledSuf, "1");
            if (en.toLowerCase().contentEquals("1")) {
                // layers w/ mode parm have 2 layers: fill & outline
                // only use one or other.
                // If this layer does not match mode, skip it
                String mode = ConfigData.getStringParm(configBase+ConfigData.layerModeSuf, "");
                if (mode.length() == 0 || (mode.length() > 0 && mode.contentEquals(layerCfgData.mode))) {
                    layer.setTitle(name);

                    // Temp code to accept existing old values
                    String vs = ConfigData.getStringParm(configBase+ConfigData.layerVisibleSuf, "0");
                    if (!vs.toLowerCase().contains("0") && !vs.toLowerCase().contains("1")) {
                        vs = "0";
                        ConfigData.getDefault().setData(configBase+ConfigData.layerVisibleSuf, "0");
                    }
                    int vis = Integer.parseInt(vs);
                    layer.setVisible(vis > 0);

                    layer.getUserData().put(WepsLayerZorderKey, zOrder);
                    String s = ConfigData.getStringParm(configBase+ConfigData.layerOpacitySuf, "1.");
                    float opacity = Float.valueOf(s.length() > 0 ? s : "1.");
                    layer.getUserData().put(WepsLayerOpacityKey, opacity);
                } else {
                    // this shows skip this layer
                    layer = null;
                }
            } else {
                // this shows layer NOT enabled
                layer = null;
            }
        }
        
        return new UpdateConfigReturn(layer, layerCfgData);
    }

    class LayerCfgData {
        public LayerCfgData () {
            cfgBase = null;
            mode = null;
            zOrder = -1;
            origName = null;
            newName = null;
        }
        public LayerCfgData (String cfgBase, String mode, int zOrder, String origName, String newName) {
            this.cfgBase = cfgBase;
            this.mode = mode;
            this.zOrder = zOrder;
            this.origName = origName;
            this.newName = newName;
        }
        String cfgBase;
        String mode;
        int zOrder;
        String origName;
        String newName;
    }
    
    LayerCfgData getLayerCfgData (String name) {
        String configBase="";
        int zOrder = 0;
        String newName = "";
        
        String mode = null;

        // Zorder always > 0... Just a way to make ZOrder follow this hierarchy
        // without hard coding Zorder at every step
        if (++zOrder > 0 && name.startsWith("Cached Soil Data")) {
            configBase = ConfigData.cachedSoilLayerBase;
        } else if (++zOrder > 0 && name.startsWith("Cached Prism Data")) {
            configBase = ConfigData.cachedPrismLayerBase;
        } else if (++zOrder > 0 && (name.startsWith("chn_adm1") || 
                                    name.startsWith("China Provinces"))) {
            configBase = ConfigData.chinaProvLayerBase;
            mode = name.contains("_nofill") ? ConfigData.layersModeValOutline : ConfigData.layersModeValFill;
            newName = "China Provinces";
            newName += mode.contentEquals(ConfigData.layersModeValOutline) ? 
                    " "+layersOutlineName : "";
        } else if (++zOrder > 0 && (name.startsWith("triangulation") || 
                                    name.startsWith("Windgen triangulation boundaries"))) {
            configBase = ConfigData.wingenTriangulationLayerBase;
            newName = "Windgen triangulation boundaries";
        } else if (++zOrder > 0 && (name.startsWith("reflected") || 
                                    name.startsWith("Reflected Windgen stations"))) {
            configBase = ConfigData.wingenReflectedLayerBase;
            newName = "Reflected Windgen stations";
        } else if (++zOrder > 0 && (name.startsWith("int_boundary") || 
                                    name.startsWith("Windgen interp. boundary"))) {
            configBase = ConfigData.wingenBoundaryLayerBase;
            newName = "Windgen interp. boundary";
        } else if (++zOrder > 0 && (name.startsWith("windgen_2018") || 
                                    name.startsWith("Windgen interp. & polygon regions"))) {
            configBase = ConfigData.wingenRegionsLayerBase;
            newName = "Windgen interp. & polygon regions";
        } else if (++zOrder > 0 && (name.startsWith("Windgen Stations") || 
                                    name.startsWith("Windgen stations"))) {
            configBase = ConfigData.wingenStationsLayerBase;
            newName = "Windgen stations";
        } else if (++zOrder > 0 && (name.startsWith("cligen_boundary") || 
                                    name.startsWith("Cligen boundary"))) {
            configBase = ConfigData.cligenBoundaryLayerBase;
            newName = "Cligen boundary";
        } else if (++zOrder > 0 && (name.startsWith("cligen_2013") || 
                                    name.startsWith("Cligen polygon regions"))) {
            configBase = ConfigData.cligenRegionsLayerBase;
            newName = "Cligen polygon regions";
        } else if (++zOrder > 0 && (name.startsWith("Cligen Stations") || 
                                    name.startsWith("Cligen stations"))) {
            configBase = ConfigData.cligenStationsLayerBase;
            newName = "Cligen stations";
        } else if (++zOrder > 0 && (name.startsWith("cmz") || 
                                    name.startsWith("NRCS CMZ regions"))) {
            configBase = ConfigData.cmzLayerBase;
            newName = "NRCS CMZ regions";
        } else if (++zOrder > 0 && (name.startsWith("co99_") || 
                                    name.startsWith("US counties"))) {
            configBase = ConfigData.usCountiesLayerBase;
            newName = "US counties";
        } else if (++zOrder > 0 && (name.startsWith("st99_") || 
                                    name.startsWith("US states"))) {
            configBase = ConfigData.usStatesLayerBase;
            mode = name.contains("_nofill") ? ConfigData.layersModeValOutline : ConfigData.layersModeValFill;
            newName = "US states";
            newName += mode.contentEquals(ConfigData.layersModeValOutline) ? 
                    " "+layersOutlineName : "";
        } else if (++zOrder > 0 && (name.startsWith("world_borders") || 
                                    name.startsWith("World borders"))) {
            configBase = ConfigData.worldBordersLayerBase;
            mode = name.contains("_nofill") ? ConfigData.layersModeValOutline : ConfigData.layersModeValFill;
            newName = "World borders";
            newName += mode.contentEquals(ConfigData.layersModeValOutline) ? 
                    " "+layersOutlineName : "";
        } else if (++zOrder > 0 && name.startsWith("Streets overlay ArcGIS")) {
            configBase = ConfigData.transportOvlArcgisLayerBase;
        } else if (++zOrder > 0 && name.startsWith("Street maps ArcGIS")) {
            configBase = ConfigData.streetsArcgisLayerBase;
        } else if (++zOrder > 0 && name.startsWith("Topographic ArcGIS")) {
            configBase = ConfigData.topoArcgisLayerBase;
        } else if (++zOrder > 0 && name.startsWith("Hillshade National Geographic")) {
            configBase = ConfigData.imageryArcgisNatgeoLayerBase;
        } else if (++zOrder > 0 && name.startsWith("Imagery ArcGis")) {
            configBase = ConfigData.imageryArcgisLayerBase;
        } else if (++zOrder > 0 && name.startsWith("Imagery NationalMap")) {
            configBase = ConfigData.imageryNationalmapLayerBase;
        }
        newName = (newName.length() > 0) ? newName : name;
        return new LayerCfgData (configBase, mode, zOrder, name, newName);
    }
    
    public void removeLayer (Layer layer) {
        // Renderer uses this also, cannot overlap
        synchronized (MapRenderLayer.RENDER_LOCK) {
            for (LayerDataContent d : layerData) {
                if (d.layer.equals(layer)) {
                    d.mapContent.removeLayer(layer);
                    d.mapContent.dispose();
                    layerData.remove(d);
                    break;
                }
            }
        }
    }
    
    public void createWmtsContent(WmtsTypes type) {
        String urlStr;
        URL url;
        WebMapTileServer wmts = null;
        WMTSCapabilities wmtsCapabilities = null;
        String titleStr = null;
        List<WMTSLayer> layers;
        WMTSLayer wmtsTileLayer = null;
        TileMatrixSet tileLayerMatSet = null;
        String tileUrlStr = null;
        MapController c_controller = viewer.getMapController();
        String copyright = "";
        
        float opacity = (float)1.;
        
        // This is redundant: it is handled in updateLayerFromConfig()
        // However, these Web calls are very slow, so check here also
        // and skip if not enabled just to speed up init time.
        String en = ConfigData.getStringParm(ConfigData.CD+type.toString()+ConfigData.layerEnabledSuf, "1");
        if (en.toLowerCase().contentEquals("1")) {
            c_controller.setZoomMax(94);
            try {        
                switch (type) {
                    case imageryNationalmapLayer:
                        urlStr = "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/WMTS";
                        // copyright is empty for this service, don't waste time with call
                        //copyright = getArcgisCopyright(urlStr);
                        url = new URL (urlStr);
                        wmts = new WebMapTileServer(url);
                        wmtsCapabilities = wmts.getCapabilities();
                        titleStr = "Imagery NationalMap";
                        layers = wmtsCapabilities.getLayerList();
                        wmtsTileLayer = getLayerByName (layers, "USGSImageryOnly");
                        tileLayerMatSet = getMatrixSetByName (wmtsTileLayer, 
                                wmtsCapabilities, "default028mm");
                        tileUrlStr = getUrlTemplateStr (wmtsTileLayer);
                        tileUrlStr = tileUrlStr.replace("{Style}", 
                                wmtsTileLayer.getStyles().get(0).getName());
                        tileUrlStr = tileUrlStr.replace("{TileMatrixSet}", 
                                tileLayerMatSet.getIdentifier());
                        break;
                    case imageryArcgisLayer:
                        urlStr = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/WMTS";
                        copyright = getArcgisCopyright(urlStr);
                        url = new URL (urlStr);
                        wmts = new WebMapTileServer(url);
                        wmtsCapabilities = wmts.getCapabilities();
                        titleStr = "Imagery ArcGis";
                        layers = wmtsCapabilities.getLayerList();
                        wmtsTileLayer = layers.get(0);
                        Attribution a = wmtsTileLayer.getAttribution();
                        tileLayerMatSet = getMatrixSetByName (wmtsTileLayer, 
                                wmtsCapabilities, "default028mm");
                        tileUrlStr = getUrlTemplateStr (wmtsTileLayer);
                        tileUrlStr = tileUrlStr.replace("{Style}", 
                                wmtsTileLayer.getStyles().get(0).getName());
                        tileUrlStr = tileUrlStr.replace("{TileMatrixSet}", 
                                tileLayerMatSet.getIdentifier());
                        break;


                    case imageryArcgisNatgeoLayer:
                        urlStr = "https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/WMTS";
                        copyright = getArcgisCopyright(urlStr);
                        url = new URL (urlStr);
                        wmts = new WebMapTileServer(url);
                        wmtsCapabilities = wmts.getCapabilities();
                        titleStr = "Hillshade National Geographic";
                        layers = wmtsCapabilities.getLayerList();
                        wmtsTileLayer = layers.get(0);
                        tileLayerMatSet = getMatrixSetByName (wmtsTileLayer, 
                                wmtsCapabilities, "default028mm");
                        tileUrlStr = getUrlTemplateStr (wmtsTileLayer);
                        tileUrlStr = tileUrlStr.replace("{Style}", 
                                wmtsTileLayer.getStyles().get(0).getName());
                        tileUrlStr = tileUrlStr.replace("{TileMatrixSet}", 
                                tileLayerMatSet.getIdentifier());
                        break;
                    case topoArcgisLayer:
                        urlStr = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/WMTS";
                        copyright = getArcgisCopyright(urlStr);
                        url = new URL (urlStr);
                        wmts = new WebMapTileServer(url);
                        wmtsCapabilities = wmts.getCapabilities();
                        titleStr = "Topographic ArcGIS";
                        layers = wmtsCapabilities.getLayerList();
                        wmtsTileLayer = layers.get(0);
                        opacity = (float).6;
                        tileLayerMatSet = getMatrixSetByName (wmtsTileLayer, 
                                wmtsCapabilities, "default028mm");
                        tileUrlStr = getUrlTemplateStr (wmtsTileLayer);
                        tileUrlStr = tileUrlStr.replace("{Style}", 
                                wmtsTileLayer.getStyles().get(0).getName());
                        tileUrlStr = tileUrlStr.replace("{TileMatrixSet}", 
                                tileLayerMatSet.getIdentifier());
                        break;
                    case streetsArcgisLayer:
                        urlStr = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/WMTS";
                        copyright = getArcgisCopyright(urlStr);
                        url = new URL (urlStr);
                        wmts = new WebMapTileServer(url);
                        wmtsCapabilities = wmts.getCapabilities();
                        titleStr = "Street maps ArcGIS";
                        layers = wmtsCapabilities.getLayerList();
                        wmtsTileLayer = layers.get(0);
                        tileLayerMatSet = getMatrixSetByName (wmtsTileLayer, 
                                wmtsCapabilities, "default028mm");
                        tileUrlStr = getUrlTemplateStr (wmtsTileLayer);
                        tileUrlStr = tileUrlStr.replace("{Style}", 
                                wmtsTileLayer.getStyles().get(0).getName());
                        tileUrlStr = tileUrlStr.replace("{TileMatrixSet}", 
                                tileLayerMatSet.getIdentifier());
                        break;
                    case transportOvlArcgisLayer:
                        urlStr = "https://server.arcgisonline.com/arcgis/rest/services/Reference/World_Transportation/MapServer/WMTS";
                        copyright = getArcgisCopyright(urlStr);
                        url = new URL (urlStr);
                        wmts = new WebMapTileServer(url);
                        wmtsCapabilities = wmts.getCapabilities();
                        titleStr = "Streets overlay ArcGIS";
                        layers = wmtsCapabilities.getLayerList();
                        wmtsTileLayer = layers.get(0);
                        tileLayerMatSet = getMatrixSetByName (wmtsTileLayer,
                                wmtsCapabilities, "default028mm");
                        tileUrlStr = getUrlTemplateStr (wmtsTileLayer);
                        tileUrlStr = tileUrlStr.replace("{Style}", 
                                wmtsTileLayer.getStyles().get(0).getName());
                        tileUrlStr = tileUrlStr.replace("{TileMatrixSet}", 
                                tileLayerMatSet.getIdentifier());
                        break;
                }

                TileService service = null;
                if (tileLayerMatSet != null && wmtsTileLayer != null) {        
                    try {
                        getWepsCacheDirTiles().mkdirs();
                        service = new WMTSTileServiceWeps(tileUrlStr, WMTSServiceType.REST, wmtsTileLayer, "", tileLayerMatSet);
                    } catch (Exception ex) {
                        service = null;
                        System.err.println ("Failed to load from server:" + tileUrlStr.toString());
                    }

                    if(service != null){
                        System.setProperty("wmts.tile.cache.size", "400");

                        double[] scaleList = service.getScaleList();
                        double max = scaleList[0];
                        ArrayList<Double> newScaleList = new ArrayList<>();
                        for (int i = 1; i < scaleList.length; i++) {
                            double d = max / scaleList[i];
                            if (d > 8. && d < 40000.) {
                                newScaleList.add(d);
                            }
                        }
                        while (newScaleList.size() > 10) {
                            for (int i = 1; i < newScaleList.size()-1; i+=2) {
                                newScaleList.remove(i);
                            }
                        }
                        int l = newScaleList.size();
                        double[] newScales = new double[l];
                        for (int i = 0; i < l; i++) {
                            newScales[i] = newScaleList.get(i);
                        }

                        TileLayer tileLayer = new TileLayer(service);
                        tileLayer.setTitle(titleStr);
                        tileLayer.getUserData().put(WepsCopyrightKey, copyright);
                        addLayer(tileLayer, service, newScales);
                    }
                }
            } catch (MalformedURLException ex) {
                logger.error("MalformedURL in createWmtsContent", ex);
            } catch (IOException ex) {
                logger.error("IOException in createWmtsContent", ex);
            } catch (ServiceException ex) {
                logger.error("ServiceException in createWmtsContent", ex);
            }
            logger.info("createWmtsContent completed");
        }
    }
    
    protected String getArcgisCopyright(String urlWmts) {
        String copyright="";
        try {
            String crUrl = urlWmts.replace("WMTS", "0?f=pjson");
            Client client = new Client();
            String jj = client.doGET(crUrl);
            JSONObject resp = new JSONObject (jj);
            copyright = resp.getString("copyrightText");
        } catch (Exception ex) {
            copyright="";
        }
        return copyright;
    }
    
    public  FeatureLayer createLatLongMarkerLayer(LatLong location) {

        Coordinate center = GISUtil.toCoordinate(location);

        SimpleFeatureTypeBuilder typeBuilder = new SimpleFeatureTypeBuilder();
        typeBuilder.setName("markerType");
        typeBuilder.setNamespaceURI("usda.weru.gis");

        typeBuilder.add("location", Point.class, DefaultGeographicCRS.WGS84);
        typeBuilder.setDefaultGeometry("location");
        typeBuilder.add("name", String.class);

        SimpleFeatureType markerType = typeBuilder.buildFeatureType();
        SimpleFeatureBuilder builder = new SimpleFeatureBuilder(markerType);

        builder.add(new org.locationtech.jts.geom.GeometryFactory().createPoint(center));
        builder.add("Current LatLong");

        DefaultFeatureCollection collection = new DefaultFeatureCollection("latlongMarker", markerType);
        SimpleFeature marker = builder.buildFeature(null);
        collection.add(marker);

        Style style = GISGUIUtil.createCrossPointStyle(Color.RED, Color.PINK);

        FeatureLayer layer = new FeatureLayer(collection, style, "Current Location");

        return layer;
    }
    
    private FeatureLayer createWindgenStationLayer() {

        SimpleFeatureTypeBuilder typeBuilder = new SimpleFeatureTypeBuilder();
        typeBuilder.setName("windgenStation");
        typeBuilder.setNamespaceURI("usda.weru.gis");

        typeBuilder.add("location", Point.class, DefaultGeographicCRS.WGS84);
        typeBuilder.setDefaultGeometry("location");
        typeBuilder.add("name", String.class);

        SimpleFeatureType markerType = typeBuilder.buildFeatureType();
        SimpleFeatureBuilder builder = new SimpleFeatureBuilder(markerType);
        DefaultFeatureCollection collection = new DefaultFeatureCollection("windgenStations", markerType);

        WindgenDataModel data = WindgenDataModel.getInstance();
        for (Station station : data.stations()) {
            builder.reset();
            builder.add(new org.locationtech.jts.geom.GeometryFactory().
                    createPoint(GISUtil.toCoordinate(station.getLatLong())));
            builder.add(station.getDisplayName());
            SimpleFeature marker = builder.buildFeature(null);
            collection.add(marker);
        }

        Style style = GISGUIUtil.createCirclePointStyle(5, Color.RED, Color.GRAY);

        FeatureLayer layer = new FeatureLayer(collection, style, "Windgen Stations");
        layer.setVisible(false);

        return layer;
    }

    private FeatureLayer createCligenStationLayer() {

        SimpleFeatureTypeBuilder typeBuilder = new SimpleFeatureTypeBuilder();
        typeBuilder.setName("cligenStation");
        typeBuilder.setNamespaceURI("usda.weru.gis");

        typeBuilder.add("location", Point.class, DefaultGeographicCRS.WGS84);
        typeBuilder.setDefaultGeometry("location");
        typeBuilder.add("name", String.class);

        SimpleFeatureType markerType = typeBuilder.buildFeatureType();
        SimpleFeatureBuilder builder = new SimpleFeatureBuilder(markerType);
        DefaultFeatureCollection collection = new DefaultFeatureCollection("cligenStations", markerType);

        CligenDataModel data = CligenDataModel.getInstance();
        for (Station station : data.stations()) {
            builder.reset();
            builder.add(new org.locationtech.jts.geom.GeometryFactory()
                    .createPoint(GISUtil.toCoordinate(station.getLatLong())));
            builder.add(station.getDisplayName());
            SimpleFeature marker = builder.buildFeature(null);
            collection.add(marker);
        }

        Style style = GISGUIUtil.createCirclePointStyle(5, Color.BLUE, Color.GRAY);

        FeatureLayer layer = new FeatureLayer(collection, style, "Cligen Stations");
        layer.setVisible(false);

        return layer;
    }

    protected FeatureLayer createPrismDataLocationLayer() {

        DefaultFeatureCollection collection;
        collection = buildPrismLayerCollection(null, false);
        prismLayerCollection = collection;
        
        Style style = GISGUIUtil.createCirclePointStyle(5, Color.GREEN, Color.RED);

        FeatureLayer layer = new FeatureLayer(collection, style, prismDataLayerName);
        layer.setVisible(false);
        prismLayer = layer;

        return layer;
    }
    
    public void updatePrismDataLocationLayer() {
        if (prismLayer.isVisible()) {
            prismLayerCollection = buildPrismLayerCollection (prismLayerCollection);
            // MEH: ugly, but all I can find to force the layer to re-draw.
            prismLayer.setVisible(false);
            prismLayer.setVisible(true);
        }
    }
        
    protected DefaultFeatureCollection buildPrismLayerCollection (DefaultFeatureCollection collection) {
        return buildPrismLayerCollection (collection, true);
    }
    protected DefaultFeatureCollection buildPrismLayerCollection (DefaultFeatureCollection collection, boolean addData) {
        
        SimpleFeatureTypeBuilder typeBuilder = new SimpleFeatureTypeBuilder();
        typeBuilder.setName("prismDataLocation");
        typeBuilder.setNamespaceURI("usda.weru.gis");

        typeBuilder.add("location", Point.class, DefaultGeographicCRS.WGS84);
        typeBuilder.setDefaultGeometry("location");
        typeBuilder.add("name", String.class);
        
        SimpleFeatureType markerType = typeBuilder.buildFeatureType();
        SimpleFeatureBuilder builder = new SimpleFeatureBuilder(markerType);
        
        if (collection == null) {
            collection = new DefaultFeatureCollection("prismDataLocations", markerType);
        } else {
            Collection<SimpleFeature> cTemp = new DefaultFeatureCollection(collection);
            collection.removeAll(cTemp);
        }
        
        if (addData) {
            File prismCacheDir = new File (About.getWepsCacheDir()+"/Prism");

            for (File f : prismCacheDir.listFiles()) {
                String name = f.getName();
                if (name.startsWith("prismCell")) {
                    //lon / lat separator in file name is 2nd period
                    int separator = name.indexOf('.',  name.indexOf('.')+1);
                    double lonFile = Double.valueOf(name.substring(9,separator));
                    double latFile = Double.valueOf(name.substring(separator+1, name.indexOf(".json")));
                    LatLong latlong = LatLong.valueOf(latFile, lonFile, USCustomary.DEGREE_ANGLE);

                    builder.reset();
                    builder.add(new org.locationtech.jts.geom.GeometryFactory()
                            .createPoint(GISUtil.toCoordinate(latlong)));
                    SimpleFeature marker = builder.buildFeature(null);
                    collection.add(marker);
                }
            }
        }
        return collection;
    }

    private FeatureLayer createCrLmodSoilDataLocationLayer() {

        SimpleFeatureTypeBuilder typeBuilder = new SimpleFeatureTypeBuilder();
        typeBuilder.setName("soilDataLocation");
        typeBuilder.setNamespaceURI("usda.weru.gis");

        typeBuilder.add("location", Point.class, DefaultGeographicCRS.WGS84);
        typeBuilder.setDefaultGeometry("location");
        typeBuilder.add("name", String.class);

        SimpleFeatureType polyType = typeBuilder.buildFeatureType();
        SimpleFeatureBuilder builder = new SimpleFeatureBuilder(polyType);
        DefaultFeatureCollection collection = new DefaultFeatureCollection("prismDataLocations", polyType);
        
        File soilCacheDir = new File (About.getWepsCacheDir()+"/Soils");
        File soilFile = new File (soilCacheDir, "CSIP Soil Service.polygons");
        try {
            FileReader soilReader = new FileReader (soilFile);
            
            String s;
            while ((s = FileReadLine(soilReader)) != null) {
                s = s.replace("Polygon:", "");
                ArrayList<LatLong> points = new ArrayList<>();
                ArrayList<Coordinate> coords = new ArrayList<>();
                
                while (s.length() > 1) {
                    int commaIdx = s.indexOf(',');
                    int semiIdx = s.indexOf(';');
                    double lonFile = Double.valueOf(s.substring(0,commaIdx));
                    double latFile = Double.valueOf(s.substring(commaIdx+1, semiIdx));
                    LatLong latlong = LatLong.valueOf(latFile, lonFile, USCustomary.DEGREE_ANGLE);
                    points.add(latlong);
                    coords.add(GISUtil.toCoordinate(latlong));
                    s = s.substring(semiIdx+1, s.length());
                }
                coords.add(coords.get(0));

                builder.reset();
                Coordinate carr[] = new Coordinate[0];
                carr = coords.toArray(carr);
                org.locationtech.jts.geom.GeometryFactory factory = new GeometryFactory();
                LinearRing ring = factory.createLinearRing(carr);
                org.locationtech.jts.geom.Polygon poly = new org.locationtech.jts.geom.Polygon(ring, null, factory);
                builder.add(poly);
                builder.set("name", "Soil data polygon");
                //builder.add("prism");
                SimpleFeature feature = builder.buildFeature(null);
                collection.add(feature);
            }
        } catch (IOException ex) {
        }
        
        Style style = GISGUIUtil.createShapeStyle(5, Color.CYAN, .5, Color.BLUE, 2);

        FeatureLayer layer = new FeatureLayer(collection, style, "Cached Soil Data");
        layer.setVisible(false);

        return layer;
    }
    
    private WMTSLayer getLayerByName (List<WMTSLayer> layerList, String name) {
        for (WMTSLayer layer : layerList) {
           if (layer.getName().contentEquals(name)) {
               return layer;
           }
        }
        return null;
    }
    
    private TileMatrixSet getMatrixSetByName (WMTSLayer layer, 
                                WMTSCapabilities capabilities, String name) {
        for (String tileMatrixId : layer.getTileMatrixLinks().keySet()) {
            if (tileMatrixId.contentEquals(name)) {
                return capabilities.getMatrixSet(tileMatrixId);
            }
        }
        return null;
    }
    
    private String getUrlTemplateStr (WMTSLayer layer) {
        String retStr = null;
        List<String> formats = layer.getFormats();
        for (String s : formats) {
            retStr = layer.getTemplate(s);
            if (retStr != null) {
                break;
            }
        }
        return retStr;
    }
    
    private static ArrayList<FeatureLayer> getLayers(DataStore store) {
        MapData test = new MapData();
        ArrayList<FeatureLayer> layers = new ArrayList<FeatureLayer>();
        try {
            if(store == null || store.getTypeNames() == null) {
                System.err.println("DataStore has not been properly instantiated.");
                return layers;
            }
            for (int i = 0; i < store.getTypeNames().length; i++) {
                String typeName = store.getTypeNames()[i];
                if(typeName != null) {
                    SimpleFeatureSource fs = store.getFeatureSource(typeName);

                    FeatureLayer fl = test.createLayer(fs);
                    layers.add(fl);
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }

        return layers;
    }
       
    protected String FileReadLine (FileReader rdr) throws IOException {
        int c;
        String s = "";
        while ((c = rdr.read()) != -1) {
            s += (char)c;
            if ((char)c == '\n') {
                break;
            }
            if ((char)c == ';') {
                int i = 1;
            }
        }
        if (c == -1 && s.length() == 0) {
            s = null;
        }
        return s;
    }
    
    @Override
    public void layerAdded(MapLayerListEvent event) {
    }

    @Override
    public void layerRemoved(MapLayerListEvent event) {
    }

    @Override
    public void layerChanged(MapLayerListEvent event) {
        Layer l = event.getElement();
        LayerCfgData d = this.getLayerCfgData(l.getTitle());
        ConfigData.getDefault().setData(d.cfgBase+ConfigData.layerVisibleSuf, 
                                              l.isVisible() ? "1" : "0");
    }

    @Override
    public void layerMoved(MapLayerListEvent event) {
    }

    @Override
    public void layerPreDispose(MapLayerListEvent event) {
    }
    
    public int findLayerInData (Layer l) {
        for (int i=0; i < layerData.size(); i++) {
            LayerDataContent d;
            if (layerData.get(i).layer.equals(l)) {
                return i;
            }
        }
        return -1;
    }

    @Override
    public void propertyChange(PropertyChangeEvent e) {
        String name = e.getPropertyName();
        if (name.endsWith("LayerVisible") || name.endsWith("LayerMode") || name.endsWith("LayerOpacity")) {
            LayerDataContent d = findContentByProp (name);
            if (d != null) {
                if (name.endsWith("LayerVisible")) {
                    boolean b = e.getNewValue().equals("1");
                    d.layer.setVisible(e.getNewValue().equals("1"));
                    viewer.refreshLayerTable();
                } else if (name.endsWith("LayerMode")) {
                    String oldTitle = d.layer.getTitle();
                    Layer newLayer = swapModeLayer(d.layerCfgData.origName);
                    if (newLayer != null) {
                        int idx = layerModel.findLayerIndex(oldTitle);
                        if (idx >= 0) {
                            layerModel.updateLayer(idx,newLayer);
                        }
                        ((MapRenderLayer) viewer.getMapController()).refresh();
                    }
                } else if (name.endsWith("LayerOpacity")) {
                    String s = (String)e.getNewValue();
                    Float f = Float.valueOf((String)e.getNewValue());
                    int j = 1;
                    d.layer.getUserData().put(WepsLayerOpacityKey, f);
                    ((MapRenderLayer) viewer.getMapController()).refresh();
                }
            }
        }
    }
    
    LayerDataContent findContentByProp (String prop) {
        for (LayerDataContent d : layerData) {
            if (d.layerCfgData.cfgBase!=null && prop.startsWith(d.layerCfgData.cfgBase)) {
                return d;
            }
        }
        return null;
    }
    
    Layer swapModeLayer (String existingName) {
        
        String newFileName = existingName.endsWith("_nofill") ?
                          existingName.substring(0, existingName.length()-7) :
                          existingName+"_nofill";
        String existingFileName=existingName+".shp";
        newFileName=newFileName+".shp";
        
        FeatureLayer existingLayer = null;
        FeatureLayer newLayer = null;
        
        GISData data = GISData.getInstance();

        //add shape layers
        for (Name name : data.getNames()) {
            if (name.toString().contentEquals(existingFileName)) {
                DataStore store = data.dataStore(name);
                ArrayList<FeatureLayer> toAdd = getLayers(store);
                for (FeatureLayer l : toAdd) {
                    l.setTitle(name.toString());
                    existingLayer = l;
                }
            } else if (name.toString().contentEquals(newFileName)) {
                DataStore store = data.dataStore(name);
                ArrayList<FeatureLayer> toAdd = getLayers(store);
                for (FeatureLayer l : toAdd) {
                    l.setTitle(name.toString());
                    newLayer = l;
                }
            }
        }
        if (newLayer != null) {
            UpdateConfigReturn r = updateLayerFromConfig (newLayer);
            if (r.layer != null) {
                MapContent content = new MapContent();
                content.addLayer(r.layer);
                content.addMapLayerListListener((MapRenderLayer) viewer.getMapController());
                content.addMapLayerListListener(layerModel);
                content.addMapLayerListListener(this);

                for (LayerDataContent d : layerData) {
                    if (d.layerCfgData.origName != null && 
                          d.layerCfgData.origName.contentEquals(existingName)) {
                        
                        d.layer = r.layer;
                        d.mapContent = content;
                        d.layerCfgData = r.cfgData;
                        return d.layer;
                    }
                }
            }
        }
        return null;
    }

}
