Merge pull request #7095 from openstreetmap/osmose
Add Osmose Q/A layer
@@ -3,7 +3,8 @@
|
||||
|
||||
.error-header-icon .qa_error-fill,
|
||||
.layer-keepRight .qa_error .qa_error-fill,
|
||||
.layer-improveOSM .qa_error .qa_error-fill {
|
||||
.layer-improveOSM .qa_error .qa_error-fill,
|
||||
.layer-osmose .qa_error .qa_error-fill {
|
||||
stroke: #333;
|
||||
stroke-width: 1.3px; /* NOTE: likely a better way to scale the icon stroke */
|
||||
}
|
||||
@@ -152,7 +153,6 @@
|
||||
color: #EC1C24;
|
||||
}
|
||||
|
||||
|
||||
/* Custom Map Data (geojson, gpx, kml, vector tile) */
|
||||
.layer-mapdata {
|
||||
pointer-events: none;
|
||||
@@ -211,4 +211,4 @@
|
||||
stroke: #000;
|
||||
stroke-width: 5px;
|
||||
stroke-miterlimit: 1;
|
||||
}
|
||||
}
|
||||
@@ -2734,18 +2734,19 @@ input.key-trap {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.error-details {
|
||||
padding: 10px;
|
||||
}
|
||||
.error-details-container {
|
||||
background: #ececec;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.error-details-description {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.error-details-description-text::first-letter {
|
||||
text-transform: capitalize;
|
||||
@@ -2753,6 +2754,24 @@ input.key-trap {
|
||||
[dir='rtl'] .error-details-description-text::first-letter {
|
||||
text-transform: none; /* #5877 */
|
||||
}
|
||||
.error-details-subsection h4 {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.error-details code {
|
||||
padding: .2em .4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
font-family: monospace;
|
||||
background-color: rgba(27,31,35,.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.error-details + .translation-link {
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.note-save .new-comment-input,
|
||||
.error-save .new-comment-input {
|
||||
@@ -5590,4 +5609,4 @@ li.hide + li.version .badge .tooltip .popover-arrow {
|
||||
}
|
||||
[dir='rtl'] .list-item-photos.list-item-mapillary-map-features .request-data-link {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
@@ -625,11 +625,14 @@ en:
|
||||
tooltip: Note data from OpenStreetMap
|
||||
title: OpenStreetMap notes
|
||||
keepRight:
|
||||
tooltip: Automatically detected map issues from keepright.at
|
||||
tooltip: Data issues detected by keepright.at
|
||||
title: KeepRight Issues
|
||||
improveOSM:
|
||||
tooltip: Missing data automatically detected by improveosm.org
|
||||
tooltip: Missing data detected by improveosm.org
|
||||
title: ImproveOSM Issues
|
||||
osmose:
|
||||
tooltip: Data issues detected by osmose.openstreetmap.fr
|
||||
title: Osmose Issues
|
||||
custom:
|
||||
tooltip: "Drag and drop a data file onto the page, or click the button to setup"
|
||||
title: Custom Map Data
|
||||
@@ -824,6 +827,13 @@ en:
|
||||
cannot_zoom: "Cannot zoom out further in current mode."
|
||||
full_screen: Toggle Full Screen
|
||||
QA:
|
||||
osmose:
|
||||
title: Osmose Issue
|
||||
detail_title: Details
|
||||
elems_title: Features
|
||||
fix_title: Fix Guidelines
|
||||
trap_title: Common Mistakes
|
||||
translation: Translations provided by Osmose
|
||||
improveOSM:
|
||||
title: ImproveOSM Detection
|
||||
geometry_types:
|
||||
@@ -1343,7 +1353,7 @@ en:
|
||||
title: Quality Assurance
|
||||
intro: "*Quality Assurance* (Q/A) tools can find improper tags, disconnected roads, and other issues with OpenStreetMap, which mappers can then fix. To view existing Q/A issues, click the {data} **Map data** panel to enable a specific Q/A layer."
|
||||
tools_h: "Tools"
|
||||
tools: "The following tools are currently supported: [KeepRight](https://www.keepright.at/) and [ImproveOSM](https://improveosm.org/en/). Expect iD to support [Osmose](https://osmose.openstreetmap.fr/) and more Q/A tools in the future."
|
||||
tools: "The following tools are currently supported: [KeepRight](https://www.keepright.at/), [ImproveOSM](https://improveosm.org/en/) and [Osmose](https://osmose.openstreetmap.fr/)."
|
||||
issues_h: "Handling Issues"
|
||||
issues: "Handling Q/A issues is similar to handling notes. Click on a marker to view the issue details in the sidebar. Each tool has its own capabilities, but generally you can comment and/or close an issue."
|
||||
field:
|
||||
@@ -2058,4 +2068,4 @@ en:
|
||||
wikidata:
|
||||
identifier: "Identifier"
|
||||
label: "Label"
|
||||
description: "Description"
|
||||
description: "Description"
|
||||
@@ -1,36 +1,92 @@
|
||||
{
|
||||
"services": {
|
||||
"improveOSM": {
|
||||
"errorTypes": {
|
||||
"ow": {
|
||||
"icon": "fas-long-arrow-alt-right",
|
||||
"category": "routing"
|
||||
},
|
||||
"mr-both": {
|
||||
"icon": "maki-car",
|
||||
"category": "geometry"
|
||||
},
|
||||
"mr-parking": {
|
||||
"icon": "maki-parking",
|
||||
"category": "geometry"
|
||||
},
|
||||
"mr-path": {
|
||||
"icon": "maki-shoe",
|
||||
"category": "geometry"
|
||||
},
|
||||
"mr-road": {
|
||||
"icon": "maki-car",
|
||||
"category": "geometry"
|
||||
},
|
||||
"tr": {
|
||||
"icon": "temaki-junction",
|
||||
"category": "routing"
|
||||
}
|
||||
"errorIcons": {
|
||||
"ow": "fas-long-arrow-alt-right",
|
||||
"mr-both": "maki-car",
|
||||
"mr-parking": "maki-parking",
|
||||
"mr-path": "maki-shoe",
|
||||
"mr-road": "maki-car",
|
||||
"tr": "temaki-junction"
|
||||
}
|
||||
},
|
||||
"keepRight": {
|
||||
"errorTypes": {
|
||||
|
||||
"osmose": {
|
||||
"errorIcons": {
|
||||
"0-1": "maki-home",
|
||||
"0-2": "maki-home",
|
||||
"1040-1": "maki-square-stroked",
|
||||
"1050-1": "maki-circle-stroked",
|
||||
"1050-1050": "maki-circle-stroked",
|
||||
"1070-1": "maki-home",
|
||||
"1070-4": "maki-dam",
|
||||
"1070-5": "maki-dam",
|
||||
"1070-8": "maki-cross",
|
||||
"1070-10": "maki-cross",
|
||||
"1150-1": "far-clone",
|
||||
"1150-2": "far-clone",
|
||||
"1150-3": "far-clone",
|
||||
"1190-10": "fas-share-alt",
|
||||
"1190-20": "fas-share-alt",
|
||||
"1190-30": "fas-share-alt",
|
||||
"1280-1": "maki-attraction",
|
||||
"2110-21101": "temaki-plaque",
|
||||
"2110-21102": "fas-shapes",
|
||||
"3040-3040": "far-times-circle",
|
||||
"3090-3090": "fas-calendar-alt",
|
||||
"3161-1": "maki-parking",
|
||||
"3161-2": "maki-parking",
|
||||
"3200-32001": "fas-vector-square",
|
||||
"3200-32002": "fas-vector-square",
|
||||
"3200-32003": "fas-vector-square",
|
||||
"3220-32200": "maki-roadblock",
|
||||
"3220-32201": "maki-roadblock",
|
||||
"3250-32501": "maki-watch",
|
||||
"4010-4010": "maki-waste-basket",
|
||||
"4010-40102": "maki-waste-basket",
|
||||
"4030-900": "fas-yin-yang",
|
||||
"4080-1": "far-dot-circle",
|
||||
"4080-2": "far-dot-circle",
|
||||
"4080-3": "far-dot-circle",
|
||||
"5010-803": "fas-sort-alpha-up",
|
||||
"5010-903": "fas-rocket",
|
||||
"5070-50703": "fas-tint-slash",
|
||||
"5070-50704": "fas-code",
|
||||
"5070-50705": "fas-question",
|
||||
"7040-1": "temaki-power_tower",
|
||||
"7040-2": "temaki-power",
|
||||
"7040-4": "maki-marker",
|
||||
"7040-6": "temaki-power",
|
||||
"7090-1": "maki-rail",
|
||||
"7090-3": "maki-circle",
|
||||
"8300-1": "fas-tachometer-alt",
|
||||
"8300-2": "fas-tachometer-alt",
|
||||
"8300-3": "fas-tachometer-alt",
|
||||
"8300-4": "fas-tachometer-alt",
|
||||
"8300-5": "fas-tachometer-alt",
|
||||
"8300-6": "fas-tachometer-alt",
|
||||
"8300-7": "fas-tachometer-alt",
|
||||
"8300-8": "fas-tachometer-alt",
|
||||
"8300-9": "fas-tachometer-alt",
|
||||
"8300-10": "fas-tachometer-alt",
|
||||
"8300-11": "fas-tachometer-alt",
|
||||
"8300-12": "fas-tachometer-alt",
|
||||
"8300-13": "fas-tachometer-alt",
|
||||
"8300-14": "fas-tachometer-alt",
|
||||
"8300-15": "fas-tachometer-alt",
|
||||
"8300-16": "fas-tachometer-alt",
|
||||
"8300-17": "fas-tachometer-alt",
|
||||
"8300-20": "temaki-height_restrictor",
|
||||
"8300-21": "fas-weight-hanging",
|
||||
"8300-32": "maki-circle-stroked",
|
||||
"8300-34": "temaki-diamond",
|
||||
"8300-39": "temaki-pedestrian",
|
||||
"8360-1": "temaki-bench",
|
||||
"8360-2": "maki-bicycle",
|
||||
"8360-3": "temaki-security_camera",
|
||||
"8360-4": "temaki-fire_hydrant",
|
||||
"8360-5": "temaki-traffic_signals",
|
||||
"9010-9010001": "fas-tags",
|
||||
"9010-9010003": "temaki-plaque"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,13 +775,17 @@
|
||||
"title": "OpenStreetMap notes"
|
||||
},
|
||||
"keepRight": {
|
||||
"tooltip": "Automatically detected map issues from keepright.at",
|
||||
"tooltip": "Data issues detected by keepright.at",
|
||||
"title": "KeepRight Issues"
|
||||
},
|
||||
"improveOSM": {
|
||||
"tooltip": "Missing data automatically detected by improveosm.org",
|
||||
"tooltip": "Missing data detected by improveosm.org",
|
||||
"title": "ImproveOSM Issues"
|
||||
},
|
||||
"osmose": {
|
||||
"tooltip": "Data issues detected by osmose.openstreetmap.fr",
|
||||
"title": "Osmose Issues"
|
||||
},
|
||||
"custom": {
|
||||
"tooltip": "Drag and drop a data file onto the page, or click the button to setup",
|
||||
"title": "Custom Map Data",
|
||||
@@ -1027,6 +1031,14 @@
|
||||
"cannot_zoom": "Cannot zoom out further in current mode.",
|
||||
"full_screen": "Toggle Full Screen",
|
||||
"QA": {
|
||||
"osmose": {
|
||||
"title": "Osmose Issue",
|
||||
"detail_title": "Details",
|
||||
"elems_title": "Features",
|
||||
"fix_title": "Fix Guidelines",
|
||||
"trap_title": "Common Mistakes",
|
||||
"translation": "Translations provided by Osmose"
|
||||
},
|
||||
"improveOSM": {
|
||||
"title": "ImproveOSM Detection",
|
||||
"geometry_types": {
|
||||
@@ -1653,7 +1665,7 @@
|
||||
"title": "Quality Assurance",
|
||||
"intro": "*Quality Assurance* (Q/A) tools can find improper tags, disconnected roads, and other issues with OpenStreetMap, which mappers can then fix. To view existing Q/A issues, click the {data} **Map data** panel to enable a specific Q/A layer.",
|
||||
"tools_h": "Tools",
|
||||
"tools": "The following tools are currently supported: [KeepRight](https://www.keepright.at/) and [ImproveOSM](https://improveosm.org/en/). Expect iD to support [Osmose](https://osmose.openstreetmap.fr/) and more Q/A tools in the future.",
|
||||
"tools": "The following tools are currently supported: [KeepRight](https://www.keepright.at/), [ImproveOSM](https://improveosm.org/en/) and [Osmose](https://osmose.openstreetmap.fr/).",
|
||||
"issues_h": "Handling Issues",
|
||||
"issues": "Handling Q/A issues is similar to handling notes. Click on a marker to view the issue details in the sidebar. Each tool has its own capabilities, but generally you can comment and/or close an issue."
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ import { modeDragNode } from './drag_node';
|
||||
import { modeDragNote } from './drag_note';
|
||||
import { uiImproveOsmEditor } from '../ui/improveOSM_editor';
|
||||
import { uiKeepRightEditor } from '../ui/keepRight_editor';
|
||||
import { uiOsmoseEditor } from '../ui/osmose_editor';
|
||||
import { utilKeybinding } from '../util';
|
||||
|
||||
|
||||
@@ -49,6 +50,16 @@ export function modeSelectError(context, selectedErrorID, selectedErrorService)
|
||||
.show(errorEditor.error(error));
|
||||
});
|
||||
break;
|
||||
case 'osmose':
|
||||
errorEditor = uiOsmoseEditor(context)
|
||||
.on('change', function() {
|
||||
context.map().pan([0,0]); // trigger a redraw
|
||||
var error = checkSelectedID();
|
||||
if (!error) return;
|
||||
context.ui().sidebar
|
||||
.show(errorEditor.error(error));
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -154,4 +165,4 @@ export function modeSelectError(context, selectedErrorID, selectedErrorService)
|
||||
|
||||
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
@@ -44,13 +44,8 @@ Object.assign(qaError.prototype, {
|
||||
if (this.service && this.error_type) {
|
||||
var serviceInfo = services[this.service];
|
||||
|
||||
if (serviceInfo) {
|
||||
var errInfo = serviceInfo.errorTypes[this.error_type];
|
||||
|
||||
if (errInfo) {
|
||||
this.icon = errInfo.icon;
|
||||
this.category = errInfo.category;
|
||||
}
|
||||
if (serviceInfo && serviceInfo.errorIcons) {
|
||||
this.icon = serviceInfo.errorIcons[this.error_type];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,4 +60,4 @@ Object.assign(qaError.prototype, {
|
||||
update: function(attrs) {
|
||||
return qaError(this, attrs); // {v: 1 + (this.v || 0)}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -436,9 +436,11 @@ export default {
|
||||
} else {
|
||||
that.removeError(d);
|
||||
if (d.newStatus === 'SOLVED') {
|
||||
// No pretty identifier, so we just use coordinates
|
||||
var closedID = d.loc[1].toFixed(5) + '/' + d.loc[0].toFixed(5);
|
||||
_erCache.closed[key + ':' + closedID] = true;
|
||||
// No error identifier, so we give a count of each category
|
||||
if (!(d.error_key in _erCache.closed)) {
|
||||
_erCache.closed[d.error_key] = 0;
|
||||
}
|
||||
_erCache.closed[d.error_key] += 1;
|
||||
}
|
||||
}
|
||||
if (callback) callback(null, d);
|
||||
@@ -486,7 +488,7 @@ export default {
|
||||
},
|
||||
|
||||
// Used to populate `closed:improveosm` changeset tag
|
||||
getClosedIDs: function() {
|
||||
return Object.keys(_erCache.closed).sort();
|
||||
getClosedCounts: function() {
|
||||
return _erCache.closed;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import serviceKeepRight from './keepRight';
|
||||
import serviceImproveOSM from './improveOSM';
|
||||
import serviceOsmose from './osmose';
|
||||
import serviceMapillary from './mapillary';
|
||||
import serviceMapRules from './maprules';
|
||||
import serviceNominatim from './nominatim';
|
||||
@@ -17,6 +18,7 @@ export var services = {
|
||||
geocoder: serviceNominatim,
|
||||
keepRight: serviceKeepRight,
|
||||
improveOSM: serviceImproveOSM,
|
||||
osmose: serviceOsmose,
|
||||
mapillary: serviceMapillary,
|
||||
openstreetcam: serviceOpenstreetcam,
|
||||
osm: serviceOsm,
|
||||
@@ -32,6 +34,7 @@ export var services = {
|
||||
export {
|
||||
serviceKeepRight,
|
||||
serviceImproveOSM,
|
||||
serviceOsmose,
|
||||
serviceMapillary,
|
||||
serviceMapRules,
|
||||
serviceNominatim,
|
||||
@@ -43,4 +46,4 @@ export {
|
||||
serviceVectorTile,
|
||||
serviceWikidata,
|
||||
serviceWikipedia
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,351 @@
|
||||
import RBush from 'rbush';
|
||||
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import { json as d3_json } from 'd3-fetch';
|
||||
|
||||
import { currentLocale } from '../util/locale';
|
||||
import { geoExtent, geoVecAdd } from '../geo';
|
||||
import { qaError } from '../osm';
|
||||
import { utilRebind, utilTiler, utilQsString } from '../util';
|
||||
import { services as qaServices } from '../../data/qa_errors.json';
|
||||
|
||||
const tiler = utilTiler();
|
||||
const dispatch = d3_dispatch('loaded');
|
||||
const _osmoseUrlRoot = 'https://osmose.openstreetmap.fr/en/api/0.3beta';
|
||||
const _osmoseItems =
|
||||
Object.keys(qaServices.osmose.errorIcons)
|
||||
.map(s => s.split('-')[0])
|
||||
.reduce((unique, item) => unique.indexOf(item) !== -1 ? unique : [...unique, item], []);
|
||||
const _erZoom = 14;
|
||||
const _stringCache = {};
|
||||
const _colorCache = {};
|
||||
|
||||
// This gets reassigned if reset
|
||||
let _erCache;
|
||||
|
||||
function abortRequest(controller) {
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
}
|
||||
}
|
||||
|
||||
function abortUnwantedRequests(cache, tiles) {
|
||||
Object.keys(cache.inflightTile).forEach(k => {
|
||||
let wanted = tiles.find(tile => k === tile.id);
|
||||
if (!wanted) {
|
||||
abortRequest(cache.inflightTile[k]);
|
||||
delete cache.inflightTile[k];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function encodeErrorRtree(d) {
|
||||
return { minX: d.loc[0], minY: d.loc[1], maxX: d.loc[0], maxY: d.loc[1], data: d };
|
||||
}
|
||||
|
||||
// replace or remove error from rtree
|
||||
function updateRtree(item, replace) {
|
||||
_erCache.rtree.remove(item, (a, b) => a.data.id === b.data.id);
|
||||
|
||||
if (replace) {
|
||||
_erCache.rtree.insert(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Errors shouldn't obscure eachother
|
||||
function preventCoincident(loc) {
|
||||
let coincident = false;
|
||||
do {
|
||||
// first time, move marker up. after that, move marker right.
|
||||
let delta = coincident ? [0.00001, 0] : [0, 0.00001];
|
||||
loc = geoVecAdd(loc, delta);
|
||||
let bbox = geoExtent(loc).bbox();
|
||||
coincident = _erCache.rtree.search(bbox).length;
|
||||
} while (coincident);
|
||||
|
||||
return loc;
|
||||
}
|
||||
|
||||
export default {
|
||||
init() {
|
||||
if (!_erCache) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
this.event = utilRebind(this, dispatch, 'on');
|
||||
},
|
||||
|
||||
reset() {
|
||||
if (_erCache) {
|
||||
Object.values(_erCache.inflightTile).forEach(abortRequest);
|
||||
}
|
||||
_erCache = {
|
||||
data: {},
|
||||
loadedTile: {},
|
||||
inflightTile: {},
|
||||
inflightPost: {},
|
||||
closed: {},
|
||||
rtree: new RBush()
|
||||
};
|
||||
},
|
||||
|
||||
loadErrors(projection) {
|
||||
let params = {
|
||||
// Tiles return a maximum # of errors
|
||||
// So we want to filter our request for only types iD supports
|
||||
item: _osmoseItems
|
||||
};
|
||||
|
||||
// determine the needed tiles to cover the view
|
||||
let tiles = tiler
|
||||
.zoomExtent([_erZoom, _erZoom])
|
||||
.getTiles(projection);
|
||||
|
||||
// abort inflight requests that are no longer needed
|
||||
abortUnwantedRequests(_erCache, tiles);
|
||||
|
||||
// issue new requests..
|
||||
tiles.forEach(tile => {
|
||||
if (_erCache.loadedTile[tile.id] || _erCache.inflightTile[tile.id]) return;
|
||||
|
||||
let [ x, y, z ] = tile.xyz;
|
||||
let url = `${_osmoseUrlRoot}/issues/${z}/${x}/${y}.json?` + utilQsString(params);
|
||||
|
||||
let controller = new AbortController();
|
||||
_erCache.inflightTile[tile.id] = controller;
|
||||
|
||||
d3_json(url, { signal: controller.signal })
|
||||
.then(data => {
|
||||
delete _erCache.inflightTile[tile.id];
|
||||
_erCache.loadedTile[tile.id] = true;
|
||||
|
||||
if (data.features) {
|
||||
data.features.forEach(issue => {
|
||||
const { item, class: error_class, uuid: identifier } = issue.properties;
|
||||
// Item is the type of error, w/ class tells us the sub-type
|
||||
const error_type = `${item}-${error_class}`;
|
||||
|
||||
// Filter out unsupported error types (some are too specific or advanced)
|
||||
if (error_type in qaServices.osmose.errorIcons) {
|
||||
let loc = issue.geometry.coordinates; // lon, lat
|
||||
loc = preventCoincident(loc);
|
||||
|
||||
let d = new qaError({
|
||||
// Info required for every error
|
||||
loc,
|
||||
service: 'osmose',
|
||||
error_type,
|
||||
// Extra details needed for this service
|
||||
identifier, // needed to query and update the error
|
||||
item // category of the issue for styling
|
||||
});
|
||||
|
||||
// Setting elems here prevents UI error detail requests
|
||||
if (d.item === 8300 || d.item === 8360) {
|
||||
d.elems = [];
|
||||
}
|
||||
|
||||
_erCache.data[d.id] = d;
|
||||
_erCache.rtree.insert(encodeErrorRtree(d));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispatch.call('loaded');
|
||||
})
|
||||
.catch(() => {
|
||||
delete _erCache.inflightTile[tile.id];
|
||||
_erCache.loadedTile[tile.id] = true;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
loadErrorDetail(d) {
|
||||
// Error details only need to be fetched once
|
||||
if (d.elems !== undefined) {
|
||||
return Promise.resolve(d);
|
||||
}
|
||||
|
||||
const url = `${_osmoseUrlRoot}/issue/${d.identifier}?langs=${currentLocale}`;
|
||||
const cacheDetails = data => {
|
||||
// Associated elements used for highlighting
|
||||
// Assign directly for immediate use in the callback
|
||||
d.elems = data.elems.map(e => e.type.substring(0,1) + e.id);
|
||||
|
||||
// Some issues have instance specific detail in a subtitle
|
||||
d.detail = data.subtitle;
|
||||
|
||||
this.replaceError(d);
|
||||
};
|
||||
|
||||
return jsonPromise(url, cacheDetails)
|
||||
.then(() => d);
|
||||
},
|
||||
|
||||
loadStrings(callback, locale=currentLocale) {
|
||||
const issueTypes = Object.keys(qaServices.osmose.errorIcons);
|
||||
|
||||
if (
|
||||
locale in _stringCache
|
||||
&& Object.keys(_stringCache[locale]).length === issueTypes.length
|
||||
) {
|
||||
if (callback) callback(null, _stringCache[locale]);
|
||||
return;
|
||||
}
|
||||
|
||||
// May be partially populated already if some requests were successful
|
||||
if (!(locale in _stringCache)) {
|
||||
_stringCache[locale] = {};
|
||||
}
|
||||
|
||||
const format = string => {
|
||||
// Some strings contain markdown syntax
|
||||
string = string.replace(/\[((?:.|\n)+?)\]\((.+?)\)/g, '<a href="$2">$1</a>');
|
||||
return string.replace(/`(.+?)`/g, '<code>$1</code>');
|
||||
};
|
||||
|
||||
// Only need to cache strings for supported issue types
|
||||
// Using multiple individual item + class requests to reduce fetched data size
|
||||
const allRequests = issueTypes.map(issueType => {
|
||||
// No need to request data we already have
|
||||
if (issueType in _stringCache[locale]) return;
|
||||
|
||||
const cacheData = data => {
|
||||
// Bunch of nested single value arrays of objects
|
||||
const [ cat = {items:[]} ] = data.categories;
|
||||
const [ item = {class:[]} ] = cat.items;
|
||||
const [ cl = null ] = item.class;
|
||||
|
||||
// If null default value is reached, data wasn't as expected (or was empty)
|
||||
if (!cl) {
|
||||
/* eslint-disable no-console */
|
||||
console.log(`Osmose strings request (${issueType}) had unexpected data`);
|
||||
/* eslint-enable no-console */
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache served item colors to automatically style issue markers later
|
||||
const { item: itemInt, color } = item;
|
||||
if (/^#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/.test(color)) {
|
||||
_colorCache[itemInt] = color;
|
||||
}
|
||||
|
||||
// Value of root key will be null if no string exists
|
||||
// If string exists, value is an object with key 'auto' for string
|
||||
const { title, detail, fix, trap } = cl;
|
||||
|
||||
let issueStrings = {};
|
||||
if (title) issueStrings.title = title.auto;
|
||||
if (detail) issueStrings.detail = format(detail.auto);
|
||||
if (trap) issueStrings.trap = format(trap.auto);
|
||||
if (fix) issueStrings.fix = format(fix.auto);
|
||||
|
||||
_stringCache[locale][issueType] = issueStrings;
|
||||
};
|
||||
|
||||
const [ item, cl ] = issueType.split('-');
|
||||
|
||||
// Osmose API falls back to English strings where untranslated or if locale doesn't exist
|
||||
const url = `${_osmoseUrlRoot}/items/${item}/class/${cl}?langs=${locale}`;
|
||||
|
||||
return jsonPromise(url, cacheData);
|
||||
});
|
||||
|
||||
Promise.all(allRequests)
|
||||
.then(() => { if (callback) callback(null, _stringCache[locale]); })
|
||||
.catch(err => { if (callback) callback(err); });
|
||||
},
|
||||
|
||||
getStrings(issueType, locale=currentLocale) {
|
||||
// No need to fallback to English, Osmose API handles this for us
|
||||
return (locale in _stringCache) ? _stringCache[locale][issueType] : {};
|
||||
},
|
||||
|
||||
getColor(itemType) {
|
||||
return (itemType in _colorCache) ? _colorCache[itemType] : '#FFFFFF';
|
||||
},
|
||||
|
||||
postUpdate(d, callback) {
|
||||
if (_erCache.inflightPost[d.id]) {
|
||||
return callback({ message: 'Error update already inflight', status: -2 }, d);
|
||||
}
|
||||
|
||||
// UI sets the status to either 'done' or 'false'
|
||||
let url = `${_osmoseUrlRoot}/issue/${d.identifier}/${d.newStatus}`;
|
||||
|
||||
let controller = new AbortController();
|
||||
_erCache.inflightPost[d.id] = controller;
|
||||
|
||||
fetch(url, { signal: controller.signal })
|
||||
.then(() => {
|
||||
delete _erCache.inflightPost[d.id];
|
||||
|
||||
this.removeError(d);
|
||||
if (d.newStatus === 'done') {
|
||||
// No error identifier, so we give a count of each category
|
||||
if (!(d.item in _erCache.closed)) {
|
||||
_erCache.closed[d.item] = 0;
|
||||
}
|
||||
_erCache.closed[d.item] += 1;
|
||||
}
|
||||
if (callback) callback(null, d);
|
||||
})
|
||||
.catch(err => {
|
||||
delete _erCache.inflightPost[d.id];
|
||||
if (callback) callback(err.message);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
// get all cached errors covering the viewport
|
||||
getErrors(projection) {
|
||||
let viewport = projection.clipExtent();
|
||||
let min = [viewport[0][0], viewport[1][1]];
|
||||
let max = [viewport[1][0], viewport[0][1]];
|
||||
let bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();
|
||||
|
||||
return _erCache.rtree.search(bbox).map(d => {
|
||||
return d.data;
|
||||
});
|
||||
},
|
||||
|
||||
// get a single error from the cache
|
||||
getError(id) {
|
||||
return _erCache.data[id];
|
||||
},
|
||||
|
||||
// replace a single error in the cache
|
||||
replaceError(error) {
|
||||
if (!(error instanceof qaError) || !error.id) return;
|
||||
|
||||
_erCache.data[error.id] = error;
|
||||
updateRtree(encodeErrorRtree(error), true); // true = replace
|
||||
return error;
|
||||
},
|
||||
|
||||
// remove a single error from the cache
|
||||
removeError(error) {
|
||||
if (!(error instanceof qaError) || !error.id) return;
|
||||
|
||||
delete _erCache.data[error.id];
|
||||
updateRtree(encodeErrorRtree(error), false); // false = remove
|
||||
},
|
||||
|
||||
// Used to populate `closed:osmose:*` changeset tags
|
||||
getClosedCounts() {
|
||||
return _erCache.closed;
|
||||
}
|
||||
};
|
||||
|
||||
function jsonPromise(url, then) {
|
||||
return new Promise((resolve, reject) => {
|
||||
d3_json(url)
|
||||
.then(data => {
|
||||
then(data);
|
||||
resolve();
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -119,8 +119,7 @@ export function svgImproveOSM(projection, context, dispatch) {
|
||||
'qa_error',
|
||||
d.service,
|
||||
'error_id-' + d.id,
|
||||
'error_type-' + d.error_type,
|
||||
'category-' + d.category
|
||||
'error_type-' + d.error_type
|
||||
].join(' ');
|
||||
});
|
||||
|
||||
@@ -258,4 +257,4 @@ export function svgImproveOSM(projection, context, dispatch) {
|
||||
|
||||
|
||||
return drawImproveOSM;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { svgDebug } from './debug';
|
||||
import { svgGeolocate } from './geolocate';
|
||||
import { svgKeepRight } from './keepRight';
|
||||
import { svgImproveOSM } from './improveOSM';
|
||||
import { svgOsmose } from './osmose';
|
||||
import { svgStreetside } from './streetside';
|
||||
import { svgMapillaryImages } from './mapillary_images';
|
||||
import { svgMapillarySigns } from './mapillary_signs';
|
||||
@@ -27,6 +28,7 @@ export function svgLayers(projection, context) {
|
||||
{ id: 'data', layer: svgData(projection, context, dispatch) },
|
||||
{ id: 'keepRight', layer: svgKeepRight(projection, context, dispatch) },
|
||||
{ id: 'improveOSM', layer: svgImproveOSM(projection, context, dispatch) },
|
||||
{ id: 'osmose', layer: svgOsmose(projection, context, dispatch) },
|
||||
{ id: 'streetside', layer: svgStreetside(projection, context, dispatch)},
|
||||
{ id: 'mapillary', layer: svgMapillaryImages(projection, context, dispatch) },
|
||||
{ id: 'mapillary-map-features', layer: svgMapillaryMapFeatures(projection, context, dispatch) },
|
||||
@@ -116,4 +118,4 @@ export function svgLayers(projection, context) {
|
||||
|
||||
|
||||
return utilRebind(drawLayers, dispatch, 'on');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
import _throttle from 'lodash-es/throttle';
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
|
||||
import { modeBrowse } from '../modes/browse';
|
||||
import { svgPointTransform } from './helpers';
|
||||
import { services } from '../services';
|
||||
|
||||
var _osmoseEnabled = false;
|
||||
var _errorService;
|
||||
|
||||
|
||||
export function svgOsmose(projection, context, dispatch) {
|
||||
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
|
||||
var minZoom = 12;
|
||||
var touchLayer = d3_select(null);
|
||||
var drawLayer = d3_select(null);
|
||||
var _osmoseVisible = false;
|
||||
|
||||
function markerPath(selection, klass) {
|
||||
selection
|
||||
.attr('class', klass)
|
||||
.attr('transform', 'translate(-10, -28)')
|
||||
.attr('points', '16,3 4,3 1,6 1,17 4,20 7,20 10,27 13,20 16,20 19,17.033 19,6');
|
||||
}
|
||||
|
||||
|
||||
// Loosely-coupled osmose service for fetching errors.
|
||||
function getService() {
|
||||
if (services.osmose && !_errorService) {
|
||||
_errorService = services.osmose;
|
||||
_errorService.on('loaded', throttledRedraw);
|
||||
} else if (!services.osmose && _errorService) {
|
||||
_errorService = null;
|
||||
}
|
||||
|
||||
return _errorService;
|
||||
}
|
||||
|
||||
|
||||
// Show the errors
|
||||
function editOn() {
|
||||
if (!_osmoseVisible) {
|
||||
_osmoseVisible = true;
|
||||
drawLayer
|
||||
.style('display', 'block');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Immediately remove the errors and their touch targets
|
||||
function editOff() {
|
||||
if (_osmoseVisible) {
|
||||
_osmoseVisible = false;
|
||||
drawLayer
|
||||
.style('display', 'none');
|
||||
drawLayer.selectAll('.qa_error.osmose')
|
||||
.remove();
|
||||
touchLayer.selectAll('.qa_error.osmose')
|
||||
.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Enable the layer. This shows the errors and transitions them to visible.
|
||||
function layerOn() {
|
||||
// Strings supplied by Osmose fetched before showing layer for first time
|
||||
// NOTE: Currently no way to change locale in iD at runtime, would need to re-call this method if that's ever implemented
|
||||
// FIXME: If layer is toggled quickly multiple requests are sent
|
||||
// FIXME: No error handling in place
|
||||
getService().loadStrings(editOn);
|
||||
|
||||
drawLayer
|
||||
.style('opacity', 0)
|
||||
.transition()
|
||||
.duration(250)
|
||||
.style('opacity', 1)
|
||||
.on('end interrupt', function () {
|
||||
dispatch.call('change');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Disable the layer. This transitions the layer invisible and then hides the errors.
|
||||
function layerOff() {
|
||||
throttledRedraw.cancel();
|
||||
drawLayer.interrupt();
|
||||
touchLayer.selectAll('.qa_error.osmose')
|
||||
.remove();
|
||||
|
||||
drawLayer
|
||||
.transition()
|
||||
.duration(250)
|
||||
.style('opacity', 0)
|
||||
.on('end interrupt', function () {
|
||||
editOff();
|
||||
dispatch.call('change');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Update the error markers
|
||||
function updateMarkers() {
|
||||
if (!_osmoseVisible || !_osmoseEnabled) return;
|
||||
|
||||
var service = getService();
|
||||
var selectedID = context.selectedErrorID();
|
||||
var data = (service ? service.getErrors(projection) : []);
|
||||
var getTransform = svgPointTransform(projection);
|
||||
|
||||
// Draw markers..
|
||||
var markers = drawLayer.selectAll('.qa_error.osmose')
|
||||
.data(data, function(d) { return d.id; });
|
||||
|
||||
// exit
|
||||
markers.exit()
|
||||
.remove();
|
||||
|
||||
// enter
|
||||
var markersEnter = markers.enter()
|
||||
.append('g')
|
||||
.attr('class', function(d) {
|
||||
return [
|
||||
'qa_error',
|
||||
d.service,
|
||||
'error_id-' + d.id,
|
||||
'error_type-' + d.error_type,
|
||||
'item-' + d.item
|
||||
].join(' ');
|
||||
});
|
||||
|
||||
markersEnter
|
||||
.append('polygon')
|
||||
.call(markerPath, 'shadow');
|
||||
|
||||
markersEnter
|
||||
.append('ellipse')
|
||||
.attr('cx', 0)
|
||||
.attr('cy', 0)
|
||||
.attr('rx', 4.5)
|
||||
.attr('ry', 2)
|
||||
.attr('class', 'stroke');
|
||||
|
||||
markersEnter
|
||||
.append('polygon')
|
||||
.attr('fill', d => getService().getColor(d.item))
|
||||
.call(markerPath, 'qa_error-fill');
|
||||
|
||||
markersEnter
|
||||
.append('use')
|
||||
.attr('transform', 'translate(-5.5, -21)')
|
||||
.attr('class', 'icon-annotation')
|
||||
.attr('width', '11px')
|
||||
.attr('height', '11px')
|
||||
.attr('xlink:href', function(d) {
|
||||
var picon = d.icon;
|
||||
|
||||
if (!picon) {
|
||||
return '';
|
||||
} else {
|
||||
var isMaki = /^maki-/.test(picon);
|
||||
return '#' + picon + (isMaki ? '-11' : '');
|
||||
}
|
||||
});
|
||||
|
||||
// update
|
||||
markers
|
||||
.merge(markersEnter)
|
||||
.sort(sortY)
|
||||
.classed('selected', function(d) { return d.id === selectedID; })
|
||||
.attr('transform', getTransform);
|
||||
|
||||
|
||||
// Draw targets..
|
||||
if (touchLayer.empty()) return;
|
||||
var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';
|
||||
|
||||
var targets = touchLayer.selectAll('.qa_error.osmose')
|
||||
.data(data, function(d) { return d.id; });
|
||||
|
||||
// exit
|
||||
targets.exit()
|
||||
.remove();
|
||||
|
||||
// enter/update
|
||||
targets.enter()
|
||||
.append('rect')
|
||||
.attr('width', '20px')
|
||||
.attr('height', '30px')
|
||||
.attr('x', '-10px')
|
||||
.attr('y', '-28px')
|
||||
.merge(targets)
|
||||
.sort(sortY)
|
||||
.attr('class', function(d) {
|
||||
return 'qa_error ' + d.service + ' target error_id-' + d.id + ' ' + fillClass;
|
||||
})
|
||||
.attr('transform', getTransform);
|
||||
|
||||
|
||||
function sortY(a, b) {
|
||||
return (a.id === selectedID) ? 1
|
||||
: (b.id === selectedID) ? -1
|
||||
: b.loc[1] - a.loc[1];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Draw the Osmose layer and schedule loading errors and updating markers.
|
||||
function drawOsmose(selection) {
|
||||
var service = getService();
|
||||
|
||||
var surface = context.surface();
|
||||
if (surface && !surface.empty()) {
|
||||
touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');
|
||||
}
|
||||
|
||||
drawLayer = selection.selectAll('.layer-osmose')
|
||||
.data(service ? [0] : []);
|
||||
|
||||
drawLayer.exit()
|
||||
.remove();
|
||||
|
||||
drawLayer = drawLayer.enter()
|
||||
.append('g')
|
||||
.attr('class', 'layer-osmose')
|
||||
.style('display', _osmoseEnabled ? 'block' : 'none')
|
||||
.merge(drawLayer);
|
||||
|
||||
if (_osmoseEnabled) {
|
||||
if (service && ~~context.map().zoom() >= minZoom) {
|
||||
editOn();
|
||||
service.loadErrors(projection);
|
||||
updateMarkers();
|
||||
} else {
|
||||
editOff();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Toggles the layer on and off
|
||||
drawOsmose.enabled = function(val) {
|
||||
if (!arguments.length) return _osmoseEnabled;
|
||||
|
||||
_osmoseEnabled = val;
|
||||
if (_osmoseEnabled) {
|
||||
layerOn();
|
||||
} else {
|
||||
layerOff();
|
||||
if (context.selectedErrorID()) {
|
||||
context.enter(modeBrowse(context));
|
||||
}
|
||||
}
|
||||
|
||||
dispatch.call('change');
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
drawOsmose.supported = function() {
|
||||
return !!getService();
|
||||
};
|
||||
|
||||
|
||||
return drawOsmose;
|
||||
}
|
||||
@@ -25,7 +25,11 @@ var readOnlyTags = [
|
||||
/^host$/,
|
||||
/^locale$/,
|
||||
/^warnings:/,
|
||||
/^resolved:/
|
||||
/^resolved:/,
|
||||
/^closed:note$/,
|
||||
/^closed:keepright$/,
|
||||
/^closed:improveosm:/,
|
||||
/^closed:osmose:/
|
||||
];
|
||||
|
||||
// treat most punctuation (except -, _, +, &) as hashtag delimiters - #4398
|
||||
@@ -134,6 +138,7 @@ export function uiCommit(context) {
|
||||
|
||||
// assign tags for closed issues and notes
|
||||
var osmClosed = osm.getClosedIDs();
|
||||
var issueType;
|
||||
if (osmClosed.length) {
|
||||
tags['closed:note'] = osmClosed.join(';').substr(0, tagCharLimit);
|
||||
}
|
||||
@@ -144,9 +149,15 @@ export function uiCommit(context) {
|
||||
}
|
||||
}
|
||||
if (services.improveOSM) {
|
||||
var iOsmClosed = services.improveOSM.getClosedIDs();
|
||||
if (iOsmClosed.length) {
|
||||
tags['closed:improveosm'] = iOsmClosed.join(';').substr(0, tagCharLimit);
|
||||
var iOsmClosed = services.improveOSM.getClosedCounts();
|
||||
for (issueType in iOsmClosed) {
|
||||
tags['closed:improveosm:' + issueType] = iOsmClosed[issueType].toString().substr(0, tagCharLimit);
|
||||
}
|
||||
}
|
||||
if (services.osmose) {
|
||||
var osmoseClosed = services.osmose.getClosedCounts();
|
||||
for (issueType in osmoseClosed) {
|
||||
tags['closed:osmose:' + issueType] = osmoseClosed[issueType].toString().substr(0, tagCharLimit);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,11 +78,11 @@ export function uiImproveOsmDetails(context) {
|
||||
|
||||
// Add click handler
|
||||
link
|
||||
.on('mouseover', function() {
|
||||
.on('mouseenter', function() {
|
||||
context.surface().selectAll(utilEntityOrMemberSelector([entityID], context.graph()))
|
||||
.classed('hover', true);
|
||||
})
|
||||
.on('mouseout', function() {
|
||||
.on('mouseleave', function() {
|
||||
context.surface().selectAll('.hover')
|
||||
.classed('hover', false);
|
||||
})
|
||||
@@ -122,6 +122,7 @@ export function uiImproveOsmDetails(context) {
|
||||
|
||||
// Don't hide entities related to this error - #5880
|
||||
context.features().forceVisible(relatedEntities);
|
||||
context.map().pan([0,0]); // trigger a redraw
|
||||
}
|
||||
|
||||
|
||||
@@ -133,4 +134,4 @@ export function uiImproveOsmDetails(context) {
|
||||
|
||||
|
||||
return improveOsmDetails;
|
||||
}
|
||||
}
|
||||
@@ -51,8 +51,7 @@ export function uiImproveOsmHeader() {
|
||||
'qa_error',
|
||||
d.service,
|
||||
'error_id-' + d.id,
|
||||
'error_type-' + d.error_type,
|
||||
'category-' + d.category
|
||||
'error_type-' + d.error_type
|
||||
].join(' ');
|
||||
});
|
||||
|
||||
@@ -94,4 +93,4 @@ export function uiImproveOsmHeader() {
|
||||
|
||||
|
||||
return improveOsmHeader;
|
||||
}
|
||||
}
|
||||
@@ -80,11 +80,11 @@ export function uiKeepRightDetails(context) {
|
||||
|
||||
// Add click handler
|
||||
link
|
||||
.on('mouseover', function() {
|
||||
.on('mouseenter', function() {
|
||||
context.surface().selectAll(utilEntityOrMemberSelector([entityID], context.graph()))
|
||||
.classed('hover', true);
|
||||
})
|
||||
.on('mouseout', function() {
|
||||
.on('mouseleave', function() {
|
||||
context.surface().selectAll('.hover')
|
||||
.classed('hover', false);
|
||||
})
|
||||
@@ -124,6 +124,7 @@ export function uiKeepRightDetails(context) {
|
||||
|
||||
// Don't hide entities related to this error - #5880
|
||||
context.features().forceVisible(relatedEntities);
|
||||
context.map().pan([0,0]); // trigger a redraw
|
||||
}
|
||||
|
||||
|
||||
@@ -135,4 +136,4 @@ export function uiKeepRightDetails(context) {
|
||||
|
||||
|
||||
return keepRightDetails;
|
||||
}
|
||||
}
|
||||
@@ -341,7 +341,7 @@ export function uiMapData(context) {
|
||||
|
||||
|
||||
function drawQAItems(selection) {
|
||||
var qaKeys = ['keepRight', 'improveOSM'];
|
||||
var qaKeys = ['keepRight', 'improveOSM', 'osmose'];
|
||||
var qaLayers = layers.all().filter(function(obj) { return qaKeys.indexOf(obj.id) !== -1; });
|
||||
|
||||
var ul = selection
|
||||
@@ -916,4 +916,4 @@ export function uiMapData(context) {
|
||||
};
|
||||
|
||||
return uiMapData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import {
|
||||
event as d3_event,
|
||||
select as d3_select
|
||||
} from 'd3-selection';
|
||||
|
||||
import { modeSelect } from '../modes/select';
|
||||
import { t } from '../util/locale';
|
||||
import { services } from '../services';
|
||||
import { utilDisplayName, utilEntityOrMemberSelector } from '../util';
|
||||
|
||||
|
||||
export function uiOsmoseDetails(context) {
|
||||
let _error;
|
||||
|
||||
function issueString(d, type) {
|
||||
if (!d) return '';
|
||||
|
||||
// Issue strings are cached from Osmose API
|
||||
const s = services.osmose.getStrings(d.error_type);
|
||||
return (type in s) ? s[type] : '';
|
||||
}
|
||||
|
||||
|
||||
function osmoseDetails(selection) {
|
||||
const details = selection.selectAll('.error-details')
|
||||
.data(
|
||||
_error ? [_error] : [],
|
||||
d => `${d.id}-${d.status || 0}`
|
||||
);
|
||||
|
||||
details.exit()
|
||||
.remove();
|
||||
|
||||
const detailsEnter = details.enter()
|
||||
.append('div')
|
||||
.attr('class', 'error-details error-details-container');
|
||||
|
||||
|
||||
// Description
|
||||
if (issueString(_error, 'detail')) {
|
||||
const div = detailsEnter
|
||||
.append('div')
|
||||
.attr('class', 'error-details-subsection');
|
||||
|
||||
div
|
||||
.append('h4')
|
||||
.text(() => t('QA.keepRight.detail_description'));
|
||||
|
||||
div
|
||||
.append('p')
|
||||
.attr('class', 'error-details-description-text')
|
||||
.html(d => issueString(d, 'detail'));
|
||||
}
|
||||
|
||||
// Elements (populated later as data is requested)
|
||||
const detailsDiv = detailsEnter
|
||||
.append('div')
|
||||
.attr('class', 'error-details-subsection');
|
||||
|
||||
const elemsDiv = detailsEnter
|
||||
.append('div')
|
||||
.attr('class', 'error-details-subsection');
|
||||
|
||||
// Suggested Fix (musn't exist for every issue type)
|
||||
if (issueString(_error, 'fix')) {
|
||||
const div = detailsEnter
|
||||
.append('div')
|
||||
.attr('class', 'error-details-subsection');
|
||||
|
||||
div
|
||||
.append('h4')
|
||||
.text(() => t('QA.osmose.fix_title'));
|
||||
|
||||
div
|
||||
.append('p')
|
||||
.html(d => issueString(d, 'fix'));
|
||||
}
|
||||
|
||||
// Common Pitfalls (musn't exist for every issue type)
|
||||
if (issueString(_error, 'trap')) {
|
||||
const div = detailsEnter
|
||||
.append('div')
|
||||
.attr('class', 'error-details-subsection');
|
||||
|
||||
div
|
||||
.append('h4')
|
||||
.text(() => t('QA.osmose.trap_title'));
|
||||
|
||||
div
|
||||
.append('p')
|
||||
.html(d => issueString(d, 'trap'));
|
||||
}
|
||||
|
||||
// Translation link below details container
|
||||
selection
|
||||
.append('div')
|
||||
.attr('class', 'translation-link')
|
||||
.append('a')
|
||||
.attr('target', '_blank')
|
||||
.attr('rel', 'noopener noreferrer') // security measure
|
||||
.attr('href', 'https://www.transifex.com/openstreetmap-france/osmose')
|
||||
.text(() => t('QA.osmose.translation'))
|
||||
.append('svg')
|
||||
.attr('class', 'icon inline')
|
||||
.append('use')
|
||||
.attr('href', '#iD-icon-out-link');
|
||||
|
||||
services.osmose.loadErrorDetail(_error)
|
||||
.then(d => {
|
||||
// No details to add if there are no associated issue elements
|
||||
if (!d.elems || d.elems.length === 0) return;
|
||||
|
||||
// TODO: Do nothing if UI has moved on by the time this resolves
|
||||
|
||||
// Things like keys and values are dynamically added to a subtitle string
|
||||
if (d.detail) {
|
||||
detailsDiv
|
||||
.append('h4')
|
||||
.attr('class', 'error-details-subtitle')
|
||||
.text(() => t('QA.osmose.detail_title'));
|
||||
|
||||
detailsDiv
|
||||
.append('p')
|
||||
.html(d => d.detail);
|
||||
}
|
||||
|
||||
// Create list of linked issue elements
|
||||
elemsDiv
|
||||
.append('h4')
|
||||
.attr('class', 'error-details-subtitle')
|
||||
.text(() => t('QA.osmose.elems_title'));
|
||||
|
||||
elemsDiv
|
||||
.append('ul')
|
||||
.attr('class', 'error-details-elements')
|
||||
.selectAll('.error_entity_link')
|
||||
.data(d.elems)
|
||||
.enter()
|
||||
.append('li')
|
||||
.append('a')
|
||||
.attr('class', 'error_entity_link')
|
||||
.text(d => d)
|
||||
.each(function() {
|
||||
const link = d3_select(this);
|
||||
const entityID = this.textContent;
|
||||
const entity = context.hasEntity(entityID);
|
||||
|
||||
// Add click handler
|
||||
link
|
||||
.on('mouseenter', () => {
|
||||
context.surface().selectAll(utilEntityOrMemberSelector([entityID], context.graph()))
|
||||
.classed('hover', true);
|
||||
})
|
||||
.on('mouseleave', () => {
|
||||
context.surface().selectAll('.hover')
|
||||
.classed('hover', false);
|
||||
})
|
||||
.on('click', () => {
|
||||
d3_event.preventDefault();
|
||||
const osmlayer = context.layers().layer('osm');
|
||||
if (!osmlayer.enabled()) {
|
||||
osmlayer.enabled(true);
|
||||
}
|
||||
|
||||
context.map().centerZoom(d.loc, 20);
|
||||
|
||||
if (entity) {
|
||||
context.enter(modeSelect(context, [entityID]));
|
||||
} else {
|
||||
context.loadEntity(entityID, () => {
|
||||
context.enter(modeSelect(context, [entityID]));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Replace with friendly name if possible
|
||||
// (The entity may not yet be loaded into the graph)
|
||||
if (entity) {
|
||||
let name = utilDisplayName(entity); // try to use common name
|
||||
|
||||
if (!name) {
|
||||
const preset = context.presets().match(entity, context.graph());
|
||||
name = preset && !preset.isFallback() && preset.name(); // fallback to preset name
|
||||
}
|
||||
|
||||
if (name) {
|
||||
this.innerText = name;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Don't hide entities related to this error - #5880
|
||||
context.features().forceVisible(d.elems);
|
||||
context.map().pan([0,0]); // trigger a redraw
|
||||
})
|
||||
.catch(err => {}); // TODO: Handle failed json request gracefully in some way
|
||||
}
|
||||
|
||||
|
||||
osmoseDetails.error = val => {
|
||||
if (!arguments.length) return _error;
|
||||
_error = val;
|
||||
return osmoseDetails;
|
||||
};
|
||||
|
||||
|
||||
return osmoseDetails;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
|
||||
import { t } from '../util/locale';
|
||||
import { services } from '../services';
|
||||
import { modeBrowse } from '../modes/browse';
|
||||
import { svgIcon } from '../svg/icon';
|
||||
|
||||
import { uiOsmoseDetails } from './osmose_details';
|
||||
import { uiOsmoseHeader } from './osmose_header';
|
||||
import { uiQuickLinks } from './quick_links';
|
||||
import { uiTooltipHtml } from './tooltipHtml';
|
||||
|
||||
import { utilRebind } from '../util';
|
||||
|
||||
|
||||
export function uiOsmoseEditor(context) {
|
||||
var dispatch = d3_dispatch('change');
|
||||
var errorDetails = uiOsmoseDetails(context);
|
||||
var errorHeader = uiOsmoseHeader(context);
|
||||
var quickLinks = uiQuickLinks();
|
||||
|
||||
var _error;
|
||||
|
||||
|
||||
function osmoseEditor(selection) {
|
||||
// quick links
|
||||
var choices = [{
|
||||
id: 'zoom_to',
|
||||
label: 'inspector.zoom_to.title',
|
||||
tooltip: function() {
|
||||
return uiTooltipHtml(t('inspector.zoom_to.tooltip_issue'), t('inspector.zoom_to.key'));
|
||||
},
|
||||
click: function zoomTo() {
|
||||
context.mode().zoomToSelected();
|
||||
}
|
||||
}];
|
||||
|
||||
|
||||
var header = selection.selectAll('.header')
|
||||
.data([0]);
|
||||
|
||||
var headerEnter = header.enter()
|
||||
.append('div')
|
||||
.attr('class', 'header fillL');
|
||||
|
||||
headerEnter
|
||||
.append('button')
|
||||
.attr('class', 'fr error-editor-close')
|
||||
.on('click', function() {
|
||||
context.enter(modeBrowse(context));
|
||||
})
|
||||
.call(svgIcon('#iD-icon-close'));
|
||||
|
||||
headerEnter
|
||||
.append('h3')
|
||||
.text(t('QA.osmose.title'));
|
||||
|
||||
|
||||
var body = selection.selectAll('.body')
|
||||
.data([0]);
|
||||
|
||||
body = body.enter()
|
||||
.append('div')
|
||||
.attr('class', 'body')
|
||||
.merge(body);
|
||||
|
||||
var editor = body.selectAll('.error-editor')
|
||||
.data([0]);
|
||||
|
||||
editor.enter()
|
||||
.append('div')
|
||||
.attr('class', 'modal-section error-editor')
|
||||
.merge(editor)
|
||||
.call(errorHeader.error(_error))
|
||||
.call(quickLinks.choices(choices))
|
||||
.call(errorDetails.error(_error))
|
||||
.call(osmoseSaveSection);
|
||||
}
|
||||
|
||||
function osmoseSaveSection(selection) {
|
||||
var isSelected = (_error && _error.id === context.selectedErrorID());
|
||||
var isShown = (_error && isSelected);
|
||||
var saveSection = selection.selectAll('.error-save')
|
||||
.data(
|
||||
(isShown ? [_error] : []),
|
||||
function(d) { return d.id + '-' + (d.status || 0); }
|
||||
);
|
||||
|
||||
// exit
|
||||
saveSection.exit()
|
||||
.remove();
|
||||
|
||||
// enter
|
||||
var saveSectionEnter = saveSection.enter()
|
||||
.append('div')
|
||||
.attr('class', 'error-save save-section cf');
|
||||
|
||||
// update
|
||||
saveSection = saveSectionEnter
|
||||
.merge(saveSection)
|
||||
.call(errorSaveButtons);
|
||||
}
|
||||
|
||||
function errorSaveButtons(selection) {
|
||||
var isSelected = (_error && _error.id === context.selectedErrorID());
|
||||
var buttonSection = selection.selectAll('.buttons')
|
||||
.data((isSelected ? [_error] : []), function(d) { return d.status + d.id; });
|
||||
|
||||
// exit
|
||||
buttonSection.exit()
|
||||
.remove();
|
||||
|
||||
// enter
|
||||
var buttonEnter = buttonSection.enter()
|
||||
.append('div')
|
||||
.attr('class', 'buttons');
|
||||
|
||||
buttonEnter
|
||||
.append('button')
|
||||
.attr('class', 'button close-button action');
|
||||
|
||||
buttonEnter
|
||||
.append('button')
|
||||
.attr('class', 'button ignore-button action');
|
||||
|
||||
|
||||
// update
|
||||
buttonSection = buttonSection
|
||||
.merge(buttonEnter);
|
||||
|
||||
buttonSection.select('.close-button')
|
||||
.text(function() {
|
||||
return t('QA.keepRight.close');
|
||||
})
|
||||
.on('click.close', function(d) {
|
||||
this.blur(); // avoid keeping focus on the button - #4641
|
||||
var errorService = services.osmose;
|
||||
if (errorService) {
|
||||
d.newStatus = 'done';
|
||||
errorService.postUpdate(d, function(err, error) {
|
||||
dispatch.call('change', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
buttonSection.select('.ignore-button')
|
||||
.text(function() {
|
||||
return t('QA.keepRight.ignore');
|
||||
})
|
||||
.on('click.ignore', function(d) {
|
||||
this.blur(); // avoid keeping focus on the button - #4641
|
||||
var errorService = services.osmose;
|
||||
if (errorService) {
|
||||
d.newStatus = 'false';
|
||||
errorService.postUpdate(d, function(err, error) {
|
||||
dispatch.call('change', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
osmoseEditor.error = function(val) {
|
||||
if (!arguments.length) return _error;
|
||||
_error = val;
|
||||
return osmoseEditor;
|
||||
};
|
||||
|
||||
|
||||
return utilRebind(osmoseEditor, dispatch, 'on');
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { services } from '../services';
|
||||
import { t } from '../util/locale';
|
||||
|
||||
|
||||
export function uiOsmoseHeader() {
|
||||
var _error;
|
||||
|
||||
|
||||
function errorTitle(d) {
|
||||
var unknown = t('inspector.unknown');
|
||||
|
||||
if (!d) return unknown;
|
||||
|
||||
// Issue titles supplied by Osmose
|
||||
var s = services.osmose.getStrings(d.error_type);
|
||||
return ('title' in s) ? s.title : unknown;
|
||||
}
|
||||
|
||||
|
||||
function osmoseHeader(selection) {
|
||||
var header = selection.selectAll('.error-header')
|
||||
.data(
|
||||
(_error ? [_error] : []),
|
||||
function(d) { return d.id + '-' + (d.status || 0); }
|
||||
);
|
||||
|
||||
header.exit()
|
||||
.remove();
|
||||
|
||||
var headerEnter = header.enter()
|
||||
.append('div')
|
||||
.attr('class', 'error-header');
|
||||
|
||||
var iconEnter = headerEnter
|
||||
.append('div')
|
||||
.attr('class', 'error-header-icon')
|
||||
.classed('new', function(d) { return d.id < 0; });
|
||||
|
||||
var svgEnter = iconEnter
|
||||
.append('svg')
|
||||
.attr('width', '20px')
|
||||
.attr('height', '30px')
|
||||
.attr('viewbox', '0 0 20 30')
|
||||
.attr('class', function(d) {
|
||||
return [
|
||||
'preset-icon-28',
|
||||
'qa_error',
|
||||
d.service,
|
||||
'error_id-' + d.id,
|
||||
'error_type-' + d.error_type,
|
||||
'item-' + d.item
|
||||
].join(' ');
|
||||
});
|
||||
|
||||
svgEnter
|
||||
.append('polygon')
|
||||
.attr('fill', d => services.osmose.getColor(d.item))
|
||||
.attr('class', 'qa_error-fill')
|
||||
.attr('points', '16,3 4,3 1,6 1,17 4,20 7,20 10,27 13,20 16,20 19,17.033 19,6');
|
||||
|
||||
svgEnter
|
||||
.append('use')
|
||||
.attr('class', 'icon-annotation')
|
||||
.attr('width', '11px')
|
||||
.attr('height', '11px')
|
||||
.attr('transform', 'translate(4.5, 7)')
|
||||
.attr('xlink:href', function(d) {
|
||||
var picon = d.icon;
|
||||
|
||||
if (!picon) {
|
||||
return '';
|
||||
} else {
|
||||
var isMaki = /^maki-/.test(picon);
|
||||
return '#' + picon + (isMaki ? '-11' : '');
|
||||
}
|
||||
});
|
||||
|
||||
headerEnter
|
||||
.append('div')
|
||||
.attr('class', 'error-header-label')
|
||||
.text(errorTitle);
|
||||
}
|
||||
|
||||
|
||||
osmoseHeader.error = function(val) {
|
||||
if (!arguments.length) return _error;
|
||||
_error = val;
|
||||
return osmoseHeader;
|
||||
};
|
||||
|
||||
|
||||
return osmoseHeader;
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { uiFeatureList } from './feature_list';
|
||||
import { uiInspector } from './inspector';
|
||||
import { uiImproveOsmEditor } from './improveOSM_editor';
|
||||
import { uiKeepRightEditor } from './keepRight_editor';
|
||||
import { uiOsmoseEditor } from './osmose_editor';
|
||||
import { uiNoteEditor } from './note_editor';
|
||||
import { textDirection } from '../util/locale';
|
||||
|
||||
@@ -26,6 +27,7 @@ export function uiSidebar(context) {
|
||||
var noteEditor = uiNoteEditor(context);
|
||||
var improveOsmEditor = uiImproveOsmEditor(context);
|
||||
var keepRightEditor = uiKeepRightEditor(context);
|
||||
var osmoseEditor = uiOsmoseEditor(context);
|
||||
var _current;
|
||||
var _wasData = false;
|
||||
var _wasNote = false;
|
||||
@@ -147,8 +149,15 @@ export function uiSidebar(context) {
|
||||
datum = errService.getError(datum.id);
|
||||
}
|
||||
|
||||
// Temporary solution while only two services
|
||||
var errEditor = (datum.service === 'keepRight') ? keepRightEditor : improveOsmEditor;
|
||||
// Currently only three possible services
|
||||
var errEditor;
|
||||
if (datum.service === 'keepRight') {
|
||||
errEditor = keepRightEditor;
|
||||
} else if (datum.service === 'osmose') {
|
||||
errEditor = osmoseEditor;
|
||||
} else {
|
||||
errEditor = improveOsmEditor;
|
||||
}
|
||||
|
||||
d3_selectAll('.qa_error.' + datum.service)
|
||||
.classed('hover', function(d) { return d.id === datum.id; });
|
||||
@@ -357,4 +366,4 @@ export function uiSidebar(context) {
|
||||
sidebar.toggle = function() {};
|
||||
|
||||
return sidebar;
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export function t(s, o, loc) {
|
||||
if (rep !== undefined) {
|
||||
if (o) {
|
||||
for (var k in o) {
|
||||
var variable = '{' + k + '}';
|
||||
var variable = '\\{' + k + '\\}';
|
||||
var re = new RegExp(variable, 'g'); // check globally for variables
|
||||
rep = rep.replace(re, o[k]);
|
||||
}
|
||||
@@ -124,4 +124,4 @@ export function languageName(context, code, options) {
|
||||
}
|
||||
}
|
||||
return code; // if not found, use the code
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,6 @@ function buildData() {
|
||||
let faIcons = {
|
||||
'fas-i-cursor': {},
|
||||
'fas-lock': {},
|
||||
'fas-long-arrow-alt-right': {},
|
||||
'fas-th-list': {},
|
||||
'fas-user-cog': {}
|
||||
};
|
||||
@@ -90,7 +89,7 @@ function buildData() {
|
||||
'dist/data/*',
|
||||
'svg/fontawesome/*.svg',
|
||||
]);
|
||||
|
||||
readQAErrorIcons(faIcons, tnpIcons);
|
||||
let categories = generateCategories(tstrings, faIcons, tnpIcons);
|
||||
let fields = generateFields(tstrings, faIcons, tnpIcons, searchableFieldIDs);
|
||||
let presets = generatePresets(tstrings, faIcons, tnpIcons, searchableFieldIDs);
|
||||
@@ -172,6 +171,27 @@ function validate(file, instance, schema) {
|
||||
}
|
||||
|
||||
|
||||
function readQAErrorIcons(faIcons, tnpIcons) {
|
||||
const qa = read('data/qa_errors.json');
|
||||
|
||||
for (const service in qa.services) {
|
||||
for (const error in qa.services[service].errorIcons) {
|
||||
const icon = qa.services[service]
|
||||
.errorIcons[error];
|
||||
|
||||
// fontawesome icon, remember for later
|
||||
if (/^fa[srb]-/.test(icon)) {
|
||||
faIcons[icon] = {};
|
||||
}
|
||||
// noun project icon, remember for later
|
||||
if (/^tnp-/.test(icon)) {
|
||||
tnpIcons[icon] = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function generateCategories(tstrings, faIcons, tnpIcons) {
|
||||
let categories = {};
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="clone" class="svg-inline--fa fa-clone fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M464 0H144c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h320c26.51 0 48-21.49 48-48v-48h48c26.51 0 48-21.49 48-48V48c0-26.51-21.49-48-48-48zM362 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h42v224c0 26.51 21.49 48 48 48h224v42a6 6 0 0 1-6 6zm96-96H150a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h308a6 6 0 0 1 6 6v308a6 6 0 0 1-6 6z"></path></svg>
|
||||
|
After Width: | Height: | Size: 578 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="dot-circle" class="svg-inline--fa fa-dot-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 56c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m0-48C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 168c-44.183 0-80 35.817-80 80s35.817 80 80 80 80-35.817 80-80-35.817-80-80-80z"></path></svg>
|
||||
|
After Width: | Height: | Size: 532 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="times-circle" class="svg-inline--fa fa-times-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z"></path></svg>
|
||||
|
After Width: | Height: | Size: 712 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="calendar-alt" class="svg-inline--fa fa-calendar-alt fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M0 464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V192H0v272zm320-196c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zm0 128c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zM192 268c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zm0 128c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-40c-6.6 0-12-5.4-12-12v-40zM64 268c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12H76c-6.6 0-12-5.4-12-12v-40zm0 128c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12H76c-6.6 0-12-5.4-12-12v-40zM400 64h-48V16c0-8.8-7.2-16-16-16h-32c-8.8 0-16 7.2-16 16v48H160V16c0-8.8-7.2-16-16-16h-32c-8.8 0-16 7.2-16 16v48H48C21.5 64 0 85.5 0 112v48h448v-48c0-26.5-21.5-48-48-48z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="question" class="svg-inline--fa fa-question fa-w-12" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M202.021 0C122.202 0 70.503 32.703 29.914 91.026c-7.363 10.58-5.093 25.086 5.178 32.874l43.138 32.709c10.373 7.865 25.132 6.026 33.253-4.148 25.049-31.381 43.63-49.449 82.757-49.449 30.764 0 68.816 19.799 68.816 49.631 0 22.552-18.617 34.134-48.993 51.164-35.423 19.86-82.299 44.576-82.299 106.405V320c0 13.255 10.745 24 24 24h72.471c13.255 0 24-10.745 24-24v-5.773c0-42.86 125.268-44.645 125.268-160.627C377.504 66.256 286.902 0 202.021 0zM192 373.459c-38.196 0-69.271 31.075-69.271 69.271 0 38.195 31.075 69.27 69.271 69.27s69.271-31.075 69.271-69.271-31.075-69.27-69.271-69.27z"></path></svg>
|
||||
|
After Width: | Height: | Size: 816 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="shapes" class="svg-inline--fa fa-shapes fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M512 320v160c0 17.67-14.33 32-32 32H320c-17.67 0-32-14.33-32-32V320c0-17.67 14.33-32 32-32h160c17.67 0 32 14.33 32 32zm-384-64C57.31 256 0 313.31 0 384s57.31 128 128 128 128-57.31 128-128-57.31-128-128-128zm351.03-32c25.34 0 41.18-26.67 28.51-48L412.51 16c-12.67-21.33-44.35-21.33-57.02 0l-95.03 160c-12.67 21.33 3.17 48 28.51 48h190.06z"></path></svg>
|
||||
|
After Width: | Height: | Size: 569 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="share-alt" class="svg-inline--fa fa-share-alt fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M352 320c-22.608 0-43.387 7.819-59.79 20.895l-102.486-64.054a96.551 96.551 0 0 0 0-41.683l102.486-64.054C308.613 184.181 329.392 192 352 192c53.019 0 96-42.981 96-96S405.019 0 352 0s-96 42.981-96 96c0 7.158.79 14.13 2.276 20.841L155.79 180.895C139.387 167.819 118.608 160 96 160c-53.019 0-96 42.981-96 96s42.981 96 96 96c22.608 0 43.387-7.819 59.79-20.895l102.486 64.054A96.301 96.301 0 0 0 256 416c0 53.019 42.981 96 96 96s96-42.981 96-96-42.981-96-96-96z"></path></svg>
|
||||
|
After Width: | Height: | Size: 694 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="sort-alpha-up" class="svg-inline--fa fa-sort-alpha-up fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M16 160h48v304a16 16 0 0 0 16 16h32a16 16 0 0 0 16-16V160h48c14.21 0 21.38-17.24 11.31-27.31l-80-96a16 16 0 0 0-22.62 0l-80 96C-5.35 142.74 1.78 160 16 160zm400 128H288a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h56l-61.26 70.45A32 32 0 0 0 272 446.37V464a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-56l61.26-70.45A32 32 0 0 0 432 321.63V304a16 16 0 0 0-16-16zm31.06-85.38l-59.27-160A16 16 0 0 0 372.72 32h-41.44a16 16 0 0 0-15.07 10.62l-59.27 160A16 16 0 0 0 272 224h24.83a16 16 0 0 0 15.23-11.08l4.42-12.92h71l4.41 12.92A16 16 0 0 0 407.16 224H432a16 16 0 0 0 15.06-21.38zM335.61 144L352 96l16.39 48z"></path></svg>
|
||||
|
After Width: | Height: | Size: 862 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="tags" class="svg-inline--fa fa-tags fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M497.941 225.941L286.059 14.059A48 48 0 0 0 252.118 0H48C21.49 0 0 21.49 0 48v204.118a48 48 0 0 0 14.059 33.941l211.882 211.882c18.744 18.745 49.136 18.746 67.882 0l204.118-204.118c18.745-18.745 18.745-49.137 0-67.882zM112 160c-26.51 0-48-21.49-48-48s21.49-48 48-48 48 21.49 48 48-21.49 48-48 48zm513.941 133.823L421.823 497.941c-18.745 18.745-49.137 18.745-67.882 0l-.36-.36L527.64 323.522c16.999-16.999 26.36-39.6 26.36-63.64s-9.362-46.641-26.36-63.64L331.397 0h48.721a48 48 0 0 1 33.941 14.059l211.882 211.882c18.745 18.745 18.745 49.137 0 67.882z"></path></svg>
|
||||
|
After Width: | Height: | Size: 778 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="tint-slash" class="svg-inline--fa fa-tint-slash fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M633.82 458.1L494.97 350.78c.52-5.57 1.03-11.16 1.03-16.87 0-111.76-99.79-153.34-146.78-311.82-7.94-28.78-49.44-30.12-58.44 0-15.52 52.34-36.87 91.96-58.49 125.68L45.47 3.37C38.49-2.05 28.43-.8 23.01 6.18L3.37 31.45C-2.05 38.42-.8 48.47 6.18 53.9l588.36 454.73c6.98 5.43 17.03 4.17 22.46-2.81l19.64-25.27c5.41-6.97 4.16-17.02-2.82-22.45zM144 333.91C144 432.35 222.72 512 320 512c44.71 0 85.37-16.96 116.4-44.7L162.72 255.78c-11.41 23.5-18.72 48.35-18.72 78.13z"></path></svg>
|
||||
|
After Width: | Height: | Size: 700 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="weight-hanging" class="svg-inline--fa fa-weight-hanging fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M510.28 445.86l-73.03-292.13c-3.8-15.19-16.44-25.72-30.87-25.72h-60.25c3.57-10.05 5.88-20.72 5.88-32 0-53.02-42.98-96-96-96s-96 42.98-96 96c0 11.28 2.3 21.95 5.88 32h-60.25c-14.43 0-27.08 10.54-30.87 25.72L1.72 445.86C-6.61 479.17 16.38 512 48.03 512h415.95c31.64 0 54.63-32.83 46.3-66.14zM256 128c-17.64 0-32-14.36-32-32s14.36-32 32-32 32 14.36 32 32-14.36 32-32 32z"></path></svg>
|
||||
|
After Width: | Height: | Size: 615 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="yin-yang" class="svg-inline--fa fa-yin-yang fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M248 8C111.03 8 0 119.03 0 256s111.03 248 248 248 248-111.03 248-248S384.97 8 248 8zm0 376c-17.67 0-32-14.33-32-32s14.33-32 32-32 32 14.33 32 32-14.33 32-32 32zm0-128c-53.02 0-96 42.98-96 96s42.98 96 96 96c-106.04 0-192-85.96-192-192S141.96 64 248 64c53.02 0 96 42.98 96 96s-42.98 96-96 96zm0-128c-17.67 0-32 14.33-32 32s14.33 32 32 32 32-14.33 32-32-14.33-32-32-32z"></path></svg>
|
||||
|
After Width: | Height: | Size: 602 B |
@@ -26,20 +26,21 @@ describe('iD.svgLayers', function () {
|
||||
it('creates default data layers', function () {
|
||||
container.call(iD.svgLayers(projection, context));
|
||||
var nodes = container.selectAll('svg .data-layer').nodes();
|
||||
expect(nodes.length).to.eql(13);
|
||||
expect(nodes.length).to.eql(14);
|
||||
expect(d3.select(nodes[0]).classed('osm')).to.be.true;
|
||||
expect(d3.select(nodes[1]).classed('notes')).to.be.true;
|
||||
expect(d3.select(nodes[2]).classed('data')).to.be.true;
|
||||
expect(d3.select(nodes[3]).classed('keepRight')).to.be.true;
|
||||
expect(d3.select(nodes[4]).classed('improveOSM')).to.be.true;
|
||||
expect(d3.select(nodes[5]).classed('streetside')).to.be.true;
|
||||
expect(d3.select(nodes[6]).classed('mapillary')).to.be.true;
|
||||
expect(d3.select(nodes[7]).classed('mapillary-map-features')).to.be.true;
|
||||
expect(d3.select(nodes[8]).classed('mapillary-signs')).to.be.true;
|
||||
expect(d3.select(nodes[9]).classed('openstreetcam')).to.be.true;
|
||||
expect(d3.select(nodes[10]).classed('debug')).to.be.true;
|
||||
expect(d3.select(nodes[11]).classed('geolocate')).to.be.true;
|
||||
expect(d3.select(nodes[12]).classed('touch')).to.be.true;
|
||||
expect(d3.select(nodes[5]).classed('osmose')).to.be.true;
|
||||
expect(d3.select(nodes[6]).classed('streetside')).to.be.true;
|
||||
expect(d3.select(nodes[7]).classed('mapillary')).to.be.true;
|
||||
expect(d3.select(nodes[8]).classed('mapillary-map-features')).to.be.true;
|
||||
expect(d3.select(nodes[9]).classed('mapillary-signs')).to.be.true;
|
||||
expect(d3.select(nodes[10]).classed('openstreetcam')).to.be.true;
|
||||
expect(d3.select(nodes[11]).classed('debug')).to.be.true;
|
||||
expect(d3.select(nodes[12]).classed('geolocate')).to.be.true;
|
||||
expect(d3.select(nodes[13]).classed('touch')).to.be.true;
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||