Files
map/src/wikimap.js

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);
}
}