Index: trunk/webapps/js/vdsLayer.js
===================================================================
--- trunk/webapps/js/vdsLayer.js	(revision 326)
+++ trunk/webapps/js/vdsLayer.js	(revision 326)
@@ -0,0 +1,178 @@
+    // Build a solid colored icon to use instead of the classic pin
+    // Use a diamond on N and E directions, circle on S and W directions
+    function dotSymbol(color) //,direction)
+    {
+//        var circle = google.maps.SymbolPath.CIRCLE;
+//        var diamond = 'M -1,0 0,-1 1,0 0,1 z';
+//        var myShape = circle;
+        var iconPath = vdsIconGreen;
+        if (color == 'red')
+        {
+           iconPath = vdsIconRed;
+        }
+        else if (color == 'yellow')
+        {
+            iconPath = vdsIconYellow;
+        }
+        return {
+//            path: iconPath,
+//            icon: 
+//                    {
+                        url: iconPath, 
+                        anchor: new google.maps.Point(6, 6)
+//                    };
+//            anchor: new google.maps.Point(6, 6),
+//            scale: 5,
+//            strokeColor: "black", // the border color
+//            strokeWeight: 1, // the border thickness
+//            fillColor: color,
+//            fillOpacity: 1.0
+        };
+    }
+
+    // Load the map data from a json file and style all the points
+    function loadVDSlayer()
+    {
+        // Load the static map data and call saveCoords when done
+        map.data.loadGeoJson(kMapStartupFile, null, saveCoords)
+//        var d = new Date();
+//        var start = d.getTime();
+        // Style the map data by applying the desired properties to each feature (marker)
+        // The function will be called every time a feature's properties are updated.
+        map.data.setStyle(function(feature)
+        {
+            // Get the postmile id 
+            var name = feature.getId();
+            // Get the desired color value
+            var ptColor = feature.getProperty("color");
+            var street = feature.getProperty("street");
+            // Build the marker
+            var iconSymbol = dotSymbol(ptColor);
+            // return the StyleOptions
+            return {
+                icon: iconSymbol,
+/*                    icon: 
+                    {
+                        url: vdsIconGreen, 
+                        anchor: new google.maps.Point(6, 6)
+                    }, */
+                title: name + " @" + street, // set rollover text
+                // set zIndex for slowed traffic to a higher value so they overlap
+                zIndex: colorZvalues[ptColor]
+            };
+        });
+    }
+    // callback when load GeoJson completes
+    // save each feature's Point as the original coordinates for later reference
+    function saveCoords(features)
+    {
+        // Iterate over all the features in the map
+        features.forEach(function(feature)
+        {
+            var pt = feature.getGeometry().get();
+            vds_coords[feature.getId()] = pt; // save the Point in a dictionary
+        });
+        // update the dot colors from the dynamic json data 
+        updateVDSlayer();
+        // go adjust the marker coordinates so dots don't overlap
+        adjustCoords(calcDistanceFactor());
+    }
+
+    // magic formula controls distance between dots proportionate to zoom factor
+    function calcDistanceFactor()
+    {
+        // 15 is maximum zoom, the point at which no adjustment is needed
+        return (.0005 * (15 - map.getZoom()));
+    }
+
+    // Adjust the coordinates of dots so they appear side-by-side
+    // The perpendicular vector for each dot has been provided,
+    // so we just need to multiply by a scaling factor (adjAmount) 
+    // @param adjAmount amount by which to adjust coordinate 
+    function adjustCoords(adjAmount)
+    {
+        // Adjust the NB points a slight amount
+        map.data.forEach(function(feature)
+        {
+            // get the name of the current feature
+            var name = feature.getId();
+            // lookup the original coordinates for this feature
+            var coords = vds_coords[name];
+
+            //retrieve the perpendicular vector (precomputed)
+            var perpx = feature.getProperty("perpx")
+            var perpy = feature.getProperty("perpy")
+                // Make adjustment and save it
+            var myLat = coords.lat() + perpy * adjAmount
+            var myLong = coords.lng() + perpx * adjAmount
+            feature.setGeometry(
+            {
+                lat: myLat,
+                lng: myLong
+            });
+        });
+    }
+
+    // update the color (as needed) for a given marker
+    function updateMarker(marker)
+    {
+        target = marker.id;
+        newColor = marker.properties.color;
+        // see if new color is different than current color
+        currentFeature = map.data.getFeatureById(target);
+        currentColor = currentFeature.getProperty("color");
+        // if a new color is desired then assign it to the feature's color property
+        if (currentColor != newColor)
+        {
+            currentFeature.setProperty("color", newColor);
+            // set zIndex for slowed traffic to a higher value so they overlap
+            currentFeature.setProperty("zIndex", colorZvalues[newColor]);
+        }
+    }
+
+
+    // Load the highways dynamic json file and update the map
+    function updateVDSlayer()
+    {
+        var parsed_JSON;
+        loadJSON(kMapPointsFile, function(response)
+        {
+            // Parse JSON string into object
+            parsed_JSON = JSON.parse(response);
+            // Process each new marker - lookup in current map
+            parsed_JSON.features.forEach(updateMarker);
+        });
+    }
+
+function initVDSbutton()
+{
+
+    var vdsBtnDiv = document.getElementById('vdsButton');
+    map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push(vdsBtnDiv)
+    vdsBtnDiv.title = 'Click to toggle vds view';
+
+    // Setup the click event listeners to toggle icon display
+    vdsBtnDiv.addEventListener('click', function()
+    {
+        vds_showing = !vds_showing;
+        // reveal or hide all the dots
+        map.data.forEach(function(feature)
+        {
+            map.data.overrideStyle(feature,
+            {
+                visible: vds_showing
+            });
+        });
+        // Determine which button image to show
+        if (vds_showing)
+        {
+            pic = "images/btnDepressed_VDS.png"
+        }
+        else
+        {
+            pic = "images/btnReady_VDS.png"
+        }
+        document.getElementById('vdsBtnImg').src = pic;
+    });
+}
+
Index: trunk/webapps/js/cctvLayer.js
===================================================================
--- trunk/webapps/js/cctvLayer.js	(revision 326)
+++ trunk/webapps/js/cctvLayer.js	(revision 326)
@@ -0,0 +1,75 @@
+function initCCTVbutton()
+{
+    var cctvBtnDiv = document.getElementById('cctvButton');
+    map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push(cctvBtnDiv)
+    cctvBtnDiv.title = 'Click to toggle cctv view';
+    // Setup the click event listeners to toggle icon display
+    cctvBtnDiv.addEventListener('click', function()
+    {
+        cctv_showing = !cctv_showing;
+        // reveal or hide all the icons
+        cctvLayer.forEach(function(feature)
+        {
+            cctvLayer.overrideStyle(feature,
+            {
+                visible: cctv_showing
+            });
+        });
+        // Determine which button image to show
+        if (cctv_showing)
+        {
+            pic = "images/btnDepressed_CCTV.png"
+        }
+        else
+        {
+            pic = "images/btnReady_CCTV.png"
+        }
+        document.getElementById('cctvBtnImg').src = pic;
+    });
+}
+function makecctvIcon(nearVDS)
+{
+    var imgIcon = cctvIcon
+    if ((typeof map.data.getFeatureById(nearVDS)) == "undefined")
+    {
+        imgIcon = cctvIconWhite;
+    }
+    return imgIcon
+}
+function loadCCTVlayer()
+{
+    var imgTag = '<IMG WIDTH="700" SRC="images/';
+    cctv_infowindow = new google.maps.InfoWindow();
+    cctvLayer = new google.maps.Data();
+    cctvLayer.loadGeoJson(kCCTVfile);
+    cctvLayer.setStyle(function(feature)
+    {
+        // return the StyleOptions
+        return {
+            icon: makecctvIcon(feature.getProperty("nearVDS")),
+            title: feature.getId() + " " +feature.getProperty('locationName'),
+            visible: false  
+        };
+    });
+    cctvLayer.addListener('click', function(event)
+    {
+        //console.log("cctv layer was clicked at " + event.feature.getId());
+        cctvIndex = event.feature.getId();
+        //console.log(this.title + " is looking for " + this.nearVDS);
+        cctvLocation = event.feature.getProperty("locationName");
+        nearVDS = map.data.getFeatureById(event.feature.getProperty("nearVDS"));
+        currentColor = nearVDS.getProperty("color");
+        var imgDir = "CCTVFast/";
+        if (currentColor == "red" || currentColor == "yellow")
+        {
+            imgDir = "CCTVSlow/"
+        }
+
+        cctv_infowindow.setContent('<div style="font-weight:bold;font-family: monospace">' +  cctvIndex + "&nbsp;" + cctvLocation + "&nbsp;" +currentColor + "<BR>" + imgTag + imgDir + cctvIndex + '.jpg">' + "</div>");
+        cctv_infowindow.setPosition(event.feature.getGeometry().get());
+        cctv_infowindow.open(map);
+
+    });
+    cctvLayer.setMap(map);
+}
+
Index: trunk/webapps/js/cmsLayer.js
===================================================================
--- trunk/webapps/js/cmsLayer.js	(revision 326)
+++ trunk/webapps/js/cmsLayer.js	(revision 326)
@@ -0,0 +1,187 @@
+function initCMSbutton()
+{
+    var cmsBtnDiv = document.getElementById('cmsButton');
+    map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push(cmsBtnDiv)
+    cmsBtnDiv.title = 'Click to toggle cms view';
+    // Setup the click event listeners to toggle icon display
+    cmsBtnDiv.addEventListener('click', function()
+    {
+        cms_showing = !cms_showing;
+        // Determine which button image to show
+        if (cms_showing)
+        {
+            pic = "images/btnDepressed_CMS.png"
+            // It's nice when icons become visible that the messages have been refreshed.
+            loadAllMessages();
+        }
+        else
+        {
+            pic = "images/btnReady_CMS.png"
+        }
+        document.getElementById('cmsBtnImg').src = pic;
+        // reveal or hide all the icons
+        cmsLayer.forEach(function(feature)
+        {
+            cmsLayer.overrideStyle(feature,
+            {
+                visible: cms_showing
+            });
+        });
+    });
+}
+function loadCMSlayer()
+{
+    cmsLayer = new google.maps.Data();
+    cmsLayer.setMap(map);
+    cmsLayer.loadGeoJson(kCMSfile);  
+    cmsLayer.setStyle(function(feature)
+    {
+        // return the StyleOptions
+        return {
+            icon: yellowFlag,
+            title: feature.getId()+ " " +feature.getProperty("location")+ " " 
+                    + feature.getProperty("street"),
+            visible: false
+        };
+    });
+    
+    cmsLayer.addListener('click', function(event)
+    {
+        var dialog = document.getElementById('dialog');
+        // Note: If the dialog is already being displayed when someone else
+        // updates the message, it won't be reflected in the dialog, until
+        // you close and reopen it.
+        dialog.style.display = 'block';
+        cmsID = event.feature.getId();
+        // Assign to the hidden field
+        document.getElementById('cmsID').value = cmsID;
+        showMessage(cmsID); // note: this is async
+        document.getElementById('cms-info-label').innerHTML = "CMS ID: " +
+            cmsID + "&nbsp;&nbsp;&nbsp;LOCATION: " + event.feature.getProperty("location");
+        // clear input fields
+        document.getElementById('msgcontent1').value = "";
+        document.getElementById('msgcontent2').value = "";
+        document.getElementById('msgcontent3').value = "";
+        document.getElementById('msgcontent1').focus();
+        var span = document.getElementsByClassName("close")[0]
+            // When the user clicks on <span> (x), close the modal
+        span.onclick = function()
+        {
+            handleDialogClose();
+        }
+    });
+}
+
+    // Center justify message text in a 16 column field
+    function justifyCMStext(message)
+    {
+        var kBlanks = "                ";
+        var padLen = (16 - message.length) / 2;
+        var padding = kBlanks.substring(0, padLen);
+        return padding + message;
+    }
+
+    function handleCMSsubmit()
+    {
+        // recover the user's response
+        var response1 = justifyCMStext(document.getElementById('msgcontent1').value.trim());
+        var response2 = justifyCMStext(document.getElementById('msgcontent2').value.trim());
+        var response3 = justifyCMStext(document.getElementById('msgcontent3').value.trim());
+        var newMsg = response1 + response2 + response3;
+        if (newMsg.length == 0)
+        {
+            alert("Nothing to Send ... Proposed is empty.");
+        }
+        else
+        {
+            document.getElementById('msgdisplay1').value = response1;
+            document.getElementById('msgdisplay2').value = response2;
+            document.getElementById('msgdisplay3').value = response3;
+            saveMessage(response1 + "|" + response2 + "|" + response3);
+        }
+    }
+
+    function handleDialogClose()
+    {
+        // hide the display
+        document.getElementById('dialog').style.display = 'none'
+    }
+
+    function handleCMSclear()
+    {
+        document.getElementById('msgdisplay1').value = "";
+        document.getElementById('msgdisplay2').value = "";
+        document.getElementById('msgdisplay3').value = "";
+        saveMessage("||");
+    }
+    // retrieve the current cms message file
+    function showMessage(cmsID)
+    {
+        loadAllMessages();  // because someone else may have made a recent update
+        // lookup the message for this cms ID
+        var message = messageDict[cmsID].cms.message;
+        // show the message in the display
+        document.getElementById('msgdisplay1').value = message.phase1.Line1;
+        document.getElementById('msgdisplay2').value = message.phase1.Line2;
+        document.getElementById('msgdisplay3').value = message.phase1.Line3;
+    }
+    function loadAllMessages()
+    {
+        loadJSON("cms_messages.json", function(response)
+        {
+            // Parse JSON string into object
+            messagejson = JSON.parse(response);
+            // Add each message to a lookup dictionary 
+            for (var i = 0; i < messagejson.data.length; i++)
+            {
+                var item = messagejson.data[i];
+                messageDict[item.cms.index] = item;
+                // Set the appropriate icon on the cms icon
+                // set a yellow flag if there's currently no message
+                if (item.cms.message.phase1.Line1 + 
+                    item.cms.message.phase1.Line2 +
+                    item.cms.message.phase1.Line3 == "")
+                {
+                    cmsLayer.overrideStyle(cmsLayer.getFeatureById(item.cms.index), {icon: yellowFlag})
+                }
+                else
+                {
+                    cmsLayer.overrideStyle(cmsLayer.getFeatureById(item.cms.index), {icon: blueFlag})
+                }
+            }
+        });
+    }
+
+    // Save an updated cms message to the file
+    function saveMessage(outMessage)
+    {
+        // Fetch cmsID from hidden field where it was put when dialog opened.
+        var cmsID = document.getElementById('cmsID').value;
+        //console.log("Saving " + outMessage + " for cmsID " + cmsID)
+        msgParts = outMessage.split("|");
+        messageDict[cmsID].cms.message.phase1.Line1 = msgParts[0];
+        messageDict[cmsID].cms.message.phase1.Line2 = msgParts[1];
+        messageDict[cmsID].cms.message.phase1.Line3 = msgParts[2];
+        // Set icon to reflect message state
+        if (outMessage == "||")
+        {
+            currentIcon = {icon: yellowFlag};
+        }
+        else
+        {
+            currentIcon = {icon: blueFlag};
+        }
+        cmsLayer.overrideStyle(cmsLayer.getFeatureById(cmsID), currentIcon)
+        outString = "{\n\t\"data\":\n\t\t" + JSON.stringify(Object.values(messageDict)) + "}";
+
+        var xhttp = new XMLHttpRequest();
+        xhttp.open("GET", "cgi-bin/saveMessage.py?msg=" + outString, true);
+        xhttp.send();
+        // Using POST might be a better idea ... haven't tried this yet
+        //      var xhr = new XMLHttpRequest();
+        //      xhr.open("POST", "/cgi-bin/saveMessage.py?", true);
+        //      xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
+        // send the collected data as JSON
+        //      xhr.send(JSON.stringify(messageList));
+    }
+
Index: trunk/webapps/js/controls.js
===================================================================
--- trunk/webapps/js/controls.js	(revision 326)
+++ trunk/webapps/js/controls.js	(revision 326)
@@ -0,0 +1,100 @@
+    // Initialize the center button (to re-center the map)
+    function initCenter()
+    {
+        var centerBtnDiv = document.getElementById('ctrButton');
+        map.controls[google.maps.ControlPosition.RIGHT_CENTER].push(centerBtnDiv)
+        centerBtnDiv.title = 'Click to recenter the map';
+
+        // Setup the click event listeners: reset center location and zoom factor
+        centerBtnDiv.addEventListener('click', function()
+        {
+            map.setCenter(centerPoint);
+            map.setZoom(initZoom);
+            clearPlacePins();
+        });
+    }
+
+    // Initialize the search box and listener 
+    function initSearch()
+    {
+        // Create the search box and link it to the UI element.
+        var input = document.getElementById('search-input');
+        var searchBox = new google.maps.places.SearchBox(input);
+        map.controls[google.maps.ControlPosition.TOP_LEFT].push(input);
+
+        // Bias the SearchBox results towards current map's viewport.
+        map.addListener('bounds_changed', function()
+        {
+            searchBox.setBounds(map.getBounds());
+        });
+
+        // Listen for the event fired when the user selects a prediction and retrieve
+        // more details for that place.
+        searchBox.addListener('places_changed', function()
+        {
+            var places = searchBox.getPlaces();
+
+            if (places.length == 0)
+            {
+                return;
+            }
+
+            clearPlacePins();
+
+            // Create a bounding region to include the search result places
+            var bounds = new google.maps.LatLngBounds();
+            // For each place, get the icon, name and location.
+            // There may be multiple search results
+            places.forEach(function(place)
+            {
+                if (!place.geometry)
+                {
+                    console.log("Returned place contains no geometry");
+                    return;
+                }
+
+                // Create a marker for each place.
+                placeMarker = new google.maps.Marker(
+                {
+                    map: map,
+                    title: place.name,
+                    position: place.geometry.location
+                })
+
+                // Click on the marker to remove it from the display
+                placeMarker.addListener('click', function()
+                {
+                    placeMarker.setMap(null);
+                });
+
+                // Add this marker to the collection of current markers 
+                placePins.push(placeMarker);
+
+                // Create a bounding region to include this place
+                if (place.geometry.viewport)
+                {
+                    // Only geocodes have viewport.
+                    bounds.union(place.geometry.viewport);
+                }
+                else
+                {
+                    bounds.extend(place.geometry.location);
+                }
+            });
+            // This will pan and zoom to the area around the marker
+            //map.fitBounds(bounds);
+            // This will center the map on the new marker(s) but not zoom
+            map.setCenter(bounds.getCenter());
+        });
+    }
+
+    // Remove any place pins from a previous search
+    function clearPlacePins()
+    {
+        placePins.forEach(function(marker)
+        {
+            marker.setMap(null);
+        });
+        placePins = [];
+    }
+
