597 lines
20 KiB
JavaScript
597 lines
20 KiB
JavaScript
import { event, select, selectAll} from 'd3-selection';
|
|
import { values, set, map } from 'd3-collection';
|
|
import { drag } from 'd3-drag';
|
|
import { zoom, zoomIdentity, zoomTransform } from 'd3-zoom';
|
|
import { forceSimulation, forceLink, forceManyBody, forceCenter, forceX, forceY, forceRadial } from 'd3-force';
|
|
import { Wiki, Page } from './wiki.js';
|
|
import EventEmitter from 'eventemitter3';
|
|
import { json } from 'd3-fetch';
|
|
import { mobilecheck } from './mobilecheck.js';
|
|
|
|
// import { ForceNet } from './forcenet.js';
|
|
|
|
export class Map {
|
|
|
|
constructor (opts) {
|
|
var width = 600,
|
|
height = 600;
|
|
this.width = width;
|
|
this.height = height;
|
|
this.zoom_level = opts.zoom || 1.5;
|
|
this.apiurl = opts.apiurl;
|
|
this.categorylabel = opts.categorylabel || "Category";
|
|
this.symbols_src = opts.symbols;
|
|
this.categorydiv = select(opts.categorydiv);
|
|
|
|
this.wiki = new Wiki(this.apiurl);
|
|
|
|
this.events = new EventEmitter();
|
|
this.active_page = null;
|
|
// this.symbols = symbols;
|
|
// this.net = new ForceNet({});
|
|
// this.net.on("nodeclick", this.nodeclick.bind(this));
|
|
this.svg = null;
|
|
this.init_svg(opts.svg);
|
|
this.historylinks = {};
|
|
this.links = null;
|
|
this.highlight_category = null;
|
|
this.show_history = true;
|
|
this.loaded = false;
|
|
this.active_url = null;
|
|
this.active_page = null;
|
|
|
|
this.simulation = forceSimulation()
|
|
//.velocityDecay(0.1)
|
|
.force("link", forceLink().id(d => d.title))
|
|
.force("charge", forceManyBody())
|
|
.force("radial", forceRadial(180, width/2, height/2));
|
|
// .force("center", forceCenter(width / 2, height / 2));
|
|
|
|
this.all_links_by_key = {};
|
|
}
|
|
|
|
async init () {
|
|
this.symbols = await json(this.symbols_src);
|
|
await this.wiki.init();
|
|
|
|
// load categories & set their page symbols
|
|
for (let i=0, l=this.symbols.length; i<l; i++) {
|
|
let sym = this.symbols[i];
|
|
if (sym.key === "default") {
|
|
let page = this.wiki.get_page_by_title("Special:AllPages");
|
|
sym.page = page;
|
|
} else {
|
|
let cat = this.wiki.get_page_by_title(this.categorylabel+":"+sym.key);
|
|
sym.page = cat;
|
|
cat.pages = await cat.get_categorymembers();
|
|
// console.log("got cat pages", cat);
|
|
for (let j=0, jlen=cat.pages.length; j<jlen; j++) {
|
|
let cp = cat.pages[j];
|
|
cp.symbol = sym.symbol;
|
|
}
|
|
}
|
|
}
|
|
|
|
// create the categories
|
|
let cat = this.categorydiv
|
|
.selectAll("div.cat")
|
|
.data(this.symbols)
|
|
.enter()
|
|
.append("div")
|
|
.attr("class", "cat icon");
|
|
cat.append("span")
|
|
.attr("class", "icon")
|
|
.style("background-image", d => "url("+d.icon+")");
|
|
cat.append("a")
|
|
.attr("class", "label")
|
|
.html(d => d.key === "default" ? "Page" : d.key)
|
|
.attr("href", "#").on("click", d => {
|
|
event.preventDefault();
|
|
this.category_click(d);
|
|
})
|
|
cat.append("span").attr("class", "count").html(d => d.tcount)
|
|
|
|
var set_active_url_from_window = () => {
|
|
var title = this.wiki.unescapeTitle(window.location.hash.substring(1)),
|
|
page = this.wiki.get_page_by_title(title),
|
|
url = page.url();
|
|
console.log("wikimap init", page.title, url);
|
|
this.set_active_url(url);
|
|
};
|
|
|
|
this.loaded = true;
|
|
if (window.location.hash) {
|
|
set_active_url_from_window();
|
|
} else if (this.active_url) {
|
|
this.set_active_url(this.active_url);
|
|
}
|
|
|
|
window.addEventListener("hashchange", e => {
|
|
console.log("hashchange");
|
|
set_active_url_from_window()
|
|
});
|
|
/*
|
|
window.addEventListener("popstate", e => {
|
|
// console.log("popstate", e);
|
|
console.log("popstate: " + document.location + ", state: " + JSON.stringify(e.state));
|
|
})
|
|
*/
|
|
}
|
|
|
|
on (message, callback, context) {
|
|
this.events.on(message, callback, context);
|
|
}
|
|
|
|
async json (src) {
|
|
// expose d3's json method
|
|
return await json(src);
|
|
}
|
|
|
|
init_svg (svg) {
|
|
this.svg = select(svg || "svg");
|
|
this.zoom = zoom()
|
|
.scaleExtent([1 / 16, 16])
|
|
.on("zoom", () => {
|
|
this.content.attr("transform", event.transform);
|
|
// console.log("transform", event.transform, this.content.attr("transform"));
|
|
})
|
|
.filter(function () {
|
|
// console.log("filter", event);
|
|
if (event.touches && event.touches.length == 1) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
this.rect = this.svg.append("rect")
|
|
.attr("width", 1000)
|
|
.attr("height", 1000)
|
|
.style("fill", "none");
|
|
if (!mobilecheck()) {
|
|
this.rect.style("pointer-events", "all")
|
|
.call(this.zoom);
|
|
}
|
|
|
|
this.content = this.svg.append("g")
|
|
.attr("id", "content"),
|
|
this.linksg = this.content.append("g")
|
|
.attr("class", "links");
|
|
this.nodesg = this.content.append("g")
|
|
.attr("class", "nodes");
|
|
|
|
// DEBUGGING
|
|
this.svg.on("click", x => {
|
|
// console.log("svg click", event.target);
|
|
if (event.target == this.svg.node()) {
|
|
console.log("(debug) BACKGROUND CLICK", this.active_page.x, this.active_page.y);
|
|
this.centerOnItem(this.active_page);
|
|
}
|
|
})
|
|
}
|
|
|
|
dragstarted (d) {
|
|
if (!event.active) this.simulation.alphaTarget(0.3).restart();
|
|
// this.simulation.restart();
|
|
d.fx = d.x;
|
|
d.fy = d.y;
|
|
}
|
|
|
|
dragged (d) {
|
|
d.fx = event.x;
|
|
d.fy = event.y;
|
|
}
|
|
|
|
dragended(d) {
|
|
if (!event.active) this.simulation.alphaTarget(0);
|
|
d.fx = null;
|
|
d.fy = null;
|
|
}
|
|
|
|
/* Links */
|
|
|
|
link_key (p1, p2) {
|
|
return (p1.title < p2.title) ?
|
|
("link_"+p1.title+"_"+p2.title) :
|
|
("link_"+p2.title+"_"+p1.title);
|
|
}
|
|
|
|
make_link (p1, p2) {
|
|
return (p1.title < p2.title) ?
|
|
{source: p1, target: p2 } :
|
|
{source: p2, target: p1 };
|
|
}
|
|
|
|
ensure_link (from_page, to_page) {
|
|
var lkey = this.link_key(from_page, to_page),
|
|
ret = this.all_links_by_key[lkey];
|
|
if (ret === undefined) {
|
|
ret = this.make_link(from_page, to_page);
|
|
this.all_links_by_key[lkey] = ret;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
// function called on iframe load
|
|
async set_active_url (url) {
|
|
console.log("wikimap.set_active_url", url);
|
|
this.active_url = url;
|
|
if (this.loaded) {
|
|
var page = this.wiki.get_page(this.active_url);
|
|
if (page) {
|
|
// console.log("calling replaceState", page.title);
|
|
window.history.replaceState (null, null, "#"+this.wiki.escapeTitle(page.title));
|
|
// window.location.hash = this.wiki.escapeTitle(page.title);
|
|
this.set_active_page(page);
|
|
} else {
|
|
console.log("wikimap: set_active_url: NO PAGE FOR", url);
|
|
}
|
|
}
|
|
}
|
|
|
|
linked_nodes_set_active (page, active) {
|
|
// deactivate linked links/nodes
|
|
for (var key in this.all_links_by_key) {
|
|
if (this.all_links_by_key.hasOwnProperty(key)) {
|
|
var link = this.all_links_by_key[key];
|
|
if (link.source == page || link.target == page) {
|
|
link.active2 = active;
|
|
link.source.active2 = active;
|
|
link.target.active2 = active;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
clear_highlight_category () {
|
|
if (this.highlight_category) {
|
|
// cleanup old pages
|
|
this.highlight_category.highlight = false;
|
|
this.highlight_category.pages.forEach(d => d.highlight = false);
|
|
}
|
|
if (this.active_page == this.highlight_category) {
|
|
this.active_page = null;
|
|
}
|
|
this.highlight_category = null;
|
|
this.categorydiv
|
|
.selectAll("div.cat")
|
|
.classed("highlight", d=> d.page ? d.page.highlight : false);
|
|
var data = {nodes: this.get_nodes(), links: values(this.all_links_by_key)};
|
|
this.update_node_counts()
|
|
this.update_graph(data);
|
|
}
|
|
|
|
get_nodes () {
|
|
var nodes = this.wiki.get_nodes();
|
|
nodes = nodes.filter(d => d.linked || d.active || d.active2 || d.highlight);
|
|
return nodes;
|
|
}
|
|
|
|
async set_active_page (page) {
|
|
console.log("wikimap: set_active_page:", page);
|
|
if (page === this.active_page) {
|
|
// console.log("page is already the active page", page, this.active_page);
|
|
return;
|
|
}
|
|
// window.location.hash = this.wiki.escapeTitle(page.title);
|
|
// window.history.pushState ({pagetitle: page.title}, page.title, "/m/"+this.wiki.escapeTitle(page.title));
|
|
// cleanup old
|
|
if (this.active_page) {
|
|
this.active_page.fx = undefined;
|
|
this.active_page.fy = undefined;
|
|
this.active_page.active = false;
|
|
// deactivate linked links/nodes
|
|
this.linked_nodes_set_active(this.active_page, false);
|
|
// ENSURE (HISTORY) LINK TO PREVIOUS NODE AND CURRENT
|
|
//var link = this.ensure_link(this.active_page, page);
|
|
// link.visited = true;
|
|
}
|
|
|
|
if (page.ns == 14) {
|
|
this.active_page = page;
|
|
if (!page.pages) {
|
|
page.pages = await page.get_categorymembers();
|
|
}
|
|
if (this.highlight_category) {
|
|
// cleanup old pages
|
|
this.highlight_category.highlight = false;
|
|
this.highlight_category.pages.forEach(d => d.highlight = false);
|
|
}
|
|
this.highlight_category = page;
|
|
this.highlight_category.highlight = true;
|
|
this.highlight_category.pages.forEach(d => d.highlight = true);
|
|
// this.update_nodes();
|
|
|
|
this.categorydiv
|
|
.selectAll("div.cat")
|
|
.classed("highlight", d=> d.page ? d.page.highlight : false);
|
|
// repetition of below... (could be improved)
|
|
// this.events.emit("page", this.active_page);
|
|
// var data = {nodes: this.wiki.get_nodes(), links: values(this.all_links_by_key)};
|
|
// this.update_node_counts()
|
|
// this.update_graph(data);
|
|
|
|
// return;
|
|
} else if (page.ns !== 0) {
|
|
console.log("SPECIAL PAGE", page);
|
|
|
|
} else {
|
|
// LOAD/ENSURE PAGE LINKS
|
|
var links_out = await page.get_links();
|
|
links_out = links_out.filter(p => (!p.redirect && p.ns == 0));
|
|
// console.log("links_out", links_out);
|
|
var links_in = await page.get_linkshere();
|
|
links_in = links_in.filter(p => (!p.redirect && p.ns == 0));
|
|
// console.log("links_in", links_in);
|
|
links_out.forEach(p => {
|
|
this.ensure_link(page, p).wiki = true;
|
|
});
|
|
links_in.forEach(p => {
|
|
this.ensure_link(p, page).wiki = true;
|
|
});
|
|
|
|
}
|
|
|
|
this.active_page = page;
|
|
// console.log("fixing active page", this.active_page);
|
|
if (this.active_page.x === undefined) {
|
|
this.active_page.x = this.width/2;
|
|
this.active_page.y = this.height/2;
|
|
}
|
|
this.active_page.fx = this.active_page.x;
|
|
this.active_page.fy = this.active_page.y;
|
|
this.active_page.active = true;
|
|
this.linked_nodes_set_active(this.active_page, true);
|
|
|
|
//setTimeout(() => {
|
|
this.centerOnItem(page, 1000);
|
|
//}, 1000);
|
|
|
|
this.events.emit("page", this.active_page);
|
|
var data = {nodes: this.get_nodes(), links: values(this.all_links_by_key)};
|
|
this.update_node_counts()
|
|
this.update_graph(data);
|
|
// this.update_nodes();
|
|
// this.update_forces();
|
|
}
|
|
|
|
update_node_counts () {
|
|
var nodes = this.wiki.get_nodes();
|
|
for (let i=0, len=nodes.length; i<len; i++) {
|
|
nodes[i].count = 0;
|
|
}
|
|
for (var key in this.all_links_by_key) {
|
|
if (this.all_links_by_key.hasOwnProperty(key)) {
|
|
var link = this.all_links_by_key[key];
|
|
link.source.count += 1;
|
|
link.target.count += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
category_click (d) {
|
|
console.log("category click", d);
|
|
if (d.page) {
|
|
if (this.highlight_category == d.page) {
|
|
this.clear_highlight_category();
|
|
} else {
|
|
this.set_active_page(d.page);
|
|
}
|
|
}
|
|
}
|
|
|
|
set_show_history (value) {
|
|
console.log("wikimap.show_history", value);
|
|
if (this.show_history !== value) {
|
|
this.show_history = value;
|
|
if (this.show_history) {
|
|
let graph = {};
|
|
graph.nodes = this.nodes;
|
|
graph.links = this.links.slice();
|
|
for (var key in this.historylinks) {
|
|
graph.links.push(this.historylinks[key])
|
|
}
|
|
this.update_graph(graph);
|
|
} else {
|
|
let graph = {};
|
|
graph.nodes = this.nodes;
|
|
graph.links = this.links;
|
|
this.update_graph(graph);
|
|
}
|
|
}
|
|
}
|
|
|
|
update_graph (graph) {
|
|
// console.log("UPDATE GRAPH", graph.nodes.length, graph.links.length);
|
|
var link = this.linksg.selectAll("line")
|
|
.data(graph.links, d => { return this.link_key(d.source.title, d.target.title) });
|
|
var link_enter = link.enter()
|
|
.append("line");
|
|
|
|
link.exit().each(d => {
|
|
d.source.linked = false;
|
|
d.target.linked = false;
|
|
}).remove();
|
|
|
|
link_enter.merge(link).each(d => {
|
|
d.source.linked = true;
|
|
d.target.linked = true;
|
|
});
|
|
|
|
var node = this.nodesg
|
|
.selectAll("g.page")
|
|
.data(graph.nodes, function (d) { return d.title });
|
|
|
|
node.exit().remove();
|
|
|
|
var that = this;
|
|
var node_enter = node.enter().append("g")
|
|
.attr("class", "page")
|
|
// .attr("class", d=>"page "+this.wiki.get_ns_classname(d.ns))
|
|
.on("click", d => {
|
|
// that.events.emit("nodeclick", d, this);
|
|
this.set_active_page(d);
|
|
// this.set_active_node(d.title);
|
|
})
|
|
.on("mouseover", function (d) {
|
|
// console.log("mouseover", this);
|
|
// d.mouse = true;
|
|
select(this).classed("mouse", true);
|
|
// that.update_nodes();
|
|
})
|
|
.on("mouseout", function (d) {
|
|
// console.log("mouseout", this);
|
|
// d.mouse = false;
|
|
select(this).classed("mouse", false);
|
|
// that.update_nodes();
|
|
})
|
|
.call(drag()
|
|
.on("start", this.dragstarted.bind(this))
|
|
.on("drag", this.dragged.bind(this))
|
|
.on("end", this.dragended.bind(this)));
|
|
|
|
|
|
node_enter.append("use")
|
|
.attr("xlink:href", d => d.symbol || "symbols.svg#Main")
|
|
.attr("class", "testcolor");
|
|
|
|
// {
|
|
// for (var i=0, l=d.cats.length; i<l; i++) {
|
|
// if (this.symbols[d.cats[i]]) {
|
|
// return this.symbols[d.cats[i]];
|
|
// }
|
|
// }
|
|
// return this.symbols.default || "default";
|
|
// });
|
|
|
|
// node_enter.append("circle")
|
|
// .attr("r", 6);
|
|
|
|
node_enter.append("text")
|
|
.text(d => d.title)
|
|
.attr("x", 10);
|
|
|
|
//node_enter.append("title")
|
|
// .text(function(d) { return d.title; });
|
|
|
|
node = node_enter.merge(node);
|
|
link = link_enter.merge(link);
|
|
|
|
node.classed("active", d=>d.active);
|
|
|
|
|
|
this.simulation
|
|
.nodes(graph.nodes)
|
|
.on("tick", ticked);
|
|
|
|
this.simulation.force("link")
|
|
.links(graph.links);
|
|
|
|
this.simulation.force("radial").radius(d => (d.linked || d.highlight) ? null : 200);
|
|
|
|
function ticked() {
|
|
link
|
|
.attr("x1", function(d) { return d.source.x; })
|
|
.attr("y1", function(d) { return d.source.y; })
|
|
.attr("x2", function(d) { return d.target.x; })
|
|
.attr("y2", function(d) { return d.target.y; });
|
|
|
|
// node
|
|
// .attr("cx", function(d) { return d.x; })
|
|
// .attr("cy", function(d) { return d.y; });
|
|
node
|
|
.attr("transform", d => `translate(${d.x},${d.y})`);
|
|
}
|
|
|
|
// document.querySelector("#page").style.background = "purple";
|
|
// return;
|
|
|
|
this.update_nodes();
|
|
this.update_forces();
|
|
// this.simulation.alphaTarget(0.3).restart();
|
|
}
|
|
|
|
update_nodes () {
|
|
var nodes = this.nodesg.selectAll("g.page");
|
|
// console.log("update_nodes", nodes.size());
|
|
nodes.classed("mouse", d=>d.mouse);
|
|
nodes.classed("active", d=>d.active);
|
|
nodes.classed("active2", d=>d.active2);
|
|
nodes.classed("highlight", d=>d.highlight);
|
|
nodes.sort((a, b) => {
|
|
// console.log("sort", a, b);
|
|
var x = a.mouse ? 10 : (a.active ? 8 : (a.active2 ? 5 : 0)),
|
|
y = b.mouse ? 10 : (b.active ? 8 : (b.active2 ? 5 : 0));
|
|
return x - y;
|
|
});
|
|
var links = this.linksg.selectAll("line");
|
|
links.classed("active2", d=>d.active2);
|
|
links.classed("history", d=>d.type == "history");
|
|
links.sort((a, b) => {
|
|
// console.log("sort", a, b);
|
|
var x = a.active2 ? 10 : (a.history ? 5 : 0),
|
|
y = b.active2 ? 10 : (b.history ? 5 : 0);
|
|
return x - y;
|
|
});
|
|
}
|
|
|
|
update_forces () {
|
|
var force = this.simulation.force("link");
|
|
// console.log("update_forces:force", force);
|
|
this.simulation.force("link").strength(d => {
|
|
if (d.source.active || d.target.active) {
|
|
return 1;
|
|
} else {
|
|
// same as d3.force's defaultStrength
|
|
return 0.5 * (1 / Math.min(d.source.count, d.target.count));
|
|
}
|
|
});
|
|
// this.simulation.alphaTarget(0.3).restart();
|
|
this.simulation.alpha(0.5).restart();
|
|
}
|
|
|
|
centerOnItem(item, duration) {
|
|
var bounds = this.svg.node().getBoundingClientRect();
|
|
var curt = zoomTransform(this.rect.node());
|
|
// console.log("centerOnItem", this.zoom_level, "item", item);
|
|
var zoom_level = this.zoom_level ? this.zoom_level : curt.k;
|
|
var ITEM_SIZE = 36;
|
|
if (item && item.x !== undefined) {
|
|
this.zoom.translateTo(duration ? this.rect.transition().duration(duration) : this.rect, item.x, item.y);
|
|
// this.zoom.translateTo(this.rect, item.x, item.y);
|
|
/*
|
|
var transform = function () {
|
|
return zoomIdentity
|
|
.translate(bounds.width / 2, bounds.height / 2)
|
|
.scale(zoom_level)
|
|
.translate(-item.x, -item.y);
|
|
};
|
|
if (duration) {
|
|
this.rect.transition().duration(duration).call(this.zoom.transform, transform);
|
|
} else {
|
|
this.rect.call(this.zoom.transform, transform);
|
|
}
|
|
*/
|
|
} else {
|
|
console.log("NO ITEM");
|
|
var transform = function () {
|
|
return zoomIdentity
|
|
.scale(1);
|
|
};
|
|
this.rect.call(this.zoom.transform, transform);
|
|
}
|
|
}
|
|
|
|
do_zoom (f, transition) {
|
|
this.rect.call(this.zoom.scaleBy, f);
|
|
}
|
|
|
|
zoom_in () {
|
|
this.do_zoom(1.25);
|
|
}
|
|
|
|
zoom_out () {
|
|
this.do_zoom(1/1.25);
|
|
}
|
|
}
|
|
|