diff --git a/dist/app.js b/dist/app.js index f1f981b..f1bcd5b 100644 --- a/dist/app.js +++ b/dist/app.js @@ -1021,6 +1021,12 @@ var app = (function (exports) { return set; } + function values(map) { + var values = []; + for (var key in map) values.push(map[key]); + return values; + } + var noop = {value: function() {}}; function dispatch() { @@ -4419,129 +4425,399 @@ var app = (function (exports) { return force; } - var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + var EOL = {}, + EOF = {}, + QUOTE = 34, + NEWLINE = 10, + RETURN = 13; + + function objectConverter(columns) { + return new Function("d", "return {" + columns.map(function(name, i) { + return JSON.stringify(name) + ": d[" + i + "]"; + }).join(",") + "}"); + } + + function customConverter(columns, f) { + var object = objectConverter(columns); + return function(row, i) { + return f(object(row), i, columns); + }; + } + + // Compute unique columns in order of discovery. + function inferColumns(rows) { + var columnSet = Object.create(null), + columns = []; + + rows.forEach(function(row) { + for (var column in row) { + if (!(column in columnSet)) { + columns.push(columnSet[column] = column); + } + } + }); + + return columns; + } + + function pad(value, width) { + var s = value + "", length = s.length; + return length < width ? new Array(width - length + 1).join(0) + s : s; + } + + function formatYear(year) { + return year < 0 ? "-" + pad(-year, 6) + : year > 9999 ? "+" + pad(year, 6) + : pad(year, 4); + } + + function formatDate(date) { + var hours = date.getUTCHours(), + minutes = date.getUTCMinutes(), + seconds = date.getUTCSeconds(), + milliseconds = date.getUTCMilliseconds(); + return isNaN(date) ? "Invalid Date" + : formatYear(date.getUTCFullYear(), 4) + "-" + pad(date.getUTCMonth() + 1, 2) + "-" + pad(date.getUTCDate(), 2) + + (milliseconds ? "T" + pad(hours, 2) + ":" + pad(minutes, 2) + ":" + pad(seconds, 2) + "." + pad(milliseconds, 3) + "Z" + : seconds ? "T" + pad(hours, 2) + ":" + pad(minutes, 2) + ":" + pad(seconds, 2) + "Z" + : minutes || hours ? "T" + pad(hours, 2) + ":" + pad(minutes, 2) + "Z" + : ""); + } + + function dsvFormat(delimiter) { + var reFormat = new RegExp("[\"" + delimiter + "\n\r]"), + DELIMITER = delimiter.charCodeAt(0); + + function parse(text, f) { + var convert, columns, rows = parseRows(text, function(row, i) { + if (convert) return convert(row, i - 1); + columns = row, convert = f ? customConverter(row, f) : objectConverter(row); + }); + rows.columns = columns || []; + return rows; + } + + function parseRows(text, f) { + var rows = [], // output rows + N = text.length, + I = 0, // current character index + n = 0, // current line number + t, // current token + eof = N <= 0, // current token followed by EOF? + eol = false; // current token followed by EOL? + + // Strip the trailing newline. + if (text.charCodeAt(N - 1) === NEWLINE) --N; + if (text.charCodeAt(N - 1) === RETURN) --N; + + function token() { + if (eof) return EOF; + if (eol) return eol = false, EOL; + + // Unescape quotes. + var i, j = I, c; + if (text.charCodeAt(j) === QUOTE) { + while (I++ < N && text.charCodeAt(I) !== QUOTE || text.charCodeAt(++I) === QUOTE); + if ((i = I) >= N) eof = true; + else if ((c = text.charCodeAt(I++)) === NEWLINE) eol = true; + else if (c === RETURN) { eol = true; if (text.charCodeAt(I) === NEWLINE) ++I; } + return text.slice(j + 1, i - 1).replace(/""/g, "\""); + } + + // Find next delimiter or newline. + while (I < N) { + if ((c = text.charCodeAt(i = I++)) === NEWLINE) eol = true; + else if (c === RETURN) { eol = true; if (text.charCodeAt(I) === NEWLINE) ++I; } + else if (c !== DELIMITER) continue; + return text.slice(j, i); + } + + // Return last token before EOF. + return eof = true, text.slice(j, N); + } + + while ((t = token()) !== EOF) { + var row = []; + while (t !== EOL && t !== EOF) row.push(t), t = token(); + if (f && (row = f(row, n++)) == null) continue; + rows.push(row); + } + + return rows; + } + + function preformatBody(rows, columns) { + return rows.map(function(row) { + return columns.map(function(column) { + return formatValue(row[column]); + }).join(delimiter); + }); + } + + function format(rows, columns) { + if (columns == null) columns = inferColumns(rows); + return [columns.map(formatValue).join(delimiter)].concat(preformatBody(rows, columns)).join("\n"); + } + + function formatBody(rows, columns) { + if (columns == null) columns = inferColumns(rows); + return preformatBody(rows, columns).join("\n"); + } + + function formatRows(rows) { + return rows.map(formatRow).join("\n"); + } + + function formatRow(row) { + return row.map(formatValue).join(delimiter); + } + + function formatValue(value) { + return value == null ? "" + : value instanceof Date ? formatDate(value) + : reFormat.test(value += "") ? "\"" + value.replace(/"/g, "\"\"") + "\"" + : value; + } + + return { + parse: parse, + parseRows: parseRows, + format: format, + formatBody: formatBody, + formatRows: formatRows + }; + } + + var csv = dsvFormat(","); + + var tsv = dsvFormat("\t"); + + function responseJson(response) { + if (!response.ok) throw new Error(response.status + " " + response.statusText); + return response.json(); + } + + function json(input, init) { + return fetch(input, init).then(responseJson); + } + + var NS = { + main: 0, + discussion: 1, + template: 10, + category: 14, + news: 3106, + web: 3116 + }; + + function strip_fragment (href) { + return href.replace(/#.?$/, ''); + } + + class Wiki { + constructor (apiurl) { + this.apiurl = apiurl; + this.pages_by_title = {}; + this.ns_names = {}; + for (var key in NS) { + var nsid = NS[key]; + this.ns_names[nsid] = key; + } + this.siteinfo = null; + } + + get_nodes () { + var ret = values(this.pages_by_title); + ret = ret.filter(p => (!p.redirect && p.ns === 0)); + return ret; + } + + get_links () { + return values(this.links); + } + + async init () { + await this.get_site_info(); + } + + async get_site_info () { + // https://en.wikipedia.org/w/api.php + var url = this.apiurl + "?action=query&meta=siteinfo&siprop=general&format=json&formatversion=2"; + var data = await json(url); + // this.siteinfo = data.query.general; + + this.server = data.query.general.server; // e.g. "http://activearchives.org" + this.articlepath = data.query.general.articlepath; // e.g. "/wiki/$1" + this.base = data.query.general.base; // e.g. "http://activearchives.org/wiki/Main_Page" + // this.sitename = data.query.general.sitename; + this.mainpage = data.query.general.mainpage; + this.wikibasepat = new RegExp(this.server+this.articlepath.replace(/\$1$/, "(.+)")); + + url = this.apiurl + "?action=query&meta=siteinfo&siprop=namespaces&format=json&formatversion=2"; + data = await json(url); + this.namespaces_by_id = data.query.namespaces; + this.namespaces_by_name = {}; + values(this.namespaces_by_id).forEach(n => { + this.namespaces_by_name[n.name] = n; + }); + // create special special entry + var special = { name: "Special", id: -17 }; + this.namespaces_by_name["Special"] = special; + this.namespaces_by_id[-17] = special; + + } + + escapeTitle (title) { + return encodeURI(title.replace(/ /g, "_")); + } + + unescapeTitle (title) { + return decodeURI(title.replace(/_/g, " ")); + } + + wiki_title_to_url (title) { + return this.server + this.articlepath.replace(/\$1$/, this.escapeTitle(title)); + } + + url_to_wiki_title (href) { + var m = this.wikibasepat.exec(strip_fragment(href)); + if (m !== null) { + return this.unescapeTitle(m[1]); + } + } + + /* sample siteinfo, see: http://activearchives.org/mw/api.php?action=query&meta=siteinfo&formatversion=2&format=json */ + + get_page (url) { + var title = this.url_to_wiki_title(url); + if (title) { + return this.get_page_by_title(title); + } + } + + get_page_by_title (title) { + var p = this.pages_by_title[title]; + if (p) { + return p; + } else { + var cpos = title.indexOf(":"), + name = title, + namespace = ""; + if (cpos >= 0) { + namespace = title.substring(0, cpos); + name = title.substring(cpos+1); + } + p = new Page(this, {title: title, name: name, ns: this.namespaces_by_name[namespace].id}); + this.pages_by_title[title] = p; + return p; + } + } + page_for_object (n, merge_data) { + if (merge_data === undefined) { merge_data = true; } + var title = n.title, + p = this.pages_by_title[title]; + if (p) { + if (merge_data) { p.merge_data(n); } + return p; + } else { + p = new Page(this, n); + this.pages_by_title[title] = p; + return p; + } + } + get_ns_classname (nsid) { + // console.log("get_ns_classname", nsid, this.ns_names[nsid]); + var ret = this.ns_names[nsid]; + console.log("classname", ret); + if (ret === undefined) { console.log("warning classname undefined for ns", nsid); } + return ret; + } + union (p1, p2) { + var union = map(p1, d=>d.title); + for (var i=0, l=p2.length; i this.wiki.page_for_object(x)); + // console.log("get_prop", pname, prefix, ret); + return ret; + } + + async get_list (lname, prefix, appendstr) { + var ret = []; + var baseurl = this.wiki.apiurl+"?action=query&list="+lname+"&format=json&formatversion=2&"+prefix+"title="+encodeURIComponent(this.title)+(appendstr || ""); + var url = baseurl; + while (true) { + var data = await json(url); + if (data.query[lname]) { + ret.push.apply(ret, data.query[lname]); + } + if (!data.continue) { break; } + url = baseurl+"&"+prefix+"continue="+data.continue[prefix+"continue"]; + } + ret = ret.map(x => this.wiki.page_for_object(x)); + // console.log("get_prop", pname, prefix, ret); + return ret; + } + + async get_links () { + return await this.get_prop("links", "pl"); + } + + async get_linkshere () { + return await this.get_prop("linkshere", "lh"); + } + + async get_categorymembers () { + return await this.get_list("categorymembers", "cm", "&cmtype=page"); + } + } function createCommonjsModule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } - var fetchJsonp = createCommonjsModule(function (module, exports) { - (function (global, factory) { - { - factory(exports, module); - } - })(commonjsGlobal, function (exports, module) { - - var defaultOptions = { - timeout: 5000, - jsonpCallback: 'callback', - jsonpCallbackFunction: null - }; - - function generateCallbackFunction() { - return 'jsonp_' + Date.now() + '_' + Math.ceil(Math.random() * 100000); - } - - function clearFunction(functionName) { - // IE8 throws an exception when you try to delete a property on window - // http://stackoverflow.com/a/1824228/751089 - try { - delete window[functionName]; - } catch (e) { - window[functionName] = undefined; - } - } - - function removeScript(scriptId) { - var script = document.getElementById(scriptId); - if (script) { - document.getElementsByTagName('head')[0].removeChild(script); - } - } - - function fetchJsonp(_url) { - var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; - - // to avoid param reassign - var url = _url; - var timeout = options.timeout || defaultOptions.timeout; - var jsonpCallback = options.jsonpCallback || defaultOptions.jsonpCallback; - - var timeoutId = undefined; - - return new Promise(function (resolve, reject) { - var callbackFunction = options.jsonpCallbackFunction || generateCallbackFunction(); - var scriptId = jsonpCallback + '_' + callbackFunction; - - window[callbackFunction] = function (response) { - resolve({ - ok: true, - // keep consistent with fetch API - json: function json() { - return Promise.resolve(response); - } - }); - - if (timeoutId) clearTimeout(timeoutId); - - removeScript(scriptId); - - clearFunction(callbackFunction); - }; - - // Check if the user set their own params, and if not add a ? to start a list of params - url += url.indexOf('?') === -1 ? '?' : '&'; - - var jsonpScript = document.createElement('script'); - jsonpScript.setAttribute('src', '' + url + jsonpCallback + '=' + callbackFunction); - if (options.charset) { - jsonpScript.setAttribute('charset', options.charset); - } - jsonpScript.id = scriptId; - document.getElementsByTagName('head')[0].appendChild(jsonpScript); - - timeoutId = setTimeout(function () { - reject(new Error('JSONP request to ' + _url + ' timed out')); - - clearFunction(callbackFunction); - removeScript(scriptId); - window[callbackFunction] = function () { - clearFunction(callbackFunction); - }; - }, timeout); - - // Caught if got 404/500 - jsonpScript.onerror = function () { - reject(new Error('JSONP request to ' + _url + ' failed')); - - clearFunction(callbackFunction); - removeScript(scriptId); - if (timeoutId) clearTimeout(timeoutId); - }; - }); - } - - // export as global function - /* - let local; - if (typeof global !== 'undefined') { - local = global; - } else if (typeof self !== 'undefined') { - local = self; - } else { - try { - local = Function('return this')(); - } catch (e) { - throw new Error('polyfill failed because global object is unavailable in this environment'); - } - } - local.fetchJsonp = fetchJsonp; - */ - - module.exports = fetchJsonp; - }); - }); - var eventemitter3 = createCommonjsModule(function (module) { var has = Object.prototype.hasOwnProperty @@ -4880,208 +5156,102 @@ var app = (function (exports) { } }); - var EOL = {}, - EOF = {}, - QUOTE = 34, - NEWLINE = 10, - RETURN = 13; + // import { ForceNet } from './forcenet.js'; - function objectConverter(columns) { - return new Function("d", "return {" + columns.map(function(name, i) { - return JSON.stringify(name) + ": d[" + i + "]"; - }).join(",") + "}"); - } + class Map$2 { - function customConverter(columns, f) { - var object = objectConverter(columns); - return function(row, i) { - return f(object(row), i, columns); - }; - } - - // Compute unique columns in order of discovery. - function inferColumns(rows) { - var columnSet = Object.create(null), - columns = []; - - rows.forEach(function(row) { - for (var column in row) { - if (!(column in columnSet)) { - columns.push(columnSet[column] = column); - } - } - }); - - return columns; - } - - function pad(value, width) { - var s = value + "", length = s.length; - return length < width ? new Array(width - length + 1).join(0) + s : s; - } - - function formatYear(year) { - return year < 0 ? "-" + pad(-year, 6) - : year > 9999 ? "+" + pad(year, 6) - : pad(year, 4); - } - - function formatDate(date) { - var hours = date.getUTCHours(), - minutes = date.getUTCMinutes(), - seconds = date.getUTCSeconds(), - milliseconds = date.getUTCMilliseconds(); - return isNaN(date) ? "Invalid Date" - : formatYear(date.getUTCFullYear(), 4) + "-" + pad(date.getUTCMonth() + 1, 2) + "-" + pad(date.getUTCDate(), 2) - + (milliseconds ? "T" + pad(hours, 2) + ":" + pad(minutes, 2) + ":" + pad(seconds, 2) + "." + pad(milliseconds, 3) + "Z" - : seconds ? "T" + pad(hours, 2) + ":" + pad(minutes, 2) + ":" + pad(seconds, 2) + "Z" - : minutes || hours ? "T" + pad(hours, 2) + ":" + pad(minutes, 2) + "Z" - : ""); - } - - function dsvFormat(delimiter) { - var reFormat = new RegExp("[\"" + delimiter + "\n\r]"), - DELIMITER = delimiter.charCodeAt(0); - - function parse(text, f) { - var convert, columns, rows = parseRows(text, function(row, i) { - if (convert) return convert(row, i - 1); - columns = row, convert = f ? customConverter(row, f) : objectConverter(row); - }); - rows.columns = columns || []; - return rows; - } - - function parseRows(text, f) { - var rows = [], // output rows - N = text.length, - I = 0, // current character index - n = 0, // current line number - t, // current token - eof = N <= 0, // current token followed by EOF? - eol = false; // current token followed by EOL? - - // Strip the trailing newline. - if (text.charCodeAt(N - 1) === NEWLINE) --N; - if (text.charCodeAt(N - 1) === RETURN) --N; - - function token() { - if (eof) return EOF; - if (eol) return eol = false, EOL; - - // Unescape quotes. - var i, j = I, c; - if (text.charCodeAt(j) === QUOTE) { - while (I++ < N && text.charCodeAt(I) !== QUOTE || text.charCodeAt(++I) === QUOTE); - if ((i = I) >= N) eof = true; - else if ((c = text.charCodeAt(I++)) === NEWLINE) eol = true; - else if (c === RETURN) { eol = true; if (text.charCodeAt(I) === NEWLINE) ++I; } - return text.slice(j + 1, i - 1).replace(/""/g, "\""); - } - - // Find next delimiter or newline. - while (I < N) { - if ((c = text.charCodeAt(i = I++)) === NEWLINE) eol = true; - else if (c === RETURN) { eol = true; if (text.charCodeAt(I) === NEWLINE) ++I; } - else if (c !== DELIMITER) continue; - return text.slice(j, i); - } - - // Return last token before EOF. - return eof = true, text.slice(j, N); - } - - while ((t = token()) !== EOF) { - var row = []; - while (t !== EOL && t !== EOF) row.push(t), t = token(); - if (f && (row = f(row, n++)) == null) continue; - rows.push(row); - } - - return rows; - } - - function preformatBody(rows, columns) { - return rows.map(function(row) { - return columns.map(function(column) { - return formatValue(row[column]); - }).join(delimiter); - }); - } - - function format(rows, columns) { - if (columns == null) columns = inferColumns(rows); - return [columns.map(formatValue).join(delimiter)].concat(preformatBody(rows, columns)).join("\n"); - } - - function formatBody(rows, columns) { - if (columns == null) columns = inferColumns(rows); - return preformatBody(rows, columns).join("\n"); - } - - function formatRows(rows) { - return rows.map(formatRow).join("\n"); - } - - function formatRow(row) { - return row.map(formatValue).join(delimiter); - } - - function formatValue(value) { - return value == null ? "" - : value instanceof Date ? formatDate(value) - : reFormat.test(value += "") ? "\"" + value.replace(/"/g, "\"\"") + "\"" - : value; - } - - return { - parse: parse, - parseRows: parseRows, - format: format, - formatBody: formatBody, - formatRows: formatRows - }; - } - - var csv = dsvFormat(","); - - var tsv = dsvFormat("\t"); - - function responseJson(response) { - if (!response.ok) throw new Error(response.status + " " + response.statusText); - return response.json(); - } - - function json(input, init) { - return fetch(input, init).then(responseJson); - } - - // import { json } from 'd3-fetch'; - - class ForceNet { - - constructor (symbols) { + constructor (opts) { var width = 600, height = 600; - this.symbols = symbols || {}; + this.apiurl = opts.apiurl; + this.init_svg(opts.svg); + this.categorylabel = opts.categorylabel || "Category"; + this.symbols_src = opts.symbols; + this.categorydiv = select(opts.categorydiv); + + this.wiki = new Wiki(this.apiurl); + this.events = new eventemitter3(); this.active_page = null; - // this.nodes = {}; + // this.symbols = symbols; + // this.net = new ForceNet({}); + // this.net.on("nodeclick", this.nodeclick.bind(this)); + this.svg = null; + 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.svg = null; - // this.historylinks = {}; + + 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); + + this.loaded = true; + if (this.active_url) { + this.set_active_url(this.active_url); + } } 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() @@ -5105,7 +5275,7 @@ var app = (function (exports) { } dragstarted (d) { - if (!event.active) this.simulation.alphaTarget(0.3).restart(); + // if (!event.active) this.simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } @@ -5116,22 +5286,199 @@ var app = (function (exports) { } dragended(d) { - if (!event.active) this.simulation.alphaTarget(0); + // if (!event.active) this.simulation.alphaTarget(0); d.fx = null; d.fy = null; } - link_key (a, b) { - return (a < b) ? ("link_"+a+"_"+b) : ("link_"+b+"_"+a); + /* Links */ + + link_key (p1, p2) { + return (p1.title < p2.title) ? + ("link_"+p1.title+"_"+p2.title) : + ("link_"+p2.title+"_"+p1.title); } - get_symbol (d, def) { - for (var i=0, l=d.cats.length; i 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.wiki.get_nodes(), links: values(this.all_links_by_key)}; + this.update_node_counts(); + this.update_graph(data); + } + + async set_active_page (page) { + console.log("wikimap: set_active_page:", page.title); + if (page === this.active_page) { + // console.log("page is already the active page", page, this.active_page); + return; + } + + // cleanup old + if (this.active_page) { + 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; + this.active_page.active = true; + this.linked_nodes_set_active(this.active_page, true); + + 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); + // this.update_nodes(); + // this.update_forces(); + } + + update_node_counts () { + var nodes = this.wiki.get_nodes(); + for (let i=0, len=nodes.length; i"page "+this.wiki.get_ns_classname(d.ns)) - .on("click", function(d) { - that.events.emit("nodeclick", d, this); + .on("click", d => { + // that.events.emit("nodeclick", d, this); + this.set_active_page(d); // this.set_active_node(d.title); }) .on("mouseover", function (d) { @@ -5176,8 +5524,9 @@ var app = (function (exports) { .on("drag", this.dragged.bind(this)) .on("end", this.dragended.bind(this))); + node_enter.append("use") - .attr("xlink:href", d => this.get_symbol(d, "default")) + .attr("xlink:href", d => d.symbol || "symbols.svg#Main") .attr("class", "testcolor"); // { @@ -5212,7 +5561,7 @@ var app = (function (exports) { this.simulation.force("link") .links(graph.links); - this.simulation.force("radial").radius(d => d.linked ? null : 200); + this.simulation.force("radial").radius(d => (d.linked || d.highlight) ? null : 200); function ticked() { link @@ -5238,7 +5587,7 @@ var app = (function (exports) { update_nodes () { var nodes = this.nodesg.selectAll("g.page"); - console.log("update_nodes", nodes.size()); + // console.log("update_nodes", nodes.size()); nodes.classed("active", d=>d.active); nodes.classed("active2", d=>d.active2); nodes.classed("highlight", d=>d.highlight); @@ -5261,7 +5610,7 @@ var app = (function (exports) { update_forces () { var force = this.simulation.force("link"); - console.log("update_forces:force", force); + // console.log("update_forces:force", force); this.simulation.force("link").strength(d => { if (d.source.active || d.target.active) { return 1; @@ -5270,306 +5619,13 @@ var app = (function (exports) { return 0.5 * (1 / Math.min(d.source.count, d.target.count)); } }); - this.simulation.alphaTarget(0.3).restart(); + // this.simulation.alphaTarget(0.3).restart(); + this.simulation.alpha(0.5).restart(); } } - class Map$2 { - - constructor (symbols) { - - this.events = new eventemitter3(); - this.active_page = null; - // this.nodes = {}; - this.symbols = symbols; - this.net = new ForceNet(symbols); - this.net.on("nodeclick", this.nodeclick.bind(this)); - // 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.svg = null; - this.historylinks = {}; - this.links = null; - this.highlight_category = null; - this.show_history = true; - } - - nodeclick (d, elt) { - console.log("nodeclick", d, elt, this); - this.set_active_node(d, elt); - } - - init_svg (svg) { - this.net.init_svg(svg); - } - - async load_json (source) { - var data = await json(source); - // console.log("got data!", data); - // index the nodes by title, init link-arity count - var index = {}; - this.nodes_by_title = index; - for (let i=0, l=data.nodes.length; i { - var source = index[x.source], - target = index[x.target]; - if (source === undefined) { - console.log("bad source", x.source); - return; - } - if (target === undefined) { - console.log("bad target", x.target); - return; - } - source.count += 1; - target.count += 1; - use_links.push({ source: source, target: target }); - }); - data.links = use_links; - this.links = data.links; - // console.log("data", data); - // calculate the node sizes (link arity) - this.net.update_graph(data); - } - - get_symbol_image_path (cname) { - var symbol = this.symbols[cname]; - if (symbol) { - let hpos = symbol.indexOf("#"), - rest = symbol.substr(hpos+1); - rest = rest.replace(/'/g, ''); - return "img/"+rest+".png"; - } - } - async load_cats (src, elt) { - var data = await json(src); - - console.log("indexing categories by title"); - var cats_by_title = {}; - for (let i=0, l=data.length; i d.tcount > 0); - console.log("post filter", data.length); - console.log("load_cats.data", data, elt); - var cat = select(elt) - .selectAll("div.cat") - .data(data) - .enter() - .append("div") - .attr("class", "cat"); - cat.classed("icon", d => this.get_symbol_image_path(d.title)); - cat.append("span").attr("class", "icon").filter(d=> this.get_symbol_image_path(d.title)).style("background-image", d => "url("+this.get_symbol_image_path(d.title)+")"); - cat.append("span").attr("class", "spacing").html(d => { - var d = d.depth, - ret = ""; - while(d) { - ret += "    "; - d-=1; - } - return ret; - }); - cat.append("a").attr("class", "label").html(d => d.title).attr("href", "#").on("click", d => { - event.preventDefault(); - this.category_click(d); - }); - cat.append("span").attr("class", "count").html(d => d.tcount); - } - - async load_legend (src, elt) { - var data = await json(src); - console.log("load_legend", data); - var cat = select(elt) - .selectAll("div.cat") - .data(data) - .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) - .attr("href", "#").on("click", d => { - event.preventDefault(); - this.category_click(d); - }); - cat.append("span").attr("class", "count").html(d => d.tcount); - } - - category_click (d) { - console.log("category click", d); - if (this.highlight_category) { - // cleanup old pages - this.highlight_category.pages.forEach(d => d.highlight = false); - } - this.highlight_category = d; - this.highlight_category.pages.forEach(d => d.highlight = true); - this.net.update_nodes(); - // set highlight category... - // all nodes with this category get .highlight = true - // make a category index ?! - } - - on (message, callback, context) { - this.events.on(message, callback, context); - } - - /* OLD STYLE with node.all_links - walk (node, links) { - var links_seen = {}; - // var node = this.ensure_node(nodename); - node.all_links.forEach(x => { - var link_key = this.link_key(node.title, x.title); - if (!links_seen[link_key]) { - links.push({source: node, target: x}); - links_seen[link_key] = true; - } - }) - return; - } - */ - set_active_title (title) { - this.set_active_node(title); - } - - activate_linked_nodes (page, active) { - // deactivate linked links/nodes - for (let i=0, l=this.links.length; i { - graph.links.push(x); - }) - // graph.links = titles.map(t => ({source: pagetitle, target: t})); - this.update_graph(graph); - // console.log("GOT DATA", titles); - // return titles; - } - */ - } - - - - // http://erg.activearchives.org/mw/api.php?action=query&prop=links&titles=Bienvenue_%C3%A0_l%E2%80%99erg - // http://erg.activearchives.org/w/api.php?action=query&prop=info&titles=Main%20Page - // Bienvenue_à_l’erg - // http://erg.activearchives.org/mw/index.php/Bienvenue_%C3%A0_l%E2%80%99erg - exports.Map = Map$2; return exports; diff --git a/dist/index.js b/dist/index.js index 07dffbf..dfa4e5c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,18 +1,14 @@ -// custom_scroller_menu( -// document.scrollingElement, -// document.getElementById("menubar"), -// document.getElementById("debug")); var svg = document.querySelector("#svg"), page = document.querySelector("#page"), iframe = document.querySelector("iframe#wikiframe"), cats = document.querySelector("#cats"), cats_contents = document.querySelector("#cats .body"), - cats_thumb = document.querySelector("#cats .thumb"), + // cats_thumb = document.querySelector("#cats .thumb"), allcatscb = document.querySelector("input#allcats"), historycb = document.querySelector("input#history"), - current_title = null, - loaded = false, + // current_title = null, + // loaded = false, wikibaseurl, wikibasepat; @@ -36,99 +32,35 @@ function resize() { } resize(); -// console.log("mediawikiapi", mediawikiapi); -var symbols = { - "Orientations": "symbols.svg#Orientations", - "Ateliers pluridisciplinaires": "symbols.svg#Ateliers_pluridisciplinaires", - "Cours de soutien à l'orientation": "symbols.svg#Cours_de_soutien_a_l'orientation", - "Cours de soutien spécifique": "symbols.svg#Cours_de_soutien_specifique", - "Cours techniques": "symbols.svg#Cours_techniques", - "Cours théoriques": "symbols.svg#Cours_theoriques", - "Enseignants": "symbols.svg#Enseignants", - "default": "symbols.svg#Main" -}; -var map = new app.Map(symbols); +var map = new app.Map({ + apiurl: "/mw/api.php", + symbols: "src/legend.json", + svg: "#svg", + categorylabel: "Catégorie", + categorydiv: "#cats .body" +}); -map.init_svg("#svg"); async function doload () { - console.log("loading map"); - await map.load_json("data/sitemap.json"); + console.log("map.init"); + await map.init(); + console.log("map.init: done"); + + // await map.load_json("data/sitemap.json"); // console.log("loading categories"); // await map.load_cats("data/cats.json", cats_contents); - await map.load_legend("src/legend.json", cats_contents); - console.log("LOADED!"); - loaded = true; - if (current_title) { - map.set_active_title(current_title); - } -} + // await map.load_legend("src/legend.json", cats_contents); + // loaded = true; -map.on("page", function (title) { - console.log("map.page", title); - var url = wiki_title_to_url(title); - iframe.src = url; -}) -// async function doload() { -// map.set_active_node(startpage.value); -// } - -function strip_fragment (href) { - var spos = href.indexOf("#"); - if (spos >= 0) { - return href.substr(0, href.indexOf("#")) - } - return href; -} -function url_to_wiki_title (href) { - href = strip_fragment(href); - var m = wikibasepat.exec(href); - if (m !== null) { - return decodeURI(m[1]).replace(/_/g, " "); - } - console.log("m", m); -} -function wiki_title_to_url (title) { - return wikibaseurl+encodeURI(title.replace(/ /g, "_")); + map.on("page", function (page) { + // console.log("map.page", page.title); + var url = page.url(); + if (iframe.src !== url) { + // console.log("setting iframe src to", url); + iframe.src = url; + } + }) } window.addEventListener("DOMContentLoaded", doload); -function strip_title_from_wiki_url (url) { - return url.substr(0, url.lastIndexOf("/")+1); -} iframe.addEventListener("load", function () { - var href = strip_fragment(iframe.contentWindow.location.href); - if (!wikibaseurl) { - wikibaseurl = strip_title_from_wiki_url(href); - wikibasepat = new RegExp(wikibaseurl+"(.+)"); - } - console.log("iframe loaded", href); - var title = url_to_wiki_title(href); - console.log("title", title); - if (title) { - current_title = title; - if (loaded) { - map.set_active_title(title); - } - } - // attempt to map url to wiki page title and update the map if it is one - + map.set_active_url(iframe.contentWindow.location.href); }); - -cats_thumb.addEventListener("click", function () { - cats.classList.toggle("expanded"); -}); -// allcats checkbox -// match current state & respond to change events -// console.log("setting checked to", cats.classList.contains("showall")) -allcatscb.checked = cats.classList.contains("showall"); -allcatscb.addEventListener("change", function () { - // console.log("allcats", allcatscb); - if (allcatscb.checked) { - cats.classList.add("showall") - } else { - cats.classList.remove("showall") - } -}) -historycb.addEventListener("change", function () { - // console.log("history", historycb.checked); - map.set_show_history(historycb.checked); -}) diff --git a/splitscreen.css b/splitscreen.css index 8744d44..9eebded 100644 --- a/splitscreen.css +++ b/splitscreen.css @@ -88,7 +88,7 @@ body { } .links line.active2 { - stroke: red; + stroke: magenta; } .links line.history { @@ -96,11 +96,11 @@ body { } .active { - stroke: red; + stroke: magenta; } .category circle { - fill: yellow; + fill: cyan; } .web circle { @@ -134,11 +134,11 @@ svg g.page { svg g.page text { visibility: hidden; fill: #222; - font: 12px sans-serif; + font: 9px sans-serif; } svg g.active text { - visibility: visible; + /*visibility: visible;*/ } svg g.mouse text { @@ -150,24 +150,44 @@ use { stroke: black; } +.highlight use { + fill: #FFF; + stroke: cyan; +} + +.active use { +/* animation-transform: 2;*/ + animation-duration: 2s; + animation-name: pulse; + animation-iteration-count: infinite; +} + +@keyframes pulse { + 0% { + transform: scale(1, 1); + } + 50% { + transform: scale(1.5, 1.5); + } + 100% { + transform: scale(1, 1); + } +} + .active use { fill: #FFF; - stroke: red; + stroke: magenta; } .active text { - stroke: black; + stroke: none; } .active2 use { fill: #FFF; - stroke: red; + stroke: magenta; } -.highlight use { - fill: #FFF; - stroke: orange; -} /* Categories */ @@ -243,6 +263,9 @@ use { color: black; text-decoration: none; } +#cats div.cat.highlight a { + color: cyan; +} #cats hr { color: white; } diff --git a/src/forcenet.js b/src/forcenet.js index 9430ac6..240be47 100644 --- a/src/forcenet.js +++ b/src/forcenet.js @@ -74,13 +74,38 @@ export class ForceNet { return (a < b) ? ("link_"+a+"_"+b) : ("link_"+b+"_"+a); } + /* + 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 }; + } + + register_link (from_page, to_page) { + var lkey = this.link_key(from_page, to_page); + if (this.links[lkey] === undefined) { + this.links[lkey] = this.make_link(from_page, to_page); + } + } + + + get_symbol (d, def) { + return "symbols.svg#Main"; + /* for (var i=0, l=d.cats.length; i (!p.redirect && p.ns === 0)); + return ret; + } + + get_links () { + return values(this.links); + } + + async init () { + await this.get_site_info(); + } + + async get_site_info () { + // https://en.wikipedia.org/w/api.php + var url = this.apiurl + "?action=query&meta=siteinfo&siprop=general&format=json&formatversion=2"; + var data = await json(url); + // this.siteinfo = data.query.general; + + this.server = data.query.general.server; // e.g. "http://activearchives.org" + this.articlepath = data.query.general.articlepath; // e.g. "/wiki/$1" + this.base = data.query.general.base; // e.g. "http://activearchives.org/wiki/Main_Page" + // this.sitename = data.query.general.sitename; + this.mainpage = data.query.general.mainpage; + this.wikibasepat = new RegExp(this.server+this.articlepath.replace(/\$1$/, "(.+)")); + + url = this.apiurl + "?action=query&meta=siteinfo&siprop=namespaces&format=json&formatversion=2"; + data = await json(url); + this.namespaces_by_id = data.query.namespaces; + this.namespaces_by_name = {}; + values(this.namespaces_by_id).forEach(n => { + this.namespaces_by_name[n.name] = n; + }); + // create special special entry + var special = { name: "Special", id: -17 }; + this.namespaces_by_name["Special"] = special; + this.namespaces_by_id[-17] = special; + + } + + escapeTitle (title) { + return encodeURI(title.replace(/ /g, "_")); + } + + unescapeTitle (title) { + return decodeURI(title.replace(/_/g, " ")); + } + + wiki_title_to_url (title) { + return this.server + this.articlepath.replace(/\$1$/, this.escapeTitle(title)); + } + + url_to_wiki_title (href) { + var m = this.wikibasepat.exec(strip_fragment(href)); + if (m !== null) { + return this.unescapeTitle(m[1]); + } + } + + /* sample siteinfo, see: http://activearchives.org/mw/api.php?action=query&meta=siteinfo&formatversion=2&format=json */ + + get_page (url) { + var title = this.url_to_wiki_title(url); + if (title) { + return this.get_page_by_title(title); + } + } + get_page_by_title (title) { var p = this.pages_by_title[title]; if (p) { return p; } else { - p = new Page(this, {title: title, ns: 0}); + var cpos = title.indexOf(":"), + name = title, + namespace = ""; + if (cpos >= 0) { + namespace = title.substring(0, cpos); + name = title.substring(cpos+1); + } + p = new Page(this, {title: title, name: name, ns: this.namespaces_by_name[namespace].id}); this.pages_by_title[title] = p; return p; } @@ -68,7 +151,8 @@ export class Page { } url () { // return this.wiki.apiurl.replace("api.php", "index.php")+"/"+encodeURIComponent(this.title); - return this.wiki.apiurl.replace("api.php", "index.php")+"/"+encodeURIComponent(this.title); + // return this.wiki.apiurl.replace("api.php", "index.php")+"/"+encodeURIComponent(this.title); + return this.wiki.wiki_title_to_url(this.title); } merge_data (node) { for (var key in node) { @@ -83,50 +167,51 @@ export class Page { var ret = []; var url = this.wiki.apiurl+"?action=query&format=json&formatversion=2&prop="+pname+"&titles="+encodeURIComponent(this.title); while (true) { - var data = await fetchJsonp(url); - var json = await data.json(); - // console.log("BACKLINKS.RAW", json); - // filter REDIRECTS + Discussion pages (ns==1) - if (json.query.pages[0]) { - var p = json.query.pages[0]; + var data = await json(url); + // var json = await data.json(); + if (data.query.pages[0]) { + var p = data.query.pages[0]; // extract any missing page info if (p.ns && !this.ns) { this.ns = p.ns; } if (p.pageid && !this.pageid) { this.pageid = p.pageid; } } - if (json.query.pages[0][pname]) { - ret.push.apply(ret, json.query.pages[0][pname]); + if (data.query.pages[0][pname]) { + ret.push.apply(ret, data.query.pages[0][pname]); } - if (!json.continue) { break; } - url = this.wiki.apiurl+"?action=query&format=json&formatversion=2&prop="+pname+"&"+prefix+"continue="+json.continue[prefix+"continue"]+"&titles="+encodeURIComponent(this.title); + if (!data.continue) { break; } + url = this.wiki.apiurl+"?action=query&format=json&formatversion=2&prop="+pname+"&"+prefix+"continue="+data.continue[prefix+"continue"]+"&titles="+encodeURIComponent(this.title); } ret = ret.map(x => this.wiki.page_for_object(x)); - // console.log("get_backlinks", ret); - console.log("get_prop", pname, prefix, ret); + // console.log("get_prop", pname, prefix, ret); return ret; } + + async get_list (lname, prefix, appendstr) { + var ret = []; + var baseurl = this.wiki.apiurl+"?action=query&list="+lname+"&format=json&formatversion=2&"+prefix+"title="+encodeURIComponent(this.title)+(appendstr || ""); + var url = baseurl; + while (true) { + var data = await json(url); + if (data.query[lname]) { + ret.push.apply(ret, data.query[lname]); + } + if (!data.continue) { break; } + url = baseurl+"&"+prefix+"continue="+data.continue[prefix+"continue"]; + } + ret = ret.map(x => this.wiki.page_for_object(x)); + // console.log("get_prop", pname, prefix, ret); + return ret; + } + async get_links () { return await this.get_prop("links", "pl"); } + async get_linkshere () { return await this.get_prop("linkshere", "lh"); } - async get_list () { - // http://localhost/mw/api.php?action=query&prop=links&titles=Bienvenue_%C3%A0_l%E2%80%99erg - var ret = []; - var url = this.wiki.apiurl+"?action=query&format=json&formatversion=2&list=backlinks&bltitle="+encodeURIComponent(this.title); - while (true) { - var data = await fetchJsonp(url); - var json = await data.json(); - // console.log("BACKLINKS.RAW", json); - // filter REDIRECTS + Discussion pages (ns==1) - var backlinks = json.query.backlinks; - // backlink = backlinks.filter(x => (!x.redirect && x.ns != 1); - ret.push.apply(ret, backlinks); - if (!json.continue) { break; } - url = this.wiki.apiurl+"?action=query&format=json&formatversion=2&list=backlinks&blcontinue="+json.continue.blcontinue+"&bltitle="+encodeURIComponent(this.title); - } - ret = ret.map(x => this.wiki.page_for_object(x)); - // console.log("get_backlinks", ret); - return ret; + + async get_categorymembers () { + return await this.get_list("categorymembers", "cm", "&cmtype=page"); } } diff --git a/src/wikimap.js b/src/wikimap.js index e6d6eea..27418a3 100644 --- a/src/wikimap.js +++ b/src/wikimap.js @@ -7,153 +7,72 @@ import { Wiki, Page } from './wiki.js'; import EventEmitter from 'eventemitter3'; import { json } from 'd3-fetch'; -import { ForceNet } from './forcenet.js'; +// import { ForceNet } from './forcenet.js'; export class Map { - constructor (symbols) { + constructor (opts) { var width = 600, height = 600; + this.apiurl = opts.apiurl; + this.init_svg(opts.svg); + 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.nodes = {}; - this.symbols = symbols; - this.net = new ForceNet(symbols); - this.net.on("nodeclick", this.nodeclick.bind(this)); - // 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.symbols = symbols; + // this.net = new ForceNet({}); + // this.net.on("nodeclick", this.nodeclick.bind(this)); this.svg = null; 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 = {}; } - nodeclick (d, elt) { - console.log("nodeclick", d, elt, this); - this.set_active_node(d, elt); - } + async init () { + this.symbols = await json(this.symbols_src); + await this.wiki.init(); - init_svg (svg) { - this.net.init_svg(svg); - } - - async load_json (source) { - var data = await json(source); - // console.log("got data!", data); - // index the nodes by title, init link-arity count - var index = {}; - this.nodes_by_title = index; - for (let i=0, l=data.nodes.length; i { - var source = index[x.source], - target = index[x.target]; - if (source === undefined) { - console.log("bad source", x.source); - return; - } - if (target === undefined) { - console.log("bad target", x.target); - return; - } - source.count += 1; - target.count += 1; - use_links.push({ source: source, target: target }); - }); - data.links = use_links; - this.links = data.links; - // console.log("data", data); - // calculate the node sizes (link arity) - this.net.update_graph(data); - } - - get_symbol_image_path (cname) { - var symbol = this.symbols[cname]; - if (symbol) { - let hpos = symbol.indexOf("#"), - rest = symbol.substr(hpos+1); - rest = rest.replace(/'/g, ''); - return "img/"+rest+".png"; - } - } - async load_cats (src, elt) { - var data = await json(src); - - console.log("indexing categories by title"); - var cats_by_title = {}; - for (let i=0, l=data.length; i d.tcount > 0) - console.log("post filter", data.length); - console.log("load_cats.data", data, elt); - var cat = select(elt) - .selectAll("div.cat") - .data(data) - .enter() - .append("div") - .attr("class", "cat"); - cat.classed("icon", d => this.get_symbol_image_path(d.title)); - cat.append("span").attr("class", "icon").filter(d=> this.get_symbol_image_path(d.title)).style("background-image", d => "url("+this.get_symbol_image_path(d.title)+")"); - cat.append("span").attr("class", "spacing").html(d => { - var d = d.depth, - ret = ""; - while(d) { - ret += "    "; - d-=1; - } - return ret; - }); - cat.append("a").attr("class", "label").html(d => d.title).attr("href", "#").on("click", d => { - event.preventDefault(); - this.category_click(d); - }) - cat.append("span").attr("class", "count").html(d => d.tcount) - } - async load_legend (src, elt) { - var data = await json(src); - console.log("load_legend", data); - var cat = select(elt) + // create the categories + let cat = this.categorydiv .selectAll("div.cat") - .data(data) + .data(this.symbols) .enter() .append("div") .attr("class", "cat icon"); @@ -162,94 +81,233 @@ export class Map { .style("background-image", d => "url("+d.icon+")"); cat.append("a") .attr("class", "label") - .html(d => d.key) + .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) - } - category_click (d) { - console.log("category click", d); - if (this.highlight_category) { - // cleanup old pages - this.highlight_category.pages.forEach(d => d.highlight = false); + this.loaded = true; + if (this.active_url) { + this.set_active_url(this.active_url); } - this.highlight_category = d; - this.highlight_category.pages.forEach(d => d.highlight = true); - this.net.update_nodes(); - // set highlight category... - // all nodes with this category get .highlight = true - // make a category index ?! } on (message, callback, context) { this.events.on(message, callback, context); } -/* OLD STYLE with node.all_links - walk (node, links) { - var links_seen = {}; - // var node = this.ensure_node(nodename); - node.all_links.forEach(x => { - var link_key = this.link_key(node.title, x.title); - if (!links_seen[link_key]) { - links.push({source: node, target: x}); - links_seen[link_key] = true; - } - }) - return; - } -*/ - set_active_title (title) { - this.set_active_node(title); + async json (src) { + // expose d3's json method + return await json(src); } - activate_linked_nodes (page, active) { - // deactivate linked links/nodes - for (let i=0, l=this.links.length; i { + this.content.attr("transform", event.transform); + // console.log("transform", event.transform, this.content.attr("transform")); + }); + this.rect = this.svg.append("rect") + .attr("width", 1000) + .attr("height", 1000) + .style("fill", "none") + .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"); } - async set_active_node (page) { - if (typeof(page) === "string") { - let pagename = page; - page = this.nodes_by_title[page]; + dragstarted (d) { + // if (!event.active) this.simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + } - if (!page) { - console.log("wikimap.set_active_node: page not found", pagename); - } + 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; + } + + async set_active_url (url) { + this.active_url = url; + if (this.loaded) { + var page = this.wiki.get_page(this.active_url); + if (page) { + 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.wiki.get_nodes(), links: values(this.all_links_by_key)}; + this.update_node_counts() + this.update_graph(data); + } + + async set_active_page (page) { + console.log("wikimap: set_active_page:", page.title); if (page === this.active_page) { // console.log("page is already the active page", page, this.active_page); return; } + + // cleanup old if (this.active_page) { this.active_page.active = false; // deactivate linked links/nodes - this.activate_linked_nodes(this.active_page, false); - // ENSURE HISTORY LINK TO PREVIOUS NODE AND CURRENT - var lkey = this.net.link_key(this.active_page.title, page.title), - source = (this.active_page.title < page.title) ? this.active_page : page, - target = (this.active_page.title < page.title) ? page : this.active_page; - this.historylinks[lkey] = {source: source, target: target, type:"history"}; + 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; this.active_page.active = true; - this.activate_linked_nodes(this.active_page, true); + this.linked_nodes_set_active(this.active_page, true); - this.events.emit("page", this.active_page.title); + this.events.emit("page", this.active_page); - this.net.update_nodes(); - this.net.update_forces(); + var data = {nodes: this.wiki.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"); - graph.nodes = values(this.wiki.pages_by_title); - graph.links = []; - this.walk(page, graph.links); - // activate historylinks - values(this.historylinks).forEach(x => { - graph.links.push(x); - }) - // graph.links = titles.map(t => ({source: pagetitle, target: t})); - this.update_graph(graph); - // console.log("GOT DATA", titles); - // return titles; + 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); + select(this).classed("mouse", true); + }) + .on("mouseout", function (d) { + // console.log("mouseout", this); + select(this).classed("mouse", false); + }) + .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("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.active ? 10 : (a.active2 ? 5 : 0), + y = b.active ? 10 : (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(); + } + + } - - -// http://erg.activearchives.org/mw/api.php?action=query&prop=links&titles=Bienvenue_%C3%A0_l%E2%80%99erg -// http://erg.activearchives.org/w/api.php?action=query&prop=info&titles=Main%20Page -// Bienvenue_à_l’erg -// http://erg.activearchives.org/mw/index.php/Bienvenue_%C3%A0_l%E2%80%99erg