modules/ui/success.js (322 lines of code) (raw):
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { select as d3_select } from 'd3-selection';
import { resolveStrings } from 'osm-community-index';
import { fileFetcher } from '../core/file_fetcher';
import { locationManager } from '../core/locations';
import { t, localizer } from '../core/localizer';
import { svgIcon } from '../svg/icon';
import { uiDisclosure } from '../ui/disclosure';
import { utilRebind } from '../util/rebind';
let _oci = null;
export function uiSuccess(context) {
const MAXEVENTS = 2;
const dispatch = d3_dispatch('cancel');
let _changeset;
let _location;
ensureOSMCommunityIndex(); // start fetching the data
function ensureOSMCommunityIndex() {
const data = fileFetcher;
return Promise.all([
data.get('oci_features'),
data.get('oci_resources'),
data.get('oci_defaults')
])
.then(vals => {
if (_oci) return _oci;
// Merge Custom Features
if (vals[0] && Array.isArray(vals[0].features)) {
locationManager.mergeCustomGeoJSON(vals[0]);
}
let ociResources = Object.values(vals[1].resources);
if (ociResources.length) {
// Resolve all locationSet features.
return locationManager.mergeLocationSets(ociResources)
.then(() => {
_oci = {
resources: ociResources,
defaults: vals[2].defaults
};
return _oci;
});
} else {
_oci = {
resources: [], // no resources?
defaults: vals[2].defaults
};
return _oci;
}
});
}
// string-to-date parsing in JavaScript is weird
function parseEventDate(when) {
if (!when) return;
let raw = when.trim();
if (!raw) return;
if (!/Z$/.test(raw)) { // if no trailing 'Z', add one
raw += 'Z'; // this forces date to be parsed as a UTC date
}
const parsed = new Date(raw);
return new Date(parsed.toUTCString().substr(0, 25)); // convert to local timezone
}
function success(selection) {
let header = selection
.append('div')
.attr('class', 'header fillL');
header
.append('h3')
.html(t.html('success.just_edited'));
header
.append('button')
.attr('class', 'close')
.on('click', () => dispatch.call('cancel'))
.call(svgIcon('#iD-icon-close'));
let body = selection
.append('div')
.attr('class', 'body save-success fillL');
let summary = body
.append('div')
.attr('class', 'save-summary');
summary
.append('h3')
.html(t.html('success.thank_you' + (_location ? '_location' : ''), { where: _location }));
summary
.append('p')
.html(t.html('success.help_html'))
.append('a')
.attr('class', 'link-out')
.attr('target', '_blank')
.attr('href', t('success.help_link_url'))
.call(svgIcon('#iD-icon-out-link', 'inline'))
.append('span')
.html(t.html('success.help_link_text'));
let osm = context.connection();
if (!osm) return;
let changesetURL = osm.changesetURL(_changeset.id);
let table = summary
.append('table')
.attr('class', 'summary-table');
let row = table
.append('tr')
.attr('class', 'summary-row');
row
.append('td')
.attr('class', 'cell-icon summary-icon')
.append('a')
.attr('target', '_blank')
.attr('href', changesetURL)
.append('svg')
.attr('class', 'logo-small')
.append('use')
.attr('xlink:href', '#iD-logo-osm');
let summaryDetail = row
.append('td')
.attr('class', 'cell-detail summary-detail');
summaryDetail
.append('a')
.attr('class', 'cell-detail summary-view-on-osm')
.attr('target', '_blank')
.attr('href', changesetURL)
.html(t.html('success.view_on_osm'));
summaryDetail
.append('div')
.html(t.html('success.changeset_id', {
changeset_id: `<a href="${changesetURL}" target="_blank">${_changeset.id}</a>`
}));
// Get OSM community index features intersecting the map..
ensureOSMCommunityIndex()
.then(oci => {
const loc = context.map().center();
const validLocations = locationManager.locationsAt(loc);
// Gather the communities
let communities = [];
oci.resources.forEach(resource => {
let area = validLocations[resource.locationSetID];
if (!area) return;
// Resolve strings
const localizer = (stringID) => t.html(`community.${stringID}`);
resource.resolved = resolveStrings(resource, oci.defaults, localizer);
communities.push({
area: area,
order: resource.order || 0,
resource: resource
});
});
// sort communities by feature area ascending, community order descending
communities.sort((a, b) => a.area - b.area || b.order - a.order);
body
.call(showCommunityLinks, communities.map(c => c.resource));
});
}
function showCommunityLinks(selection, resources) {
let communityLinks = selection
.append('div')
.attr('class', 'save-communityLinks');
communityLinks
.append('h3')
.html(t.html('success.like_osm'));
let table = communityLinks
.append('table')
.attr('class', 'community-table');
let row = table.selectAll('.community-row')
.data(resources);
let rowEnter = row.enter()
.append('tr')
.attr('class', 'community-row');
rowEnter
.append('td')
.attr('class', 'cell-icon community-icon')
.append('a')
.attr('target', '_blank')
.attr('href', d => d.resolved.url)
.append('svg')
.attr('class', 'logo-small')
.append('use')
.attr('xlink:href', d => `#community-${d.type}`);
let communityDetail = rowEnter
.append('td')
.attr('class', 'cell-detail community-detail');
communityDetail
.each(showCommunityDetails);
communityLinks
.append('div')
.attr('class', 'community-missing')
.html(t.html('success.missing'))
.append('a')
.attr('class', 'link-out')
.attr('target', '_blank')
.call(svgIcon('#iD-icon-out-link', 'inline'))
.attr('href', 'https://github.com/osmlab/osm-community-index/issues')
.append('span')
.html(t.html('success.tell_us'));
}
function showCommunityDetails(d) {
let selection = d3_select(this);
let communityID = d.id;
selection
.append('div')
.attr('class', 'community-name')
.html(d.resolved.nameHTML);
selection
.append('div')
.attr('class', 'community-description')
.html(d.resolved.descriptionHTML);
// Create an expanding section if any of these are present..
if (d.resolved.extendedDescriptionHTML || (d.languageCodes && d.languageCodes.length)) {
selection
.append('div')
.call(uiDisclosure(context, `community-more-${d.id}`, false)
.expanded(false)
.updatePreference(false)
.label(t.html('success.more'))
.content(showMore)
);
}
let nextEvents = (d.events || [])
.map(event => {
event.date = parseEventDate(event.when);
return event;
})
.filter(event => { // date is valid and future (or today)
const t = event.date.getTime();
const now = (new Date()).setHours(0,0,0,0);
return !isNaN(t) && t >= now;
})
.sort((a, b) => { // sort by date ascending
return a.date < b.date ? -1 : a.date > b.date ? 1 : 0;
})
.slice(0, MAXEVENTS); // limit number of events shown
if (nextEvents.length) {
selection
.append('div')
.call(uiDisclosure(context, `community-events-${d.id}`, false)
.expanded(false)
.updatePreference(false)
.label(t.html('success.events'))
.content(showNextEvents)
)
.select('.hide-toggle')
.append('span')
.attr('class', 'badge-text')
.html(nextEvents.length);
}
function showMore(selection) {
let more = selection.selectAll('.community-more')
.data([0]);
let moreEnter = more.enter()
.append('div')
.attr('class', 'community-more');
if (d.resolved.extendedDescriptionHTML) {
moreEnter
.append('div')
.attr('class', 'community-extended-description')
.html(d.resolved.extendedDescriptionHTML);
}
if (d.languageCodes && d.languageCodes.length) {
const languageList = d.languageCodes
.map(code => localizer.languageName(code))
.join(', ');
moreEnter
.append('div')
.attr('class', 'community-languages')
.html(t.html('success.languages', { languages: languageList }));
}
}
function showNextEvents(selection) {
let events = selection
.append('div')
.attr('class', 'community-events');
let item = events.selectAll('.community-event')
.data(nextEvents);
let itemEnter = item.enter()
.append('div')
.attr('class', 'community-event');
itemEnter
.append('div')
.attr('class', 'community-event-name')
.append('a')
.attr('target', '_blank')
.attr('href', d => d.url)
.html(d => {
let name = d.name;
if (d.i18n && d.id) {
name = t(`community.${communityID}.events.${d.id}.name`, { default: name });
}
return name;
});
itemEnter
.append('div')
.attr('class', 'community-event-when')
.html(d => {
let options = { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' };
if (d.date.getHours() || d.date.getMinutes()) { // include time if it has one
options.hour = 'numeric';
options.minute = 'numeric';
}
return d.date.toLocaleString(localizer.localeCode(), options);
});
itemEnter
.append('div')
.attr('class', 'community-event-where')
.html(d => {
let where = d.where;
if (d.i18n && d.id) {
where = t(`community.${communityID}.events.${d.id}.where`, { default: where });
}
return where;
});
itemEnter
.append('div')
.attr('class', 'community-event-description')
.html(d => {
let description = d.description;
if (d.i18n && d.id) {
description = t(`community.${communityID}.events.${d.id}.description`, { default: description });
}
return description;
});
}
}
success.changeset = function(val) {
if (!arguments.length) return _changeset;
_changeset = val;
return success;
};
success.location = function(val) {
if (!arguments.length) return _location;
_location = val;
return success;
};
return utilRebind(success, dispatch, 'on');
}