|
<!DOCTYPE html> |
|
<html xmlns="http://www.w3.org/1999/xhtml"> |
|
<head> |
|
|
|
<title>Editing TopoJSON Properties</title> |
|
|
|
<!-- jquery --> |
|
<script src="//code.jquery.com/jquery-2.0.3.min.js" type="text/javascript"></script> |
|
<script src="//code.jquery.com/ui/1.10.3/jquery-ui.js" type="text/javascript"></script> |
|
|
|
<!-- bootstrap --> |
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css"> |
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap-theme.min.css"> |
|
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script> |
|
|
|
<!-- google maps --> |
|
<script src="https://maps.googleapis.com/maps/api/js?sensor=false"></script> |
|
|
|
<!-- custom resources --> |
|
<script src="//cdnjs.cloudflare.com/ajax/libs/topojson/1.1.0/topojson.min.js"></script> |
|
|
|
<script type="text/javascript"> |
|
var map, data, currentFeature, topojsonFeaturesObject; |
|
|
|
// this is the correct way of zooming... |
|
function zoom(map) { |
|
var bounds = new google.maps.LatLngBounds(); |
|
map.data.forEach(function (feature) { |
|
processPoints(feature.getGeometry(), bounds.extend, bounds); |
|
}); |
|
map.fitBounds(bounds); |
|
} |
|
// this gets the extends into thisArg using the callback function (bounds.extend) for each item in the geometry |
|
function processPoints(geometry, callback, thisArg) { |
|
if (geometry instanceof google.maps.LatLng) { |
|
callback.call(thisArg, geometry); |
|
} else if (geometry instanceof google.maps.Data.Point) { |
|
callback.call(thisArg, geometry.get()); |
|
} else { |
|
geometry.getArray().forEach(function (g) { |
|
processPoints(g, callback, thisArg); |
|
}); |
|
} |
|
} |
|
|
|
function clone(obj) { |
|
var copy; |
|
|
|
// Handle the 3 simple types, and null or undefined |
|
if (null == obj || "object" != typeof obj) return obj; |
|
|
|
// Handle Date |
|
if (obj instanceof Date) { |
|
copy = new Date(); |
|
copy.setTime(obj.getTime()); |
|
return copy; |
|
} |
|
|
|
// Handle Array |
|
if (obj instanceof Array) { |
|
copy = []; |
|
for (var i = 0, len = obj.length; i < len; i++) { |
|
copy[i] = clone(obj[i]); |
|
} |
|
return copy; |
|
} |
|
|
|
// Handle Object |
|
if (obj instanceof Object) { |
|
copy = {}; |
|
for (var attr in obj) { |
|
if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]); |
|
} |
|
return copy; |
|
} |
|
|
|
throw new Error("Unable to copy obj! Its type isn't supported."); |
|
} |
|
|
|
function addMessage(message, id) { |
|
id = id || "#messages"; |
|
$(id).append("<div class='alert alert-danger'><a href=\"#\" class=\"close\" data-dismiss=\"alert\">×</a>" + message + "</div>"); |
|
} |
|
|
|
function updateOutput() { |
|
var dataOutput = clone(data); |
|
dataOutput.objects[topojsonFeaturesObject].geometries.forEach(function (d, i) { |
|
delete d.properties.__id; |
|
}); |
|
|
|
$("#output").val(JSON.stringify(dataOutput)); |
|
} |
|
|
|
function showData(jsonData) { |
|
data = jsonData; |
|
|
|
// get the features object |
|
topojsonFeaturesObject = null; |
|
for (var key in data.objects) { |
|
if (data.objects[key].type != null) |
|
topojsonFeaturesObject = key; |
|
} |
|
if (!topojsonFeaturesObject) { |
|
addMessage("No data.objects Features Object"); |
|
return; |
|
} |
|
|
|
// add an __id property for linking topojson to map |
|
data.objects[topojsonFeaturesObject].geometries.forEach(function (d, i) { |
|
// create a new properties object if required |
|
d.properties = d.properties || {}; |
|
d.properties.__id = i.toString(); |
|
}); |
|
|
|
// create map |
|
$('#tabs a:first').tab('show'); |
|
map = new google.maps.Map(document.getElementById("map"), { zoom: 6, mapTypeId: google.maps.MapTypeId.ROADMAP }); |
|
|
|
// get geojson from topojson |
|
var geoJson = topojson.feature(data, data.objects[topojsonFeaturesObject]); |
|
|
|
// add to the map |
|
map.data.addGeoJson(geoJson); |
|
|
|
// set the default style |
|
map.data.setStyle({ |
|
strokeColor: "#000000", |
|
strokeWeight: 1, |
|
strokeOpacity: 0.85, |
|
fillOpacity: 0.1, |
|
fillColor: '#000000' |
|
}); |
|
|
|
map.data.addListener('click', function (event) { |
|
// store the clicked feature/shape |
|
currentFeature = event.feature; |
|
$("#propertyValue").val(""); |
|
// show the modal |
|
$("#modalMessages").empty(); |
|
|
|
var geometry = $.grep(data.objects[topojsonFeaturesObject].geometries, function (g) { |
|
return g.properties.__id == currentFeature.getProperty("__id"); |
|
})[0]; |
|
var copy = clone(geometry); |
|
delete copy.properties.__id; |
|
|
|
$("#properties").html(JSON.stringify(copy.properties, null, 2)); |
|
$("#propertyModal").modal('show'); |
|
}); |
|
|
|
zoom(map); |
|
|
|
$("#results").removeClass("hide"); |
|
updateOutput(); |
|
} |
|
|
|
$(function () { |
|
|
|
$('#propertyModal').on('shown.bs.modal', function () { |
|
$('#propertyValue').focus(); |
|
}); |
|
|
|
$("#propertyValue").on('paste', function () { |
|
setTimeout(function () { |
|
$("#btnSetProperty").trigger("click"); |
|
}, 100); |
|
}); |
|
|
|
$("#btnSetProperty").click(function () { |
|
// get id (link between topojson & map) |
|
var __id = currentFeature.getProperty('__id'), |
|
propertyName = $("#propertyName").val(), |
|
propertyValue = $("#propertyValue").val(); |
|
|
|
// check there is a propertyName |
|
if (propertyName == "") { |
|
addMessage("Property Name is missing", "#modalMessages"); |
|
return; |
|
} |
|
|
|
// set property... |
|
data.objects[topojsonFeaturesObject].geometries.forEach(function (d, i) { |
|
if (d.properties.__id === __id) |
|
d.properties[propertyName] = propertyValue; |
|
}); |
|
|
|
// update the output |
|
updateOutput(); |
|
|
|
// modify the style of the shape |
|
map.data.overrideStyle(currentFeature, { |
|
fillOpacity: .8, |
|
fillColor: 'black' |
|
}); |
|
|
|
// hide modal |
|
$("#propertyModal").modal('hide'); |
|
}); |
|
|
|
$("#btnLoad").click(function () { |
|
// clear messages |
|
$("#messages").empty(); |
|
// check if input or Url |
|
if ($("#topojsonSourceUrl").prop("checked")) { |
|
// load url |
|
$.getJSON($("#topojsonUrl").val(), function (jsonData) { |
|
showData(jsonData); |
|
}); |
|
} else { |
|
// set input |
|
showData(JSON.parse($("#topojsonText").val())); |
|
} |
|
}); |
|
|
|
// for running in github gist / bl.ocks.org |
|
if (window.self.frameElement) window.self.frameElement.style.height = "900px"; |
|
}); |
|
</script> |
|
|
|
<style type="text/css"> |
|
.form-horizontal .control-label { |
|
text-align: left; |
|
} |
|
|
|
#output { |
|
font-family: monospace, Courier; |
|
font-size: 9pt; |
|
} |
|
|
|
body { |
|
padding: 20px; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
|
|
<h1>TopoJSON Property Editor</h1> |
|
|
|
<fieldset class="form-horizontal"> |
|
|
|
<div id="messages"></div> |
|
|
|
<div class="form-group"> |
|
<label for="topojsonSource" class="col-sm-2 control-label">TopoJSON Source:</label> |
|
<div class="col-sm-10"> |
|
<label class="radio-inline"> |
|
<input type="radio" id="topojsonSourceUrl" name="topojsonSource" value="Url" onclick="$('.sourceRow').toggleClass('hide');" checked="checked" /> |
|
Url |
|
</label> |
|
<label class="radio-inline"> |
|
<input type="radio" id="topojsonSourceInput" name="topojsonSource" value="Input" onclick="$('.sourceRow').toggleClass('hide');" /> |
|
Input |
|
</label> |
|
</div> |
|
</div> |
|
|
|
<div class="form-group sourceRow"> |
|
<label for="topojsonUrl" class="col-sm-2 control-label">TopoJSON Url:</label> |
|
<div class="col-sm-5"> |
|
<input type="text" id="topojsonUrl" class="form-control" placeholder="http://www.site.com/file.json" value="districts.topo.json" /> |
|
</div> |
|
</div> |
|
|
|
<div class="form-group hide sourceRow"> |
|
<label for="topojsonText" class="col-sm-2 control-label text-left">TopoJSON Url:</label> |
|
<div class="col-sm-5"> |
|
<textarea id="topojsonText" rows="6" class="form-control" placeholder="{'type' 'Topology' ,'transform'{ ..."></textarea> |
|
</div> |
|
</div> |
|
|
|
<p> |
|
<span class="btn btn-primary" id="btnLoad">Load</span> |
|
</p> |
|
|
|
</fieldset> |
|
|
|
<div id="results" class="row hide"> |
|
|
|
<!-- Nav tabs --> |
|
<ul id="tabs" class="nav nav-tabs" role="tablist"> |
|
<li class="active"><a href="#tabMap" role="tab" data-toggle="tab">Map</a></li> |
|
<li><a href="#tabOutput" role="tab" data-toggle="tab">TopoJSON Output</a></li> |
|
</ul> |
|
|
|
<!-- Tab panes --> |
|
<div class="tab-content"> |
|
|
|
<br /> |
|
|
|
<div class="tab-pane active" id="tabMap"> |
|
|
|
<div id="map" style="width:100%;height:500px;"></div> |
|
|
|
</div> |
|
|
|
<div class="tab-pane" id="tabOutput"> |
|
|
|
<textarea id="output" rows="22" class="form-control" placeholder="output..."></textarea> |
|
|
|
</div> |
|
|
|
</div> |
|
</div> |
|
|
|
<div class="modal" id="propertyModal" tabindex="-1"> |
|
<div class="modal-dialog"> |
|
<div class="modal-content panel-default"> |
|
|
|
<div class="modal-header panel-heading"> |
|
<a href="#" class="close" data-dismiss="modal">×</a> |
|
Set Property |
|
</div> |
|
|
|
<div class="modal-body"> |
|
|
|
<div id="modalMessages"></div> |
|
|
|
<fieldset class="form-horizontal"> |
|
<div class="form-group"> |
|
<label for="properties" class="col-sm-6 control-label">Current Properties:</label> |
|
<div class="col-sm-6"> |
|
<pre id="properties" style="margin-bottom:0;"></pre> |
|
</div> |
|
</div> |
|
<div class="form-group"> |
|
<label for="propertyName" class="col-sm-6 control-label">Property Name:</label> |
|
<div class="col-sm-6"> |
|
<input type="text" id="propertyName" class="form-control" value="id" /> |
|
</div> |
|
</div> |
|
<div class="form-group"> |
|
<label for="propertyValue" class="col-sm-6 control-label">Property Value:</label> |
|
<div class="col-sm-6"> |
|
<input type="text" id="propertyValue" class="form-control" /> |
|
</div> |
|
</div> |
|
</fieldset> |
|
|
|
</div> |
|
|
|
<div class="modal-footer panel-footer"> |
|
<button id="btnSetProperty" class="btn btn-primary">Set Property</button> |
|
<button class="btn btn-default" data-dismiss="modal">Cancel</button> |
|
</div> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
</body> |
|
</html> |