Create a gist now

Instantly share code, notes, and snippets.

@capesean /README.md
Last active Jan 18, 2016

What would you like to do?
TopoJSON Property Editor

TopoJSON Property Editor

  1. Supply the tool with some topoJSON data (via url or direct input), and it will draw the map.
  2. Click one of the shapes and you can view and edit the properties of the topoJSON object.
  3. Click the Output tab and you can copy the full, modified topoJSON.

View on http://bl.ocks.org/

Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
<!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\">&times;</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">&times;</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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment