diff --git a/Makefile b/Makefile
index 55ed971e2..c22d5a80a 100644
--- a/Makefile
+++ b/Makefile
@@ -67,6 +67,7 @@ dist/iD.js: \
js/id/services/mapillary.js \
js/id/services/nominatim.js \
js/id/services/taginfo.js \
+ js/id/services/wikidata.js \
js/id/services/wikipedia.js \
js/id/util.js \
js/id/util/session_mutex.js \
diff --git a/data/presets.yaml b/data/presets.yaml
index 4c0534523..137cddb63 100644
--- a/data/presets.yaml
+++ b/data/presets.yaml
@@ -1045,7 +1045,7 @@ en:
# 'width=*'
label: Width (Meters)
wikipedia:
- # 'wikipedia=*'
+ # 'wikipedia=*, wikidata=*'
label: Wikipedia
presets:
address:
diff --git a/data/presets/fields.json b/data/presets/fields.json
index ee2a6a2f1..203c632a5 100644
--- a/data/presets/fields.json
+++ b/data/presets/fields.json
@@ -1414,6 +1414,10 @@
},
"wikipedia": {
"key": "wikipedia",
+ "keys": [
+ "wikipedia",
+ "wikidata"
+ ],
"type": "wikipedia",
"icon": "wikipedia",
"universal": true,
diff --git a/data/presets/fields/wikipedia.json b/data/presets/fields/wikipedia.json
index 8f4a67d85..466c95520 100644
--- a/data/presets/fields/wikipedia.json
+++ b/data/presets/fields/wikipedia.json
@@ -1,5 +1,6 @@
{
"key": "wikipedia",
+ "keys": ["wikipedia", "wikidata"],
"type": "wikipedia",
"icon": "wikipedia",
"universal": true,
diff --git a/index.html b/index.html
index 80d184a2f..cb539b9cc 100644
--- a/index.html
+++ b/index.html
@@ -43,6 +43,7 @@
+
diff --git a/js/id/services/wikidata.js b/js/id/services/wikidata.js
new file mode 100644
index 000000000..0718e2fdd
--- /dev/null
+++ b/js/id/services/wikidata.js
@@ -0,0 +1,22 @@
+iD.services.wikidata = function() {
+ var wiki = {},
+ endpoint = 'https://www.wikidata.org/w/api.php?';
+
+ // Given a Wikipedia language and article title, return an array of
+ // corresponding Wikidata entities.
+ wiki.itemsByTitle = function(lang, title, callback) {
+ lang = lang || 'en';
+ d3.jsonp(endpoint + iD.util.qsString({
+ action: 'wbgetentities',
+ format: 'json',
+ sites: lang.replace(/-/g, '_') + 'wiki',
+ titles: title,
+ languages: 'en', // shrink response by filtering to one language
+ callback: '{callback}'
+ }), function(data) {
+ callback(title, data.entities || {});
+ });
+ };
+
+ return wiki;
+};
diff --git a/js/id/ui/preset/localized.js b/js/id/ui/preset/localized.js
index 657331fbe..f41d2c191 100644
--- a/js/id/ui/preset/localized.js
+++ b/js/id/ui/preset/localized.js
@@ -4,7 +4,7 @@ iD.ui.preset.localized = function(field, context) {
input, localizedInputs, wikiTitles,
entity;
- function i(selection) {
+ function localized(selection) {
input = selection.selectAll('.localized-main')
.data([0]);
@@ -203,7 +203,7 @@ iD.ui.preset.localized = function(field, context) {
.value(function(d) { return d.value; });
}
- i.tags = function(tags) {
+ localized.tags = function(tags) {
// Fetch translations from wikipedia
if (tags.wikipedia && !wikiTitles) {
wikiTitles = {};
@@ -228,13 +228,15 @@ iD.ui.preset.localized = function(field, context) {
localizedInputs.call(render, postfixed.reverse());
};
- i.focus = function() {
+ localized.focus = function() {
input.node().focus();
};
- i.entity = function(_) {
+ localized.entity = function(_) {
+ if (!arguments.length) return entity;
entity = _;
+ return localized;
};
- return d3.rebind(i, dispatch, 'on');
+ return d3.rebind(localized, dispatch, 'on');
};
diff --git a/js/id/ui/preset/textarea.js b/js/id/ui/preset/textarea.js
index b609aed72..92d18aa63 100644
--- a/js/id/ui/preset/textarea.js
+++ b/js/id/ui/preset/textarea.js
@@ -2,7 +2,7 @@ iD.ui.preset.textarea = function(field) {
var dispatch = d3.dispatch('change'),
input;
- function i(selection) {
+ function textarea(selection) {
input = selection.selectAll('textarea')
.data([0]);
@@ -25,13 +25,13 @@ iD.ui.preset.textarea = function(field) {
};
}
- i.tags = function(tags) {
+ textarea.tags = function(tags) {
input.value(tags[field.key] || '');
};
- i.focus = function() {
+ textarea.focus = function() {
input.node().focus();
};
- return d3.rebind(i, dispatch, 'on');
+ return d3.rebind(textarea, dispatch, 'on');
};
diff --git a/js/id/ui/preset/wikipedia.js b/js/id/ui/preset/wikipedia.js
index b14ac9655..b129eb9ba 100644
--- a/js/id/ui/preset/wikipedia.js
+++ b/js/id/ui/preset/wikipedia.js
@@ -1,9 +1,10 @@
iD.ui.preset.wikipedia = function(field, context) {
var dispatch = d3.dispatch('change'),
wikipedia = iD.services.wikipedia(),
+ wikidata = iD.services.wikidata(),
link, entity, lang, title;
- function i(selection) {
+ function wiki(selection) {
var langcombo = d3.combobox()
.fetcher(function(value, cb) {
var v = value.toLowerCase();
@@ -54,7 +55,7 @@ iD.ui.preset.wikipedia = function(field, context) {
title
.call(titlecombo)
- .on('blur', change)
+ .on('blur', blur)
.on('change', change);
link = selection.selectAll('a.wiki-link')
@@ -81,14 +82,19 @@ iD.ui.preset.wikipedia = function(field, context) {
function changeLang() {
lang.value(language()[1]);
- change();
+ change(true);
}
- function change() {
+ function blur() {
+ change(true);
+ }
+
+ function change(skipWikidata) {
var value = title.value(),
m = value.match(/https?:\/\/([-a-z]+)\.wikipedia\.org\/(?:wiki|\1-[-a-z]+)\/([^#]+)(?:#(.+))?/),
l = m && _.find(iD.data.wikipedia, function(d) { return m[1] === d[2]; }),
- anchor;
+ anchor,
+ syncTags = {};
if (l) {
// Normalize title http://www.mediawiki.org/wiki/API:Query#Title_normalization
@@ -107,12 +113,47 @@ iD.ui.preset.wikipedia = function(field, context) {
title.value(value);
}
- var t = {};
- t[field.key] = value ? language()[2] + ':' + value : undefined;
- dispatch.change(t);
+ syncTags.wikipedia = value ? language()[2] + ':' + value : undefined;
+ if (!skipWikidata) {
+ syncTags.wikidata = undefined;
+ }
+
+ dispatch.change(syncTags);
+
+
+ if (skipWikidata || !value || !language()[2]) return;
+
+ // attempt asynchronous update of wikidata tag..
+ var initEntityId = entity.id,
+ initWikipedia = context.entity(initEntityId).tags.wikipedia;
+
+ wikidata.itemsByTitle(language()[2], value, function (title, data) {
+ // 1. most recent change was a tag change
+ var annotation = t('operations.change_tags.annotation'),
+ currAnnotation = context.history().undoAnnotation();
+ if (currAnnotation !== annotation) return;
+
+ // 2. same entity exists and still selected
+ var selectedIds = context.selectedIDs(),
+ currEntityId = selectedIds.length > 0 && selectedIds[0];
+ if (currEntityId !== initEntityId) return;
+
+ // 3. wikipedia value has not changed
+ var currTags = _.clone(context.entity(currEntityId).tags),
+ qids = data && Object.keys(data);
+ if (initWikipedia !== currTags.wikipedia) return;
+
+ // ok to coalesce the update of wikidata tag into the previous tag change
+ currTags.wikidata = qids && _.find(qids, function (id) {
+ return id.match(/^Q\d+$/);
+ });
+
+ context.overwrite(iD.actions.ChangeTags(currEntityId, currTags), annotation);
+ dispatch.change(currTags);
+ });
}
- i.tags = function(tags) {
+ wiki.tags = function(tags) {
var value = tags[field.key] || '',
m = value.match(/([^:]+):([^#]+)(?:#(.+))?/),
l = m && _.find(iD.data.wikipedia, function(d) { return m[1] === d[2]; }),
@@ -131,7 +172,7 @@ iD.ui.preset.wikipedia = function(field, context) {
}
}
link.attr('href', 'https://' + m[1] + '.wikipedia.org/wiki/' +
- m[2].replace(/ /g, '_') + (anchor ? ('#' + anchor) : ''));
+ m[2].replace(/ /g, '_') + (anchor ? ('#' + anchor) : ''));
// unrecognized value format
} else {
@@ -143,13 +184,15 @@ iD.ui.preset.wikipedia = function(field, context) {
}
};
- i.entity = function(_) {
+ wiki.entity = function(_) {
+ if (!arguments.length) return entity;
entity = _;
+ return wiki;
};
- i.focus = function() {
+ wiki.focus = function() {
title.node().focus();
};
- return d3.rebind(i, dispatch, 'on');
+ return d3.rebind(wiki, dispatch, 'on');
};
diff --git a/test/index.html b/test/index.html
index 15775fc36..24aa35963 100644
--- a/test/index.html
+++ b/test/index.html
@@ -48,6 +48,7 @@
+
diff --git a/test/spec/ui/preset/wikipedia.js b/test/spec/ui/preset/wikipedia.js
index f90ab6e09..b0d5764c6 100644
--- a/test/spec/ui/preset/wikipedia.js
+++ b/test/spec/ui/preset/wikipedia.js
@@ -1,13 +1,46 @@
describe('iD.ui.preset.wikipedia', function() {
- var selection, field;
+ var entity, context, selection, field, wikiDelay, selectedId;
+
+ function wikidataStub() {
+ wikidataStub.itemsByTitle = function(lang, title, callback) {
+ var data = {Q216353: {id: 'Q216353'}};
+ if (wikiDelay) {
+ window.setTimeout(function () { callback(title, data); }, wikiDelay);
+ }
+ else {
+ callback(title, data);
+ }
+ }
+ return wikidataStub;
+ }
+
+ function changeTags(changed) {
+ var annotation = t('operations.change_tags.annotation');
+ var tags = _.extend({}, entity.tags, changed);
+ context.perform(iD.actions.ChangeTags(entity.id, tags), annotation);
+ }
beforeEach(function() {
+ entity = iD.Node({id: 'n12345'});
+ selectedId = entity.id;
+ context = iD();
+ context.history().merge([entity]);
selection = d3.select(document.createElement('div'));
- field = iD().presets(iD.data.presets).presets().field('wikipedia');
+ field = context.presets(iD.data.presets).presets().field('wikipedia');
+ wikiDelay = 0;
+
+ sinon.stub(iD.services, 'wikidata', wikidataStub);
+ sinon.stub(context, 'selectedIDs', function() { return [selectedId]; });
});
+ afterEach(function() {
+ iD.services.wikidata.restore();
+ context.selectedIDs.restore();
+ });
+
+
it('recognizes lang:title format', function() {
- var wikipedia = iD.ui.preset.wikipedia(field, {});
+ var wikipedia = iD.ui.preset.wikipedia(field, context);
selection.call(wikipedia);
wikipedia.tags({wikipedia: 'en:Title'});
expect(selection.selectAll('.wiki-lang').value()).to.equal('English');
@@ -15,44 +48,108 @@ describe('iD.ui.preset.wikipedia', function() {
expect(selection.selectAll('.wiki-link').attr('href')).to.equal('https://en.wikipedia.org/wiki/Title');
});
- it('sets a new value', function() {
- var wikipedia = iD.ui.preset.wikipedia(field, {});
+ it('sets language, value, wikidata', function() {
+ var wikipedia = iD.ui.preset.wikipedia(field, context).entity(entity);
+ wikipedia.on('change', changeTags);
selection.call(wikipedia);
- wikipedia.on('change', function(tags) {
- expect(tags).to.eql({wikipedia: undefined});
- });
-
+ var spy = sinon.spy();
+ wikipedia.on('change.spy', spy);
selection.selectAll('.wiki-lang').value('Deutsch');
- happen.once(selection.selectAll('.wiki-lang').node(), {type: 'change'});
-
- wikipedia.on('change', function(tags) {
- expect(tags).to.eql({wikipedia: 'de:Title'});
- });
+ happen.once(selection.selectAll('.wiki-lang').node(), { type: 'change' });
+ happen.once(selection.selectAll('.wiki-lang').node(), { type: 'blur' });
+ expect(spy.callCount).to.equal(2);
+ expect(spy.firstCall).to.have.been.calledWith({ wikipedia: undefined }); // on change
+ expect(spy.secondCall).to.have.been.calledWith({ wikipedia: undefined }); // on blur
+ spy = sinon.spy();
+ wikipedia.on('change.spy', spy);
selection.selectAll('.wiki-title').value('Title');
- happen.once(selection.selectAll('.wiki-title').node(), {type: 'change'});
+ happen.once(selection.selectAll('.wiki-title').node(), { type: 'change' });
+ happen.once(selection.selectAll('.wiki-title').node(), { type: 'blur' });
+ expect(spy.callCount).to.equal(3);
+ expect(spy.firstCall).to.have.been.calledWith({ wikipedia: 'de:Title', wikidata: undefined }); // on change
+ expect(spy.secondCall).to.have.been.calledWith({ wikipedia: 'de:Title', wikidata: 'Q216353' }); // wikidata async
+ expect(spy.thirdCall).to.have.been.calledWith({ wikipedia: 'de:Title' }); // on blur
});
it('recognizes pasted URLs', function() {
- var wikipedia = iD.ui.preset.wikipedia(field, {});
+ var wikipedia = iD.ui.preset.wikipedia(field, context).entity(entity);
+ wikipedia.on('change', changeTags);
selection.call(wikipedia);
selection.selectAll('.wiki-title').value('http://de.wikipedia.org/wiki/Title');
- happen.once(selection.selectAll('.wiki-title').node(), {type: 'change'});
-
+ happen.once(selection.selectAll('.wiki-title').node(), { type: 'change' });
expect(selection.selectAll('.wiki-lang').value()).to.equal('Deutsch');
expect(selection.selectAll('.wiki-title').value()).to.equal('Title');
});
it('preserves existing language', function() {
- selection.call(iD.ui.preset.wikipedia(field, {}));
+ selection.call(iD.ui.preset.wikipedia(field, context));
selection.selectAll('.wiki-lang').value('Deutsch');
- var wikipedia = iD.ui.preset.wikipedia(field, {});
+ var wikipedia = iD.ui.preset.wikipedia(field, context);
selection.call(wikipedia);
wikipedia.tags({});
expect(selection.selectAll('.wiki-lang').value()).to.equal('Deutsch');
});
+
+ it('does not set delayed wikidata tag if wikipedia field has changed', function(done) {
+ var wikipedia = iD.ui.preset.wikipedia(field, context).entity(entity);
+ wikipedia.on('change', changeTags);
+ selection.call(wikipedia);
+ wikiDelay = 20;
+
+ var spy = sinon.spy();
+ wikipedia.on('change.spy', spy);
+ selection.selectAll('.wiki-lang').value('Deutsch');
+ selection.selectAll('.wiki-title').value('Skip');
+ happen.once(selection.selectAll('.wiki-title').node(), { type: 'change' });
+ happen.once(selection.selectAll('.wiki-title').node(), { type: 'blur' });
+
+ window.setTimeout(function() {
+ selection.selectAll('.wiki-title').value('Title');
+ happen.once(selection.selectAll('.wiki-title').node(), { type: 'change' });
+ happen.once(selection.selectAll('.wiki-title').node(), { type: 'blur' });
+ }, 10);
+
+ window.setTimeout(function() {
+ expect(spy.callCount).to.equal(5);
+ expect(spy.getCall(0)).to.have.been.calledWith({ wikipedia: 'de:Skip', wikidata: undefined }); // 'Skip' on change
+ expect(spy.getCall(1)).to.have.been.calledWith({ wikipedia: 'de:Skip' }); // 'Skip' on blur
+ expect(spy.getCall(2)).to.have.been.calledWith({ wikipedia: 'de:Title', wikidata: undefined }); // 'Title' on change +10ms
+ expect(spy.getCall(3)).to.have.been.calledWith({ wikipedia: 'de:Title' }); // 'Title' on blur +10ms
+ // skip delayed wikidata for 'Skip' // 'Skip' wikidata +20ms
+ expect(spy.getCall(4)).to.have.been.calledWith({ wikipedia: 'de:Title', wikidata: 'Q216353' }); // 'Title' wikidata +40ms
+ done();
+ }, 50);
+ });
+
+ it('does not set delayed wikidata tag if selected entity has changed', function(done) {
+ var wikipedia = iD.ui.preset.wikipedia(field, context).entity(entity);
+ wikipedia.on('change', changeTags);
+ selection.call(wikipedia);
+ wikiDelay = 20;
+
+ var spy = sinon.spy();
+ wikipedia.on('change.spy', spy);
+ selection.selectAll('.wiki-lang').value('Deutsch');
+ selection.selectAll('.wiki-title').value('Title');
+ happen.once(selection.selectAll('.wiki-title').node(), { type: 'change' });
+ happen.once(selection.selectAll('.wiki-title').node(), { type: 'blur' });
+
+ window.setTimeout(function() {
+ selectedId = 'w-123'; // user clicked on something else..
+ }, 10);
+
+ window.setTimeout(function() {
+ expect(spy.callCount).to.equal(2);
+ expect(spy.getCall(0)).to.have.been.calledWith({ wikipedia: 'de:Title', wikidata: undefined }); // 'Title' on change
+ expect(spy.getCall(1)).to.have.been.calledWith({ wikipedia: 'de:Title' }); // 'Title' on blur
+ // wikidata tag not changed because another entity is now selected
+ done();
+ }, 50);
+ });
+
});