Index: /trunk/src/cptms/cptms_map_v55.html
===================================================================
--- /trunk/src/cptms/cptms_map_v55.html	(revision 286)
+++ /trunk/src/cptms/cptms_map_v55.html	(revision 286)
@@ -0,0 +1,443 @@
+<!DOCTYPE html>
+<html>
+  <head>
+  <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
+    <title>CPTMS Map v0.5.5</title> 
+    <style>
+      /* Set the size of the div element that contains the map */
+      #mapdiv {
+        height: 100%;
+        width: 100%;  
+       }
+        /* Makes the page fill the window. */
+      html, body {
+        height: 100%;
+        margin: 0;
+        padding: 0;
+      }
+      #search-input {
+        background-color: #17263c;  /* #CD853F;  /*#E6E6FA; /* lavender */
+        color: #E6E6FA;  /* #FFEFD5; */
+        font-family: Roboto;
+        font-size: 18px;
+        font-weight: 400;
+        margin-left: 12px;
+        padding: 0 11px 0 13px;
+        text-overflow: ellipsis;
+        border-color: #746855;   /* #4d90fe; */
+        width: 400px;
+      }
+
+      #search-input:focus {
+        border-color: #E6E6FA; 
+      }
+
+      #ctrButton {
+        font-size: 40px;
+        margin-right: 9px;
+        background-color: #fff;
+        color: #47476b;
+        cursor: pointer;
+      }
+
+    </style>
+  </head>
+  <body>
+    <!-- Version 5.5 renames title to CPTMS, loads static data on startup and dynamic data
+         every ten seconds.  
+         Version 5.4 adds Search box and Center button
+         Version 5.3 fixed dot color update defect, increased refresh rate to 10 sec.
+         Version 5.2 places red dots overlapping yellow dots.
+         Version 5.1 removes the map and street view buttons and the H3 tag.
+         Version 5 uses precomputed perpendicular vector in dot adjustment function
+         Version 4 Adjust the spacing between dots when the map is zoomed.
+         Version 3 does loadGeoJson only once, and subsequently does an ajax load
+         of the highways file, and selectively updates only those placePins whose
+         color has changed. 
+         @author jdalbey  2019.2.17
+    -->
+    <!-- The text area input for the Search Box -->
+    <input id="search-input" class="controls" type="text" placeholder="Search Box">
+    <!--The div element where the map appears -->
+    <div id="mapdiv"></div>
+    <!--The div element where the center button appears -->
+    <div id="ctrButton">&#x2295;</div>
+    <script>
+    // a global variable for the google map
+    var map;  
+    // a global dictionary to lookup a station's original coordinates
+    var vds_coords = {};
+    // a global variable to hold locations of marked search places
+    var placePins = [];
+    // Constant name of dynamic json data file created by CADserver
+    var kMapPointsFile = "highways.json";
+    // Constant name of initial (static) highways file used once at startup
+    var kMapStartupFile = "highways_startup.json";
+    // Constant for map center location: The John Wayne Airport
+    //var centerPoint = {lat: 33.687228, lng: -117.872148};
+    // Constant for map center location in District 12
+    var centerPoint = {lat: 33.693385, lng: -117.798937};
+    // Initial map zoom
+    var initZoom = 11;
+    // Dot colors used in traffic model to indicate free-flowing, slowed, and stopped traffic
+    // and their associated zvalues so slower traffic dots are more visible.
+    var colorZvalues = {"lime":10,"yellow":20,"red":30};
+
+    // 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,postmileID) //,direction)
+    {
+        var circle = google.maps.SymbolPath.CIRCLE;
+        var diamond = 'M -1,0 0,-1 1,0 0,1 z';
+        var myShape = circle;
+        // See if postmile name contains N or W letters
+        //if ((postmileID.indexOf('N') != -1) || (postmileID.indexOf('W') != -1))
+        //{
+        //   myShape = diamond
+        //}
+        return {
+            path: myShape,
+            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 loadMapData()
+    {
+        // Load the static map data and call saveCoords when done
+        map.data.loadGeoJson(kMapStartupFile,null,saveCoords)
+
+        // 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) 
+        {
+            //console.log(feature.getId() + " " + feature.getGeometry().get());
+            // 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,name);
+            // return the StyleOptions
+            return {
+                    icon: iconSymbol,
+                    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 
+        updateMap();
+        // 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 adjusment 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)
+    {
+        //console.log("adjusting coordinates");
+        // 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]);
+            //console.log(target+" updated to "+newColor);
+        }
+     }
+
+    // Load the dynamic highways file via ajax
+    // Ref: https://codepen.io/KryptoniteDove/post/load-json-file-locally-using-pure-javascript
+     function loadJSON(callback) {   
+
+        var xobj = new XMLHttpRequest();
+            xobj.overrideMimeType("application/json");
+        xobj.open('GET', kMapPointsFile, true); 
+        xobj.onreadystatechange = function () {
+              if (xobj.readyState == 4 && xobj.status == "200") {
+                // Required use of an anonymous callback as .open will NOT return a value but simply returns undefined in asynchronous mode
+                callback(xobj.responseText);
+              }
+        };
+        xobj.send(null);  
+     }
+
+     // Load the highways dynamic json file and update the map
+     function updateMap()
+     {
+        var parsed_JSON;
+        loadJSON(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);
+        });
+     }
+
+    // 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 = [];
+    }
+
+    // Initialize the map and load the points
+    function initMap() 
+    {
+        // Declare the map and where it belongs on the page
+        map = new google.maps.Map( document.getElementById('mapdiv'), 
+        {
+            zoom: initZoom, 
+            center: centerPoint,
+            styles: night_mode,
+            mapTypeControl: false,
+            streetViewControl: false  
+        });
+        // setup the search box and center button
+        initSearch();
+        initCenter();
+
+        loadMapData();  // go load the map data
+
+        // Start a timer to refresh the map every 10 seconds
+        var myTimer = setInterval(updateMap, 10000);
+        // Listen for zoom changes and move the placePins so as to keep a nice
+        // visual distance between them appropriate to the zoom factor
+        map.addListener('zoom_changed', function() {
+            // fetch how much the map is currently zoomed
+            currentZoom = map.getZoom(); 
+            //console.log("Zoom changed to ",currentZoom);
+            // only bother adjusting within this range
+            if ((currentZoom <16) && (currentZoom>10))
+            {
+                // magic formula controls distance between dots
+                adjustCoords(calcDistanceFactor());
+            }
+        });
+
+    }
+
+    // Styles array for Night Mode map
+    // Ref: https://developers.google.com/maps/documentation/javascript/styling
+    var night_mode = [
+                {elementType: 'geometry', stylers: [{color: '#242f3e'}]},
+                {elementType: 'labels.text.stroke', stylers: [{color: '#242f3e'}]},
+                {elementType: 'labels.text.fill', stylers: [{color: '#746855'}]},
+                {
+                  featureType: 'administrative.locality',
+                  elementType: 'labels.text.fill',
+                  stylers: [{color: '#d59563'}]
+                },
+                {
+                  featureType: 'poi',
+                  elementType: 'labels.text.fill',
+                  stylers: [{color: '#d59563'}]
+                },
+                {
+                  featureType: 'poi.park',
+                  elementType: 'geometry',
+                  stylers: [{color: '#263c3f'}]
+                },
+                {
+                  featureType: 'poi.park',
+                  elementType: 'labels.text.fill',
+                  stylers: [{color: '#6b9a76'}]
+                },
+                {
+                  featureType: 'road',
+                  elementType: 'geometry',
+                  stylers: [{color: '#38414e'}]
+                },
+                {
+                  featureType: 'road',
+                  elementType: 'geometry.stroke',
+                  stylers: [{color: '#212a37'}]
+                },
+                {
+                  featureType: 'road',
+                  elementType: 'labels.text.fill',
+                  stylers: [{color: '#9ca5b3'}]
+                },
+                {
+                  featureType: 'road.highway',
+                  elementType: 'geometry',
+                  stylers: [{color: '#746855'}]
+                },
+                {
+                  featureType: 'road.highway',
+                  elementType: 'geometry.stroke',
+                  stylers: [{color: '#1f2835'}]
+                },
+                {
+                  featureType: 'road.highway',
+                  elementType: 'labels.text.fill',
+                  stylers: [{color: '#f3d19c'}]
+                },
+                {
+                  featureType: 'transit',
+                  elementType: 'geometry',
+                  stylers: [{color: '#2f3948'}]
+                },
+                {
+                  featureType: 'transit.station',
+                  elementType: 'labels.text.fill',
+                  stylers: [{color: '#d59563'}]
+                },
+                {
+                  featureType: 'water',
+                  elementType: 'geometry',
+                  stylers: [{color: '#17263c'}]
+                },
+                {
+                  featureType: 'water',
+                  elementType: 'labels.text.fill',
+                  stylers: [{color: '#515c6d'}]
+                },
+                {
+                  featureType: 'water',
+                  elementType: 'labels.text.stroke',
+                  stylers: [{color: '#17263c'}]
+                }
+              ]
+
+    // Using John's API Key
+    </script>
+    <script async defer
+    src="https://maps.googleapis.com/maps/api/js?key=AIzaSyD6iTyN0DjP-9OVkAgicyp4tkC10naE_B8&libraries=places&callback=initMap">
+    </script>
+  </body>
+</html>
