Although I find myself using mainly R and arcGIS for analysis; I still find Google Maps a very useful platform. The below shows how I put together a small website to allow me to access several key features in one convenient location.

My HTML header contains:

<title>Maps MultiTool</title>
<script src="https://maps.googleapis.com/maps/api/js?&libraries=geometry"></script>
<script src="jquery.min.js"></script>
<script src="markerwithlabel.js"></script>
<script src="sha.js"></script>
<script src="TravelTimeKeyV1.js"></script>
<script src="MapTools.js"></script>

sha.js” is a useful cryptographic library which I use throughout the script
“TravelTimeKeyV1.js” is a file on the network which contains my Google Maps for Work API credentials
“MapTools.js” contains all of my code and will follow below.

Step 0-A: Set-up the map and define helper functions

I decided to initialise my Map around London with a small modification to the original style:

map1

var map; 	
var geocoder = new google.maps.Geocoder();
var bounds = new google.maps.LatLngBounds();
var MARKERS = [];
var CIRCLES = [];
var POLYLINES = [];
var ICON = '';
var LABELNO = 0;

    //Initialise
	function load() {
		// Initialize around City, London
		var my_lat = 51.518175;
		var my_lng = -0.129064;
		var styleArray = [{featureType:"all",stylers:[{saturation:-30},{lightness:30}]}]
		var mapOptions = {
				styles: styleArray,
				center: new google.maps.LatLng(my_lat, my_lng),
				zoom: 12
		};
		map = new google.maps.Map(document.getElementById('map'),
			mapOptions);
	}

Step 0-B: Some helper-functions

	//Test for number
	function isNumeric(n) {
		return !isNaN(parseFloat(n)) && isFinite(n);
	}	
	//Get time
	function getDateTime() {
		var now     = new Date(); 
		var year    = now.getFullYear();
		var month   = now.getMonth()+1; 
		var day     = now.getDate();
		var hour    = now.getHours();
		var minute  = now.getMinutes();
		var second  = now.getSeconds(); 
		if(month.toString().length == 1) {
			var month = '0'+month;
		}
		if(day.toString().length == 1) {
			var day = '0'+day;
		}   
		if(hour.toString().length == 1) {
			var hour = '0'+hour;
		}
		if(minute.toString().length == 1) {
			var minute = '0'+minute;
		}
		if(second.toString().length == 1) {
			var second = '0'+second;
		}   
		var dateTime = year+'/'+month+'/'+day+' '+hour+':'+minute+':'+second;   
		return dateTime;
	}
	//Search Map for Address
	//This uses the geocoding service not the API since we only have one request
	function search_address(address) {
	    geocoder.geocode({
	        'address': address
	    }, function(results, status) {
	        if (status == google.maps.GeocoderStatus.OK) {
	            var loc = results[0].geometry.location
	                //Pan to location
	            map.panTo(loc)
	                //Zoom to location
	            //map.setZoom(16)
	        } else {
	            alert("Not found: " + status)
	        }
	    })	
	}
	//Search Map for Marker
	function search_marker(marker_name) {
		for (var i = 0; i < MARKERS.length; i++) {
			if (MARKERS[i].labelContent == marker_name) {
				map.panTo(MARKERS[i].getPosition())
				break;
			}
		}			
	}

Step 0-C: Authentication

To get a boost in performance I chose not to use the standard directions and geocoding functions from the Maps Javascript API but to use the actual Geocoding and Directions APIs. Below is the authentication process:

	//Sign Google's Cryptographic Key
	function google_key_signer(url, key) {
		console.log(url)
		console.log(key)
		cut_url = url.replace('http://maps.googleapis.com','');
		key = key.replace('-', '+');
		key = key.replace('_', '/'); 
		
		var hmacObj = new jsSHA(cut_url, 'TEXT');
		var hmacOutput = hmacObj.getHMAC(key,'B64','SHA-1','B64');
		
		hmacOutput = hmacOutput.split('+').join('-');
		hmacOutput = hmacOutput.split('/').join('_');
		
		return url + '&signature=' + hmacOutput;
	}
	//Encode URL for proxy
	function proxyUrlJsonp(url) {
			var encodedUrl = encodeURIComponent(url);
			var proxyUrl = encodedUrl;
		return proxyUrl;
	}

Step 0-D: Cleaning-Up

Some house-keeping but the functions below allow the user to clear the output and start again when interacting with the map:

	//Clear directions
	function clear_directions() {
		// Clear textboxes
		from_address.value = ""
		to_address.value = ""
		travel_result.value = ""
		// Drop polylines
		for (var i = 0; i < POLYLINES.length; i++) {
			POLYLINES[i].setMap(null);
		}
	}
	//Clear markers and textboxes
	function clear_markers() {
			// Clear textboxes
			markerInput.value = "";
			markerOutput.value = "";
			// Drop markers
			for (var i = 0; i < MARKERS.length; i++){
				MARKERS[i].setMap(null);
			}
			// Clear Array
			MARKERS = [];
			// Reset label button
			document.getElementById("toggle_label").value="Hide Labels";
			// Reset map
			map.setZoom(12);
			map.panTo(new google.maps.LatLng(51.518175, -0.129064));
			// Reset label counter
			LABELNO = 0;
			// Reset map bounds
			bounds = new google.maps.LatLngBounds();
	}	
	//Clear circles
	function clear_circles() {
		for (var i = 0; i < CIRCLES.length; i++) {
			CIRCLES[i].setMap(null);
		}
		// Clear Array
		CIRCLES = [];
	}
	//Clear overlaps
	function clear_overlaps() {
		overlap_output.value = ""
	}
	//Clear clusters
	function clear_clusters() {
		cluster_output.value = ""
	}	
	// Clear network clusters
	function clear_network () {
			network_output.value = '';
			network_input.value = '';
	}
	//Toggle labels
	function toggle_labels() {
		if (document.getElementById("toggle_label").value=="Hide Labels") {
			for (var i = 0; i < MARKERS.length; i++) {
				MARKERS[i].set("labelVisible", false) 	
			}
			document.getElementById("toggle_label").value="Show Labels";
		} else {
			for (var i = 0; i < MARKERS.length; i++) {
				MARKERS[i].set("labelVisible", true) 	
			}
			document.getElementById("toggle_label").value="Hide Labels";	
		}		
	}

Step 1: Plotting (and geocoding markers)

mapgep

The below may appear slightly long-winded, however I wanted to be able to enter the input for the markers in four different ways and for the function to recognise how to deal with each one, e.g.:

“A, W5 2RR”
“W5 2RR”
“A, 51.51806, -0.30446”
“51.51806, -0.30446”

Hence, if the function believes a pair of coordinates was entered then it goes straight ahead to plotting them on the map; otherwise it geocodes them using the Geocoding API rather than geocoding function:

geocoder.geocode({‘address’: address}, function(results, status) {if (status == google.maps.GeocoderStatus.OK) {…

v.s:

var signed_url = google_key_signer(encodeURI(‘http://maps.googleapis.com/maps/api/geocode/json?&#8217; + ‘address=’ + input_line_column[1] + ‘&client=’ + CR_NAME),CR_KEY ); $.getJSON(‘http://jsonp.afeld.me/?callback=?&url=&#8217; + proxyUrlJsonp(signed_url),function(data){…

I used the “jsonp.afeld.me” proxy to load my Cross-Domain AJAX request. I’m not very knowledgeable on this stuff so if there is a better way then would be glad to hear it! I used the markerwithlabel library for all of my markers (toggling the labels on/off with a button).

	//Plot and geocode Markers
	function plot_markers(marker_input,colour,custom_icon) {
		markerOutput.value += getDateTime() + "\n"
		//Icon colour
		if (custom_icon != "") {
			ICON = custom_icon.trim()
		} else {	
			ICON = 'http://labs.google.com/ridefinder/images/mm_20_' + colour + '.png';
		}
		console.log(ICON)
		var input_line = marker_input.trim().split('\n');
		var input_line_column = input_line[0].split(',')
		//3 parts with label or 2 parts without label such that 1 and 2 are numeric
		if 	(
			(input_line_column.length == 3)||
			(input_line_column.length == 2)&&
				isNumeric(input_line_column[0])&&
					isNumeric(input_line_column[1])
			) {
			for (var i = 0; i < input_line.length; i++){
				var input_line_column = input_line[i].split(',')
				if (input_line_column.length==2){
					input_line_column = ['label'+LABELNO,input_line_column[0],input_line_column[1]]
				}
				var marker_label = input_line_column[0]
					console.log('Plotting')
					// Label, Latitude, Longitude
					// Straight plot
					var point_i = new google.maps.LatLng(
						parseFloat(input_line_column[1]),
						parseFloat(input_line_column[2]));
						
					draw_markers(point_i,marker_label)
					
					markerOutput.value += "OK:(" + input_line[i] + ")\n"
			}
			markerOutput.value += getDateTime() + "\n"
			map.fitBounds(bounds)
			//Zoom
			if (map.getZoom()==21) {map.setZoom=12}
			//Increment labelno
			LABELNO += 1
		// 2 parts with label or 1 part without label 
		} else if 	(
					(input_line_column.length == 2)||
					(input_line_column.length == 1)
					) {
			console.log('Geocoding')
			var i = 0;
			var l = input_line.length;
			// Geocode using ajax.getJSON (faster)
			function goGeocode() {
				var input_line_column = input_line[i].split(',')
				if (input_line_column.length==1){
					input_line_column = ['label'+LABELNO,input_line_column]
				}
				var signed_url = google_key_signer(encodeURI('http://maps.googleapis.com/maps/api/geocode/json?' + 
					'address=' + input_line_column[1] + '&client=' + CR_NAME),CR_KEY );
				console.log(signed_url);
				$.getJSON('http://jsonp.afeld.me/?callback=?&url=' + proxyUrlJsonp(signed_url),			
					function(data){ 
						if (data.status=="OK") {	
							var temp = data.results[0].geometry.location
							draw_markers(temp,input_line_column[0])			
							markerOutput.value +=  temp.lat.toFixed(5) + "," + temp.lng.toFixed(5) + ",OK:(" + input_line[i] + ")," + data.results[0].geometry.location_type + "," + data.results[0].types + "\n"
						} else {
							markerOutput.value += "FAIL:(" + input_line[i] + ")\n"
						}
						i++		
						if(i < l) { window.setTimeout(goGeocode, 1); }	
						if(i ==l) {
							markerOutput.value += getDateTime() + "\n"
							// If one marker on map preserve zoom
							if ((l==1)&&(LABELNO==0)) {
								map.panTo(temp)
							} else {
							map.fitBounds(bounds)
							}
							//Increment labelno
							LABELNO += 1
						}						
					}					
				)	
			}	
			goGeocode()		
		} else {
			//Invalid format
			alert("Invalid Format"); 
		}
	}
	//Draw markers
	function draw_markers(location, label) {
		var marker = new MarkerWithLabel({
			map: map,
			position: location,
			labelContent: label,
			//labelAnchor: new google.maps.Point(50, 0),
			labelClass: "labels", // the CSS class for the label
			labelStyle: {
				opacity: 0.85
			},
			icon: ICON
		})
		// Add to array
		MARKERS.push(marker)
		// Add to bounds
		bounds.extend(marker.position)
	}

Step 2: Drawing Circles

circ

map_geocode2

//Draw circles
	//Draw circles
	function draw_circles(radius_metres, colour, cir_lab) {
		if (isNumeric(radius_metres)) {
			var num_radius = parseFloat(radius_metres)
		} else {
			alert ("Radius is not a number")
		}
		if (cir_lab == "") {
			for (var i = 0; i < MARKERS.length; i++) {
				var circle = new google.maps.Circle({
						center: MARKERS[i].getPosition(),
						radius: num_radius,
						strokeColor: colour,
						strokeOpacity: 0.15,
						strokeWeight: 2,
						fillColor: colour,
						fillOpacity: 0.15,
						map: map
				})
				// Add to array
				CIRCLES.push(circle)
			}
		} else {
			for (var i = 0; i < MARKERS.length; i++) {
						if (MARKERS[i].labelContent == cir_lab) {
							var circle = new google.maps.Circle({
									center: MARKERS[i].getPosition(),
									radius: num_radius,
									strokeColor: colour,
									strokeOpacity: 0.15,
									strokeWeight: 2,
									fillColor: colour,
									fillOpacity: 0.15,
									map: map
							})
							// Add to array
							CIRCLES.push(circle)
						}
				}
		}
	}

Step 3: Distances (driving and straight-line)

map_geocode3

map_geocode3b

Just like with the geocoding above, for the distance calculations I use the Directions API instead of the JavaScript function: DirectionsService().route(). The result (whether the straight-line distance or the driving distance) is then displayed on the map using poly-lines. Out of interest – for the straight-line distance – I’m curious which model is used? Since it is called ‘spherical’ I guess it is less accurate than the Vincenty approximation which assumes an ellipsoid?

From Wikipedia:

Vincenty’s formulae are two related iterative methods used in geodesy to calculate the distance between two points on the surface of a spheroid, developed by Thaddeus Vincenty (1975a) They are based on the assumption that the figure of the Earth is an oblate spheroid, and hence are more accurate than methods such as great-circle distance which assume a spherical Earth.

	//Straight line distance
	function fly_distance(from, to) {
	travel_result.value += getDateTime() + "\n"
		var input_from = from.trim().split('\n');
		var input_to = to.trim().split('\n');
		if (input_from.length != input_to.length) {
			alert('Length of origin does not match length of destination')
		} else {
				var input_from_column = input_from[0].split(',')
				if (input_from_column.length == 2) {
					//Calculate lat, lng distances
					for (var i = 0; i < input_from.length ; i++) {
						var lat_from = parseFloat(input_from[i].split(',')[0])
						var lng_from = parseFloat(input_from[i].split(',')[1])
						var lat_to = parseFloat(input_to[i].split(',')[0])
						var lng_to = parseFloat(input_to[i].split(',')[1])
						var distance_between = google.maps.geometry.spherical.computeDistanceBetween(
							new google.maps.LatLng(lat_from,lng_from),
							new google.maps.LatLng(lat_to,lng_to)
						)	
						travel_result.value += (distance_between / 1000 / 1.609344).toFixed(2) + "Mi,OK:(" + input_from[i] + " to " + input_to[i] + ")\n"
						// Draw polyline
						var flightPath = new google.maps.Polyline({
							path: [
								new google.maps.LatLng(lat_from,lng_from),
								new google.maps.LatLng(lat_to,lng_to)
								],
							geodesic: true,
							strokeWeight: 2,
							strokeColor: 'grey'
						});
						flightPath.setMap(map);	
						POLYLINES.push(flightPath)		
					}
				} else {
					alert("Crow-flies distances not currently available for non-geocoded data")
				}
		}
	}
	//Directions API Driving distance
	function drive_distance(from, to) {
		travel_result.value += getDateTime() + "\n"
		var input_from = from.trim().split('\n');
		var input_to = to.trim().split('\n');
		if (input_from.length != input_to.length) {
			alert('Length of origin does not match length of destination')
		} else {
			//All matches
			console.log('Distances')
			var i = 0;
			var l = input_from.length;
			// Distances using ajax.getJSON (faster)
			function goTravel() {
				var signed_url = google_key_signer(encodeURI('http://maps.googleapis.com/maps/api/directions/json?' + 
					'origin=' + input_from[i] + '&destination=' + input_to[i] + '&client=' + CR_NAME),CR_KEY );
				console.log(signed_url);
				$.getJSON('http://jsonp.afeld.me/?callback=?&url=' + proxyUrlJsonp(signed_url),
					function(data){ 
						if (data.status=="OK") {	
							// Results to textbox
							var temp = data.routes[0].legs[0]
							travel_result.value += (temp.duration.value / 60).toFixed(2) + "min," + (temp.distance.value / 1000 / 1.609344).toFixed(2) + "Mi,OK:(" + input_from[i] + " to " + input_to[i] + ")," + temp.start_location.lat.toFixed(5) + "," + temp.start_location.lng.toFixed(5) + "," + temp.end_location.lat.toFixed(5) + "," + temp.end_location.lng.toFixed(5) + "\n"
							console.log('Drawing polyline')
							// Draw polyline
							var temp = data.routes[0].legs[0].steps
							console.log(temp)
							for (var j = 0; j < temp.length; j++) {
								var from_step = temp[j].start_location
								var to_step = temp[j].end_location
								var flightPath = new google.maps.Polyline({
									path: [from_step,to_step],
									geodesic: true,
									strokeWeight: 2,
									strokeColor: 'black'
								});
								flightPath.setMap(map);	
								POLYLINES.push(flightPath)						
							}
						} else {
							travel_result.value += "(" + input_from[i] + " to " + input_to[i] + "):FAIL\n"
						}
						i++		
						if(i < l) { window.setTimeout(goTravel, 1); }
						if(i ==l) {	
							travel_result.value += getDateTime() + "\n"
							map.fitBounds(bounds)
						}
					}					
				)		
			}	
			goTravel()		
		}
	}

Step 4: Finding Overlaps

map_geocode2

map_geocode2b

To calculate the overlaps I implicitly calculate a full distance matrix (except the diagonal) rather than a triangular matrix; this was more lazy coding and should be changed. However, initially I thought I would do this drive-times (which are not symmetric).

As an example one can see that 3367 overlaps with 3371 and 3375 (they actually form one cluster which we will see below)

	//Calculate overlaps
	function calculate_overlaps(overlap_rad) {
		// Validation
		if (isNumeric(overlap_rad)) {
			var num_radius = parseFloat(overlap_rad)
		} else {
			alert ("Radius is not a number")
		}
		overlap_output.value += getDateTime() + "\n"
		var counter = 1
		// Triangular matrix (no diagonal)
		for (var i = 0; i < MARKERS.length; i++) {
			var names_array = []
			var name_i = MARKERS[i].labelContent
			var point_i = MARKERS[i].getPosition()
			for (var j = 0; j < MARKERS.length; j++) {
				if (j != i) {
					var name_j = MARKERS[j].labelContent
					var point_j = MARKERS[j].getPosition()	
					var distance_between = google.maps.geometry.spherical.computeDistanceBetween(
						point_i,
						point_j
						)
						// Overlap
						if (distance_between <= num_radius) { // Add overlap marker to array names_array.push(name_j) } } } // Output result if (names_array.length>0) {
				overlap_output.value += "(" + num_radius + "metre overlap no." + 
					counter + ") " + name_i + " and " + names_array.toString() + "\n"
				counter += 1
			}
		}
		overlap_output.value += getDateTime() + "\n"
	}

Step 4: Clustering

Hierarchical clustering is a useful way to identify clusters on the map, however more generally it is a way of combining data into groups. For example; with the general ‘calculate network connections’ button one can input network edges

connections

And discover the connections between the edges. Just out of interest – a pythonic approach to the above is below and it uses the networkx connected_components command:

import csv
import networkx as nx        

data_edges = []

# Read in-data
with open('H:/cluster_test.csv') as f:
    for x in csv.reader(f):
        data_edges.append(x)
        
# Add edges
G=nx.Graph()
G.add_edges_from(data_edges)

# Cluster
connections_nx = nx.connected_components(G)
output_list = []
counter = 1
for con in connections_nx:
    for item in con:
        output_list.append(("C%d_%s"%(counter,item)))
    counter+=1

The above method is generic and takes as an input the edges of a network  i.e. a connection has been established (perhaps based on one metric or several) and is taken implicitly. The below function calculates connections assuming the connection is a “distance less than x metres”. Hence the method for this is:

  1. Calculate a triangular matrix (calculate_distance_triangle)
  2. Extract just the markers which are within x metres of each other (trim_distances)
  3. Pass them to the (same) clustering function (the_clusterer)

map_geocode2

map_geocode4

We can see that the markers: 3371, 3367, 3375 form one cluster (“Cluster 1”).

	/////////////////////////////////////////////
	// Part A: Distance input for clustering
	/////////////////////////////////////////////	
	//Calculate clusters
	function calculate_clusters(cluster_rad) {
		// Validation
		if (isNumeric(cluster_rad)) {
			cluster_output.value += getDateTime() + "\n"
			calculate_distance_triangle(parseFloat(cluster_rad))
			cluster_output.value += getDateTime() + "\n"
		} else {
			alert ("Radius is not a number")
		}		
		
	}
	//Distance triangle
	function calculate_distance_triangle(cluster_rad) {
		var distance_array = []
	    var k = 0;
	        for (var i = 0; i < MARKERS.length - 1; i++) {
				var name_i = MARKERS[i].labelContent
				var point_i = MARKERS[i].getPosition();
	            for (var j = i + 1; j < MARKERS.length; j++) {
					var name_j = MARKERS[j].labelContent
					var point_j = MARKERS[j].getPosition()
	                var distance_between = google.maps.geometry.spherical.computeDistanceBetween(point_i, point_j)
	                distance_array[k] = {
	                    0: name_i,
	                    1: name_j,
	                    2: distance_between
	                }
	                k++
	            }
	        }
	    //Keep distances within threshold
	    trim_distances(distance_array,cluster_rad)
	}
	//Cut distances to threshold
	function trim_distances(distance_array,cluster_rad) {
	    var k = 0
	    var cluster_array = []
	    for (var i = 0; i < distance_array.length; i++) {
	        if (distance_array[i][2] <= cluster_rad) {
	            cluster_array[k] = {
	                0: distance_array[i][0],
	                1: distance_array[i][1]
	            }
	            k++
	        }
	    }
	    //Cluster the markers
		console.log(cluster_array)
	    the_clusterer(cluster_array, cluster_rad)
	}
	/////////////////////////////////////////////
	// Part B: Generalised input for clustering
	/////////////////////////////////////////////
	//Network analysis
	function network_cluster(input_array) {
		var line_arr = input_array.trim().split('\n');
		var input_arr = [];
		var temp = [];
		for (var i = 0; i < line_arr.length ; i++){ temp = line_arr[i].trim().split(',') if (temp[0]!=temp[1]){ input_arr.push(temp) } } if ((input_arr.length > 1)&(input_arr[0].length == 2)) {
			the_clusterer(input_arr,'other')
		} else {
			alert('Invalid format for network analysis, enter: A,B (new line) B,D (new line) Z,F (new line), etc.')
		}
	}
	/////////////////////////////////////////////
	// Part C: Agglomerative Clustering
	/////////////////////////////////////////////
	//Agglomerative clustering
	function the_clusterer(cluster_array, cluster_rad) {
	    console.log('Clustering distance array')
	    for (var i = 0; i < cluster_array.length - 1; i++) {
	        if (typeof(cluster_array[i]) != 'undefined') {
	            var size_outer = 0,
	                key_outer;
	            for (key_outer in cluster_array[i]) {
	                if (cluster_array[i].hasOwnProperty(key_outer));
	                size_outer++;
	            }
	            for (var j = 0; j < size_outer; j++) {
	                for (var k = i + 1; k < cluster_array.length; k++) {
	                    if (typeof(cluster_array[i]) != 'undefined') {
	                        var size_inner = 0,
	                            key_inner;
	                        for (key_inner in cluster_array[k]) {
	                            if (cluster_array[k].hasOwnProperty(key_inner));
	                            size_inner++;
	                        }
	                        var found_outer = 0
	                        for (var l = 0; l < size_inner; l++) { if (found_outer === 1) { break; } if (cluster_array[k][l] == cluster_array[i][j]) { found_outer++ } } if (found_outer > 0) {
	                            for (var l = 0; l < size_inner; l++) {
	                                var found_inner = 0
	                                for (var m = 0; m < size_outer; m++) {
	                                    if (cluster_array[i][m] == cluster_array[k][l]) {
	                                        found_inner++
	                                    }
	                                }
	                                if (found_inner == 0) {
	                                    cluster_array[i][size_outer] = cluster_array[k][l]
	                                    size_outer++
	                                }
	                            }
	                            delete cluster_array[k];
	                        }
	                    }
	                }
	            }
	        }
	    }
		if (cluster_rad!="other"){
			// [A]Specific distance metric
			cluster_output.value += "Agglomerative clusters for " + cluster_rad + "metres:\n"
			var k = 0
			var markers_clustered = []
			for (var i = 0; i < cluster_array.length; i++) { var size_outer = 0, key_outer; for (key_outer in cluster_array[i]) { if (cluster_array[i].hasOwnProperty(key_outer)); size_outer++; } if (size_outer > 0) {
					k++
					var temp_line = "Cluster " + k + ":"
					for (var j = 0; j < size_outer; j++) {
						temp_line = temp_line + " Marker " + cluster_array[i][j]
						markers_clustered.push(cluster_array[i][j])
					}
					console.log(temp_line)
					cluster_output.value += temp_line + ".\n"
				}
			}
			//Add in markers not in groups:
			for (var i = 0; i < MARKERS.length; i++) {
					var name_i = MARKERS[i].labelContent
					if (markers_clustered.indexOf(name_i)==-1){
						k++
						var temp_line = "Cluster " + k + ": Marker " + name_i
						cluster_output.value += temp_line + ".\n"
					}
			}
		} else {
			// [B]General metric
			var k = 0
			var markers_clustered = []
			for (var i = 0; i < cluster_array.length; i++) { var size_outer = 0, key_outer; for (key_outer in cluster_array[i]) { if (cluster_array[i].hasOwnProperty(key_outer)); size_outer++; } if (size_outer > 0) {
					k++
					var temp_line = "Cluster " + k + ":"
					for (var j = 0; j < size_outer; j++) {
						if (j==0) {
							temp_line = temp_line + cluster_array[i][j]
						} else {
							temp_line = temp_line + "," + cluster_array[i][j]
						}
						markers_clustered.push(cluster_array[i][j])
					}
					console.log(temp_line)
					network_output.value += temp_line + ".\n"
				}
			}	
			
		}
	}