Skip to content

Instantly share code, notes, and snippets.

@natevw
Created November 29, 2011 04:00
Show Gist options
  • Save natevw/1403349 to your computer and use it in GitHub Desktop.
Save natevw/1403349 to your computer and use it in GitHub Desktop.
Hacked up version of <https://github.com/natevw/Metakaolin> polygon editor progress for <http://bl.ocks.org/1403349>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Metakaolin — Edit "{{ title }}"</title>
<script src="https://raw.github.com/mbostock/d3/817358a7b270c5fa80a317910eba20ff06555d4e/d3.min.js"></script>
<script src="https://raw.github.com/andyet/fermata/master/fermata.js"></script>
<script src="https://raw.github.com/simplegeo/polymaps/master/polymaps.min.js"></script>
<style>
#map { position: absolute; top: 1em; bottom: 1em; left: 0; right: 0; padding: 0; margin: 0; }
</style>
</head>
<body>
<div id="map"></div>
<script>
var po_editor = function () {
var editor = po.layer(load);
editor.tile(false);
editor.cache.size(0);
editor.geometry = function (geom) {
if (!arguments.length) {
return dumpGeometry();
}
resetNodes();
loadGeometry(geom);
editor.reload();
return editor;
}
var nodeObjects, nodeTicket;
function resetNodes() {
nodeObjects = {};
nodeTicket = 0;
}
function createNode(pt) {
nodeTicket += 1;
var n = {id:nodeTicket, position:pt, connectedTo:{}};
nodeObjects[n.id] = n;
return n;
}
function destroyNode(n) {
Object.keys(n.connectedTo).forEach(function (id) {
disconnectNodes(n, nodeObjects[id]);
});
delete nodeObjects[n.id];
}
function connectNodes(a, b) {
var c = {};
a.connectedTo[b.id] = c;
b.connectedTo[a.id] = c;
return c;
}
function disconnectNodes(a, b) {
delete a.connectedTo[b.id];
delete b.connectedTo[a.id];
}
// loadGeometry/dumpGeometry convert between GeoJSON and internal "linked nodes" representation
function loadGeometry(geom) {
var LOAD = {
"Point": function (pt) {
createNode(pt);
},
"LineString": function (a) {
var prevNode;
a.forEach(function (pt) {
var n = createNode(pt);
if (prevNode) {
connectNodes(prevNode, n);
}
prevNode = n;
});
},
"Polygon": function (a) {
a.forEach(function (ring) {
var firstNode, prevNode;
ring.slice(0,-1).forEach(function (pt) {
var n = createNode(pt);
if (prevNode) {
connectNodes(prevNode, n);
} else {
firstNode = n;
}
prevNode = n;
});
connectNodes(firstNode, prevNode);
});
}
};
if (LOAD[geom.type]) {
LOAD[geom.type](geom.coordinates);
} else if (geom.type.indexOf("Multi") === 0) {
geom.coordinates.forEach(LOAD[geom.type.slice("Multi".length)]);
} else if (geom.type == "GeometryCollection") {
geom.geometries.forEach(loadGeometry);
} else {
throw Error("Unknown geometry type: " + geom.type);
}
}
function dumpGeometry() {
// TODO: convert nodeObjects graph into simplest equivalent GeoJSON object (anything from Point -> GeometryCollection)
}
// three core interactions: "extend" a point (/refine a segment), "break" a connection, "merge" two points
// to add a new point, tap an existing one and pull its extension onto the map
// to unlink segments, tap a point or segment and
// to delete a point, drop it onto another — this combines their connections as well and can be used to link segments together
function _stopEvent(e) { e.preventDefault(); e.stopPropagation(); }
function load(tile, tileProj) {
tile.element = po.svg('g');
var connectionsLayer = po.svg('g'),
nodesLayer = po.svg('g'),
chromeLayer = po.svg('g');
tile.element.appendChild(connectionsLayer);
tile.element.appendChild(nodesLayer);
tile.element.appendChild(chromeLayer);
function setupNodeUI(n, precaptured) {
if (!n.ui) {
n.ui = {};
n.ui.el = po.svg('circle');
n.ui.el.setAttribute('r', 5);
n.ui.el.setAttribute('stroke-width', 2);
n.ui.el._graph_node = n;
}
if (n.ui.el.parentNode !== nodesLayer) {
n.ui.el.removeEventListener('mousedown', n.ui.mousedownListener, false);
n.ui.el.addEventListener('mousedown', n.ui.mousedownListener = function (e) {
_stopEvent(e);
if (n.ui.createNew) {
var MAX_CONNECTIONS = 2;
var coord = _getLocation(e);
var nn = createNode([coord.lon, coord.lat]),
nc = (Object.keys(n.connectedTo).length < MAX_CONNECTIONS) ? connectNodes(n, nn) : null;
if (nc) setupConnectionUI(nc, nn), setupConnectionUI(nc, n);
setupNodeUI(nn, 'precaptured');
nn.ui.targetNode = n;
} else setupCapture();
}, false);
nodesLayer.appendChild(n.ui.el);
}
function setupCapture() {
n.ui.el.setAttribute('fill', "yellow");
capture('mouse', {
move: function (e) {
_stopEvent(e);
var coord = _getLocation(e);
n.position[0] = coord.lon, n.position[1] = coord.lat;
_setPosition(n.ui.el, 'c_', n.position);
Object.keys(n.connectedTo).forEach(function (cid) {
setupConnectionUI(n.connectedTo[cid], n);
});
// cheatly hit testing, HT http://stackoverflow.com/questions/2174640/hit-testing-svg-shapes
n.ui.el.style.setProperty('display', "none");
var targetNode, targetConnection, targetEl = document.elementFromPoint(e.pageX, e.pageY);
if (targetEl.parentNode === nodesLayer) {
targetNode = targetEl._graph_node;
} else if (targetEl.parentNode === connectionsLayer) {
targetConnection = targetEl._graph_connecion;
}
n.ui.el.style.removeProperty('display');
var MAX_CONNECTIONS = 2; // prevent full-fledged node networks from springing up
if (targetNode) {
var combinedConnections = {};
Object.keys(targetNode.connectedTo).forEach(function (cid) { combinedConnections[cid] = true; });
Object.keys(n.connectedTo).forEach(function (cid) { combinedConnections[cid] = true; });
delete combinedConnections[n.id], delete combinedConnections[targetNode.id];
if (Object.keys(combinedConnections).length > MAX_CONNECTIONS) {
targetNode = void 0;
}
}
if (n.ui.targetNode && targetNode !== n.ui.targetNode) {
n.ui.targetNode.ui.el.removeAttribute('fill');
}
if (targetNode) {
n.ui.targetNode = targetNode;
n.ui.targetNode.ui.el.setAttribute('fill', "yellow");
} else {
delete n.ui.targetNode;
}
},
up: function (e) {
_stopEvent(e);
n.ui.el.removeAttribute('fill');
uncapture('mouse');
if (n.ui.targetNode) { // remove after merging unique connections to target node
var tn = n.ui.targetNode;
tn.ui.el.removeAttribute('fill');
Object.keys(n.connectedTo).forEach(function (cid) {
var c = n.connectedTo[cid],
cn = nodeObjects[cid];
removeConnectionUI(c);
disconnectNodes(cn, n);
if (cn !== tn && !tn.connectedTo[cid]) {
var nc = connectNodes(cn, tn);
setupConnectionUI(nc, tn), setupConnectionUI(nc, cn);
}
});
removeNodeUI(n);
destroyNode(n);
} else {
n.ui.createNew = true;
n.ui.el.setAttribute('stroke', "green");
setTimeout(function () {
delete n.ui.createNew;
n.ui.el.removeAttribute('stroke');
}, 300);
}
}
});
}
if (precaptured) setupCapture();
_setPosition(n.ui.el, 'c_', n.position);
}
function removeNodeUI(n) {
nodesLayer.removeChild(n.ui.el);
delete n.ui;
}
function setupConnectionUI(c, n) {
if (!c.ui) {
c.ui = {};
c.ui.el = po.svg('line');
c.ui.el.setAttribute('stroke', "grey");
c.ui.el.setAttribute('stroke-width', 5);
c.ui.el._graph_connecion = c;
c.ui.newVertex = po.svg('circle');
c.ui.newVertex.setAttribute('r', 5);
c.ui.newVertex.setAttribute('fill', "none");
c.ui.newVertex.setAttribute('stroke', "green");
c.ui.newVertex.setAttribute('stroke-width', 2);
}
if (c.ui.el.parentNode !== connectionsLayer) {
c.ui.el.removeEventListener('mouseover', c.ui.mouseoverListener, false);
c.ui.el.addEventListener('mouseover', c.ui.mouseoverListener = function (e) {
_stopEvent(e);
chromeLayer.appendChild(c.ui.newVertex);
}, false);
c.ui.el.removeEventListener('mousemove', c.ui.mousemoveListener, false);
c.ui.el.addEventListener('mousemove', c.ui.mousemoveListener = function (e) {
_stopEvent(e);
var coord = _getLocation(e),
pos = [coord.lon, coord.lat];
_setPosition(c.ui.newVertex, 'c_', pos);
if (_dist(c.ui.n1.position, pos) > 10 &&
_dist(c.ui.n2.position, pos) > 10) c.ui.newVertex.setAttribute('stroke', "green");
else c.ui.newVertex.setAttribute('stroke', "red");
}, false);
c.ui.el.removeEventListener('mouseout', c.ui.mouseoutListener, false);
c.ui.el.addEventListener('mouseout', c.ui.mouseoutListener = function (e) {
_stopEvent(e);
chromeLayer.removeChild(c.ui.newVertex);
}, false);
c.ui.el.removeEventListener('mousedown', c.ui.mousedownListener, false);
c.ui.el.addEventListener('mousedown', c.ui.mousedownListener = function (e) {
_stopEvent(e);
var coord = _getLocation(e);
disconnectNodes(c.ui.n1, c.ui.n2);
var nn = createNode([coord.lon, coord.lat]),
c1 = (_dist(c.ui.n1.position, nn.position) > 10) ? connectNodes(c.ui.n1, nn) : null,
c2 = (_dist(c.ui.n2.position, nn.position) > 10) ? connectNodes(c.ui.n2, nn) : null;
if (c1) setupConnectionUI(c1, nn), setupConnectionUI(c1, c.ui.n1);
if (c2) setupConnectionUI(c2, nn), setupConnectionUI(c2, c.ui.n2);
setupNodeUI(nn, 'precaptured');
removeConnectionUI(c);
}, false);
connectionsLayer.appendChild(c.ui.el);
}
var nN = (c.ui.n1 && c.ui.n1 !== n) ? 2 : 1;
c.ui['n'+nN] = n; // freshen storage of which node owns which end
_setPosition(c.ui.el, '_'+nN, n.position);
}
function removeConnectionUI(c) {
if (c.ui.newVertex.parentNode) {
c.ui.el.removeEventListener('mouseout', c.ui.mouseoutListener, false);
chromeLayer.removeChild(c.ui.newVertex);
}
connectionsLayer.removeChild(c.ui.el);
delete c.ui;
}
var proj = tileProj(tile).locationPoint;
function _setPosition(el, attr, pos) {
var pt = proj({lat:pos[1], lon:pos[0]});
el.setAttribute(attr.replace('_','x'), pt.x);
el.setAttribute(attr.replace('_','y'), pt.y);
return pt;
}
function _getLocation(e) {
var mouse = editor.map().mouse(e);
return editor.map().pointLocation(mouse);
}
function _dist(pos1, pos2) {
var pt1 = proj({lat:pos1[1], lon:pos1[0]}),
pt2 = proj({lat:pos2[1], lon:pos2[0]}),
dx = pt2.x - pt1.x, dy = pt2.y - pt1.y;
return Math.sqrt(dx*dx + dy*dy);
}
Object.keys(nodeObjects).forEach(function (id) {
var node = nodeObjects[id];
setupNodeUI(node);
Object.keys(node.connectedTo).forEach(function (cid) {
var connection = node.connectedTo[cid];
setupConnectionUI(connection, node);
});
});
}
// on mouse/touch down, an object can "capture" that input and these window move/up listeners will dispatch to its handlers
var inputOwners = {};
function capture(source, events) {
inputOwners[source] = events;
}
function uncapture(source) {
delete inputOwners[source];
}
window.addEventListener('mousemove', function (e) {
var owner = inputOwners['mouse'];
if (owner && owner.move) {
return owner.move(e);
};
}, false);
window.addEventListener('mouseup', function (e) {
var owner = inputOwners['mouse'];
if (owner && owner.up) {
return owner.up(e);
};
}, false);
return editor;
};
</script>
<script>
var po = org.polymaps,
map = po.map().container(d3.select('#map').append("svg:svg").node());
map.add(po.interact());
map.add(po.image()
.url(po.url("http://{S}tile.cloudmade.com"
+ "/51664f123b50414da85e963f92c721f0"
+ "/998/256/{Z}/{X}/{Y}.png")
.hosts(["a.", "b.", "c.", ""])));
if (navigator.geolocation) navigator.geolocation.getCurrentPosition(function (p) {
map.center({lat:p.coords.latitude, lon:p.coords.longitude}).zoom(14);
addEditablePoint();
}, addEditablePoint); else addEditablePoint();
function addEditablePoint () {
var shape = {type:"Polygon", coordinates:[[]]},
size = 0.00125,
center = map.center();
shape.coordinates[0].push([center.lon - size, center.lat + size]);
shape.coordinates[0].push([center.lon + size, center.lat + size]);
shape.coordinates[0].push([center.lon + size, center.lat - size]);
shape.coordinates[0].push([center.lon - size, center.lat - size]);
shape.coordinates[0].push([center.lon - size, center.lat + size]);
if (1) {
shape.type = (1) ? "LineString" : "MultiPoint";
shape.coordinates = shape.coordinates[0];
shape.coordinates.pop();
} else if (1) {
shape = {type:"GeometryCollection", geometries:[shape]};
}
var editor = po_editor();
editor.geometry(shape);
map.add(editor);
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment