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