From acec177e3452dcba0f063832cee5b6460d1644ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie?= Date: Thu, 6 Feb 2020 20:14:53 +0100 Subject: [PATCH 001/127] change car=* to motorcar=* on charging_station --- data/deprecated.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/deprecated.json b/data/deprecated.json index 30dadaf07..8de8d5b6d 100644 --- a/data/deprecated.json +++ b/data/deprecated.json @@ -67,6 +67,10 @@ "old": {"amenity": "ev_charging"}, "replace": {"amenity": "charging_station"} }, + { + "old": {"amenity": "charging_station", "car": "*"}, + "replace": {"amenity": "charging_station", "motorcar": "$1"} + }, { "old": {"amenity": "fire_hydrant"}, "replace": {"emergency": "fire_hydrant"} From 6976251ac27d00cb0f98013eafee5c57066e8f0c Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Thu, 30 Jan 2020 17:37:52 +0000 Subject: [PATCH 002/127] Handle almost junction where end node are close Based on distances at which nodes are joined by the quickfix and the validation extends ahead of an end node, if both ways have close end nodes and joining them would result in a small change of angle for the edited way then they will be joined. Closes #7201 --- modules/validations/almost_junction.js | 52 +++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/modules/validations/almost_junction.js b/modules/validations/almost_junction.js index cbf3aa168..ef587ea20 100644 --- a/modules/validations/almost_junction.js +++ b/modules/validations/almost_junction.js @@ -1,7 +1,7 @@ import { geoExtent, geoLineIntersection, geoMetersToLat, geoMetersToLon, geoSphericalDistance, geoVecInterp, geoHasSelfIntersections, - geoSphericalClosestNode + geoSphericalClosestNode, geoAngle } from '../geo'; import { actionAddMidpoint } from '../actions/add_midpoint'; @@ -213,6 +213,42 @@ export function validationAlmostJunction(context) { return true; } + function findNearbyEndNodes(node, way2) { + return [ + way2.nodes[0], + way2.nodes[way2.nodes.length - 1] + ].map(d => graph.entity(d)) + .filter(d => { + // Node cannot be near to itself, but other endnode of same way could be + // 4.25m based on extending 5m ahead and .75m quick fix node joining + return d.id !== node.id + && geoSphericalDistance(node.loc, d.loc) <= 4.25; + }); + } + + function findAlmostCollinear(midNode, tipNode, endNodes) { + // Both nodes could be close, so want to join whichever is closest to collinear + let mostCollinear; + let minAngle = Infinity; + + // Checks midNode -> tipNode -> endNode for collinearity + endNodes.forEach(endNode => { + const a1 = geoAngle(midNode, tipNode, context.projection) + Math.PI; + const a2 = geoAngle(midNode, endNode, context.projection) + Math.PI; + const diff = Math.max(a1, a2) - Math.min(a1, a2); + + if (diff < minAngle) { + mostCollinear = endNode; + minAngle = diff; + } + }); + + /* 9° threshold set by considering right angle triangle + based on .75m node joining threshold and 5m extension */ + if (minAngle <= 9 * Math.PI / 180) return mostCollinear; + + return null; + } function canConnectByExtend(way, endNodeIdx) { var EXTEND_TH_METERS = 5; @@ -253,6 +289,20 @@ export function validationAlmostJunction(context) { nB = graph.entity(nBid); var crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]); if (crossLoc) { + // When endpoints are close, just join if resulting small change in angle (#7201) + let nearEndNodes = findNearbyEndNodes(tipNode, way2); + if (nearEndNodes.length > 0) { + let collinear = findAlmostCollinear(midNode, tipNode, nearEndNodes); + if (collinear) { + return { + node: tipNode, + wid: way2.id, + edge: [collinear.id, collinear.id], + cross_loc: collinear.loc + }; + } + } + return { node: tipNode, wid: way2.id, From 917c8f0b94da0bece8cab64ce0f9fa0307200222 Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Fri, 31 Jan 2020 17:23:49 +0000 Subject: [PATCH 003/127] Add tests for close endpoint almost junctions --- test/spec/validations/almost_junction.js | 139 +++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/test/spec/validations/almost_junction.js b/test/spec/validations/almost_junction.js index 83cef265e..05223d467 100644 --- a/test/spec/validations/almost_junction.js +++ b/test/spec/validations/almost_junction.js @@ -126,6 +126,79 @@ describe('iD.validations.almost_junction', function () { ); } + function closeEndNodesSmallAngle() { + // Vertical path + var n1 = iD.osmNode({id: 'n-1', loc: [0.0003247, 22.4423866]}); + var n2 = iD.osmNode({id: 'n-2', loc: [0.0003060, 22.4432671]}); + var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: { highway: 'path' }}); + + context.perform( + iD.actionAddEntity(n1), + iD.actionAddEntity(n2), + iD.actionAddEntity(w1) + ); + + // Angled path with end node within 4.25m and change of angle <9° + var n3 = iD.osmNode({id: 'n-3', loc: [0.0003379, 22.4423861]}); + var n4 = iD.osmNode({id: 'n-4', loc: [0.0004354, 22.4421312]}); + var w2 = iD.osmWay({id: 'w-2', nodes: ['n-3', 'n-4'], tags: { highway: 'path' }}); + + context.perform( + iD.actionAddEntity(n3), + iD.actionAddEntity(n4), + iD.actionAddEntity(w2) + ); + + return n1; + } + + function closeEndNodesBigAngle() { + // Vertical path + var n1 = iD.osmNode({id: 'n-1', loc: [0, 22.4427453]}); + var n2 = iD.osmNode({id: 'n-2', loc: [0, 22.4429806]}); + var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: { highway: 'path' }}); + + context.perform( + iD.actionAddEntity(n1), + iD.actionAddEntity(n2), + iD.actionAddEntity(w1) + ); + + // Horizontal path with end node within 4.25m and change of angle >9° + var n3 = iD.osmNode({id: 'n-3', loc: [0.0000199, 22.4427801]}); + var n4 = iD.osmNode({id: 'n-4', loc: [0.0002038, 22.4427801]}); + var w2 = iD.osmWay({id: 'w-2', nodes: ['n-3', 'n-4'], tags: { highway: 'path' }}); + + context.perform( + iD.actionAddEntity(n3), + iD.actionAddEntity(n4), + iD.actionAddEntity(w2) + ); + + return n1; + } + + function closeEndNodesSmallAngleSelf() { + // Square path that ends within 4.25m of itself and change of angle <9° + var n1 = iD.osmNode({id: 'n-1', loc: [0, 22.4427453]}); + var n2 = iD.osmNode({id: 'n-2', loc: [0, 22.4429811]}); + var n3 = iD.osmNode({id: 'n-3', loc: [0.0001923, 22.4429811]}); + var n4 = iD.osmNode({id: 'n-4', loc: [0.0001923, 22.4427523]}); + var n5 = iD.osmNode({id: 'n-5', loc: [0.0000134, 22.4427523]}); + var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2', 'n-3', 'n-4', 'n-5'], tags: { highway: 'path' }}); + + context.perform( + iD.actionAddEntity(n1), + iD.actionAddEntity(n2), + iD.actionAddEntity(n3), + iD.actionAddEntity(n4), + iD.actionAddEntity(n5), + iD.actionAddEntity(w1) + ); + + return n1; + } + function validate() { var validator = iD.validationAlmostJunction(context); var changes = context.history().changes(); @@ -220,4 +293,70 @@ describe('iD.validations.almost_junction', function () { expect(issues).to.have.lengthOf(0); }); + // TODO: Test case of both endpoints of another way close, should prioritise smaller angle change + it('joins close endpoints if insignificant angle change', function() { + var n1 = closeEndNodesSmallAngle(); + var issues = validate(); + expect(issues).to.have.lengthOf(1); + var issue = issues[0]; + expect(issue.type).to.eql('almost_junction'); + expect(issue.subtype).to.eql('highway-highway'); + expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds[0]).to.eql('w-2'); + expect(issue.entityIds[1]).to.eql('n-3'); + expect(issue.entityIds[2]).to.eql('w-1'); + + // Duplicate edge nodes means endpoints will be joined + expect(issue.data.edge).to.have.lengthOf(2); + expect(issue.data.edge[0]).to.eql('n-1'); + expect(issue.data.edge[1]).to.eql('n-1'); + + // Crossing set to loc of end node means endpoints will be joined + expect(issue.data.cross_loc).to.have.lengthOf(2); + expect(issue.data.cross_loc).to.eql(n1.loc); + }); + + it('won\'t join close endpoints if significant angle change', function() { + var n1 = closeEndNodesBigAngle(); + var issues = validate(); + expect(issues).to.have.lengthOf(1); + var issue = issues[0]; + expect(issue.type).to.eql('almost_junction'); + expect(issue.subtype).to.eql('highway-highway'); + expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds[0]).to.eql('w-2'); + expect(issue.entityIds[1]).to.eql('n-3'); + expect(issue.entityIds[2]).to.eql('w-1'); + + // Differing edge nodes means endpoints won't be joined + expect(issue.data.edge).to.have.lengthOf(2); + expect(issue.data.edge[0]).to.eql('n-1'); + expect(issue.data.edge[1]).to.eql('n-2'); + + // Crossing different from loc of end node means endpoints won't be joined + expect(issue.data.cross_loc).to.have.lengthOf(2); + expect(issue.data.cross_loc).to.not.eql(n1.loc); + }); + + it('joins close endpoints of the same way', function() { + var n1 = closeEndNodesSmallAngleSelf(); + var issues = validate(); + expect(issues).to.have.lengthOf(1); + var issue = issues[0]; + expect(issue.type).to.eql('almost_junction'); + expect(issue.subtype).to.eql('highway-highway'); + expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds[0]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-5'); + expect(issue.entityIds[2]).to.eql('w-1'); + + // Duplicate edge nodes means endpoints will be joined + expect(issue.data.edge).to.have.lengthOf(2); + expect(issue.data.edge[0]).to.eql('n-1'); + expect(issue.data.edge[1]).to.eql('n-1'); + + // Crossing set to loc of end node means endpoints will be joined + expect(issue.data.cross_loc).to.have.lengthOf(2); + expect(issue.data.cross_loc).to.eql(n1.loc); + }); }); From ec6ad0000b9fa6c82de94eebed02c4cc0e5386cb Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Fri, 31 Jan 2020 17:37:06 +0000 Subject: [PATCH 004/127] Show derivation of close node thresholds in code --- modules/validations/almost_junction.js | 28 +++++++++++++++----------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/modules/validations/almost_junction.js b/modules/validations/almost_junction.js index ef587ea20..a1168b78a 100644 --- a/modules/validations/almost_junction.js +++ b/modules/validations/almost_junction.js @@ -18,8 +18,13 @@ import { services } from '../services'; * Look for roads that can be connected to other roads with a short extension */ export function validationAlmostJunction(context) { - var type = 'almost_junction'; - + const type = 'almost_junction'; + const EXTEND_TH_METERS = 5; + const WELD_TH_METERS = 0.75; + // Comes from considering bounding case of parallel ways + const CLOSE_NODE_TH = EXTEND_TH_METERS - WELD_TH_METERS; + // Comes from considering bounding case of perpendicular ways + const SIG_ANGLE_TH = Math.atan(WELD_TH_METERS / EXTEND_TH_METERS); function isHighway(entity) { return entity.type === 'way' && @@ -89,7 +94,7 @@ export function validationAlmostJunction(context) { var annotation = t('issues.fix.connect_almost_junction.annotation'); // already a point nearby, just connect to that - if (closestNodeInfo.distance < 0.75) { + if (closestNodeInfo.distance < WELD_TH_METERS) { context.perform( actionMergeNodes([closestNodeInfo.node.id, endNode.id], closestNodeInfo.node.loc), annotation @@ -220,15 +225,14 @@ export function validationAlmostJunction(context) { ].map(d => graph.entity(d)) .filter(d => { // Node cannot be near to itself, but other endnode of same way could be - // 4.25m based on extending 5m ahead and .75m quick fix node joining return d.id !== node.id - && geoSphericalDistance(node.loc, d.loc) <= 4.25; + && geoSphericalDistance(node.loc, d.loc) <= CLOSE_NODE_TH; }); } - function findAlmostCollinear(midNode, tipNode, endNodes) { + function findSmallJoinAngle(midNode, tipNode, endNodes) { // Both nodes could be close, so want to join whichever is closest to collinear - let mostCollinear; + let joinTo; let minAngle = Infinity; // Checks midNode -> tipNode -> endNode for collinearity @@ -238,14 +242,14 @@ export function validationAlmostJunction(context) { const diff = Math.max(a1, a2) - Math.min(a1, a2); if (diff < minAngle) { - mostCollinear = endNode; + joinTo = endNode; minAngle = diff; } }); - /* 9° threshold set by considering right angle triangle - based on .75m node joining threshold and 5m extension */ - if (minAngle <= 9 * Math.PI / 180) return mostCollinear; + /* Threshold set by considering right angle triangle + based on node joining threshold and extension distance */ + if (minAngle <= SIG_ANGLE_TH) return joinTo; return null; } @@ -292,7 +296,7 @@ export function validationAlmostJunction(context) { // When endpoints are close, just join if resulting small change in angle (#7201) let nearEndNodes = findNearbyEndNodes(tipNode, way2); if (nearEndNodes.length > 0) { - let collinear = findAlmostCollinear(midNode, tipNode, nearEndNodes); + let collinear = findSmallJoinAngle(midNode, tipNode, nearEndNodes); if (collinear) { return { node: tipNode, From 3c7b0925344d14adf586662146c3dc1924b2735b Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Fri, 31 Jan 2020 17:47:19 +0000 Subject: [PATCH 005/127] Update almost junction validation with ES6 syntax --- modules/validations/almost_junction.js | 118 ++++++++++++------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/modules/validations/almost_junction.js b/modules/validations/almost_junction.js index a1168b78a..a95a6855a 100644 --- a/modules/validations/almost_junction.js +++ b/modules/validations/almost_junction.js @@ -27,38 +27,39 @@ export function validationAlmostJunction(context) { const SIG_ANGLE_TH = Math.atan(WELD_TH_METERS / EXTEND_TH_METERS); function isHighway(entity) { - return entity.type === 'way' && - osmRoutableHighwayTagValues[entity.tags.highway]; + return entity.type === 'way' + && osmRoutableHighwayTagValues[entity.tags.highway]; } function isTaggedAsNotContinuing(node) { - return node.tags.noexit === 'yes' || - node.tags.amenity === 'parking_entrance' || - (node.tags.entrance && node.tags.entrance !== 'no'); + return node.tags.noexit === 'yes' + || node.tags.amenity === 'parking_entrance' + || (node.tags.entrance && node.tags.entrance !== 'no'); } - var validation = function checkAlmostJunction(entity, graph) { + const validation = function checkAlmostJunction(entity, graph) { if (!isHighway(entity)) return []; if (entity.isDegenerate()) return []; - var tree = context.history().tree(); - var issues = []; + const tree = context.history().tree(); + const extendableNodeInfos = findConnectableEndNodesByExtension(entity); - var extendableNodeInfos = findConnectableEndNodesByExtension(entity); - extendableNodeInfos.forEach(function(extendableNodeInfo) { + let issues = []; + + extendableNodeInfos.forEach(extendableNodeInfo => { issues.push(new validationIssue({ - type: type, + type, subtype: 'highway-highway', severity: 'warning', - message: function(context) { - var entity1 = context.hasEntity(this.entityIds[0]); + message(context) { + const entity1 = context.hasEntity(this.entityIds[0]); if (this.entityIds[0] === this.entityIds[2]) { return entity1 ? t('issues.almost_junction.self.message', { feature: utilDisplayLabel(entity1, context) }) : ''; } else { - var entity2 = context.hasEntity(this.entityIds[2]); + const entity2 = context.hasEntity(this.entityIds[2]); return (entity1 && entity2) ? t('issues.almost_junction.message', { feature: utilDisplayLabel(entity1, context), feature2: utilDisplayLabel(entity2, context) @@ -81,18 +82,18 @@ export function validationAlmostJunction(context) { function makeFixes(context) { - var fixes = [new validationIssueFix({ + let fixes = [new validationIssueFix({ icon: 'iD-icon-abutment', title: t('issues.fix.connect_features.title'), - onClick: function(context) { - var endNodeId = this.issue.entityIds[1]; - var endNode = context.entity(endNodeId); - var targetEdge = this.issue.data.edge; - var crossLoc = this.issue.data.cross_loc; - var edgeNodes = [context.entity(targetEdge[0]), context.entity(targetEdge[1])]; - var closestNodeInfo = geoSphericalClosestNode(edgeNodes, crossLoc); + onClick(context) { + const endNodeId = this.issue.entityIds[1]; + const endNode = context.entity(endNodeId); + const targetEdge = this.issue.data.edge; + const crossLoc = this.issue.data.cross_loc; + const edgeNodes = [context.entity(targetEdge[0]), context.entity(targetEdge[1])]; + const closestNodeInfo = geoSphericalClosestNode(edgeNodes, crossLoc); - var annotation = t('issues.fix.connect_almost_junction.annotation'); + const annotation = t('issues.fix.connect_almost_junction.annotation'); // already a point nearby, just connect to that if (closestNodeInfo.distance < WELD_TH_METERS) { context.perform( @@ -109,15 +110,15 @@ export function validationAlmostJunction(context) { } })]; - var node = context.hasEntity(this.entityIds[1]); + const node = context.hasEntity(this.entityIds[1]); if (node && !node.hasInterestingTags()) { // node has no descriptive tags, suggest noexit fix fixes.push(new validationIssueFix({ icon: 'maki-barrier', title: t('issues.fix.tag_as_disconnected.title'), - onClick: function(context) { - var nodeID = this.issue.entityIds[1]; - var tags = Object.assign({}, context.entity(nodeID).tags); + onClick(context) { + const nodeID = this.issue.entityIds[1]; + const tags = Object.assign({}, context.entity(nodeID).tags); tags.noexit = 'yes'; context.perform( actionChangeTags(nodeID, tags), @@ -143,7 +144,7 @@ export function validationAlmostJunction(context) { function isExtendableCandidate(node, way) { // can not accurately test vertices on tiles not downloaded from osm - #5938 - var osm = services.osm; + const osm = services.osm; if (osm && !osm.isDataLoaded(node.loc)) { return false; } @@ -151,8 +152,8 @@ export function validationAlmostJunction(context) { return false; } - var occurences = 0; - for (var index in way.nodes) { + let occurences = 0; + for (const index in way.nodes) { if (way.nodes[index] === node.id) { occurences += 1; if (occurences > 1) { @@ -165,18 +166,18 @@ export function validationAlmostJunction(context) { function findConnectableEndNodesByExtension(way) { - var results = []; + let results = []; if (way.isClosed()) return results; - var testNodes; - var indices = [0, way.nodes.length - 1]; - indices.forEach(function(nodeIndex) { - var nodeID = way.nodes[nodeIndex]; - var node = graph.entity(nodeID); + let testNodes; + const indices = [0, way.nodes.length - 1]; + indices.forEach(nodeIndex => { + const nodeID = way.nodes[nodeIndex]; + const node = graph.entity(nodeID); if (!isExtendableCandidate(node, way)) return; - var connectionInfo = canConnectByExtend(way, nodeIndex); + const connectionInfo = canConnectByExtend(way, nodeIndex); if (!connectionInfo) return; testNodes = graph.childNodes(way).slice(); // shallow copy @@ -207,11 +208,11 @@ export function validationAlmostJunction(context) { !(hasTag(way.tags, 'tunnel') && hasTag(way2.tags, 'tunnel'))) return false; // must have equivalent layers and levels - var layer1 = way.tags.layer || '0', + const layer1 = way.tags.layer || '0', layer2 = way2.tags.layer || '0'; if (layer1 !== layer2) return false; - var level1 = way.tags.level || '0', + const level1 = way.tags.level || '0', level2 = way2.tags.level || '0'; if (level1 !== level2) return false; @@ -255,43 +256,42 @@ export function validationAlmostJunction(context) { } function canConnectByExtend(way, endNodeIdx) { - var EXTEND_TH_METERS = 5; - var tipNid = way.nodes[endNodeIdx]; // the 'tip' node for extension point - var midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2]; // the other node of the edge - var tipNode = graph.entity(tipNid); - var midNode = graph.entity(midNid); - var lon = tipNode.loc[0]; - var lat = tipNode.loc[1]; - var lon_range = geoMetersToLon(EXTEND_TH_METERS, lat) / 2; - var lat_range = geoMetersToLat(EXTEND_TH_METERS) / 2; - var queryExtent = geoExtent([ + const tipNid = way.nodes[endNodeIdx]; // the 'tip' node for extension point + const midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2]; // the other node of the edge + const tipNode = graph.entity(tipNid); + const midNode = graph.entity(midNid); + const lon = tipNode.loc[0]; + const lat = tipNode.loc[1]; + const lon_range = geoMetersToLon(EXTEND_TH_METERS, lat) / 2; + const lat_range = geoMetersToLat(EXTEND_TH_METERS) / 2; + const queryExtent = geoExtent([ [lon - lon_range, lat - lat_range], [lon + lon_range, lat + lat_range] ]); // first, extend the edge of [midNode -> tipNode] by EXTEND_TH_METERS and find the "extended tip" location - var edgeLen = geoSphericalDistance(midNode.loc, tipNode.loc); - var t = EXTEND_TH_METERS / edgeLen + 1.0; - var extTipLoc = geoVecInterp(midNode.loc, tipNode.loc, t); + const edgeLen = geoSphericalDistance(midNode.loc, tipNode.loc); + const t = EXTEND_TH_METERS / edgeLen + 1.0; + const extTipLoc = geoVecInterp(midNode.loc, tipNode.loc, t); // then, check if the extension part [tipNode.loc -> extTipLoc] intersects any other ways - var intersected = tree.intersects(queryExtent, graph); - for (var i = 0; i < intersected.length; i++) { - var way2 = intersected[i]; + const intersected = tree.intersects(queryExtent, graph); + for (let i = 0; i < intersected.length; i++) { + let way2 = intersected[i]; if (!isHighway(way2)) continue; if (!canConnectWays(way, way2)) continue; - for (var j = 0; j < way2.nodes.length - 1; j++) { - var nAid = way2.nodes[j], + for (let j = 0; j < way2.nodes.length - 1; j++) { + let nAid = way2.nodes[j], nBid = way2.nodes[j + 1]; if (nAid === tipNid || nBid === tipNid) continue; - var nA = graph.entity(nAid), + let nA = graph.entity(nAid), nB = graph.entity(nBid); - var crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]); + let crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]); if (crossLoc) { // When endpoints are close, just join if resulting small change in angle (#7201) let nearEndNodes = findNearbyEndNodes(tipNode, way2); From 006e69244904c1de596af35d6a643c53a4abbdb3 Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Fri, 31 Jan 2020 17:49:19 +0000 Subject: [PATCH 006/127] Convert to 2 space indentation --- modules/validations/almost_junction.js | 588 ++++++++++++------------- 1 file changed, 294 insertions(+), 294 deletions(-) diff --git a/modules/validations/almost_junction.js b/modules/validations/almost_junction.js index a95a6855a..f832aebbb 100644 --- a/modules/validations/almost_junction.js +++ b/modules/validations/almost_junction.js @@ -1,7 +1,7 @@ import { - geoExtent, geoLineIntersection, geoMetersToLat, geoMetersToLon, - geoSphericalDistance, geoVecInterp, geoHasSelfIntersections, - geoSphericalClosestNode, geoAngle + geoExtent, geoLineIntersection, geoMetersToLat, geoMetersToLon, + geoSphericalDistance, geoVecInterp, geoHasSelfIntersections, + geoSphericalClosestNode, geoAngle } from '../geo'; import { actionAddMidpoint } from '../actions/add_midpoint'; @@ -18,309 +18,309 @@ import { services } from '../services'; * Look for roads that can be connected to other roads with a short extension */ export function validationAlmostJunction(context) { - const type = 'almost_junction'; - const EXTEND_TH_METERS = 5; - const WELD_TH_METERS = 0.75; - // Comes from considering bounding case of parallel ways - const CLOSE_NODE_TH = EXTEND_TH_METERS - WELD_TH_METERS; - // Comes from considering bounding case of perpendicular ways - const SIG_ANGLE_TH = Math.atan(WELD_TH_METERS / EXTEND_TH_METERS); + const type = 'almost_junction'; + const EXTEND_TH_METERS = 5; + const WELD_TH_METERS = 0.75; + // Comes from considering bounding case of parallel ways + const CLOSE_NODE_TH = EXTEND_TH_METERS - WELD_TH_METERS; + // Comes from considering bounding case of perpendicular ways + const SIG_ANGLE_TH = Math.atan(WELD_TH_METERS / EXTEND_TH_METERS); - function isHighway(entity) { - return entity.type === 'way' - && osmRoutableHighwayTagValues[entity.tags.highway]; - } + function isHighway(entity) { + return entity.type === 'way' + && osmRoutableHighwayTagValues[entity.tags.highway]; + } - function isTaggedAsNotContinuing(node) { - return node.tags.noexit === 'yes' - || node.tags.amenity === 'parking_entrance' - || (node.tags.entrance && node.tags.entrance !== 'no'); + function isTaggedAsNotContinuing(node) { + return node.tags.noexit === 'yes' + || node.tags.amenity === 'parking_entrance' + || (node.tags.entrance && node.tags.entrance !== 'no'); + } + + + const validation = function checkAlmostJunction(entity, graph) { + if (!isHighway(entity)) return []; + if (entity.isDegenerate()) return []; + + const tree = context.history().tree(); + const extendableNodeInfos = findConnectableEndNodesByExtension(entity); + + let issues = []; + + extendableNodeInfos.forEach(extendableNodeInfo => { + issues.push(new validationIssue({ + type, + subtype: 'highway-highway', + severity: 'warning', + message(context) { + const entity1 = context.hasEntity(this.entityIds[0]); + if (this.entityIds[0] === this.entityIds[2]) { + return entity1 ? t('issues.almost_junction.self.message', { + feature: utilDisplayLabel(entity1, context) + }) : ''; + } else { + const entity2 = context.hasEntity(this.entityIds[2]); + return (entity1 && entity2) ? t('issues.almost_junction.message', { + feature: utilDisplayLabel(entity1, context), + feature2: utilDisplayLabel(entity2, context) + }) : ''; + } + }, + reference: showReference, + entityIds: [entity.id, extendableNodeInfo.node.id, extendableNodeInfo.wid], + loc: extendableNodeInfo.node.loc, + hash: JSON.stringify(extendableNodeInfo.node.loc), + data: { + edge: extendableNodeInfo.edge, + cross_loc: extendableNodeInfo.cross_loc + }, + dynamicFixes: makeFixes + })); + }); + + return issues; + + + function makeFixes(context) { + let fixes = [new validationIssueFix({ + icon: 'iD-icon-abutment', + title: t('issues.fix.connect_features.title'), + onClick(context) { + const endNodeId = this.issue.entityIds[1]; + const endNode = context.entity(endNodeId); + const targetEdge = this.issue.data.edge; + const crossLoc = this.issue.data.cross_loc; + const edgeNodes = [context.entity(targetEdge[0]), context.entity(targetEdge[1])]; + const closestNodeInfo = geoSphericalClosestNode(edgeNodes, crossLoc); + + const annotation = t('issues.fix.connect_almost_junction.annotation'); + // already a point nearby, just connect to that + if (closestNodeInfo.distance < WELD_TH_METERS) { + context.perform( + actionMergeNodes([closestNodeInfo.node.id, endNode.id], closestNodeInfo.node.loc), + annotation + ); + // else add the end node to the edge way + } else { + context.perform( + actionAddMidpoint({loc: crossLoc, edge: targetEdge}, endNode), + annotation + ); + } + } + })]; + + const node = context.hasEntity(this.entityIds[1]); + if (node && !node.hasInterestingTags()) { + // node has no descriptive tags, suggest noexit fix + fixes.push(new validationIssueFix({ + icon: 'maki-barrier', + title: t('issues.fix.tag_as_disconnected.title'), + onClick(context) { + const nodeID = this.issue.entityIds[1]; + const tags = Object.assign({}, context.entity(nodeID).tags); + tags.noexit = 'yes'; + context.perform( + actionChangeTags(nodeID, tags), + t('issues.fix.tag_as_disconnected.annotation') + ); + } + })); + } + + return fixes; } - const validation = function checkAlmostJunction(entity, graph) { - if (!isHighway(entity)) return []; - if (entity.isDegenerate()) return []; - - const tree = context.history().tree(); - const extendableNodeInfos = findConnectableEndNodesByExtension(entity); - - let issues = []; - - extendableNodeInfos.forEach(extendableNodeInfo => { - issues.push(new validationIssue({ - type, - subtype: 'highway-highway', - severity: 'warning', - message(context) { - const entity1 = context.hasEntity(this.entityIds[0]); - if (this.entityIds[0] === this.entityIds[2]) { - return entity1 ? t('issues.almost_junction.self.message', { - feature: utilDisplayLabel(entity1, context) - }) : ''; - } else { - const entity2 = context.hasEntity(this.entityIds[2]); - return (entity1 && entity2) ? t('issues.almost_junction.message', { - feature: utilDisplayLabel(entity1, context), - feature2: utilDisplayLabel(entity2, context) - }) : ''; - } - }, - reference: showReference, - entityIds: [entity.id, extendableNodeInfo.node.id, extendableNodeInfo.wid], - loc: extendableNodeInfo.node.loc, - hash: JSON.stringify(extendableNodeInfo.node.loc), - data: { - edge: extendableNodeInfo.edge, - cross_loc: extendableNodeInfo.cross_loc - }, - dynamicFixes: makeFixes - })); - }); - - return issues; + function showReference(selection) { + selection.selectAll('.issue-reference') + .data([0]) + .enter() + .append('div') + .attr('class', 'issue-reference') + .text(t('issues.almost_junction.highway-highway.reference')); + } - function makeFixes(context) { - let fixes = [new validationIssueFix({ - icon: 'iD-icon-abutment', - title: t('issues.fix.connect_features.title'), - onClick(context) { - const endNodeId = this.issue.entityIds[1]; - const endNode = context.entity(endNodeId); - const targetEdge = this.issue.data.edge; - const crossLoc = this.issue.data.cross_loc; - const edgeNodes = [context.entity(targetEdge[0]), context.entity(targetEdge[1])]; - const closestNodeInfo = geoSphericalClosestNode(edgeNodes, crossLoc); + function isExtendableCandidate(node, way) { + // can not accurately test vertices on tiles not downloaded from osm - #5938 + const osm = services.osm; + if (osm && !osm.isDataLoaded(node.loc)) { + return false; + } + if (isTaggedAsNotContinuing(node) || graph.parentWays(node).length !== 1) { + return false; + } - const annotation = t('issues.fix.connect_almost_junction.annotation'); - // already a point nearby, just connect to that - if (closestNodeInfo.distance < WELD_TH_METERS) { - context.perform( - actionMergeNodes([closestNodeInfo.node.id, endNode.id], closestNodeInfo.node.loc), - annotation - ); - // else add the end node to the edge way - } else { - context.perform( - actionAddMidpoint({loc: crossLoc, edge: targetEdge}, endNode), - annotation - ); - } - } - })]; + let occurences = 0; + for (const index in way.nodes) { + if (way.nodes[index] === node.id) { + occurences += 1; + if (occurences > 1) { + return false; + } + } + } + return true; + } - const node = context.hasEntity(this.entityIds[1]); - if (node && !node.hasInterestingTags()) { - // node has no descriptive tags, suggest noexit fix - fixes.push(new validationIssueFix({ - icon: 'maki-barrier', - title: t('issues.fix.tag_as_disconnected.title'), - onClick(context) { - const nodeID = this.issue.entityIds[1]; - const tags = Object.assign({}, context.entity(nodeID).tags); - tags.noexit = 'yes'; - context.perform( - actionChangeTags(nodeID, tags), - t('issues.fix.tag_as_disconnected.annotation') - ); - } - })); + + function findConnectableEndNodesByExtension(way) { + let results = []; + if (way.isClosed()) return results; + + let testNodes; + const indices = [0, way.nodes.length - 1]; + indices.forEach(nodeIndex => { + const nodeID = way.nodes[nodeIndex]; + const node = graph.entity(nodeID); + + if (!isExtendableCandidate(node, way)) return; + + const connectionInfo = canConnectByExtend(way, nodeIndex); + if (!connectionInfo) return; + + testNodes = graph.childNodes(way).slice(); // shallow copy + testNodes[nodeIndex] = testNodes[nodeIndex].move(connectionInfo.cross_loc); + + // don't flag issue if connecting the ways would cause self-intersection + if (geoHasSelfIntersections(testNodes, nodeID)) return; + + results.push(connectionInfo); + }); + + return results; + } + + function hasTag(tags, key) { + return tags[key] !== undefined && tags[key] !== 'no'; + } + + function canConnectWays(way, way2) { + + // allow self-connections + if (way.id === way2.id) return true; + + // if one is bridge or tunnel, both must be bridge or tunnel + if ((hasTag(way.tags, 'bridge') || hasTag(way2.tags, 'bridge')) && + !(hasTag(way.tags, 'bridge') && hasTag(way2.tags, 'bridge'))) return false; + if ((hasTag(way.tags, 'tunnel') || hasTag(way2.tags, 'tunnel')) && + !(hasTag(way.tags, 'tunnel') && hasTag(way2.tags, 'tunnel'))) return false; + + // must have equivalent layers and levels + const layer1 = way.tags.layer || '0', + layer2 = way2.tags.layer || '0'; + if (layer1 !== layer2) return false; + + const level1 = way.tags.level || '0', + level2 = way2.tags.level || '0'; + if (level1 !== level2) return false; + + return true; + } + + function findNearbyEndNodes(node, way2) { + return [ + way2.nodes[0], + way2.nodes[way2.nodes.length - 1] + ].map(d => graph.entity(d)) + .filter(d => { + // Node cannot be near to itself, but other endnode of same way could be + return d.id !== node.id + && geoSphericalDistance(node.loc, d.loc) <= CLOSE_NODE_TH; + }); + } + + function findSmallJoinAngle(midNode, tipNode, endNodes) { + // Both nodes could be close, so want to join whichever is closest to collinear + let joinTo; + let minAngle = Infinity; + + // Checks midNode -> tipNode -> endNode for collinearity + endNodes.forEach(endNode => { + const a1 = geoAngle(midNode, tipNode, context.projection) + Math.PI; + const a2 = geoAngle(midNode, endNode, context.projection) + Math.PI; + const diff = Math.max(a1, a2) - Math.min(a1, a2); + + if (diff < minAngle) { + joinTo = endNode; + minAngle = diff; + } + }); + + /* Threshold set by considering right angle triangle + based on node joining threshold and extension distance */ + if (minAngle <= SIG_ANGLE_TH) return joinTo; + + return null; + } + + function canConnectByExtend(way, endNodeIdx) { + const tipNid = way.nodes[endNodeIdx]; // the 'tip' node for extension point + const midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2]; // the other node of the edge + const tipNode = graph.entity(tipNid); + const midNode = graph.entity(midNid); + const lon = tipNode.loc[0]; + const lat = tipNode.loc[1]; + const lon_range = geoMetersToLon(EXTEND_TH_METERS, lat) / 2; + const lat_range = geoMetersToLat(EXTEND_TH_METERS) / 2; + const queryExtent = geoExtent([ + [lon - lon_range, lat - lat_range], + [lon + lon_range, lat + lat_range] + ]); + + // first, extend the edge of [midNode -> tipNode] by EXTEND_TH_METERS and find the "extended tip" location + const edgeLen = geoSphericalDistance(midNode.loc, tipNode.loc); + const t = EXTEND_TH_METERS / edgeLen + 1.0; + const extTipLoc = geoVecInterp(midNode.loc, tipNode.loc, t); + + // then, check if the extension part [tipNode.loc -> extTipLoc] intersects any other ways + const intersected = tree.intersects(queryExtent, graph); + for (let i = 0; i < intersected.length; i++) { + let way2 = intersected[i]; + + if (!isHighway(way2)) continue; + + if (!canConnectWays(way, way2)) continue; + + for (let j = 0; j < way2.nodes.length - 1; j++) { + let nAid = way2.nodes[j], + nBid = way2.nodes[j + 1]; + + if (nAid === tipNid || nBid === tipNid) continue; + + let nA = graph.entity(nAid), + nB = graph.entity(nBid); + let crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]); + if (crossLoc) { + // When endpoints are close, just join if resulting small change in angle (#7201) + let nearEndNodes = findNearbyEndNodes(tipNode, way2); + if (nearEndNodes.length > 0) { + let collinear = findSmallJoinAngle(midNode, tipNode, nearEndNodes); + if (collinear) { + return { + node: tipNode, + wid: way2.id, + edge: [collinear.id, collinear.id], + cross_loc: collinear.loc + }; + } } - return fixes; + return { + node: tipNode, + wid: way2.id, + edge: [nA.id, nB.id], + cross_loc: crossLoc + }; + } } + } + return null; + } + }; + validation.type = type; - function showReference(selection) { - selection.selectAll('.issue-reference') - .data([0]) - .enter() - .append('div') - .attr('class', 'issue-reference') - .text(t('issues.almost_junction.highway-highway.reference')); - } - - - function isExtendableCandidate(node, way) { - // can not accurately test vertices on tiles not downloaded from osm - #5938 - const osm = services.osm; - if (osm && !osm.isDataLoaded(node.loc)) { - return false; - } - if (isTaggedAsNotContinuing(node) || graph.parentWays(node).length !== 1) { - return false; - } - - let occurences = 0; - for (const index in way.nodes) { - if (way.nodes[index] === node.id) { - occurences += 1; - if (occurences > 1) { - return false; - } - } - } - return true; - } - - - function findConnectableEndNodesByExtension(way) { - let results = []; - if (way.isClosed()) return results; - - let testNodes; - const indices = [0, way.nodes.length - 1]; - indices.forEach(nodeIndex => { - const nodeID = way.nodes[nodeIndex]; - const node = graph.entity(nodeID); - - if (!isExtendableCandidate(node, way)) return; - - const connectionInfo = canConnectByExtend(way, nodeIndex); - if (!connectionInfo) return; - - testNodes = graph.childNodes(way).slice(); // shallow copy - testNodes[nodeIndex] = testNodes[nodeIndex].move(connectionInfo.cross_loc); - - // don't flag issue if connecting the ways would cause self-intersection - if (geoHasSelfIntersections(testNodes, nodeID)) return; - - results.push(connectionInfo); - }); - - return results; - } - - function hasTag(tags, key) { - return tags[key] !== undefined && tags[key] !== 'no'; - } - - function canConnectWays(way, way2) { - - // allow self-connections - if (way.id === way2.id) return true; - - // if one is bridge or tunnel, both must be bridge or tunnel - if ((hasTag(way.tags, 'bridge') || hasTag(way2.tags, 'bridge')) && - !(hasTag(way.tags, 'bridge') && hasTag(way2.tags, 'bridge'))) return false; - if ((hasTag(way.tags, 'tunnel') || hasTag(way2.tags, 'tunnel')) && - !(hasTag(way.tags, 'tunnel') && hasTag(way2.tags, 'tunnel'))) return false; - - // must have equivalent layers and levels - const layer1 = way.tags.layer || '0', - layer2 = way2.tags.layer || '0'; - if (layer1 !== layer2) return false; - - const level1 = way.tags.level || '0', - level2 = way2.tags.level || '0'; - if (level1 !== level2) return false; - - return true; - } - - function findNearbyEndNodes(node, way2) { - return [ - way2.nodes[0], - way2.nodes[way2.nodes.length - 1] - ].map(d => graph.entity(d)) - .filter(d => { - // Node cannot be near to itself, but other endnode of same way could be - return d.id !== node.id - && geoSphericalDistance(node.loc, d.loc) <= CLOSE_NODE_TH; - }); - } - - function findSmallJoinAngle(midNode, tipNode, endNodes) { - // Both nodes could be close, so want to join whichever is closest to collinear - let joinTo; - let minAngle = Infinity; - - // Checks midNode -> tipNode -> endNode for collinearity - endNodes.forEach(endNode => { - const a1 = geoAngle(midNode, tipNode, context.projection) + Math.PI; - const a2 = geoAngle(midNode, endNode, context.projection) + Math.PI; - const diff = Math.max(a1, a2) - Math.min(a1, a2); - - if (diff < minAngle) { - joinTo = endNode; - minAngle = diff; - } - }); - - /* Threshold set by considering right angle triangle - based on node joining threshold and extension distance */ - if (minAngle <= SIG_ANGLE_TH) return joinTo; - - return null; - } - - function canConnectByExtend(way, endNodeIdx) { - const tipNid = way.nodes[endNodeIdx]; // the 'tip' node for extension point - const midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2]; // the other node of the edge - const tipNode = graph.entity(tipNid); - const midNode = graph.entity(midNid); - const lon = tipNode.loc[0]; - const lat = tipNode.loc[1]; - const lon_range = geoMetersToLon(EXTEND_TH_METERS, lat) / 2; - const lat_range = geoMetersToLat(EXTEND_TH_METERS) / 2; - const queryExtent = geoExtent([ - [lon - lon_range, lat - lat_range], - [lon + lon_range, lat + lat_range] - ]); - - // first, extend the edge of [midNode -> tipNode] by EXTEND_TH_METERS and find the "extended tip" location - const edgeLen = geoSphericalDistance(midNode.loc, tipNode.loc); - const t = EXTEND_TH_METERS / edgeLen + 1.0; - const extTipLoc = geoVecInterp(midNode.loc, tipNode.loc, t); - - // then, check if the extension part [tipNode.loc -> extTipLoc] intersects any other ways - const intersected = tree.intersects(queryExtent, graph); - for (let i = 0; i < intersected.length; i++) { - let way2 = intersected[i]; - - if (!isHighway(way2)) continue; - - if (!canConnectWays(way, way2)) continue; - - for (let j = 0; j < way2.nodes.length - 1; j++) { - let nAid = way2.nodes[j], - nBid = way2.nodes[j + 1]; - - if (nAid === tipNid || nBid === tipNid) continue; - - let nA = graph.entity(nAid), - nB = graph.entity(nBid); - let crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]); - if (crossLoc) { - // When endpoints are close, just join if resulting small change in angle (#7201) - let nearEndNodes = findNearbyEndNodes(tipNode, way2); - if (nearEndNodes.length > 0) { - let collinear = findSmallJoinAngle(midNode, tipNode, nearEndNodes); - if (collinear) { - return { - node: tipNode, - wid: way2.id, - edge: [collinear.id, collinear.id], - cross_loc: collinear.loc - }; - } - } - - return { - node: tipNode, - wid: way2.id, - edge: [nA.id, nB.id], - cross_loc: crossLoc - }; - } - } - } - return null; - } - }; - - validation.type = type; - - return validation; + return validation; } From d96fde53dbd28e87c178a2cdd1089225d23c739d Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Fri, 31 Jan 2020 17:58:53 +0000 Subject: [PATCH 007/127] Add test for almost junction with both close ends --- test/spec/validations/almost_junction.js | 54 +++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/test/spec/validations/almost_junction.js b/test/spec/validations/almost_junction.js index 05223d467..b70851d84 100644 --- a/test/spec/validations/almost_junction.js +++ b/test/spec/validations/almost_junction.js @@ -199,6 +199,36 @@ describe('iD.validations.almost_junction', function () { return n1; } + function closeEndNodesBothSmallAngle() { + // Square path with both endpoints near eachother + var n1 = iD.osmNode({id: 'n-1', loc: [0, 22.4427453]}); + var n2 = iD.osmNode({id: 'n-2', loc: [0, 22.4429810]}); + var n3 = iD.osmNode({id: 'n-3', loc: [0.0000063, 22.4429810]}); + var n4 = iD.osmNode({id: 'n-4', loc: [0.0000063, 22.4427483]}); + var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2', 'n-3', 'n-4'], tags: { highway: 'path' }}); + + context.perform( + iD.actionAddEntity(n1), + iD.actionAddEntity(n2), + iD.actionAddEntity(n3), + iD.actionAddEntity(n4), + iD.actionAddEntity(w1) + ); + + // Horizontal path with end node within 4.25m and change of angle >9° (to both endpoints) + var n5 = iD.osmNode({id: 'n-5', loc: [0.0000124, 22.4427458]}); + var n6 = iD.osmNode({id: 'n-6', loc: [0.0000445, 22.4427449]}); + var w2 = iD.osmWay({id: 'w-2', nodes: ['n-5', 'n-6'], tags: { highway: 'path' }}); + + context.perform( + iD.actionAddEntity(n5), + iD.actionAddEntity(n6), + iD.actionAddEntity(w2) + ); + + return n1; + } + function validate() { var validator = iD.validationAlmostJunction(context); var changes = context.history().changes(); @@ -293,7 +323,6 @@ describe('iD.validations.almost_junction', function () { expect(issues).to.have.lengthOf(0); }); - // TODO: Test case of both endpoints of another way close, should prioritise smaller angle change it('joins close endpoints if insignificant angle change', function() { var n1 = closeEndNodesSmallAngle(); var issues = validate(); @@ -359,4 +388,27 @@ describe('iD.validations.almost_junction', function () { expect(issue.data.cross_loc).to.have.lengthOf(2); expect(issue.data.cross_loc).to.eql(n1.loc); }); + + + it('joins to close endpoint with smaller angle change', function() { + var n1 = closeEndNodesBothSmallAngle(); + var issues = validate(); + expect(issues).to.have.lengthOf(1); + var issue = issues[0]; + expect(issue.type).to.eql('almost_junction'); + expect(issue.subtype).to.eql('highway-highway'); + expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds[0]).to.eql('w-2'); + expect(issue.entityIds[1]).to.eql('n-5'); + expect(issue.entityIds[2]).to.eql('w-1'); + + // Duplicate edge nodes means endpoints will be joined + expect(issue.data.edge).to.have.lengthOf(2); + expect(issue.data.edge[0]).to.eql('n-1'); + expect(issue.data.edge[1]).to.eql('n-1'); + + // Crossing set to loc of end node means endpoints will be joined + expect(issue.data.cross_loc).to.have.lengthOf(2); + expect(issue.data.cross_loc).to.eql(n1.loc); + }); }); From 0b4a690a08e435846db2697224b7ebee1026ce9e Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Sun, 2 Feb 2020 12:37:15 +0000 Subject: [PATCH 008/127] Move close endpoint handling into quick fix Just a bit more optimised since this is only relevant to check when the quick fix is used --- modules/validations/almost_junction.js | 114 +++++++++++++------------ 1 file changed, 59 insertions(+), 55 deletions(-) diff --git a/modules/validations/almost_junction.js b/modules/validations/almost_junction.js index f832aebbb..d98044c1a 100644 --- a/modules/validations/almost_junction.js +++ b/modules/validations/almost_junction.js @@ -54,12 +54,12 @@ export function validationAlmostJunction(context) { severity: 'warning', message(context) { const entity1 = context.hasEntity(this.entityIds[0]); - if (this.entityIds[0] === this.entityIds[2]) { + if (this.entityIds[0] === this.entityIds[3]) { return entity1 ? t('issues.almost_junction.self.message', { feature: utilDisplayLabel(entity1, context) }) : ''; } else { - const entity2 = context.hasEntity(this.entityIds[2]); + const entity2 = context.hasEntity(this.entityIds[3]); return (entity1 && entity2) ? t('issues.almost_junction.message', { feature: utilDisplayLabel(entity1, context), feature2: utilDisplayLabel(entity2, context) @@ -67,7 +67,12 @@ export function validationAlmostJunction(context) { } }, reference: showReference, - entityIds: [entity.id, extendableNodeInfo.node.id, extendableNodeInfo.wid], + entityIds: [ + entity.id, + extendableNodeInfo.mid.id, + extendableNodeInfo.node.id, + extendableNodeInfo.wid, + ], loc: extendableNodeInfo.node.loc, hash: JSON.stringify(extendableNodeInfo.node.loc), data: { @@ -80,20 +85,35 @@ export function validationAlmostJunction(context) { return issues; - function makeFixes(context) { let fixes = [new validationIssueFix({ icon: 'iD-icon-abutment', title: t('issues.fix.connect_features.title'), onClick(context) { - const endNodeId = this.issue.entityIds[1]; + const annotation = t('issues.fix.connect_almost_junction.annotation'); + const [, midNodeId, endNodeId, crossWayId] = this.issue.entityIds; + const midNode = context.entity(midNodeId); const endNode = context.entity(endNodeId); + const crossWay = context.entity(crossWayId); + + // When endpoints are close, just join if resulting small change in angle (#7201) + const nearEndNodes = findNearbyEndNodes(endNode, crossWay); + if (nearEndNodes.length > 0) { + const collinear = findSmallJoinAngle(midNode, endNode, nearEndNodes); + if (collinear) { + context.perform( + actionMergeNodes([collinear.id, endNode.id], collinear.loc), + annotation + ); + return; + } + } + const targetEdge = this.issue.data.edge; const crossLoc = this.issue.data.cross_loc; const edgeNodes = [context.entity(targetEdge[0]), context.entity(targetEdge[1])]; const closestNodeInfo = geoSphericalClosestNode(edgeNodes, crossLoc); - const annotation = t('issues.fix.connect_almost_junction.annotation'); // already a point nearby, just connect to that if (closestNodeInfo.distance < WELD_TH_METERS) { context.perform( @@ -110,14 +130,14 @@ export function validationAlmostJunction(context) { } })]; - const node = context.hasEntity(this.entityIds[1]); + const node = context.hasEntity(this.entityIds[2]); if (node && !node.hasInterestingTags()) { // node has no descriptive tags, suggest noexit fix fixes.push(new validationIssueFix({ icon: 'maki-barrier', title: t('issues.fix.tag_as_disconnected.title'), onClick(context) { - const nodeID = this.issue.entityIds[1]; + const nodeID = this.issue.entityIds[2]; const tags = Object.assign({}, context.entity(nodeID).tags); tags.noexit = 'yes'; context.perform( @@ -131,7 +151,6 @@ export function validationAlmostJunction(context) { return fixes; } - function showReference(selection) { selection.selectAll('.issue-reference') .data([0]) @@ -141,7 +160,6 @@ export function validationAlmostJunction(context) { .text(t('issues.almost_junction.highway-highway.reference')); } - function isExtendableCandidate(node, way) { // can not accurately test vertices on tiles not downloaded from osm - #5938 const osm = services.osm; @@ -164,7 +182,6 @@ export function validationAlmostJunction(context) { return true; } - function findConnectableEndNodesByExtension(way) { let results = []; if (way.isClosed()) return results; @@ -192,37 +209,10 @@ export function validationAlmostJunction(context) { return results; } - function hasTag(tags, key) { - return tags[key] !== undefined && tags[key] !== 'no'; - } - - function canConnectWays(way, way2) { - - // allow self-connections - if (way.id === way2.id) return true; - - // if one is bridge or tunnel, both must be bridge or tunnel - if ((hasTag(way.tags, 'bridge') || hasTag(way2.tags, 'bridge')) && - !(hasTag(way.tags, 'bridge') && hasTag(way2.tags, 'bridge'))) return false; - if ((hasTag(way.tags, 'tunnel') || hasTag(way2.tags, 'tunnel')) && - !(hasTag(way.tags, 'tunnel') && hasTag(way2.tags, 'tunnel'))) return false; - - // must have equivalent layers and levels - const layer1 = way.tags.layer || '0', - layer2 = way2.tags.layer || '0'; - if (layer1 !== layer2) return false; - - const level1 = way.tags.level || '0', - level2 = way2.tags.level || '0'; - if (level1 !== level2) return false; - - return true; - } - - function findNearbyEndNodes(node, way2) { + function findNearbyEndNodes(node, way) { return [ - way2.nodes[0], - way2.nodes[way2.nodes.length - 1] + way.nodes[0], + way.nodes[way.nodes.length - 1] ].map(d => graph.entity(d)) .filter(d => { // Node cannot be near to itself, but other endnode of same way could be @@ -255,6 +245,33 @@ export function validationAlmostJunction(context) { return null; } + function hasTag(tags, key) { + return tags[key] !== undefined && tags[key] !== 'no'; + } + + function canConnectWays(way, way2) { + + // allow self-connections + if (way.id === way2.id) return true; + + // if one is bridge or tunnel, both must be bridge or tunnel + if ((hasTag(way.tags, 'bridge') || hasTag(way2.tags, 'bridge')) && + !(hasTag(way.tags, 'bridge') && hasTag(way2.tags, 'bridge'))) return false; + if ((hasTag(way.tags, 'tunnel') || hasTag(way2.tags, 'tunnel')) && + !(hasTag(way.tags, 'tunnel') && hasTag(way2.tags, 'tunnel'))) return false; + + // must have equivalent layers and levels + const layer1 = way.tags.layer || '0', + layer2 = way2.tags.layer || '0'; + if (layer1 !== layer2) return false; + + const level1 = way.tags.level || '0', + level2 = way2.tags.level || '0'; + if (level1 !== level2) return false; + + return true; + } + function canConnectByExtend(way, endNodeIdx) { const tipNid = way.nodes[endNodeIdx]; // the 'tip' node for extension point const midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2]; // the other node of the edge @@ -293,21 +310,8 @@ export function validationAlmostJunction(context) { nB = graph.entity(nBid); let crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]); if (crossLoc) { - // When endpoints are close, just join if resulting small change in angle (#7201) - let nearEndNodes = findNearbyEndNodes(tipNode, way2); - if (nearEndNodes.length > 0) { - let collinear = findSmallJoinAngle(midNode, tipNode, nearEndNodes); - if (collinear) { - return { - node: tipNode, - wid: way2.id, - edge: [collinear.id, collinear.id], - cross_loc: collinear.loc - }; - } - } - return { + mid: midNode, node: tipNode, wid: way2.id, edge: [nA.id, nB.id], From d83675f43ec2faffb5f91434b0d7cb67c528a3a7 Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Sun, 2 Feb 2020 13:10:02 +0000 Subject: [PATCH 009/127] Update almost junction tests to reflect change --- test/spec/validations/almost_junction.js | 109 ++++++++++------------- 1 file changed, 47 insertions(+), 62 deletions(-) diff --git a/test/spec/validations/almost_junction.js b/test/spec/validations/almost_junction.js index b70851d84..3f5cd2b14 100644 --- a/test/spec/validations/almost_junction.js +++ b/test/spec/validations/almost_junction.js @@ -148,8 +148,6 @@ describe('iD.validations.almost_junction', function () { iD.actionAddEntity(n4), iD.actionAddEntity(w2) ); - - return n1; } function closeEndNodesBigAngle() { @@ -174,8 +172,6 @@ describe('iD.validations.almost_junction', function () { iD.actionAddEntity(n4), iD.actionAddEntity(w2) ); - - return n1; } function closeEndNodesSmallAngleSelf() { @@ -195,8 +191,6 @@ describe('iD.validations.almost_junction', function () { iD.actionAddEntity(n5), iD.actionAddEntity(w1) ); - - return n1; } function closeEndNodesBothSmallAngle() { @@ -225,8 +219,6 @@ describe('iD.validations.almost_junction', function () { iD.actionAddEntity(n6), iD.actionAddEntity(w2) ); - - return n1; } function validate() { @@ -252,10 +244,11 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds).to.have.lengthOf(4); expect(issue.entityIds[0]).to.eql('w-1'); - expect(issue.entityIds[1]).to.eql('n-1'); - expect(issue.entityIds[2]).to.eql('w-2'); + expect(issue.entityIds[1]).to.eql('n-2'); + expect(issue.entityIds[2]).to.eql('n-1'); + expect(issue.entityIds[3]).to.eql('w-2'); expect(issue.loc).to.have.lengthOf(2); expect(issue.loc[0]).to.eql(22.42357); @@ -282,10 +275,11 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds).to.have.lengthOf(4); expect(issue.entityIds[0]).to.eql('w-1'); - expect(issue.entityIds[1]).to.eql('n-1'); - expect(issue.entityIds[2]).to.eql('w-2'); + expect(issue.entityIds[1]).to.eql('n-2'); + expect(issue.entityIds[2]).to.eql('n-1'); + expect(issue.entityIds[3]).to.eql('w-2'); expect(issue.loc).to.have.lengthOf(2); expect(issue.loc[0]).to.eql(22.42357); @@ -324,91 +318,82 @@ describe('iD.validations.almost_junction', function () { }); it('joins close endpoints if insignificant angle change', function() { - var n1 = closeEndNodesSmallAngle(); + closeEndNodesSmallAngle(); var issues = validate(); expect(issues).to.have.lengthOf(1); var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds).to.have.lengthOf(4); expect(issue.entityIds[0]).to.eql('w-2'); - expect(issue.entityIds[1]).to.eql('n-3'); - expect(issue.entityIds[2]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-4'); + expect(issue.entityIds[2]).to.eql('n-3'); + expect(issue.entityIds[3]).to.eql('w-1'); - // Duplicate edge nodes means endpoints will be joined - expect(issue.data.edge).to.have.lengthOf(2); - expect(issue.data.edge[0]).to.eql('n-1'); - expect(issue.data.edge[1]).to.eql('n-1'); - - // Crossing set to loc of end node means endpoints will be joined - expect(issue.data.cross_loc).to.have.lengthOf(2); - expect(issue.data.cross_loc).to.eql(n1.loc); + issue.fixes(context)[0].onClick(context); + var w1 = context.entity('w-1'); + var w2 = context.entity('w-2'); + var joined = w2.nodes[0] === w1.nodes[0]; + expect(joined).to.be.true; }); it('won\'t join close endpoints if significant angle change', function() { - var n1 = closeEndNodesBigAngle(); + closeEndNodesBigAngle(); var issues = validate(); expect(issues).to.have.lengthOf(1); var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds).to.have.lengthOf(4); expect(issue.entityIds[0]).to.eql('w-2'); - expect(issue.entityIds[1]).to.eql('n-3'); - expect(issue.entityIds[2]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-4'); + expect(issue.entityIds[2]).to.eql('n-3'); + expect(issue.entityIds[3]).to.eql('w-1'); - // Differing edge nodes means endpoints won't be joined - expect(issue.data.edge).to.have.lengthOf(2); - expect(issue.data.edge[0]).to.eql('n-1'); - expect(issue.data.edge[1]).to.eql('n-2'); - - // Crossing different from loc of end node means endpoints won't be joined - expect(issue.data.cross_loc).to.have.lengthOf(2); - expect(issue.data.cross_loc).to.not.eql(n1.loc); + issue.fixes(context)[0].onClick(context); + var w1 = context.entity('w-1'); + var w2 = context.entity('w-2'); + var joined = w2.nodes[0] === w1.nodes[0]; + expect(joined).not.to.be.true; }); it('joins close endpoints of the same way', function() { - var n1 = closeEndNodesSmallAngleSelf(); + closeEndNodesSmallAngleSelf(); var issues = validate(); expect(issues).to.have.lengthOf(1); var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds).to.have.lengthOf(4); expect(issue.entityIds[0]).to.eql('w-1'); - expect(issue.entityIds[1]).to.eql('n-5'); - expect(issue.entityIds[2]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-4'); + expect(issue.entityIds[2]).to.eql('n-5'); + expect(issue.entityIds[3]).to.eql('w-1'); - // Duplicate edge nodes means endpoints will be joined - expect(issue.data.edge).to.have.lengthOf(2); - expect(issue.data.edge[0]).to.eql('n-1'); - expect(issue.data.edge[1]).to.eql('n-1'); - - // Crossing set to loc of end node means endpoints will be joined - expect(issue.data.cross_loc).to.have.lengthOf(2); - expect(issue.data.cross_loc).to.eql(n1.loc); + issue.fixes(context)[0].onClick(context); + var w = context.entity('w-1'); + var joined = w.nodes[0] === w.nodes[w.nodes.length - 1]; + expect(joined).to.be.true; }); it('joins to close endpoint with smaller angle change', function() { - var n1 = closeEndNodesBothSmallAngle(); + closeEndNodesBothSmallAngle(); var issues = validate(); expect(issues).to.have.lengthOf(1); var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds).to.have.lengthOf(4); expect(issue.entityIds[0]).to.eql('w-2'); - expect(issue.entityIds[1]).to.eql('n-5'); - expect(issue.entityIds[2]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-6'); + expect(issue.entityIds[2]).to.eql('n-5'); + expect(issue.entityIds[3]).to.eql('w-1'); - // Duplicate edge nodes means endpoints will be joined - expect(issue.data.edge).to.have.lengthOf(2); - expect(issue.data.edge[0]).to.eql('n-1'); - expect(issue.data.edge[1]).to.eql('n-1'); - - // Crossing set to loc of end node means endpoints will be joined - expect(issue.data.cross_loc).to.have.lengthOf(2); - expect(issue.data.cross_loc).to.eql(n1.loc); + issue.fixes(context)[0].onClick(context); + var w1 = context.entity('w-1'); + var w2 = context.entity('w-2'); + var joined = w2.nodes[0] === w1.nodes[0]; + expect(joined).to.be.true; }); }); From 142126f7fec7bd96b608a26454425de0d78fa32c Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Sun, 2 Feb 2020 15:46:41 +0000 Subject: [PATCH 010/127] Store node for quick fix in data not entityIds Realised these are used for highlighting and should remain as is. --- modules/validations/almost_junction.js | 14 ++++---- test/spec/validations/almost_junction.js | 42 ++++++++++-------------- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/modules/validations/almost_junction.js b/modules/validations/almost_junction.js index d98044c1a..67fc15979 100644 --- a/modules/validations/almost_junction.js +++ b/modules/validations/almost_junction.js @@ -54,12 +54,12 @@ export function validationAlmostJunction(context) { severity: 'warning', message(context) { const entity1 = context.hasEntity(this.entityIds[0]); - if (this.entityIds[0] === this.entityIds[3]) { + if (this.entityIds[0] === this.entityIds[2]) { return entity1 ? t('issues.almost_junction.self.message', { feature: utilDisplayLabel(entity1, context) }) : ''; } else { - const entity2 = context.hasEntity(this.entityIds[3]); + const entity2 = context.hasEntity(this.entityIds[2]); return (entity1 && entity2) ? t('issues.almost_junction.message', { feature: utilDisplayLabel(entity1, context), feature2: utilDisplayLabel(entity2, context) @@ -69,13 +69,13 @@ export function validationAlmostJunction(context) { reference: showReference, entityIds: [ entity.id, - extendableNodeInfo.mid.id, extendableNodeInfo.node.id, extendableNodeInfo.wid, ], loc: extendableNodeInfo.node.loc, hash: JSON.stringify(extendableNodeInfo.node.loc), data: { + midId: extendableNodeInfo.mid.id, edge: extendableNodeInfo.edge, cross_loc: extendableNodeInfo.cross_loc }, @@ -91,8 +91,8 @@ export function validationAlmostJunction(context) { title: t('issues.fix.connect_features.title'), onClick(context) { const annotation = t('issues.fix.connect_almost_junction.annotation'); - const [, midNodeId, endNodeId, crossWayId] = this.issue.entityIds; - const midNode = context.entity(midNodeId); + const [, endNodeId, crossWayId] = this.issue.entityIds; + const midNode = context.entity(this.issue.data.midId); const endNode = context.entity(endNodeId); const crossWay = context.entity(crossWayId); @@ -130,14 +130,14 @@ export function validationAlmostJunction(context) { } })]; - const node = context.hasEntity(this.entityIds[2]); + const node = context.hasEntity(this.entityIds[1]); if (node && !node.hasInterestingTags()) { // node has no descriptive tags, suggest noexit fix fixes.push(new validationIssueFix({ icon: 'maki-barrier', title: t('issues.fix.tag_as_disconnected.title'), onClick(context) { - const nodeID = this.issue.entityIds[2]; + const nodeID = this.issue.entityIds[1]; const tags = Object.assign({}, context.entity(nodeID).tags); tags.noexit = 'yes'; context.perform( diff --git a/test/spec/validations/almost_junction.js b/test/spec/validations/almost_junction.js index 3f5cd2b14..5cbdce551 100644 --- a/test/spec/validations/almost_junction.js +++ b/test/spec/validations/almost_junction.js @@ -244,11 +244,10 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(4); + expect(issue.entityIds).to.have.lengthOf(3); expect(issue.entityIds[0]).to.eql('w-1'); - expect(issue.entityIds[1]).to.eql('n-2'); - expect(issue.entityIds[2]).to.eql('n-1'); - expect(issue.entityIds[3]).to.eql('w-2'); + expect(issue.entityIds[1]).to.eql('n-1'); + expect(issue.entityIds[2]).to.eql('w-2'); expect(issue.loc).to.have.lengthOf(2); expect(issue.loc[0]).to.eql(22.42357); @@ -275,11 +274,10 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(4); + expect(issue.entityIds).to.have.lengthOf(3); expect(issue.entityIds[0]).to.eql('w-1'); - expect(issue.entityIds[1]).to.eql('n-2'); - expect(issue.entityIds[2]).to.eql('n-1'); - expect(issue.entityIds[3]).to.eql('w-2'); + expect(issue.entityIds[1]).to.eql('n-1'); + expect(issue.entityIds[2]).to.eql('w-2'); expect(issue.loc).to.have.lengthOf(2); expect(issue.loc[0]).to.eql(22.42357); @@ -324,11 +322,10 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(4); + expect(issue.entityIds).to.have.lengthOf(3); expect(issue.entityIds[0]).to.eql('w-2'); - expect(issue.entityIds[1]).to.eql('n-4'); - expect(issue.entityIds[2]).to.eql('n-3'); - expect(issue.entityIds[3]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-3'); + expect(issue.entityIds[2]).to.eql('w-1'); issue.fixes(context)[0].onClick(context); var w1 = context.entity('w-1'); @@ -344,11 +341,10 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(4); + expect(issue.entityIds).to.have.lengthOf(3); expect(issue.entityIds[0]).to.eql('w-2'); - expect(issue.entityIds[1]).to.eql('n-4'); - expect(issue.entityIds[2]).to.eql('n-3'); - expect(issue.entityIds[3]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-3'); + expect(issue.entityIds[2]).to.eql('w-1'); issue.fixes(context)[0].onClick(context); var w1 = context.entity('w-1'); @@ -364,11 +360,10 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(4); + expect(issue.entityIds).to.have.lengthOf(3); expect(issue.entityIds[0]).to.eql('w-1'); - expect(issue.entityIds[1]).to.eql('n-4'); - expect(issue.entityIds[2]).to.eql('n-5'); - expect(issue.entityIds[3]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-5'); + expect(issue.entityIds[2]).to.eql('w-1'); issue.fixes(context)[0].onClick(context); var w = context.entity('w-1'); @@ -384,11 +379,10 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(4); + expect(issue.entityIds).to.have.lengthOf(3); expect(issue.entityIds[0]).to.eql('w-2'); - expect(issue.entityIds[1]).to.eql('n-6'); - expect(issue.entityIds[2]).to.eql('n-5'); - expect(issue.entityIds[3]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-5'); + expect(issue.entityIds[2]).to.eql('w-1'); issue.fixes(context)[0].onClick(context); var w1 = context.entity('w-1'); From 64ae360dac4f531377d5e29143f1dfa5ced7f4de Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Sun, 2 Feb 2020 15:57:17 +0000 Subject: [PATCH 011/127] Add verbs to test spec for almost junctions --- test/spec/validations/almost_junction.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/spec/validations/almost_junction.js b/test/spec/validations/almost_junction.js index 5cbdce551..89334b08f 100644 --- a/test/spec/validations/almost_junction.js +++ b/test/spec/validations/almost_junction.js @@ -237,7 +237,7 @@ describe('iD.validations.almost_junction', function () { expect(issues).to.have.lengthOf(0); }); - it('horizontal and vertical road, closer than threshold', function() { + it('flags horizontal and vertical road closer than threshold', function() { horizontalVertialCloserThanThd(); var issues = validate(); expect(issues).to.have.lengthOf(1); @@ -267,7 +267,7 @@ describe('iD.validations.almost_junction', function () { expect(issues).to.have.lengthOf(0); }); - it('horizontal and tilted road, closer than threshold', function() { + it('flags horizontal and tilted road closer than threshold', function() { horizontalTiltedCloserThanThd(); var issues = validate(); expect(issues).to.have.lengthOf(1); @@ -297,19 +297,19 @@ describe('iD.validations.almost_junction', function () { expect(issues).to.have.lengthOf(0); }); - it('horizontal and vertical road, further than threshold', function() { + it('ignores horizontal and vertical road further than threshold', function() { horizontalVertialFurtherThanThd(); var issues = validate(); expect(issues).to.have.lengthOf(0); }); - it('horizontal and vertical road, closer than threshold but with noexit tag', function() { + it('ignores horizontal and vertical road closer than threshold, but with noexit tag', function() { horizontalVertialWithNoExit(); var issues = validate(); expect(issues).to.have.lengthOf(0); }); - it('two horizontal roads, closer than threshold', function() { + it('ignores two horizontal roads closer than threshold', function() { twoHorizontalCloserThanThd(); var issues = validate(); expect(issues).to.have.lengthOf(0); From 24f6b246ea725dfbafb85ad6751defe9df021165 Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Sun, 16 Feb 2020 13:03:52 +0000 Subject: [PATCH 012/127] Add Osmose translation to contributing guidelines --- CONTRIBUTING.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c9b02c433..666f1b546 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -245,6 +245,13 @@ These are separate translations for uniformity reasons and because some language may translate "type" differently in "type of aeroway" and "type of amenity", for example. +**Why can't I find the Osmose QA layer translations?** The Osmose QA strings are + pulled in from the external Osmose API. You can contribute to the + [Osmose Transifex project](https://www.transifex.com/projects/p/osmose/) + and the results will be seen in iD once deployed. + +Note that if you want to add/update English translations in Osmose then you will + need to head on over to the [Osmose backend source code](https://github.com/osm-fr/osmose-backend). ## Adding New Strings for Translation or Updating Existing Strings @@ -443,4 +450,4 @@ Additionally here is a step-by-step workflow example for beginners: 6. Navigate back to your "id" project - https://github.com/{{yourgithubaccount}}/iD -7. Follow this [Article about Pull Requests](https://help.github.com/articles/about-pull-requests/) to create a new pull request for your change +7. Follow this [Article about Pull Requests](https://help.github.com/articles/about-pull-requests/) to create a new pull request for your change \ No newline at end of file From 753ae3bdd29766db14b85927f05482cfdf1a4709 Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Sun, 16 Feb 2020 22:38:34 +0000 Subject: [PATCH 013/127] Use marked for Osmose strings markdown - More reliable method of handling markdown - Change external links in the strings within the details code --- modules/services/osmose.js | 21 +++++++-------------- modules/ui/osmose_details.js | 20 ++++++++++++++++---- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/modules/services/osmose.js b/modules/services/osmose.js index 28f253e74..478180af0 100644 --- a/modules/services/osmose.js +++ b/modules/services/osmose.js @@ -3,6 +3,8 @@ import RBush from 'rbush'; import { dispatch as d3_dispatch } from 'd3-dispatch'; import { json as d3_json } from 'd3-fetch'; +import marked from 'marked'; + import { currentLocale } from '../util/locale'; import { geoExtent, geoVecAdd } from '../geo'; import { QAItem } from '../osm'; @@ -60,16 +62,6 @@ function preventCoincident(loc) { return loc; } -// Osmose strings can contain markdown formatting -function parseMarkdown(string) { - // URLs - string = string.replace(/\[((?:.|\n)+?)\]\((.+?)\)/g, - '$1'); - - // Code snippets - return string.replace(/`(.+?)`/g, '$1'); -} - export default { title: 'osmose', @@ -182,7 +174,7 @@ export default { issue.elems = data.elems.map(e => e.type.substring(0,1) + e.id); // Some issues have instance specific detail in a subtitle - issue.detail = parseMarkdown(data.subtitle); + issue.detail = marked(data.subtitle); this.replaceItem(issue); }; @@ -235,11 +227,12 @@ export default { // If string exists, value is an object with key 'auto' for string const { title, detail, fix, trap } = cl; + // Osmose titles shouldn't contain markdown let issueStrings = {}; if (title) issueStrings.title = title.auto; - if (detail) issueStrings.detail = parseMarkdown(detail.auto); - if (trap) issueStrings.trap = parseMarkdown(trap.auto); - if (fix) issueStrings.fix = parseMarkdown(fix.auto); + if (detail) issueStrings.detail = marked(detail.auto); + if (trap) issueStrings.trap = marked(trap.auto); + if (fix) issueStrings.fix = marked(fix.auto); _cache.strings[locale][itemType] = issueStrings; }; diff --git a/modules/ui/osmose_details.js b/modules/ui/osmose_details.js index 6cd6568ef..e4ea677ef 100644 --- a/modules/ui/osmose_details.js +++ b/modules/ui/osmose_details.js @@ -50,7 +50,10 @@ export function uiOsmoseDetails(context) { div .append('p') .attr('class', 'qa-details-description-text') - .html(d => issueString(d, 'detail')); + .html(d => issueString(d, 'detail')) + .selectAll('a') + .attr('rel', 'noopener') + .attr('target', '_blank'); } // Elements (populated later as data is requested) @@ -74,7 +77,10 @@ export function uiOsmoseDetails(context) { div .append('p') - .html(d => issueString(d, 'fix')); + .html(d => issueString(d, 'fix')) + .selectAll('a') + .attr('rel', 'noopener') + .attr('target', '_blank'); } // Common Pitfalls (musn't exist for every issue type) @@ -89,7 +95,10 @@ export function uiOsmoseDetails(context) { div .append('p') - .html(d => issueString(d, 'trap')); + .html(d => issueString(d, 'trap')) + .selectAll('a') + .attr('rel', 'noopener') + .attr('target', '_blank'); } // Save current item to check if UI changed by time request resolves @@ -113,7 +122,10 @@ export function uiOsmoseDetails(context) { detailsDiv .append('p') - .html(d => d.detail); + .html(d => d.detail) + .selectAll('a') + .attr('rel', 'noopener') + .attr('target', '_blank'); } // Create list of linked issue elements From 56d3aa95d18655503e4ed3f547836a83a52440cd Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Mon, 17 Feb 2020 09:37:03 -0800 Subject: [PATCH 014/127] Add info about 2.x preview site to README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1dc7d7943..ff57f64e3 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ if you're looking for something to do. * [Translate!](https://github.com/openstreetmap/iD/blob/master/CONTRIBUTING.md#translating) * Test a prerelease version of iD: * Stable mirror of `release` branch: https://preview.ideditor.com/release - * Development mirror of `master` branch + latest translations: https://preview.ideditor.com/master + * Development mirror of `2.x` branch + latest translations: https://2-x--ideditor.netlify.com + * Development mirror of v3 prototype branch (`master`): https://preview.ideditor.com/master Come on in, the water's lovely. More help? Ping `bhousel` or `quincylvania` on: * [OpenStreetMap US Slack](https://slack.openstreetmap.us/) From b16f37499ab7fc597eb69e1bb0f5ea6bb5f246c5 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 18 Feb 2020 08:41:23 -0800 Subject: [PATCH 015/127] Add terms and use direction_vertex field for spike strip preset (re: #7347) --- data/presets.yaml | 1 + data/presets/presets.json | 2 +- data/presets/presets/barrier/spikes.json | 10 +++++++++- dist/locales/en.json | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/data/presets.yaml b/data/presets.yaml index 926089a1b..9db09440f 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -4188,6 +4188,7 @@ en: barrier/spikes: # barrier=spikes name: Spike Strip + # 'terms: one-way traffic treadles,stingers,stop sticks,tire deflation device,tire shredders,traffic spikes' terms: '' barrier/stile: # barrier=stile diff --git a/data/presets/presets.json b/data/presets/presets.json index a6ad30ae5..609990786 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -321,7 +321,7 @@ "barrier/lift_gate": {"icon": "temaki-lift_gate", "fields": ["access", "opening_hours"], "geometry": ["vertex", "line"], "tags": {"barrier": "lift_gate"}, "name": "Lift Gate"}, "barrier/retaining_wall": {"icon": "temaki-wall", "fields": ["height", "material"], "geometry": ["line", "area"], "tags": {"barrier": "retaining_wall"}, "name": "Retaining Wall"}, "barrier/sally_port": {"icon": "fas-dungeon", "geometry": ["vertex"], "tags": {"barrier": "sally_port"}, "terms": ["Postern", "castle side gate"], "name": "Sally Port"}, - "barrier/spikes": {"icon": "temaki-spike_strip", "fields": ["direction", "access", "height", "colour"], "geometry": ["vertex"], "tags": {"barrier": "spikes"}, "name": "Spike Strip", "matchScore": 0.5}, + "barrier/spikes": {"icon": "temaki-spike_strip", "fields": ["direction_vertex", "access", "height", "colour"], "geometry": ["vertex"], "tags": {"barrier": "spikes"}, "terms": ["one-way traffic treadles", "stingers", "stop sticks", "tire deflation device", "tire shredders", "traffic spikes"], "name": "Spike Strip", "matchScore": 0.5}, "barrier/stile": {"icon": "maki-roadblock", "fields": ["access", "stile", "material"], "geometry": ["vertex"], "tags": {"barrier": "stile"}, "name": "Stile"}, "barrier/swing_gate": {"icon": "temaki-gate", "fields": ["{barrier/gate}"], "moreFields": ["{barrier/gate}"], "geometry": ["vertex"], "tags": {"barrier": "swing_gate"}, "name": "Swing Gate"}, "barrier/toll_booth": {"icon": "maki-roadblock", "fields": ["access", "building_area", "payment_multi", "currency_multi"], "moreFields": ["address", "email", "fax", "opening_hours", "phone", "website"], "geometry": ["vertex", "area"], "tags": {"barrier": "toll_booth"}, "name": "Toll Booth"}, diff --git a/data/presets/presets/barrier/spikes.json b/data/presets/presets/barrier/spikes.json index 769bb5e03..d7f9bd732 100644 --- a/data/presets/presets/barrier/spikes.json +++ b/data/presets/presets/barrier/spikes.json @@ -1,7 +1,7 @@ { "icon": "temaki-spike_strip", "fields": [ - "direction", + "direction_vertex", "access", "height", "colour" @@ -12,6 +12,14 @@ "tags": { "barrier": "spikes" }, + "terms": [ + "one-way traffic treadles", + "stingers", + "stop sticks", + "tire deflation device", + "tire shredders", + "traffic spikes" + ], "name": "Spike Strip", "matchScore": 0.50 } diff --git a/dist/locales/en.json b/dist/locales/en.json index 6bd122bac..7837c428e 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -6168,7 +6168,7 @@ }, "barrier/spikes": { "name": "Spike Strip", - "terms": "" + "terms": "one-way traffic treadles,stingers,stop sticks,tire deflation device,tire shredders,traffic spikes" }, "barrier/stile": { "name": "Stile", From 5c150718a3cd8e9299632b246ad7511a194222ad Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 18 Feb 2020 10:40:27 -0800 Subject: [PATCH 016/127] Make all panes inherit from a standard uiPane class (re: #7368) --- css/80_app.css | 67 +++++--------- modules/ui/index.js | 4 - modules/ui/init.js | 61 +++++-------- modules/ui/pane.js | 118 +++++++++++++++++++++++++ modules/ui/{ => panes}/background.js | 88 +++++-------------- modules/ui/{ => panes}/help.js | 84 ++++-------------- modules/ui/{ => panes}/issues.js | 122 ++++++-------------------- modules/ui/{ => panes}/map_data.js | 90 +++++-------------- modules/ui/{ => panes}/preferences.js | 70 +++------------ 9 files changed, 263 insertions(+), 441 deletions(-) create mode 100644 modules/ui/pane.js rename modules/ui/{ => panes}/background.js (82%) rename modules/ui/{ => panes}/help.js (86%) rename modules/ui/{ => panes}/issues.js (85%) rename modules/ui/{ => panes}/map_data.js (92%) rename modules/ui/{ => panes}/preferences.js (56%) diff --git a/css/80_app.css b/css/80_app.css index a8f7a63e9..72ba5f369 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -440,19 +440,6 @@ button[disabled].action:hover { color: #333; } -.notification-badge { - display: block; - position: absolute; - width: 10px; - height: 10px; - right: 7px; - top: 9px; -} - -.notification-badge.hide { - display: none; -} - /* Toolbar / Persistent UI Elements ------------------------------------------------------- */ @@ -556,7 +543,7 @@ button.save .count { text-align: center; } -.help-wrap svg.icon.pre-text.add-note, +.help-pane svg.icon.pre-text.add-note, button.add-note svg.icon { height: 15px; width: 15px; @@ -573,7 +560,7 @@ button.add-note svg.icon { margin-left: 4px; margin-right: unset; } -.help-wrap svg.icon.pre-text.add-note { +.help-pane svg.icon.pre-text.add-note { margin-left: 3px; margin-right: 3px; } @@ -3185,10 +3172,6 @@ button.autofix.action.active { /*fill: #597be7;*/ } -.notification-badge.warning { - color: #ffdf5c; -} - /* error styles */ .errors-list, @@ -3232,10 +3215,6 @@ button.autofix.action.active { /*fill: #597be7;*/ } -.notification-badge.error { - color: #ff0c05; -} - /* Issues Pane */ .issues-options-container { @@ -3584,7 +3563,7 @@ li.issue-fix-item:not(.actionable) .fix-icon { z-index: 10; } -.map-pane.help-wrap { +.map-pane.help-pane { width: 600px; } @@ -3624,12 +3603,12 @@ li.issue-fix-item:not(.actionable) .fix-icon { /* Help ------------------------------------------------------- */ -.help-wrap p { +.help-pane p { font-size: 15px; margin-bottom: 20px; } -.help-wrap .left-content .body p code { +.help-pane .left-content .body p code { padding: 3px 4px; font-size: 12px; color: #555; @@ -3642,14 +3621,14 @@ li.issue-fix-item:not(.actionable) .fix-icon { box-shadow: inset 0 -1px 0 #bbb; } -.help-wrap .left-content .icon.pre-text { +.help-pane .left-content .icon.pre-text { vertical-align: text-top; margin-right: 0; margin-left: 0; display: inline-block; } -.help-wrap .toc { +.help-pane .toc { width: 40%; float: right; margin-left: 20px; @@ -3657,68 +3636,68 @@ li.issue-fix-item:not(.actionable) .fix-icon { padding-left: 5px; } -.help-wrap .toc li a, -.help-wrap .nav a { +.help-pane .toc li a, +.help-pane .nav a { display: block; border: 1px solid #ccc; padding: 5px 10px; } -.help-wrap .toc li a { +.help-pane .toc li a { border-bottom: 0; } -.help-wrap .toc li a:hover, -.help-wrap .nav a:hover { +.help-pane .toc li a:hover, +.help-pane .nav a:hover { background: #ececec; } -.help-wrap .toc li a.selected { +.help-pane .toc li a.selected { background: #e8ebff; } -.help-wrap .toc li:first-child a { +.help-pane .toc li:first-child a { border-radius: 4px 4px 0 0; } -.help-wrap .toc li:nth-last-child(3) a { +.help-pane .toc li:nth-last-child(3) a { border-bottom: 1px solid #ccc; border-radius: 0 0 4px 4px } -.help-wrap .toc li.shortcuts a, -.help-wrap .toc li.walkthrough a { +.help-pane .toc li.shortcuts a, +.help-pane .toc li.walkthrough a { overflow: hidden; margin-top: 10px; border-bottom: 1px solid #ccc; border-radius: 4px; } -.help-wrap .toc li.walkthrough a { +.help-pane .toc li.walkthrough a { text-align: center; } -.help-wrap .nav { +.help-pane .nav { position: relative; padding-bottom: 30px; } -.help-wrap .nav a { +.help-pane .nav a { float: left; width: 50%; text-align: center; } -.help-wrap .nav a:first-child { +.help-pane .nav a:first-child { border-radius: 4px 0 0 4px; } -.help-wrap .nav a:last-child:not(:only-child) { +.help-pane .nav a:last-child:not(:only-child) { border-radius: 0 4px 4px 0; border-left: 0; } -.help-wrap .nav a:only-child { +.help-pane .nav a:only-child { width: 100%; border-radius: 4px; } diff --git a/modules/ui/index.js b/modules/ui/index.js index 2c973f43d..e663bed92 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -1,7 +1,6 @@ export { uiInit } from './init'; export { uiAccount } from './account'; export { uiAttribution } from './attribution'; -export { uiBackground } from './background'; export { uiBackgroundDisplayOptions } from './background_display_options'; export { uiBackgroundOffset } from './background_offset'; export { uiChangesetEditor } from './changeset_editor'; @@ -27,7 +26,6 @@ export { uiFlash } from './flash'; export { uiFormFields } from './form_fields'; export { uiFullScreen } from './full_screen'; export { uiGeolocate } from './geolocate'; -export { uiHelp } from './help'; export { uiImproveOsmComments } from './improveOSM_comments'; export { uiImproveOsmDetails } from './improveOSM_details'; export { uiImproveOsmEditor } from './improveOSM_editor'; @@ -40,7 +38,6 @@ export { uiKeepRightEditor } from './keepRight_editor'; export { uiKeepRightHeader } from './keepRight_header'; export { uiLasso } from './lasso'; export { uiLoading } from './loading'; -export { uiMapData } from './map_data'; export { uiMapInMap } from './map_in_map'; export { uiModal } from './modal'; export { uiNotice } from './notice'; @@ -48,7 +45,6 @@ export { uiNoteComments } from './note_comments'; export { uiNoteEditor } from './note_editor'; export { uiNoteHeader } from './note_header'; export { uiNoteReport } from './note_report'; -export { uiPreferences } from './preferences'; export { uiPresetEditor } from './preset_editor'; export { uiPresetIcon } from './preset_icon'; export { uiPresetList } from './preset_list'; diff --git a/modules/ui/init.js b/modules/ui/init.js index 3aa7dd7c8..b893cbf60 100644 --- a/modules/ui/init.js +++ b/modules/ui/init.js @@ -14,22 +14,17 @@ import { utilGetDimensions } from '../util/dimensions'; import { uiAccount } from './account'; import { uiAttribution } from './attribution'; -import { uiBackground } from './background'; import { uiContributors } from './contributors'; import { uiFeatureInfo } from './feature_info'; import { uiFullScreen } from './full_screen'; import { uiGeolocate } from './geolocate'; -import { uiHelp } from './help'; import { uiInfo } from './info'; import { uiIntro } from './intro'; -import { uiIssues } from './issues'; import { uiIssuesInfo } from './issues_info'; import { uiLoading } from './loading'; -import { uiMapData } from './map_data'; import { uiMapInMap } from './map_in_map'; import { uiNotice } from './notice'; import { uiPhotoviewer } from './photoviewer'; -import { uiPreferences } from './preferences'; import { uiRestore } from './restore'; import { uiScale } from './scale'; import { uiShortcuts } from './shortcuts'; @@ -42,6 +37,11 @@ import { uiVersion } from './version'; import { uiZoom } from './zoom'; import { uiCmd } from './cmd'; +import { uiBackground } from './panes/background'; +import { uiHelp } from './panes/help'; +import { uiIssues } from './panes/issues'; +import { uiMapData } from './panes/map_data'; +import { uiPreferences } from './panes/preferences'; export function uiInit(context) { var _initCounter = 0; @@ -106,35 +106,20 @@ export function uiInit(context) { .attr('class', 'map-control geolocate-control') .call(uiGeolocate(context)); - var background = uiBackground(context); - controls - .append('div') - .attr('class', 'map-control background-control') - .call(background.renderToggleButton); + var uiPanes = [ + uiBackground(context), + uiMapData(context), + uiIssues(context), + uiPreferences(context), + uiHelp(context) + ]; - var mapData = uiMapData(context); - controls - .append('div') - .attr('class', 'map-control map-data-control') - .call(mapData.renderToggleButton); - - var issues = uiIssues(context); - controls - .append('div') - .attr('class', 'map-control map-issues-control') - .call(issues.renderToggleButton); - - var preferences = uiPreferences(context); - controls - .append('div') - .attr('class', 'map-control preferences-control') - .call(preferences.renderToggleButton); - - var help = uiHelp(context); - controls - .append('div') - .attr('class', 'map-control help-control') - .call(help.renderToggleButton); + uiPanes.forEach(function(pane) { + controls + .append('div') + .attr('class', 'map-control ' + pane.id + '-control') + .call(pane.renderToggleButton); + }); content .append('div') @@ -252,12 +237,10 @@ export function uiInit(context) { .append('div') .attr('class', 'map-panes'); - panes - .call(background.renderPane) - .call(mapData.renderPane) - .call(issues.renderPane) - .call(preferences.renderPane) - .call(help.renderPane); + uiPanes.forEach(function(pane) { + panes + .call(pane.renderPane); + }); ui.info = uiInfo(context); diff --git a/modules/ui/pane.js b/modules/ui/pane.js new file mode 100644 index 000000000..1d221ba59 --- /dev/null +++ b/modules/ui/pane.js @@ -0,0 +1,118 @@ +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { svgIcon } from '../svg/icon'; +import { textDirection } from '../util/locale'; +import { tooltip } from '../util/tooltip'; +import { uiTooltipHtml } from './tooltipHtml'; + + +export function uiPane(id, context) { + + var _key; + var _title = ''; + var _description = ''; + var _iconName = ''; + + var _paneSelection = d3_select(null); + + var _paneTooltip; + + var pane = { + id: id + }; + + pane.title = function(val) { + if (!arguments.length) return _title; + _title = val; + return pane; + }; + + pane.key = function(val) { + if (!arguments.length) return _key; + _key = val; + return pane; + }; + + pane.description = function(val) { + if (!arguments.length) return _description; + _description = val; + return pane; + }; + + pane.iconName = function(val) { + if (!arguments.length) return _iconName; + _iconName = val; + return pane; + }; + + pane.selection = function() { + return _paneSelection; + }; + + function hidePane() { + context.ui().togglePanes(); + } + + pane.togglePane = function() { + if (d3_event) d3_event.preventDefault(); + _paneTooltip.hide(); + context.ui().togglePanes(!_paneSelection.classed('shown') ? _paneSelection : undefined); + }; + + pane.renderToggleButton = function(selection) { + + if (!_paneTooltip) { + _paneTooltip = tooltip() + .placement((textDirection === 'rtl') ? 'right' : 'left') + .html(true) + .title(uiTooltipHtml(_description, _key)); + } + + selection + .append('button') + .on('click', pane.togglePane) + .call(svgIcon('#' + _iconName, 'light')) + .call(_paneTooltip); + }; + + pane.renderContent = function() { + // override + }; + + pane.renderPane = function(selection) { + + _paneSelection = selection + .append('div') + .attr('class', 'fillL map-pane hide ' + id + '-pane') + .attr('pane', id); + + var heading = _paneSelection + .append('div') + .attr('class', 'pane-heading'); + + heading + .append('h2') + .text(_title); + + heading + .append('button') + .on('click', hidePane) + .call(svgIcon('#iD-icon-close')); + + + _paneSelection + .append('div') + .attr('class', 'pane-content') + .call(pane.renderContent); + + if (_key) { + context.keybinding() + .on(_key, pane.togglePane); + } + }; + + return pane; +} diff --git a/modules/ui/background.js b/modules/ui/panes/background.js similarity index 82% rename from modules/ui/background.js rename to modules/ui/panes/background.js index 29003ecb3..90c9f8f36 100644 --- a/modules/ui/background.js +++ b/modules/ui/panes/background.js @@ -3,22 +3,22 @@ import _debounce from 'lodash-es/debounce'; import { descending as d3_descending, ascending as d3_ascending } from 'd3-array'; import { event as d3_event, select as d3_select } from 'd3-selection'; -import { t, textDirection } from '../util/locale'; -import { svgIcon } from '../svg/icon'; -import { uiBackgroundDisplayOptions } from './background_display_options'; -import { uiBackgroundOffset } from './background_offset'; -import { uiCmd } from './cmd'; -import { uiDisclosure } from './disclosure'; -import { uiMapInMap } from './map_in_map'; -import { uiSettingsCustomBackground } from './settings/custom_background'; -import { uiTooltipHtml } from './tooltipHtml'; -import { tooltip } from '../util/tooltip'; +import { t, textDirection } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; +import { svgIcon } from '../../svg/icon'; +import { uiBackgroundDisplayOptions } from '../background_display_options'; +import { uiBackgroundOffset } from '../background_offset'; +import { uiCmd } from '../cmd'; +import { uiDisclosure } from '../disclosure'; +import { uiMapInMap } from '../map_in_map'; +import { uiSettingsCustomBackground } from '../settings/custom_background'; +import { uiTooltipHtml } from '../tooltipHtml'; +import { uiPane } from '../pane'; export function uiBackground(context) { - var key = t('background.key'); - var _pane = d3_select(null); + var _key = t('background.key'); var _customSource = context.background().findSource('custom'); var _previousBackground = context.background().findSource(context.storage('background-last-used-toggle')); @@ -51,7 +51,7 @@ export function uiBackground(context) { .html(true) .title(function() { var tip = '
' + t('background.switch') + '
'; - return uiTooltipHtml(tip, uiCmd('⌘' + key)); + return uiTooltipHtml(tip, uiCmd('⌘' + _key)); }) ); } else if (description || isOverflowing) { @@ -292,11 +292,11 @@ export function uiBackground(context) { function update() { - if (!_pane.select('.disclosure-wrap-background_list').classed('hide')) { + if (!backgroundPane.selection().select('.disclosure-wrap-background_list').classed('hide')) { updateBackgroundList(); } - if (!_pane.select('.disclosure-wrap-overlay_list').classed('hide')) { + if (!backgroundPane.selection().select('.disclosure-wrap-overlay_list').classed('hide')) { updateOverlayList(); } @@ -318,55 +318,14 @@ export function uiBackground(context) { } } - var paneTooltip = tooltip() - .placement((textDirection === 'rtl') ? 'right' : 'left') - .html(true) - .title(uiTooltipHtml(t('background.description'), key)); - uiBackground.togglePane = function() { - if (d3_event) d3_event.preventDefault(); - paneTooltip.hide(); - context.ui().togglePanes(!_pane.classed('shown') ? _pane : undefined); - }; + var backgroundPane = uiPane('background', context) + .key(_key) + .title(t('background.title')) + .description(t('background.description')) + .iconName('iD-icon-layers'); - function hidePane() { - context.ui().togglePanes(); - } - - uiBackground.renderToggleButton = function(selection) { - - selection - .append('button') - .on('click', uiBackground.togglePane) - .call(svgIcon('#iD-icon-layers', 'light')) - .call(paneTooltip); - }; - - uiBackground.renderPane = function(selection) { - - _pane = selection - .append('div') - .attr('class', 'fillL map-pane background-pane hide') - .attr('pane', 'background'); - - - var heading = _pane - .append('div') - .attr('class', 'pane-heading'); - - heading - .append('h2') - .text(t('background.title')); - - heading - .append('button') - .on('click', hidePane) - .call(svgIcon('#iD-icon-close')); - - - var content = _pane - .append('div') - .attr('class', 'pane-content'); + backgroundPane.renderContent = function(content) { // background list content @@ -411,9 +370,8 @@ export function uiBackground(context) { update(); context.keybinding() - .on(key, uiBackground.togglePane) - .on(uiCmd('⌘' + key), quickSwitch); + .on(uiCmd('⌘' + _key), quickSwitch); }; - return uiBackground; + return backgroundPane; } diff --git a/modules/ui/help.js b/modules/ui/panes/help.js similarity index 86% rename from modules/ui/help.js rename to modules/ui/panes/help.js index d2e56f2d1..c23bde88e 100644 --- a/modules/ui/help.js +++ b/modules/ui/panes/help.js @@ -1,23 +1,17 @@ -import { - event as d3_event, - select as d3_select -} from 'd3-selection'; import marked from 'marked'; -import { svgIcon } from '../svg/icon'; -import { uiCmd } from './cmd'; -import { uiIntro } from './intro/intro'; -import { uiShortcuts } from './shortcuts'; -import { uiTooltipHtml } from './tooltipHtml'; +import { svgIcon } from '../../svg/icon'; +import { uiCmd } from '../cmd'; +import { uiIntro } from '../intro/intro'; +import { uiShortcuts } from '../shortcuts'; +import { uiTooltipHtml } from '../tooltipHtml'; +import { uiPane } from '../pane'; -import { t, textDirection } from '../util/locale'; -import { tooltip } from '../util/tooltip'; -import { icon } from './intro/helper'; +import { t, textDirection } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; +import { icon } from '../intro/helper'; export function uiHelp(context) { - var key = t('help.key'); - - var _pane = d3_select(null); var docKeys = [ ['help', [ @@ -281,36 +275,18 @@ export function uiHelp(context) { }; }); - var paneTooltip = tooltip() - .placement((textDirection === 'rtl') ? 'right' : 'left') - .html(true) - .title(uiTooltipHtml(t('help.title'), key)); + var helpPane = uiPane('help', context) + .key(t('help.key')) + .title(t('help.title')) + .description(t('help.title')) + .iconName('iD-icon-help'); - function hidePane() { - context.ui().togglePanes(); - } - - uiHelp.togglePane = function() { - if (d3_event) d3_event.preventDefault(); - paneTooltip.hide(); - context.ui().togglePanes(!_pane.classed('shown') ? _pane : undefined); - }; - - uiHelp.renderToggleButton = function(selection) { - - selection.append('button') - .on('click', uiHelp.togglePane) - .call(svgIcon('#iD-icon-help', 'light')) - .call(paneTooltip); - }; - - - uiHelp.renderPane = function(selection) { + helpPane.renderContent = function(content) { function clickHelp(d, i) { var rtl = (textDirection === 'rtl'); content.property('scrollTop', 0); - doctitle.html(d.title); + helpPane.selection().select('.pane-heading h2').html(d.title); body.html(d.html); body.selectAll('a') @@ -373,29 +349,6 @@ export function uiHelp(context) { context.container().call(uiShortcuts(context), true); } - - _pane = selection.append('div') - .attr('class', 'help-wrap map-pane fillL hide') - .attr('pane', 'help'); - - var heading = _pane - .append('div') - .attr('class', 'pane-heading'); - - var doctitle = heading - .append('h2') - .text(t('help.title')); - - heading - .append('button') - .on('click', hidePane) - .call(svgIcon('#iD-icon-close')); - - - var content = _pane - .append('div') - .attr('class', 'pane-content'); - var toc = content .append('ul') .attr('class', 'toc'); @@ -454,10 +407,7 @@ export function uiHelp(context) { clickHelp(docs[0], 0); - context.keybinding() - .on(key, uiHelp.togglePane); - }; - return uiHelp; + return helpPane; } diff --git a/modules/ui/issues.js b/modules/ui/panes/issues.js similarity index 85% rename from modules/ui/issues.js rename to modules/ui/panes/issues.js index 780741282..38705d204 100644 --- a/modules/ui/issues.js +++ b/modules/ui/panes/issues.js @@ -2,19 +2,18 @@ import _debounce from 'lodash-es/debounce'; import { event as d3_event, select as d3_select } from 'd3-selection'; -import { t, textDirection } from '../util/locale'; -import { tooltip } from '../util/tooltip'; +import { t } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; //import { actionNoop } from '../actions/noop'; -import { geoSphericalDistance } from '../geo'; -import { svgIcon } from '../svg/icon'; -import { uiDisclosure } from './disclosure'; -import { uiTooltipHtml } from './tooltipHtml'; -import { utilGetSetValue, utilHighlightEntities, utilNoAuto } from '../util'; +import { geoSphericalDistance } from '../../geo'; +import { svgIcon } from '../../svg/icon'; +import { uiDisclosure } from '../disclosure'; +import { utilGetSetValue, utilHighlightEntities, utilNoAuto } from '../../util'; +import { uiPane } from '../pane'; export function uiIssues(context) { - var key = t('issues.key'); var MINSQUARE = 0; var MAXSQUARE = 20; @@ -23,8 +22,6 @@ export function uiIssues(context) { var _errorsSelection = d3_select(null); var _warningsSelection = d3_select(null); var _rulesList = d3_select(null); - var _pane = d3_select(null); - var _toggleButton = d3_select(null); var _errors = []; var _warnings = []; @@ -42,22 +39,6 @@ export function uiIssues(context) { ); - function addNotificationBadge(selection) { - var d = 10; - selection.selectAll('svg.notification-badge') - .data([0]) - .enter() - .append('svg') - .attr('viewbox', '0 0 ' + d + ' ' + d) - .attr('class', 'notification-badge hide') - .append('circle') - .attr('cx', d / 2) - .attr('cy', d / 2) - .attr('r', (d / 2) - 1) - .attr('fill', 'currentColor'); - } - - function renderErrorsList(selection) { _errorsSelection = selection .call(drawIssuesList, 'errors', _errors); @@ -402,7 +383,7 @@ export function uiIssues(context) { var opts = cases[type]; var hiddenIssues = context.validator().getIssues(opts); if (hiddenIssues.length) { - _pane.select('.issues-none .details') + issuesPane.selection().select('.issues-none .details') .text(t( 'issues.no_issues.hidden_issues.' + type, { count: hiddenIssues.length.toString() } @@ -410,7 +391,7 @@ export function uiIssues(context) { return; } } - _pane.select('.issues-none .details') + issuesPane.selection().select('.issues-none .details') .text(t('issues.no_issues.hidden_issues.none')); } @@ -465,7 +446,7 @@ export function uiIssues(context) { messageType = 'no_edits'; } - _pane.select('.issues-none .message') + issuesPane.selection().select('.issues-none .message') .text(t('issues.no_issues.message.' + messageType)); } @@ -487,31 +468,25 @@ export function uiIssues(context) { _warnings = _warnings.slice(0, 1000); - _toggleButton.selectAll('.notification-badge') - .classed('error', (_errors.length > 0)) - .classed('warning', (_errors.length === 0 && _warnings.length > 0)) - .classed('hide', (_errors.length === 0 && _warnings.length === 0)); - - - _pane.selectAll('.issues-errors') + issuesPane.selection().selectAll('.issues-errors') .classed('hide', _errors.length === 0); if (_errors.length > 0) { - _pane.selectAll('.hide-toggle-issues_errors .hide-toggle-text') + issuesPane.selection().selectAll('.hide-toggle-issues_errors .hide-toggle-text') .text(t('issues.errors.list_title', { count: errorCount })); - if (!_pane.select('.disclosure-wrap-issues_errors').classed('hide')) { + if (!issuesPane.selection().select('.disclosure-wrap-issues_errors').classed('hide')) { _errorsSelection .call(drawIssuesList, 'errors', _errors); } } - _pane.selectAll('.issues-warnings') + issuesPane.selection().selectAll('.issues-warnings') .classed('hide', _warnings.length === 0); if (_warnings.length > 0) { - _pane.selectAll('.hide-toggle-issues_warnings .hide-toggle-text') + issuesPane.selection().selectAll('.hide-toggle-issues_warnings .hide-toggle-text') .text(t('issues.warnings.list_title', { count: warningCount })); - if (!_pane.select('.disclosure-wrap-issues_warnings').classed('hide')) { + if (!issuesPane.selection().select('.disclosure-wrap-issues_warnings').classed('hide')) { _warningsSelection .call(drawIssuesList, 'warnings', _warnings); renderIgnoredIssuesReset(_warningsSelection); @@ -520,14 +495,14 @@ export function uiIssues(context) { var hasIssues = _warnings.length > 0 || _errors.length > 0; - var issuesNone = _pane.select('.issues-none'); + var issuesNone = issuesPane.selection().select('.issues-none'); issuesNone.classed('hide', hasIssues); if (!hasIssues) { renderIgnoredIssuesReset(issuesNone); setNoIssuesText(); } - if (!_pane.select('.disclosure-wrap-issues_rules').classed('hide')) { + if (!issuesPane.selection().select('.disclosure-wrap-issues_rules').classed('hide')) { updateRulesList(); } @@ -654,58 +629,14 @@ export function uiIssues(context) { } - function hidePane() { - context.ui().togglePanes(); - } + var issuesPane = uiPane('issues', context) + .key(t('issues.key')) + .title(t('issues.title')) + .description(t('issues.title')) + .iconName('iD-icon-alert'); - - var paneTooltip = tooltip() - .placement((textDirection === 'rtl') ? 'right' : 'left') - .html(true) - .title(uiTooltipHtml(t('issues.title'), key)); - - - - uiIssues.togglePane = function() { - if (d3_event) d3_event.preventDefault(); - paneTooltip.hide(); - context.ui().togglePanes(!_pane.classed('shown') ? _pane : undefined); - }; - - - uiIssues.renderToggleButton = function(selection) { - _toggleButton = selection - .append('button') - .on('click', uiIssues.togglePane) - .call(svgIcon('#iD-icon-alert', 'light')) - .call(addNotificationBadge) - .call(paneTooltip); - }; - - - uiIssues.renderPane = function(selection) { - _pane = selection - .append('div') - .attr('class', 'fillL map-pane issues-pane hide') - .attr('pane', 'map-issues'); - - var heading = _pane - .append('div') - .attr('class', 'pane-heading'); - - heading - .append('h2') - .text(t('issues.title')); - - heading - .append('button') - .on('click', hidePane) - .call(svgIcon('#iD-icon-close')); - - var content = _pane - .append('div') - .attr('class', 'pane-content'); + issuesPane.renderContent = function(content) { content .append('div') @@ -743,10 +674,7 @@ export function uiIssues(context) { ); // update(); - - context.keybinding() - .on(key, uiIssues.togglePane); }; - return uiIssues; + return issuesPane; } diff --git a/modules/ui/map_data.js b/modules/ui/panes/map_data.js similarity index 92% rename from modules/ui/map_data.js rename to modules/ui/panes/map_data.js index d6362a518..9686f87fc 100644 --- a/modules/ui/map_data.js +++ b/modules/ui/panes/map_data.js @@ -3,19 +3,19 @@ import { select as d3_select } from 'd3-selection'; -import { svgIcon } from '../svg/icon'; -import { t, textDirection } from '../util/locale'; -import { tooltip } from '../util/tooltip'; -import { geoExtent } from '../geo'; -import { modeBrowse } from '../modes/browse'; -import { uiDisclosure } from './disclosure'; -import { uiSettingsCustomData } from './settings/custom_data'; -import { uiTooltipHtml } from './tooltipHtml'; -import { uiCmd } from './cmd'; +import { svgIcon } from '../../svg/icon'; +import { t, textDirection } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; +import { geoExtent } from '../../geo'; +import { modeBrowse } from '../../modes/browse'; +import { uiDisclosure } from '../disclosure'; +import { uiSettingsCustomData } from '../settings/custom_data'; +import { uiTooltipHtml } from '../tooltipHtml'; +import { uiCmd } from '../cmd'; +import { uiPane } from '../pane'; export function uiMapData(context) { - var key = t('map_data.key'); var osmDataToggleKey = uiCmd('⌥' + t('area_fill.wireframe.key')); var features = context.features().keys(); var layers = context.layers(); @@ -24,8 +24,6 @@ export function uiMapData(context) { var settingsCustomData = uiSettingsCustomData(context) .on('change', customChanged); - var _pane = d3_select(null); - var _fillSelected = context.storage('area-fill') || 'partial'; var _dataLayerContainer = d3_select(null); var _photoOverlayContainer = d3_select(null); @@ -775,16 +773,16 @@ export function uiMapData(context) { function update() { - if (!_pane.select('.disclosure-wrap-data_layers').classed('hide')) { + if (!mapDataPane.selection().select('.disclosure-wrap-data_layers').classed('hide')) { updateDataLayers(); } - if (!_pane.select('.disclosure-wrap-photo_overlays').classed('hide')) { + if (!mapDataPane.selection().select('.disclosure-wrap-photo_overlays').classed('hide')) { updatePhotoOverlays(); } - if (!_pane.select('.disclosure-wrap-fill_area').classed('hide')) { + if (!mapDataPane.selection().select('.disclosure-wrap-fill_area').classed('hide')) { updateFillList(); } - if (!_pane.select('.disclosure-wrap-map_features').classed('hide')) { + if (!mapDataPane.selection().select('.disclosure-wrap-map_features').classed('hide')) { updateFeatureList(); } @@ -809,56 +807,13 @@ export function uiMapData(context) { context.map().pan([0,0]); // trigger a redraw } - var paneTooltip = tooltip() - .placement((textDirection === 'rtl') ? 'right' : 'left') - .html(true) - .title(uiTooltipHtml(t('map_data.description'), key)); - - function hidePane() { - context.ui().togglePanes(); - } - - uiMapData.togglePane = function() { - if (d3_event) d3_event.preventDefault(); - paneTooltip.hide(); - context.ui().togglePanes(!_pane.classed('shown') ? _pane : undefined); - }; - - uiMapData.renderToggleButton = function(selection) { - - selection - .append('button') - .on('click', uiMapData.togglePane) - .call(svgIcon('#iD-icon-data', 'light')) - .call(paneTooltip); - }; - - - uiMapData.renderPane = function(selection) { - - _pane = selection - .append('div') - .attr('class', 'fillL map-pane map-data-pane hide') - .attr('pane', 'map-data'); - - var heading = _pane - .append('div') - .attr('class', 'pane-heading'); - - heading - .append('h2') - .text(t('map_data.title')); - - heading - .append('button') - .on('click', hidePane) - .call(svgIcon('#iD-icon-close')); - - - var content = _pane - .append('div') - .attr('class', 'pane-content'); + var mapDataPane = uiPane('map-data', context) + .key(t('map_data.key')) + .title(t('map_data.title')) + .description(t('map_data.description')) + .iconName('iD-icon-data'); + mapDataPane.renderContent = function(content) { // data layers content @@ -905,7 +860,6 @@ export function uiMapData(context) { setFill(_fillSelected); context.keybinding() - .on(key, uiMapData.togglePane) .on(t('area_fill.wireframe.key'), toggleWireframe) .on(osmDataToggleKey, function() { d3_event.preventDefault(); @@ -915,5 +869,5 @@ export function uiMapData(context) { .on(t('map_data.highlight_edits.key'), toggleHighlightEdited); }; - return uiMapData; -} \ No newline at end of file + return mapDataPane; +} diff --git a/modules/ui/preferences.js b/modules/ui/panes/preferences.js similarity index 56% rename from modules/ui/preferences.js rename to modules/ui/panes/preferences.js index 1b4364378..db284ace5 100644 --- a/modules/ui/preferences.js +++ b/modules/ui/panes/preferences.js @@ -1,23 +1,15 @@ -import { event as d3_event, select as d3_select } from 'd3-selection'; +import { event as d3_event } from 'd3-selection'; -import { svgIcon } from '../svg/icon'; -import { t, textDirection } from '../util/locale'; -import { tooltip } from '../util/tooltip'; -import { uiDisclosure } from './disclosure'; -import { uiTooltipHtml } from './tooltipHtml'; +import { svgIcon } from '../../svg/icon'; +import { t } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; +import { uiDisclosure } from '../disclosure'; +import { uiPane } from '../pane'; export function uiPreferences(context) { - const key = t('preferences.key'); - let _pane = d3_select(null); let _showThirdPartyIcons = context.storage('preferences.privacy.thirdpartyicons') || 'true'; - const paneTooltip = tooltip() - .placement((textDirection === 'rtl') ? 'right' : 'left') - .html(true) - .title(uiTooltipHtml(t('preferences.description'), key)); - - function renderPrivacyOptions(selection) { // enter let privacyOptionsListEnter = selection.selectAll('.privacy-options-list') @@ -74,46 +66,13 @@ export function uiPreferences(context) { } } + let preferencesPane = uiPane('preferences', context) + .key(t('preferences.key')) + .title(t('preferences.title')) + .description(t('preferences.description')) + .iconName('fas-user-cog'); - uiPreferences.togglePane = () => { - if (d3_event) d3_event.preventDefault(); - paneTooltip.hide(); - context.ui().togglePanes(!_pane.classed('shown') ? _pane : undefined); - }; - - - uiPreferences.renderToggleButton = (selection) => { - selection - .append('button') - .on('click', uiPreferences.togglePane) - .call(svgIcon('#fas-user-cog', 'light')) - .call(paneTooltip); - }; - - - uiPreferences.renderPane = (selection) => { - _pane = selection - .append('div') - .attr('class', 'fillL map-pane preferences-pane hide') - .attr('pane', 'preferences'); - - let heading = _pane - .append('div') - .attr('class', 'pane-heading'); - - heading - .append('h2') - .text(t('preferences.title')); - - heading - .append('button') - .on('click', () => context.ui().togglePanes()) - .call(svgIcon('#iD-icon-close')); - - - let content = _pane - .append('div') - .attr('class', 'pane-content'); + preferencesPane.renderContent = (content) => { content .append('div') @@ -122,10 +81,7 @@ export function uiPreferences(context) { .title(t('preferences.privacy.title')) .content(renderPrivacyOptions) ); - - context.keybinding() - .on(key, uiPreferences.togglePane); }; - return uiPreferences; + return preferencesPane; } From d8a0f030ccddcc2fad073b621886142f5859a226 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 18 Feb 2020 13:33:12 -0800 Subject: [PATCH 017/127] Add "pane" prefix to pane UI objects Move background and overlay layer lists to their own files --- modules/ui/index.js | 2 - modules/ui/init.js | 20 +- modules/ui/panes/background.js | 329 ++---------------- modules/ui/panes/help.js | 2 +- modules/ui/panes/issues.js | 2 +- modules/ui/panes/map_data.js | 2 +- modules/ui/panes/preferences.js | 2 +- .../background_display_options.js | 8 +- modules/ui/sections/background_list.js | 268 ++++++++++++++ .../ui/{ => sections}/background_offset.js | 8 +- modules/ui/sections/overlay_list.js | 124 +++++++ 11 files changed, 441 insertions(+), 326 deletions(-) rename modules/ui/{ => sections}/background_display_options.js (95%) create mode 100644 modules/ui/sections/background_list.js rename modules/ui/{ => sections}/background_offset.js (96%) create mode 100644 modules/ui/sections/overlay_list.js diff --git a/modules/ui/index.js b/modules/ui/index.js index e663bed92..256d57a7f 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -1,8 +1,6 @@ export { uiInit } from './init'; export { uiAccount } from './account'; export { uiAttribution } from './attribution'; -export { uiBackgroundDisplayOptions } from './background_display_options'; -export { uiBackgroundOffset } from './background_offset'; export { uiChangesetEditor } from './changeset_editor'; export { uiCmd } from './cmd'; export { uiCombobox } from './combobox'; diff --git a/modules/ui/init.js b/modules/ui/init.js index b893cbf60..7c0d33160 100644 --- a/modules/ui/init.js +++ b/modules/ui/init.js @@ -37,11 +37,11 @@ import { uiVersion } from './version'; import { uiZoom } from './zoom'; import { uiCmd } from './cmd'; -import { uiBackground } from './panes/background'; -import { uiHelp } from './panes/help'; -import { uiIssues } from './panes/issues'; -import { uiMapData } from './panes/map_data'; -import { uiPreferences } from './panes/preferences'; +import { uiPaneBackground } from './panes/background'; +import { uiPaneHelp } from './panes/help'; +import { uiPaneIssues } from './panes/issues'; +import { uiPaneMapData } from './panes/map_data'; +import { uiPanePreferences } from './panes/preferences'; export function uiInit(context) { var _initCounter = 0; @@ -107,11 +107,11 @@ export function uiInit(context) { .call(uiGeolocate(context)); var uiPanes = [ - uiBackground(context), - uiMapData(context), - uiIssues(context), - uiPreferences(context), - uiHelp(context) + uiPaneBackground(context), + uiPaneMapData(context), + uiPaneIssues(context), + uiPanePreferences(context), + uiPaneHelp(context) ]; uiPanes.forEach(function(pane) { diff --git a/modules/ui/panes/background.js b/modules/ui/panes/background.js index 90c9f8f36..88d4eba13 100644 --- a/modules/ui/panes/background.js +++ b/modules/ui/panes/background.js @@ -1,304 +1,36 @@ import _debounce from 'lodash-es/debounce'; -import { descending as d3_descending, ascending as d3_ascending } from 'd3-array'; import { event as d3_event, select as d3_select } from 'd3-selection'; -import { t, textDirection } from '../../util/locale'; -import { tooltip } from '../../util/tooltip'; -import { svgIcon } from '../../svg/icon'; -import { uiBackgroundDisplayOptions } from '../background_display_options'; -import { uiBackgroundOffset } from '../background_offset'; +import { t } from '../../util/locale'; import { uiCmd } from '../cmd'; -import { uiDisclosure } from '../disclosure'; -import { uiMapInMap } from '../map_in_map'; -import { uiSettingsCustomBackground } from '../settings/custom_background'; -import { uiTooltipHtml } from '../tooltipHtml'; import { uiPane } from '../pane'; +import { uiBackgroundDisplayOptions } from '../sections/background_display_options'; +import { uiBackgroundList } from '../sections/background_list'; +import { uiBackgroundOffset } from '../sections/background_offset'; +import { uiOverlayList } from '../sections/overlay_list'; -export function uiBackground(context) { +export function uiPaneBackground(context) { var _key = t('background.key'); - var _customSource = context.background().findSource('custom'); - var _previousBackground = context.background().findSource(context.storage('background-last-used-toggle')); - - var _backgroundList = d3_select(null); - var _overlayList = d3_select(null); + var _backgroundListContainer = d3_select(null); + var _overlayListContainer = d3_select(null); var _displayOptionsContainer = d3_select(null); var _offsetContainer = d3_select(null); + var backgroundList = uiBackgroundList(context); var backgroundDisplayOptions = uiBackgroundDisplayOptions(context); var backgroundOffset = uiBackgroundOffset(context); - - var settingsCustomBackground = uiSettingsCustomBackground(context) - .on('change', customChanged); - - - function setTooltips(selection) { - selection.each(function(d, i, nodes) { - var item = d3_select(this).select('label'); - var span = item.select('span'); - var placement = (i < nodes.length / 2) ? 'bottom' : 'top'; - var description = d.description(); - var isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth')); - - item.call(tooltip().destroyAny); - - if (d === _previousBackground) { - item.call(tooltip() - .placement(placement) - .html(true) - .title(function() { - var tip = '
' + t('background.switch') + '
'; - return uiTooltipHtml(tip, uiCmd('⌘' + _key)); - }) - ); - } else if (description || isOverflowing) { - item.call(tooltip() - .placement(placement) - .title(description || d.name()) - ); - } - }); - } - - - function updateLayerSelections(selection) { - function active(d) { - return context.background().showsLayer(d); - } - - selection.selectAll('li') - .classed('active', active) - .classed('switch', function(d) { return d === _previousBackground; }) - .call(setTooltips) - .selectAll('input') - .property('checked', active); - } - - - function chooseBackground(d) { - if (d.id === 'custom' && !d.template()) { - return editCustom(); - } - - d3_event.preventDefault(); - _previousBackground = context.background().baseLayerSource(); - context.storage('background-last-used-toggle', _previousBackground.id); - context.storage('background-last-used', d.id); - context.background().baseLayerSource(d); - _backgroundList.call(updateLayerSelections); - document.activeElement.blur(); - } - - - function customChanged(d) { - if (d && d.template) { - _customSource.template(d.template); - chooseBackground(_customSource); - } else { - _customSource.template(''); - chooseBackground(context.background().findSource('none')); - } - } - - - function editCustom() { - d3_event.preventDefault(); - context.container() - .call(settingsCustomBackground); - } - - - function chooseOverlay(d) { - d3_event.preventDefault(); - context.background().toggleOverlayLayer(d); - _overlayList.call(updateLayerSelections); - document.activeElement.blur(); - } - - - function drawListItems(layerList, type, change, filter) { - var sources = context.background() - .sources(context.map().extent(), context.map().zoom(), true) - .filter(filter); - - var layerLinks = layerList.selectAll('li') - .data(sources, function(d) { return d.name(); }); - - layerLinks.exit() - .remove(); - - var enter = layerLinks.enter() - .append('li') - .classed('layer-custom', function(d) { return d.id === 'custom'; }) - .classed('best', function(d) { return d.best(); }); - - var label = enter - .append('label'); - - label - .append('input') - .attr('type', type) - .attr('name', 'layers') - .on('change', change); - - label - .append('span') - .text(function(d) { return d.name(); }); - - enter.filter(function(d) { return d.id === 'custom'; }) - .append('button') - .attr('class', 'layer-browse') - .call(tooltip() - .title(t('settings.custom_background.tooltip')) - .placement((textDirection === 'rtl') ? 'right' : 'left') - ) - .on('click', editCustom) - .call(svgIcon('#iD-icon-more')); - - enter.filter(function(d) { return d.best(); }) - .append('div') - .attr('class', 'best') - .call(tooltip() - .title(t('background.best_imagery')) - .placement((textDirection === 'rtl') ? 'right' : 'left') - ) - .append('span') - .html('★'); - - - layerList.selectAll('li') - .sort(sortSources); - - layerList - .call(updateLayerSelections); - - - function sortSources(a, b) { - return a.best() && !b.best() ? -1 - : b.best() && !a.best() ? 1 - : d3_descending(a.area(), b.area()) || d3_ascending(a.name(), b.name()) || 0; - } - } - - - function renderBackgroundList(selection) { - - // the background list - var container = selection.selectAll('.layer-background-list') - .data([0]); - - _backgroundList = container.enter() - .append('ul') - .attr('class', 'layer-list layer-background-list') - .attr('dir', 'auto') - .merge(container); - - - // add minimap toggle below list - var bgExtrasListEnter = selection.selectAll('.bg-extras-list') - .data([0]) - .enter() - .append('ul') - .attr('class', 'layer-list bg-extras-list'); - - var minimapLabelEnter = bgExtrasListEnter - .append('li') - .attr('class', 'minimap-toggle-item') - .append('label') - .call(tooltip() - .html(true) - .title(uiTooltipHtml(t('background.minimap.tooltip'), t('background.minimap.key'))) - .placement('top') - ); - - minimapLabelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function() { - d3_event.preventDefault(); - uiMapInMap.toggle(); - }); - - minimapLabelEnter - .append('span') - .text(t('background.minimap.description')); - - - var panelLabelEnter = bgExtrasListEnter - .append('li') - .attr('class', 'background-panel-toggle-item') - .append('label') - .call(tooltip() - .html(true) - .title(uiTooltipHtml(t('background.panel.tooltip'), uiCmd('⌘⇧' + t('info_panels.background.key')))) - .placement('top') - ); - - panelLabelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function() { - d3_event.preventDefault(); - context.ui().info.toggle('background'); - }); - - panelLabelEnter - .append('span') - .text(t('background.panel.description')); - - - // "Info / Report a Problem" link - selection.selectAll('.imagery-faq') - .data([0]) - .enter() - .append('div') - .attr('class', 'imagery-faq') - .append('a') - .attr('target', '_blank') - .call(svgIcon('#iD-icon-out-link', 'inline')) - .attr('href', 'https://github.com/openstreetmap/iD/blob/master/FAQ.md#how-can-i-report-an-issue-with-background-imagery') - .append('span') - .text(t('background.imagery_problem_faq')); - - updateBackgroundList(); - } - - - function renderOverlayList(selection) { - var container = selection.selectAll('.layer-overlay-list') - .data([0]); - - _overlayList = container.enter() - .append('ul') - .attr('class', 'layer-list layer-overlay-list') - .attr('dir', 'auto') - .merge(container); - - updateOverlayList(); - } - - function updateBackgroundList() { - _backgroundList - .call(drawListItems, 'radio', chooseBackground, function(d) { return !d.isHidden() && !d.overlay; }); - } - - function updateOverlayList() { - _overlayList - .call(drawListItems, 'checkbox', chooseOverlay, function(d) { return !d.isHidden() && d.overlay; }); - } - + var overlayList = uiOverlayList(context); function update() { - if (!backgroundPane.selection().select('.disclosure-wrap-background_list').classed('hide')) { - updateBackgroundList(); - } + _backgroundListContainer + .call(backgroundList); - if (!backgroundPane.selection().select('.disclosure-wrap-overlay_list').classed('hide')) { - updateOverlayList(); - } + _overlayListContainer + .call(overlayList); _displayOptionsContainer .call(backgroundDisplayOptions); @@ -307,18 +39,21 @@ export function uiBackground(context) { .call(backgroundOffset); } - function quickSwitch() { if (d3_event) { d3_event.stopImmediatePropagation(); d3_event.preventDefault(); } - if (_previousBackground) { - chooseBackground(_previousBackground); + var previousBackground = context.background().findSource(context.storage('background-last-used-toggle')); + if (previousBackground) { + var newPreviousBackground = context.background().baseLayerSource(); + context.storage('background-last-used-toggle', newPreviousBackground.id); + context.storage('background-last-used', previousBackground.id); + context.background().baseLayerSource(previousBackground); + document.activeElement.blur(); } } - var backgroundPane = uiPane('background', context) .key(_key) .title(t('background.title')) @@ -328,22 +63,14 @@ export function uiBackground(context) { backgroundPane.renderContent = function(content) { // background list - content + _backgroundListContainer = content .append('div') - .attr('class', 'background-background-list-container') - .call(uiDisclosure(context, 'background_list', true) - .title(t('background.backgrounds')) - .content(renderBackgroundList) - ); + .attr('class', 'background-background-list-container'); // overlay list - content + _overlayListContainer = content .append('div') - .attr('class', 'background-overlay-list-container') - .call(uiDisclosure(context, 'overlay_list', true) - .title(t('background.overlays')) - .content(renderOverlayList) - ); + .attr('class', 'background-overlay-list-container'); // display options _displayOptionsContainer = content @@ -355,6 +82,8 @@ export function uiBackground(context) { .append('div') .attr('class', 'background-offset'); + update(); + // add listeners context.map() @@ -362,13 +91,9 @@ export function uiBackground(context) { _debounce(function() { window.requestIdleCallback(update); }, 1000) ); - context.background() .on('change.background-update', update); - - update(); - context.keybinding() .on(uiCmd('⌘' + _key), quickSwitch); }; diff --git a/modules/ui/panes/help.js b/modules/ui/panes/help.js index c23bde88e..eec1a42d3 100644 --- a/modules/ui/panes/help.js +++ b/modules/ui/panes/help.js @@ -11,7 +11,7 @@ import { t, textDirection } from '../../util/locale'; import { tooltip } from '../../util/tooltip'; import { icon } from '../intro/helper'; -export function uiHelp(context) { +export function uiPaneHelp(context) { var docKeys = [ ['help', [ diff --git a/modules/ui/panes/issues.js b/modules/ui/panes/issues.js index 38705d204..ad0e2885e 100644 --- a/modules/ui/panes/issues.js +++ b/modules/ui/panes/issues.js @@ -13,7 +13,7 @@ import { utilGetSetValue, utilHighlightEntities, utilNoAuto } from '../../util'; import { uiPane } from '../pane'; -export function uiIssues(context) { +export function uiPaneIssues(context) { var MINSQUARE = 0; var MAXSQUARE = 20; diff --git a/modules/ui/panes/map_data.js b/modules/ui/panes/map_data.js index 9686f87fc..15dd49eec 100644 --- a/modules/ui/panes/map_data.js +++ b/modules/ui/panes/map_data.js @@ -15,7 +15,7 @@ import { uiCmd } from '../cmd'; import { uiPane } from '../pane'; -export function uiMapData(context) { +export function uiPaneMapData(context) { var osmDataToggleKey = uiCmd('⌥' + t('area_fill.wireframe.key')); var features = context.features().keys(); var layers = context.layers(); diff --git a/modules/ui/panes/preferences.js b/modules/ui/panes/preferences.js index db284ace5..af16b0fe9 100644 --- a/modules/ui/panes/preferences.js +++ b/modules/ui/panes/preferences.js @@ -7,7 +7,7 @@ import { uiDisclosure } from '../disclosure'; import { uiPane } from '../pane'; -export function uiPreferences(context) { +export function uiPanePreferences(context) { let _showThirdPartyIcons = context.storage('preferences.privacy.thirdpartyicons') || 'true'; function renderPrivacyOptions(selection) { diff --git a/modules/ui/background_display_options.js b/modules/ui/sections/background_display_options.js similarity index 95% rename from modules/ui/background_display_options.js rename to modules/ui/sections/background_display_options.js index f22f5d7e8..9632dcb4c 100644 --- a/modules/ui/background_display_options.js +++ b/modules/ui/sections/background_display_options.js @@ -3,10 +3,10 @@ import { select as d3_select } from 'd3-selection'; -import { t, textDirection } from '../util/locale'; -import { svgIcon } from '../svg/icon'; -import { uiDisclosure } from './disclosure'; -import { utilDetect } from '../util/detect'; +import { t, textDirection } from '../../util/locale'; +import { svgIcon } from '../../svg/icon'; +import { uiDisclosure } from '../disclosure'; +import { utilDetect } from '../../util/detect'; export function uiBackgroundDisplayOptions(context) { diff --git a/modules/ui/sections/background_list.js b/modules/ui/sections/background_list.js new file mode 100644 index 000000000..49c5265a5 --- /dev/null +++ b/modules/ui/sections/background_list.js @@ -0,0 +1,268 @@ +import { descending as d3_descending, ascending as d3_ascending } from 'd3-array'; +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { t, textDirection } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; +import { svgIcon } from '../../svg/icon'; +import { uiCmd } from '../cmd'; +import { uiDisclosure } from '../disclosure'; +import { uiSettingsCustomBackground } from '../settings/custom_background'; +import { uiMapInMap } from '../map_in_map'; +import { uiTooltipHtml } from '../tooltipHtml'; + +export function uiBackgroundList(context) { + + var _backgroundList = d3_select(null); + + var _customSource = context.background().findSource('custom'); + + var settingsCustomBackground = uiSettingsCustomBackground(context) + .on('change', customChanged); + + function previousBackgroundID() { + return context.storage('background-last-used-toggle'); + } + + function render(selection) { + + // the background list + var container = selection.selectAll('.layer-background-list') + .data([0]); + + _backgroundList = container.enter() + .append('ul') + .attr('class', 'layer-list layer-background-list') + .attr('dir', 'auto') + .merge(container); + + + // add minimap toggle below list + var bgExtrasListEnter = selection.selectAll('.bg-extras-list') + .data([0]) + .enter() + .append('ul') + .attr('class', 'layer-list bg-extras-list'); + + var minimapLabelEnter = bgExtrasListEnter + .append('li') + .attr('class', 'minimap-toggle-item') + .append('label') + .call(tooltip() + .html(true) + .title(uiTooltipHtml(t('background.minimap.tooltip'), t('background.minimap.key'))) + .placement('top') + ); + + minimapLabelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function() { + d3_event.preventDefault(); + uiMapInMap.toggle(); + }); + + minimapLabelEnter + .append('span') + .text(t('background.minimap.description')); + + + var panelLabelEnter = bgExtrasListEnter + .append('li') + .attr('class', 'background-panel-toggle-item') + .append('label') + .call(tooltip() + .html(true) + .title(uiTooltipHtml(t('background.panel.tooltip'), uiCmd('⌘⇧' + t('info_panels.background.key')))) + .placement('top') + ); + + panelLabelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function() { + d3_event.preventDefault(); + context.ui().info.toggle('background'); + }); + + panelLabelEnter + .append('span') + .text(t('background.panel.description')); + + + // "Info / Report a Problem" link + selection.selectAll('.imagery-faq') + .data([0]) + .enter() + .append('div') + .attr('class', 'imagery-faq') + .append('a') + .attr('target', '_blank') + .call(svgIcon('#iD-icon-out-link', 'inline')) + .attr('href', 'https://github.com/openstreetmap/iD/blob/master/FAQ.md#how-can-i-report-an-issue-with-background-imagery') + .append('span') + .text(t('background.imagery_problem_faq')); + + updateBackgroundList(); + } + + function setTooltips(selection) { + selection.each(function(d, i, nodes) { + var item = d3_select(this).select('label'); + var span = item.select('span'); + var placement = (i < nodes.length / 2) ? 'bottom' : 'top'; + var description = d.description(); + var isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth')); + + item.call(tooltip().destroyAny); + + if (d.id === previousBackgroundID()) { + item.call(tooltip() + .placement(placement) + .html(true) + .title(function() { + var tip = '
' + t('background.switch') + '
'; + return uiTooltipHtml(tip, uiCmd('⌘' + t('background.key'))); + }) + ); + } else if (description || isOverflowing) { + item.call(tooltip() + .placement(placement) + .title(description || d.name()) + ); + } + }); + } + + function updateBackgroundList() { + _backgroundList + .call(drawListItems, 'radio', chooseBackground, function(d) { return !d.isHidden() && !d.overlay; }); + } + + function drawListItems(layerList, type, change, filter) { + var sources = context.background() + .sources(context.map().extent(), context.map().zoom(), true) + .filter(filter); + + var layerLinks = layerList.selectAll('li') + .data(sources, function(d) { return d.name(); }); + + layerLinks.exit() + .remove(); + + var enter = layerLinks.enter() + .append('li') + .classed('layer-custom', function(d) { return d.id === 'custom'; }) + .classed('best', function(d) { return d.best(); }); + + var label = enter + .append('label'); + + label + .append('input') + .attr('type', type) + .attr('name', 'layers') + .on('change', change); + + label + .append('span') + .text(function(d) { return d.name(); }); + + enter.filter(function(d) { return d.id === 'custom'; }) + .append('button') + .attr('class', 'layer-browse') + .call(tooltip() + .title(t('settings.custom_background.tooltip')) + .placement((textDirection === 'rtl') ? 'right' : 'left') + ) + .on('click', editCustom) + .call(svgIcon('#iD-icon-more')); + + enter.filter(function(d) { return d.best(); }) + .append('div') + .attr('class', 'best') + .call(tooltip() + .title(t('background.best_imagery')) + .placement((textDirection === 'rtl') ? 'right' : 'left') + ) + .append('span') + .html('★'); + + + layerList.selectAll('li') + .sort(sortSources); + + layerList + .call(updateLayerSelections); + + + function sortSources(a, b) { + return a.best() && !b.best() ? -1 + : b.best() && !a.best() ? 1 + : d3_descending(a.area(), b.area()) || d3_ascending(a.name(), b.name()) || 0; + } + } + + function updateLayerSelections(selection) { + function active(d) { + return context.background().showsLayer(d); + } + + selection.selectAll('li') + .classed('active', active) + .classed('switch', function(d) { return d.id === previousBackgroundID(); }) + .call(setTooltips) + .selectAll('input') + .property('checked', active); + } + + + function chooseBackground(d) { + if (d.id === 'custom' && !d.template()) { + return editCustom(); + } + + d3_event.preventDefault(); + var previousBackground = context.background().baseLayerSource(); + context.storage('background-last-used-toggle', previousBackground.id); + context.storage('background-last-used', d.id); + context.background().baseLayerSource(d); + document.activeElement.blur(); + } + + + function customChanged(d) { + if (d && d.template) { + _customSource.template(d.template); + chooseBackground(_customSource); + } else { + _customSource.template(''); + chooseBackground(context.background().findSource('none')); + } + } + + + function editCustom() { + d3_event.preventDefault(); + context.container() + .call(settingsCustomBackground); + } + + + function backgroundList(selection) { + selection + .call(uiDisclosure(context, 'background_list', true) + .title(t('background.backgrounds')) + .content(render) + ); + } + + context.background() + .on('change.background_list', function() { + _backgroundList.call(updateLayerSelections); + }); + + return backgroundList; +} diff --git a/modules/ui/background_offset.js b/modules/ui/sections/background_offset.js similarity index 96% rename from modules/ui/background_offset.js rename to modules/ui/sections/background_offset.js index 00016ab1c..407626576 100644 --- a/modules/ui/background_offset.js +++ b/modules/ui/sections/background_offset.js @@ -4,10 +4,10 @@ import { selectAll as d3_selectAll } from 'd3-selection'; -import { t, textDirection } from '../util/locale'; -import { geoMetersToOffset, geoOffsetToMeters } from '../geo'; -import { svgIcon } from '../svg/icon'; -import { uiDisclosure } from './disclosure'; +import { t, textDirection } from '../../util/locale'; +import { geoMetersToOffset, geoOffsetToMeters } from '../../geo'; +import { svgIcon } from '../../svg/icon'; +import { uiDisclosure } from '../disclosure'; export function uiBackgroundOffset(context) { diff --git a/modules/ui/sections/overlay_list.js b/modules/ui/sections/overlay_list.js new file mode 100644 index 000000000..757d5ebdc --- /dev/null +++ b/modules/ui/sections/overlay_list.js @@ -0,0 +1,124 @@ +import { descending as d3_descending, ascending as d3_ascending } from 'd3-array'; +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { t } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; +import { uiDisclosure } from '../disclosure'; + +export function uiOverlayList(context) { + + var _overlayList = d3_select(null); + + function render(selection) { + + var container = selection.selectAll('.layer-overlay-list') + .data([0]); + + _overlayList = container.enter() + .append('ul') + .attr('class', 'layer-list layer-overlay-list') + .attr('dir', 'auto') + .merge(container); + + updateOverlayList(); + } + + function setTooltips(selection) { + selection.each(function(d, i, nodes) { + var item = d3_select(this).select('label'); + var span = item.select('span'); + var placement = (i < nodes.length / 2) ? 'bottom' : 'top'; + var description = d.description(); + var isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth')); + + item.call(tooltip().destroyAny); + + if (description || isOverflowing) { + item.call(tooltip() + .placement(placement) + .title(description || d.name()) + ); + } + }); + } + + function updateLayerSelections(selection) { + function active(d) { + return context.background().showsLayer(d); + } + + selection.selectAll('li') + .classed('active', active) + .call(setTooltips) + .selectAll('input') + .property('checked', active); + } + + + function chooseOverlay(d) { + d3_event.preventDefault(); + context.background().toggleOverlayLayer(d); + _overlayList.call(updateLayerSelections); + document.activeElement.blur(); + } + + function drawListItems(layerList, type, change, filter) { + var sources = context.background() + .sources(context.map().extent(), context.map().zoom(), true) + .filter(filter); + + var layerLinks = layerList.selectAll('li') + .data(sources, function(d) { return d.name(); }); + + layerLinks.exit() + .remove(); + + var enter = layerLinks.enter() + .append('li'); + + var label = enter + .append('label'); + + label + .append('input') + .attr('type', type) + .attr('name', 'layers') + .on('change', change); + + label + .append('span') + .text(function(d) { return d.name(); }); + + + layerList.selectAll('li') + .sort(sortSources); + + layerList + .call(updateLayerSelections); + + + function sortSources(a, b) { + return a.best() && !b.best() ? -1 + : b.best() && !a.best() ? 1 + : d3_descending(a.area(), b.area()) || d3_ascending(a.name(), b.name()) || 0; + } + } + + function updateOverlayList() { + _overlayList + .call(drawListItems, 'checkbox', chooseOverlay, function(d) { return !d.isHidden() && d.overlay; }); + } + + function overlayList(selection) { + selection + .call(uiDisclosure(context, 'overlay_list', true) + .title(t('background.overlays')) + .content(render) + ); + } + + return overlayList; +} From 5c04ad3eb526d27b466baca192e76ce7a1cd0c74 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 18 Feb 2020 20:02:55 -0800 Subject: [PATCH 018/127] Add uiSection class as standard component for pane sections Move validation rules and privacy preferences to their own section objects --- modules/ui/pane.js | 17 +- modules/ui/panes/background.js | 84 ++------ modules/ui/panes/issues.js | 204 ++---------------- modules/ui/panes/preferences.js | 79 +------ modules/ui/section.js | 75 +++++++ .../ui/sections/background_display_options.js | 52 ++--- modules/ui/sections/background_list.js | 40 ++-- modules/ui/sections/background_offset.js | 27 +-- modules/ui/sections/overlay_list.js | 50 ++--- modules/ui/sections/privacy.js | 74 +++++++ modules/ui/sections/validation_rules.js | 183 ++++++++++++++++ 11 files changed, 454 insertions(+), 431 deletions(-) create mode 100644 modules/ui/section.js create mode 100644 modules/ui/sections/privacy.js create mode 100644 modules/ui/sections/validation_rules.js diff --git a/modules/ui/pane.js b/modules/ui/pane.js index 1d221ba59..c96caa105 100644 --- a/modules/ui/pane.js +++ b/modules/ui/pane.js @@ -15,6 +15,7 @@ export function uiPane(id, context) { var _title = ''; var _description = ''; var _iconName = ''; + var _sections; // array of uiSection objects var _paneSelection = d3_select(null); @@ -48,6 +49,12 @@ export function uiPane(id, context) { return pane; }; + pane.sections = function(val) { + if (!arguments.length) return _sections; + _sections = val; + return pane; + }; + pane.selection = function() { return _paneSelection; }; @@ -78,8 +85,14 @@ export function uiPane(id, context) { .call(_paneTooltip); }; - pane.renderContent = function() { - // override + pane.renderContent = function(selection) { + // override to fully customize content + + if (_sections) { + _sections.forEach(function(section) { + selection.call(section.render); + }); + } }; pane.renderPane = function(selection) { diff --git a/modules/ui/panes/background.js b/modules/ui/panes/background.js index 88d4eba13..ddc894c86 100644 --- a/modules/ui/panes/background.js +++ b/modules/ui/panes/background.js @@ -1,6 +1,4 @@ -import _debounce from 'lodash-es/debounce'; - -import { event as d3_event, select as d3_select } from 'd3-selection'; +import { event as d3_event } from 'd3-selection'; import { t } from '../../util/locale'; import { uiCmd } from '../cmd'; @@ -13,31 +11,8 @@ import { uiOverlayList } from '../sections/overlay_list'; export function uiPaneBackground(context) { - var _key = t('background.key'); - - var _backgroundListContainer = d3_select(null); - var _overlayListContainer = d3_select(null); - var _displayOptionsContainer = d3_select(null); - var _offsetContainer = d3_select(null); - - var backgroundList = uiBackgroundList(context); - var backgroundDisplayOptions = uiBackgroundDisplayOptions(context); - var backgroundOffset = uiBackgroundOffset(context); - var overlayList = uiOverlayList(context); - - function update() { - _backgroundListContainer - .call(backgroundList); - - _overlayListContainer - .call(overlayList); - - _displayOptionsContainer - .call(backgroundDisplayOptions); - - _offsetContainer - .call(backgroundOffset); - } + context.keybinding() + .on(uiCmd('⌘' + t('background.key')), quickSwitch); function quickSwitch() { if (d3_event) { @@ -46,57 +21,24 @@ export function uiPaneBackground(context) { } var previousBackground = context.background().findSource(context.storage('background-last-used-toggle')); if (previousBackground) { - var newPreviousBackground = context.background().baseLayerSource(); - context.storage('background-last-used-toggle', newPreviousBackground.id); + var currentBackground = context.background().baseLayerSource(); + context.storage('background-last-used-toggle', currentBackground.id); context.storage('background-last-used', previousBackground.id); context.background().baseLayerSource(previousBackground); - document.activeElement.blur(); } } var backgroundPane = uiPane('background', context) - .key(_key) + .key(t('background.key')) .title(t('background.title')) .description(t('background.description')) - .iconName('iD-icon-layers'); - - backgroundPane.renderContent = function(content) { - - // background list - _backgroundListContainer = content - .append('div') - .attr('class', 'background-background-list-container'); - - // overlay list - _overlayListContainer = content - .append('div') - .attr('class', 'background-overlay-list-container'); - - // display options - _displayOptionsContainer = content - .append('div') - .attr('class', 'background-display-options'); - - // offset controls - _offsetContainer = content - .append('div') - .attr('class', 'background-offset'); - - update(); - - - // add listeners - context.map() - .on('move.background-update', - _debounce(function() { window.requestIdleCallback(update); }, 1000) - ); - - context.background() - .on('change.background-update', update); - - context.keybinding() - .on(uiCmd('⌘' + _key), quickSwitch); - }; + .iconName('iD-icon-layers') + .sections([ + uiBackgroundList(context), + uiOverlayList(context), + uiBackgroundDisplayOptions(context), + uiBackgroundOffset(context) + ]); return backgroundPane; } diff --git a/modules/ui/panes/issues.js b/modules/ui/panes/issues.js index ad0e2885e..9088697fd 100644 --- a/modules/ui/panes/issues.js +++ b/modules/ui/panes/issues.js @@ -3,25 +3,24 @@ import _debounce from 'lodash-es/debounce'; import { event as d3_event, select as d3_select } from 'd3-selection'; import { t } from '../../util/locale'; -import { tooltip } from '../../util/tooltip'; //import { actionNoop } from '../actions/noop'; import { geoSphericalDistance } from '../../geo'; import { svgIcon } from '../../svg/icon'; import { uiDisclosure } from '../disclosure'; -import { utilGetSetValue, utilHighlightEntities, utilNoAuto } from '../../util'; +import { utilHighlightEntities } from '../../util'; import { uiPane } from '../pane'; +import { uiValidationRules } from '../sections/validation_rules'; export function uiPaneIssues(context) { - var MINSQUARE = 0; - var MAXSQUARE = 20; - var DEFAULTSQUARE = 5; // see also unsquare_way.js - var _errorsSelection = d3_select(null); var _warningsSelection = d3_select(null); - var _rulesList = d3_select(null); + + var _rulesListContainer = d3_select(null); + + var _validationRules = uiValidationRules(context); var _errors = []; var _warnings = []; @@ -313,69 +312,6 @@ export function uiPaneIssues(context) { }); } - - function renderRulesList(selection) { - var container = selection.selectAll('.issues-rulelist-container') - .data([0]); - - var containerEnter = container.enter() - .append('div') - .attr('class', 'issues-rulelist-container'); - - containerEnter - .append('ul') - .attr('class', 'layer-list issue-rules-list'); - - var ruleLinks = containerEnter - .append('div') - .attr('class', 'issue-rules-links section-footer'); - - ruleLinks - .append('a') - .attr('class', 'issue-rules-link') - .attr('href', '#') - .text(t('issues.enable_all')) - .on('click', function() { - context.validator().disableRules([]); - }); - - ruleLinks - .append('a') - .attr('class', 'issue-rules-link') - .attr('href', '#') - .text(t('issues.disable_all')) - .on('click', function() { - var keys = context.validator().getRuleKeys(); - context.validator().disableRules(keys); - }); - - - // Update - container = container - .merge(containerEnter); - - _rulesList = container.selectAll('.issue-rules-list'); - - updateRulesList(); - } - - - function updateRulesList() { - var ruleKeys = context.validator().getRuleKeys(); - _rulesList - .call(drawListItems, ruleKeys, 'checkbox', 'rule', toggleRule, isRuleEnabled); - } - - - function isRuleEnabled(d) { - return context.validator().isRuleEnabled(d); - } - - - function toggleRule(d) { - context.validator().toggleRule(d); - } - function setNoIssuesText() { function checkForHiddenIssues(cases) { @@ -502,10 +438,8 @@ export function uiPaneIssues(context) { setNoIssuesText(); } - if (!issuesPane.selection().select('.disclosure-wrap-issues_rules').classed('hide')) { - updateRulesList(); - } - + _rulesListContainer + .call(_validationRules.render); function byDistance(a, b) { return a.dist - b.dist; @@ -519,116 +453,6 @@ export function uiPaneIssues(context) { } - function drawListItems(selection, data, type, name, change, active) { - var items = selection.selectAll('li') - .data(data); - - // Exit - items.exit() - .remove(); - - // Enter - var enter = items.enter() - .append('li'); - - if (name === 'rule') { - enter - .call(tooltip() - .title(function(d) { return t('issues.' + d + '.tip'); }) - .placement('top') - ); - } - - var label = enter - .append('label'); - - label - .append('input') - .attr('type', type) - .attr('name', name) - .on('change', change); - - label - .append('span') - .html(function(d) { - var params = {}; - if (d === 'unsquare_way') { - params.val = ''; - } - return t('issues.' + d + '.title', params); - }); - - // Update - items = items - .merge(enter); - - items - .classed('active', active) - .selectAll('input') - .property('checked', active) - .property('indeterminate', false); - - - // user-configurable square threshold - var degStr = context.storage('validate-square-degrees'); - if (degStr === null) { - degStr = '' + DEFAULTSQUARE; - } - - var span = items.selectAll('.square-degrees'); - var input = span.selectAll('.square-degrees-input') - .data([0]); - - // enter / update - input.enter() - .append('input') - .attr('type', 'number') - .attr('min', '' + MINSQUARE) - .attr('max', '' + MAXSQUARE) - .attr('step', '0.5') - .attr('class', 'square-degrees-input') - .call(utilNoAuto) - .on('click', function () { - d3_event.preventDefault(); - d3_event.stopPropagation(); - this.select(); - }) - .on('keyup', function () { - if (d3_event.keyCode === 13) { // enter - this.blur(); - this.select(); - } - }) - .on('blur', changeSquare) - .merge(input) - .property('value', degStr); - } - - - function changeSquare() { - var input = d3_select(this); - var degStr = utilGetSetValue(input).trim(); - var degNum = parseFloat(degStr, 10); - - if (!isFinite(degNum)) { - degNum = DEFAULTSQUARE; - } else if (degNum > MAXSQUARE) { - degNum = MAXSQUARE; - } else if (degNum < MINSQUARE) { - degNum = MINSQUARE; - } - - degNum = Math.round(degNum * 10 ) / 10; // round to 1 decimal - degStr = '' + degNum; - - input - .property('value', degStr); - - context.storage('validate-square-degrees', degStr); - context.validator().reloadUnsquareIssues(); - } - - var issuesPane = uiPane('issues', context) .key(t('issues.key')) .title(t('issues.title')) @@ -664,16 +488,12 @@ export function uiPaneIssues(context) { .content(renderWarningsList) ); - // rules - content + // rules list + _rulesListContainer = content .append('div') - .attr('class', 'issues-rules') - .call(uiDisclosure(context, 'issues_rules', false) - .title(t('issues.rules.title')) - .content(renderRulesList) - ); + .attr('class', 'issues-rules'); - // update(); + update(); }; return issuesPane; diff --git a/modules/ui/panes/preferences.js b/modules/ui/panes/preferences.js index af16b0fe9..907a6c374 100644 --- a/modules/ui/panes/preferences.js +++ b/modules/ui/panes/preferences.js @@ -1,87 +1,18 @@ -import { event as d3_event } from 'd3-selection'; -import { svgIcon } from '../../svg/icon'; import { t } from '../../util/locale'; -import { tooltip } from '../../util/tooltip'; -import { uiDisclosure } from '../disclosure'; import { uiPane } from '../pane'; - +import { uiSectionPrivacy } from '../sections/privacy'; export function uiPanePreferences(context) { - let _showThirdPartyIcons = context.storage('preferences.privacy.thirdpartyicons') || 'true'; - - function renderPrivacyOptions(selection) { - // enter - let privacyOptionsListEnter = selection.selectAll('.privacy-options-list') - .data([0]) - .enter() - .append('ul') - .attr('class', 'layer-list privacy-options-list'); - - let thirdPartyIconsEnter = privacyOptionsListEnter - .append('li') - .attr('class', 'privacy-third-party-icons-item') - .append('label') - .call(tooltip() - .title(t('preferences.privacy.third_party_icons.tooltip')) - .placement('bottom') - ); - - thirdPartyIconsEnter - .append('input') - .attr('type', 'checkbox') - .on('change', () => { - d3_event.preventDefault(); - _showThirdPartyIcons = (_showThirdPartyIcons === 'true') ? 'false' : 'true'; - context.storage('preferences.privacy.thirdpartyicons', _showThirdPartyIcons); - update(); - }); - - thirdPartyIconsEnter - .append('span') - .text(t('preferences.privacy.third_party_icons.description')); - - - // Privacy Policy link - selection.selectAll('.privacy-link') - .data([0]) - .enter() - .append('div') - .attr('class', 'privacy-link') - .append('a') - .attr('target', '_blank') - .call(svgIcon('#iD-icon-out-link', 'inline')) - .attr('href', 'https://github.com/openstreetmap/iD/blob/master/PRIVACY.md') - .append('span') - .text(t('preferences.privacy.privacy_link')); - - update(); - - - function update() { - selection.selectAll('.privacy-third-party-icons-item') - .classed('active', (_showThirdPartyIcons === 'true')) - .select('input') - .property('checked', (_showThirdPartyIcons === 'true')); - } - } let preferencesPane = uiPane('preferences', context) .key(t('preferences.key')) .title(t('preferences.title')) .description(t('preferences.description')) - .iconName('fas-user-cog'); - - preferencesPane.renderContent = (content) => { - - content - .append('div') - .attr('class', 'preferences-privacy') - .call(uiDisclosure(context, 'preferences_third_party', true) - .title(t('preferences.privacy.title')) - .content(renderPrivacyOptions) - ); - }; + .iconName('fas-user-cog') + .sections([ + uiSectionPrivacy(context) + ]); return preferencesPane; } diff --git a/modules/ui/section.js b/modules/ui/section.js new file mode 100644 index 000000000..a5f7372f4 --- /dev/null +++ b/modules/ui/section.js @@ -0,0 +1,75 @@ +import { + select as d3_select +} from 'd3-selection'; + +import { uiDisclosure } from './disclosure'; + +// A unit of controls or info to be used in a layout, such as within a pane. +// Can be labeled and collapsible. +export function uiSection(id, context) { + + var _disclosure; + var _title; + var _expandedByDefault = true; + + var _containerSelection = d3_select(null); + + var section = { + id: id + }; + + section.title = function(val) { + if (!arguments.length) return _title; + _title = val; + return section; + }; + + section.expandedByDefault = function(val) { + if (!arguments.length) return _expandedByDefault; + _expandedByDefault = val; + return section; + }; + + // may be called multiple times + section.render = function(selection) { + + _containerSelection = selection + .selectAll('.section-' + id) + .data([0]); + + var sectionEnter = _containerSelection + .enter() + .append('div') + .attr('class', 'section section-' + id); + + _containerSelection = sectionEnter + .merge(_containerSelection); + + _containerSelection + .call(section.renderContent); + }; + + // may be called multiple times + section.renderContent = function(containerSelection) { + + if (section.renderDisclosureContent && _title) { + if (!_disclosure) { + _disclosure = uiDisclosure(context, id.replace(/-/g, '_'), _expandedByDefault) + .title(_title) + .content(section.renderDisclosureContent); + } + containerSelection + .call(_disclosure); + } + }; + + // override to enable disclosure + section.renderDisclosureContent = undefined; + + section.rerenderContent = function() { + _containerSelection + .call(section.renderContent); + }; + + return section; +} diff --git a/modules/ui/sections/background_display_options.js b/modules/ui/sections/background_display_options.js index 9632dcb4c..7113caedc 100644 --- a/modules/ui/sections/background_display_options.js +++ b/modules/ui/sections/background_display_options.js @@ -5,29 +5,31 @@ import { import { t, textDirection } from '../../util/locale'; import { svgIcon } from '../../svg/icon'; -import { uiDisclosure } from '../disclosure'; +import { uiSection } from '../section'; import { utilDetect } from '../../util/detect'; export function uiBackgroundDisplayOptions(context) { - var detected = utilDetect(); - var storedOpacity = context.storage('background-opacity'); - var minVal = 0.25; - var maxVal = detected.cssfilters ? 2 : 1; - var sliders = detected.cssfilters + var section = uiSection('background-display-options', context) + .title(t('background.display_options')); + + var _detected = utilDetect(); + var _storedOpacity = context.storage('background-opacity'); + var _minVal = 0.25; + var _maxVal = _detected.cssfilters ? 2 : 1; + + var _sliders = _detected.cssfilters ? ['brightness', 'contrast', 'saturation', 'sharpness'] : ['brightness']; var _options = { - brightness: (storedOpacity !== null ? (+storedOpacity) : 1), + brightness: (_storedOpacity !== null ? (+_storedOpacity) : 1), contrast: 1, saturation: 1, sharpness: 1 }; - var _selection = d3_select(null); - function clamp(x, min, max) { return Math.max(min, Math.min(x, max)); @@ -39,7 +41,7 @@ export function uiBackgroundDisplayOptions(context) { val = d3_event.target.value; } - val = clamp(val, minVal, maxVal); + val = clamp(val, _minVal, _maxVal); _options[d] = val; context.background()[d](val); @@ -48,12 +50,11 @@ export function uiBackgroundDisplayOptions(context) { context.storage('background-opacity', val); } - _selection - .call(render); + section.rerenderContent(); } - function render(selection) { + section.renderDisclosureContent = function(selection) { var container = selection.selectAll('.display-options-container') .data([0]); @@ -63,7 +64,7 @@ export function uiBackgroundDisplayOptions(context) { // add slider controls var slidersEnter = containerEnter.selectAll('.display-control') - .data(sliders) + .data(_sliders) .enter() .append('div') .attr('class', function(d) { return 'display-control display-control-' + d; }); @@ -78,8 +79,8 @@ export function uiBackgroundDisplayOptions(context) { .append('input') .attr('class', function(d) { return 'display-option-input display-option-input-' + d; }) .attr('type', 'range') - .attr('min', minVal) - .attr('max', maxVal) + .attr('min', _minVal) + .attr('max', _maxVal) .attr('step', '0.05') .on('input', function(d) { var val = d3_select(this).property('value'); @@ -103,8 +104,8 @@ export function uiBackgroundDisplayOptions(context) { .attr('href', '#') .text(t('background.reset_all')) .on('click', function() { - for (var i = 0; i < sliders.length; i++) { - updateValue(sliders[i],1); + for (var i = 0; i < _sliders.length; i++) { + updateValue(_sliders[i],1); } }); @@ -125,19 +126,8 @@ export function uiBackgroundDisplayOptions(context) { if (containerEnter.size() && _options.brightness !== 1) { context.background().brightness(_options.brightness); } - } + }; - function backgroundDisplayOptions(selection) { - _selection = selection; - - selection - .call(uiDisclosure(context, 'background_display_options', true) - .title(t('background.display_options')) - .content(render) - ); - } - - - return backgroundDisplayOptions; + return section; } diff --git a/modules/ui/sections/background_list.js b/modules/ui/sections/background_list.js index 49c5265a5..20c9ed340 100644 --- a/modules/ui/sections/background_list.js +++ b/modules/ui/sections/background_list.js @@ -1,3 +1,4 @@ +import _debounce from 'lodash-es/debounce'; import { descending as d3_descending, ascending as d3_ascending } from 'd3-array'; import { event as d3_event, @@ -8,9 +9,9 @@ import { t, textDirection } from '../../util/locale'; import { tooltip } from '../../util/tooltip'; import { svgIcon } from '../../svg/icon'; import { uiCmd } from '../cmd'; -import { uiDisclosure } from '../disclosure'; import { uiSettingsCustomBackground } from '../settings/custom_background'; import { uiMapInMap } from '../map_in_map'; +import { uiSection } from '../section'; import { uiTooltipHtml } from '../tooltipHtml'; export function uiBackgroundList(context) { @@ -19,14 +20,17 @@ export function uiBackgroundList(context) { var _customSource = context.background().findSource('custom'); - var settingsCustomBackground = uiSettingsCustomBackground(context) + var _settingsCustomBackground = uiSettingsCustomBackground(context) .on('change', customChanged); + var section = uiSection('background-list', context) + .title(t('background.backgrounds')); + function previousBackgroundID() { return context.storage('background-last-used-toggle'); } - function render(selection) { + section.renderDisclosureContent = function(selection) { // the background list var container = selection.selectAll('.layer-background-list') @@ -105,8 +109,9 @@ export function uiBackgroundList(context) { .append('span') .text(t('background.imagery_problem_faq')); - updateBackgroundList(); - } + _backgroundList + .call(drawListItems, 'radio', chooseBackground, function(d) { return !d.isHidden() && !d.overlay; }); + }; function setTooltips(selection) { selection.each(function(d, i, nodes) { @@ -136,11 +141,6 @@ export function uiBackgroundList(context) { }); } - function updateBackgroundList() { - _backgroundList - .call(drawListItems, 'radio', chooseBackground, function(d) { return !d.isHidden() && !d.overlay; }); - } - function drawListItems(layerList, type, change, filter) { var sources = context.background() .sources(context.map().extent(), context.map().zoom(), true) @@ -247,22 +247,22 @@ export function uiBackgroundList(context) { function editCustom() { d3_event.preventDefault(); context.container() - .call(settingsCustomBackground); + .call(_settingsCustomBackground); } - function backgroundList(selection) { - selection - .call(uiDisclosure(context, 'background_list', true) - .title(t('background.backgrounds')) - .content(render) - ); - } - context.background() .on('change.background_list', function() { _backgroundList.call(updateLayerSelections); }); - return backgroundList; + context.map() + .on('move.background_list', + _debounce(function() { + // layers in-view may have changed due to map move + window.requestIdleCallback(section.rerenderContent); + }, 1000) + ); + + return section; } diff --git a/modules/ui/sections/background_offset.js b/modules/ui/sections/background_offset.js index 407626576..4bbcac373 100644 --- a/modules/ui/sections/background_offset.js +++ b/modules/ui/sections/background_offset.js @@ -7,11 +7,16 @@ import { import { t, textDirection } from '../../util/locale'; import { geoMetersToOffset, geoOffsetToMeters } from '../../geo'; import { svgIcon } from '../../svg/icon'; -import { uiDisclosure } from '../disclosure'; +import { uiSection } from '../section'; export function uiBackgroundOffset(context) { - var directions = [ + + var section = uiSection('background-offset', context) + .title(t('background.fix_misalignment')) + .expandedByDefault(false); + + var _directions = [ ['right', [0.5, 0]], ['top', [0, -0.5]], ['left', [-0.5, 0]], @@ -129,7 +134,7 @@ export function uiBackgroundOffset(context) { } - function render(selection) { + section.renderDisclosureContent = function(selection) { var container = selection.selectAll('.nudge-container') .data([0]); @@ -156,7 +161,7 @@ export function uiBackgroundOffset(context) { containerEnter .append('div') .selectAll('button') - .data(directions).enter() + .data(_directions).enter() .append('button') .attr('class', function(d) { return d[0] + ' nudge'; }) .on('contextmenu', d3_eventCancel) @@ -177,20 +182,10 @@ export function uiBackgroundOffset(context) { .call(svgIcon('#iD-icon-' + (textDirection === 'rtl' ? 'redo' : 'undo'))); updateValue(); - } - - - function backgroundOffset(selection) { - selection - .call(uiDisclosure(context, 'background_offset', false) - .title(t('background.fix_misalignment')) - .content(render) - ); - } - + }; context.background() .on('change.backgroundOffset-update', updateValue); - return backgroundOffset; + return section; } diff --git a/modules/ui/sections/overlay_list.js b/modules/ui/sections/overlay_list.js index 757d5ebdc..f8797dce0 100644 --- a/modules/ui/sections/overlay_list.js +++ b/modules/ui/sections/overlay_list.js @@ -1,3 +1,4 @@ +import _debounce from 'lodash-es/debounce'; import { descending as d3_descending, ascending as d3_ascending } from 'd3-array'; import { event as d3_event, @@ -6,26 +7,15 @@ import { import { t } from '../../util/locale'; import { tooltip } from '../../util/tooltip'; -import { uiDisclosure } from '../disclosure'; +import { uiSection } from '../section'; export function uiOverlayList(context) { + var section = uiSection('overlay-list', context) + .title(t('background.overlays')); + var _overlayList = d3_select(null); - function render(selection) { - - var container = selection.selectAll('.layer-overlay-list') - .data([0]); - - _overlayList = container.enter() - .append('ul') - .attr('class', 'layer-list layer-overlay-list') - .attr('dir', 'auto') - .merge(container); - - updateOverlayList(); - } - function setTooltips(selection) { selection.each(function(d, i, nodes) { var item = d3_select(this).select('label'); @@ -107,18 +97,28 @@ export function uiOverlayList(context) { } } - function updateOverlayList() { + section.renderDisclosureContent = function(selection) { + + var container = selection.selectAll('.layer-overlay-list') + .data([0]); + + _overlayList = container.enter() + .append('ul') + .attr('class', 'layer-list layer-overlay-list') + .attr('dir', 'auto') + .merge(container); + _overlayList .call(drawListItems, 'checkbox', chooseOverlay, function(d) { return !d.isHidden() && d.overlay; }); - } + }; - function overlayList(selection) { - selection - .call(uiDisclosure(context, 'overlay_list', true) - .title(t('background.overlays')) - .content(render) - ); - } + context.map() + .on('move.overlay_list', + _debounce(function() { + // layers in-view may have changed due to map move + window.requestIdleCallback(section.rerenderContent); + }, 1000) + ); - return overlayList; + return section; } diff --git a/modules/ui/sections/privacy.js b/modules/ui/sections/privacy.js new file mode 100644 index 000000000..ffb168f50 --- /dev/null +++ b/modules/ui/sections/privacy.js @@ -0,0 +1,74 @@ +import { + event as d3_event +} from 'd3-selection'; + +import { t } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; +import { svgIcon } from '../../svg/icon'; +import { uiSection } from '../section'; + +export function uiSectionPrivacy(context) { + + let section = uiSection('preferences-third-party', context) + .title(t('preferences.privacy.title')); + + let _showThirdPartyIcons = context.storage('preferences.privacy.thirdpartyicons') || 'true'; + + section.renderDisclosureContent = function(selection) { + // enter + let privacyOptionsListEnter = selection.selectAll('.privacy-options-list') + .data([0]) + .enter() + .append('ul') + .attr('class', 'layer-list privacy-options-list'); + + let thirdPartyIconsEnter = privacyOptionsListEnter + .append('li') + .attr('class', 'privacy-third-party-icons-item') + .append('label') + .call(tooltip() + .title(t('preferences.privacy.third_party_icons.tooltip')) + .placement('bottom') + ); + + thirdPartyIconsEnter + .append('input') + .attr('type', 'checkbox') + .on('change', () => { + d3_event.preventDefault(); + _showThirdPartyIcons = (_showThirdPartyIcons === 'true') ? 'false' : 'true'; + context.storage('preferences.privacy.thirdpartyicons', _showThirdPartyIcons); + update(); + }); + + thirdPartyIconsEnter + .append('span') + .text(t('preferences.privacy.third_party_icons.description')); + + + // Privacy Policy link + selection.selectAll('.privacy-link') + .data([0]) + .enter() + .append('div') + .attr('class', 'privacy-link') + .append('a') + .attr('target', '_blank') + .call(svgIcon('#iD-icon-out-link', 'inline')) + .attr('href', 'https://github.com/openstreetmap/iD/blob/master/PRIVACY.md') + .append('span') + .text(t('preferences.privacy.privacy_link')); + + update(); + + + function update() { + selection.selectAll('.privacy-third-party-icons-item') + .classed('active', (_showThirdPartyIcons === 'true')) + .select('input') + .property('checked', (_showThirdPartyIcons === 'true')); + } + }; + + return section; +} diff --git a/modules/ui/sections/validation_rules.js b/modules/ui/sections/validation_rules.js new file mode 100644 index 000000000..ca8eb532a --- /dev/null +++ b/modules/ui/sections/validation_rules.js @@ -0,0 +1,183 @@ +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { t } from '../../util/locale'; +import { utilGetSetValue, utilNoAuto } from '../../util'; +import { tooltip } from '../../util/tooltip'; +import { uiSection } from '../section'; + +export function uiValidationRules(context) { + + var MINSQUARE = 0; + var MAXSQUARE = 20; + var DEFAULTSQUARE = 5; // see also unsquare_way.js + + var section = uiSection('issues-rules', context) + .title(t('issues.rules.title')); + + section.renderDisclosureContent = function(selection) { + var container = selection.selectAll('.issues-rulelist-container') + .data([0]); + + var containerEnter = container.enter() + .append('div') + .attr('class', 'issues-rulelist-container'); + + containerEnter + .append('ul') + .attr('class', 'layer-list issue-rules-list'); + + var ruleLinks = containerEnter + .append('div') + .attr('class', 'issue-rules-links section-footer'); + + ruleLinks + .append('a') + .attr('class', 'issue-rules-link') + .attr('href', '#') + .text(t('issues.enable_all')) + .on('click', function() { + context.validator().disableRules([]); + }); + + ruleLinks + .append('a') + .attr('class', 'issue-rules-link') + .attr('href', '#') + .text(t('issues.disable_all')) + .on('click', function() { + var keys = context.validator().getRuleKeys(); + context.validator().disableRules(keys); + }); + + + // Update + container = container + .merge(containerEnter); + + var ruleKeys = context.validator().getRuleKeys(); + + container.selectAll('.issue-rules-list') + .call(drawListItems, ruleKeys, 'checkbox', 'rule', toggleRule, isRuleEnabled); + }; + + function drawListItems(selection, data, type, name, change, active) { + var items = selection.selectAll('li') + .data(data); + + // Exit + items.exit() + .remove(); + + // Enter + var enter = items.enter() + .append('li'); + + if (name === 'rule') { + enter + .call(tooltip() + .title(function(d) { return t('issues.' + d + '.tip'); }) + .placement('top') + ); + } + + var label = enter + .append('label'); + + label + .append('input') + .attr('type', type) + .attr('name', name) + .on('change', change); + + label + .append('span') + .html(function(d) { + var params = {}; + if (d === 'unsquare_way') { + params.val = ''; + } + return t('issues.' + d + '.title', params); + }); + + // Update + items = items + .merge(enter); + + items + .classed('active', active) + .selectAll('input') + .property('checked', active) + .property('indeterminate', false); + + + // user-configurable square threshold + var degStr = context.storage('validate-square-degrees'); + if (degStr === null) { + degStr = '' + DEFAULTSQUARE; + } + + var span = items.selectAll('.square-degrees'); + var input = span.selectAll('.square-degrees-input') + .data([0]); + + // enter / update + input.enter() + .append('input') + .attr('type', 'number') + .attr('min', '' + MINSQUARE) + .attr('max', '' + MAXSQUARE) + .attr('step', '0.5') + .attr('class', 'square-degrees-input') + .call(utilNoAuto) + .on('click', function () { + d3_event.preventDefault(); + d3_event.stopPropagation(); + this.select(); + }) + .on('keyup', function () { + if (d3_event.keyCode === 13) { // enter + this.blur(); + this.select(); + } + }) + .on('blur', changeSquare) + .merge(input) + .property('value', degStr); + } + + function changeSquare() { + var input = d3_select(this); + var degStr = utilGetSetValue(input).trim(); + var degNum = parseFloat(degStr, 10); + + if (!isFinite(degNum)) { + degNum = DEFAULTSQUARE; + } else if (degNum > MAXSQUARE) { + degNum = MAXSQUARE; + } else if (degNum < MINSQUARE) { + degNum = MINSQUARE; + } + + degNum = Math.round(degNum * 10 ) / 10; // round to 1 decimal + degStr = '' + degNum; + + input + .property('value', degStr); + + context.storage('validate-square-degrees', degStr); + context.validator().reloadUnsquareIssues(); + } + + function isRuleEnabled(d) { + return context.validator().isRuleEnabled(d); + } + + function toggleRule(d) { + context.validator().toggleRule(d); + } + + return section; +} From b921ca3cb4e777cd0f9f703139335a699688d7ce Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 19 Feb 2020 11:23:02 -0800 Subject: [PATCH 019/127] Move map features and map style options to their own pane section objects (re: #7368) Handle area fill and change highlighting logic in the rendererMap object and issue change events --- modules/renderer/map.js | 48 +++++- modules/ui/panes/map_data.js | 201 ++++------------------- modules/ui/sections/map_features.js | 127 ++++++++++++++ modules/ui/sections/map_style_options.js | 101 ++++++++++++ 4 files changed, 303 insertions(+), 174 deletions(-) create mode 100644 modules/ui/sections/map_features.js create mode 100644 modules/ui/sections/map_style_options.js diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 4db517f5e..cdd2b37f0 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -30,7 +30,7 @@ function clamp(num, min, max) { export function rendererMap(context) { - var dispatch = d3_dispatch('move', 'drawn', 'crossEditableZoom'); + var dispatch = d3_dispatch('move', 'drawn', 'crossEditableZoom', 'changeHighlighting', 'changeAreaFill'); var projection = context.projection; var curtainProjection = context.curtainProjection; var drawLayers = svgLayers(projection, context); @@ -182,6 +182,9 @@ export function rendererMap(context) { } }); + // must call after surface init + updateAreaFill(); + context.on('enter.map', function() { if (map.editableDataEnabled(true /* skip zoom check */) && !_isTransformed) { // redraw immediately any objects affected by a change in selectedIDs. @@ -980,6 +983,49 @@ export function rendererMap(context) { }; + map.toggleHighlightEdited = function() { + surface.classed('highlight-edited', !surface.classed('highlight-edited')); + map.pan([0,0]); // trigger a redraw + dispatch.call('changeHighlighting', this); + }; + + + map.areaFillOptions = ['wireframe', 'partial', 'full']; + + map.activeAreaFill = function(val) { + if (!arguments.length) return context.storage('area-fill') || 'partial'; + + context.storage('area-fill', val); + if (val !== 'wireframe') { + context.storage('area-fill-toggle', val); + } + updateAreaFill(); + map.pan([0,0]); // trigger a redraw + dispatch.call('changeAreaFill', this); + return map; + }; + + map.toggleWireframe = function() { + + var activeFill = map.activeAreaFill(); + + if (activeFill === 'wireframe') { + activeFill = context.storage('area-fill-toggle') || 'partial'; + } else { + activeFill = 'wireframe'; + } + + map.activeAreaFill(activeFill); + }; + + function updateAreaFill() { + var activeFill = map.activeAreaFill(); + map.areaFillOptions.forEach(function(opt) { + surface.classed('fill-' + opt, Boolean(opt === activeFill)); + }); + } + + map.layers = drawLayers; diff --git a/modules/ui/panes/map_data.js b/modules/ui/panes/map_data.js index 15dd49eec..0649a5f18 100644 --- a/modules/ui/panes/map_data.js +++ b/modules/ui/panes/map_data.js @@ -14,29 +14,22 @@ import { uiTooltipHtml } from '../tooltipHtml'; import { uiCmd } from '../cmd'; import { uiPane } from '../pane'; +import { uiSectionMapFeatures } from '../sections/map_features'; +import { uiSectionMapStyleOptions } from '../sections/map_style_options'; export function uiPaneMapData(context) { var osmDataToggleKey = uiCmd('⌥' + t('area_fill.wireframe.key')); - var features = context.features().keys(); var layers = context.layers(); - var fills = ['wireframe', 'partial', 'full']; var settingsCustomData = uiSettingsCustomData(context) .on('change', customChanged); - var _fillSelected = context.storage('area-fill') || 'partial'; var _dataLayerContainer = d3_select(null); var _photoOverlayContainer = d3_select(null); - var _fillList = d3_select(null); - var _featureList = d3_select(null); - var _visualDiffList = d3_select(null); var _QAList = d3_select(null); - - function showsFeature(d) { - return context.features().enabled(d); - } - + var _mapStyleOptionsSection = uiSectionMapStyleOptions(context); + var _mapFeaturesSection = uiSectionMapFeatures(context); function autoHiddenFeature(d) { if (d.type === 'kr_error') return context.errors().autoHidden(d); @@ -44,12 +37,6 @@ export function uiPaneMapData(context) { } - function clickFeature(d) { - context.features().toggle(d); - update(); - } - - function showsQA(d) { var QAKeys = [d]; var QALayers = layers.all().filter(function(obj) { return QAKeys.indexOf(obj.id) !== -1; }); @@ -66,32 +53,9 @@ export function uiPaneMapData(context) { } - function showsFill(d) { - return _fillSelected === d; - } - - - function setFill(d) { - fills.forEach(function(opt) { - context.surface().classed('fill-' + opt, Boolean(opt === d)); - }); - - _fillSelected = d; - context.storage('area-fill', d); - if (d !== 'wireframe') { - context.storage('area-fill-toggle', d); - } - update(); - } - - function toggleHighlightEdited() { d3_event.preventDefault(); - var surface = context.surface(); - surface.classed('highlight-edited', !surface.classed('highlight-edited')); - updateVisualDiffList(); - - context.map().pan([0,0]); // trigger a redraw + context.map().toggleHighlightEdited(); } @@ -610,14 +574,11 @@ export function uiPaneMapData(context) { .html(true) .title(function(d) { var tip = t(name + '.' + d + '.tooltip'); - var key = (d === 'wireframe' ? t('area_fill.wireframe.key') : null); - if (d === 'highlight_edits') key = t('map_data.highlight_edits.key'); - - if ((name === 'feature' || name === 'keepRight') && autoHiddenFeature(d)) { + if (name === 'keepRight' && autoHiddenFeature(d)) { var msg = showsLayer('osm') ? t('map_data.autohidden') : t('map_data.osmhidden'); tip += '
' + msg + '
'; } - return uiTooltipHtml(tip, key); + return uiTooltipHtml(tip); }) .placement('top') ); @@ -644,7 +605,7 @@ export function uiPaneMapData(context) { .selectAll('input') .property('checked', active) .property('indeterminate', function(d) { - return ((name === 'feature' || name === 'keepRight') && autoHiddenFeature(d)); + return name === 'keepRight' && autoHiddenFeature(d); }); } @@ -673,73 +634,6 @@ export function uiPaneMapData(context) { updatePhotoOverlays(); } - - function renderStyleOptions(selection) { - var container = selection.selectAll('.layer-fill-list') - .data([0]); - - _fillList = container.enter() - .append('ul') - .attr('class', 'layer-list layer-fill-list') - .merge(container); - - updateFillList(); - - var container2 = selection.selectAll('.layer-visual-diff-list') - .data([0]); - - _visualDiffList = container2.enter() - .append('ul') - .attr('class', 'layer-list layer-visual-diff-list') - .merge(container2); - - updateVisualDiffList(); - } - - - function renderFeatureList(selection) { - var container = selection.selectAll('.layer-feature-list-container') - .data([0]); - - var containerEnter = container.enter() - .append('div') - .attr('class', 'layer-feature-list-container'); - - containerEnter - .append('ul') - .attr('class', 'layer-list layer-feature-list'); - - var footer = containerEnter - .append('div') - .attr('class', 'feature-list-links section-footer'); - - footer - .append('a') - .attr('class', 'feature-list-link') - .attr('href', '#') - .text(t('issues.enable_all')) - .on('click', function() { - context.features().enableAll(); - }); - - footer - .append('a') - .attr('class', 'feature-list-link') - .attr('href', '#') - .text(t('issues.disable_all')) - .on('click', function() { - context.features().disableAll(); - }); - - // Update - container = container - .merge(containerEnter); - - _featureList = container.selectAll('.layer-feature-list'); - - updateFeatureList(); - } - function updatePhotoOverlays() { _photoOverlayContainer .call(drawPhotoItems) @@ -754,40 +648,23 @@ export function uiPaneMapData(context) { .call(drawVectorItems); // Beta - Detroit mapping challenge } - function updateFillList() { - _fillList - .call(drawListItems, fills, 'radio', 'area_fill', setFill, showsFill); - } - - function updateVisualDiffList() { - _visualDiffList - .call(drawListItems, ['highlight_edits'], 'checkbox', 'visual_diff', toggleHighlightEdited, function() { - return context.surface().classed('highlight-edited'); - }); - } - - function updateFeatureList() { - _featureList - .call(drawListItems, features, 'checkbox', 'feature', clickFeature, showsFeature); - } - function update() { if (!mapDataPane.selection().select('.disclosure-wrap-data_layers').classed('hide')) { updateDataLayers(); + + _QAList + .call(drawListItems, ['keep-right'], 'checkbox', 'QA', function(d) { toggleLayer(d); }, showsQA); } if (!mapDataPane.selection().select('.disclosure-wrap-photo_overlays').classed('hide')) { updatePhotoOverlays(); } - if (!mapDataPane.selection().select('.disclosure-wrap-fill_area').classed('hide')) { - updateFillList(); - } - if (!mapDataPane.selection().select('.disclosure-wrap-map_features').classed('hide')) { - updateFeatureList(); - } - _QAList - .call(drawListItems, ['keep-right'], 'checkbox', 'QA', function(d) { toggleLayer(d); }, showsQA); + mapDataPane.selection().select('.map-data-area-fills') + .call(_mapStyleOptionsSection.render); + + mapDataPane.selection().select('.map-data-feature-filters') + .call(_mapFeaturesSection.render); } @@ -796,15 +673,7 @@ export function uiPaneMapData(context) { d3_event.preventDefault(); d3_event.stopPropagation(); } - - if (_fillSelected === 'wireframe') { - _fillSelected = context.storage('area-fill-toggle') || 'partial'; - } else { - _fillSelected = 'wireframe'; - } - - setFill(_fillSelected); - context.map().pan([0,0]); // trigger a redraw + context.map().toggleWireframe(); } var mapDataPane = uiPane('map-data', context) @@ -836,38 +705,24 @@ export function uiPaneMapData(context) { // area fills content .append('div') - .attr('class', 'map-data-area-fills') - .call(uiDisclosure(context, 'fill_area', false) - .title(t('map_data.style_options')) - .content(renderStyleOptions) - ); + .attr('class', 'map-data-area-fills'); // feature filters content .append('div') - .attr('class', 'map-data-feature-filters') - .call(uiDisclosure(context, 'map_features', false) - .title(t('map_data.map_features')) - .content(renderFeatureList) - ); - - - // add listeners - context.features() - .on('change.map_data-update', update); + .attr('class', 'map-data-feature-filters'); update(); - setFill(_fillSelected); - - context.keybinding() - .on(t('area_fill.wireframe.key'), toggleWireframe) - .on(osmDataToggleKey, function() { - d3_event.preventDefault(); - d3_event.stopPropagation(); - toggleLayer('osm'); - }) - .on(t('map_data.highlight_edits.key'), toggleHighlightEdited); }; + context.keybinding() + .on(t('area_fill.wireframe.key'), toggleWireframe) + .on(osmDataToggleKey, function() { + d3_event.preventDefault(); + d3_event.stopPropagation(); + toggleLayer('osm'); + }) + .on(t('map_data.highlight_edits.key'), toggleHighlightEdited); + return mapDataPane; } diff --git a/modules/ui/sections/map_features.js b/modules/ui/sections/map_features.js new file mode 100644 index 000000000..54e2c27f0 --- /dev/null +++ b/modules/ui/sections/map_features.js @@ -0,0 +1,127 @@ +import { t } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; +import { uiSection } from '../section'; +import { uiTooltipHtml } from '../tooltipHtml'; + +export function uiSectionMapFeatures(context) { + + var _features = context.features().keys(); + + var section = uiSection('map-features', context) + .title(t('map_data.map_features')) + .expandedByDefault(false); + + section.renderDisclosureContent = function(selection) { + + var container = selection.selectAll('.layer-feature-list-container') + .data([0]); + + var containerEnter = container.enter() + .append('div') + .attr('class', 'layer-feature-list-container'); + + containerEnter + .append('ul') + .attr('class', 'layer-list layer-feature-list'); + + var footer = containerEnter + .append('div') + .attr('class', 'feature-list-links section-footer'); + + footer + .append('a') + .attr('class', 'feature-list-link') + .attr('href', '#') + .text(t('issues.enable_all')) + .on('click', function() { + context.features().enableAll(); + }); + + footer + .append('a') + .attr('class', 'feature-list-link') + .attr('href', '#') + .text(t('issues.disable_all')) + .on('click', function() { + context.features().disableAll(); + }); + + // Update + container = container + .merge(containerEnter); + + container.selectAll('.layer-feature-list') + .call(drawListItems, _features, 'checkbox', 'feature', clickFeature, showsFeature); + }; + + function drawListItems(selection, data, type, name, change, active) { + var items = selection.selectAll('li') + .data(data); + + // Exit + items.exit() + .remove(); + + // Enter + var enter = items.enter() + .append('li') + .call(tooltip() + .html(true) + .title(function(d) { + var tip = t(name + '.' + d + '.tooltip'); + if (autoHiddenFeature(d)) { + var msg = showsLayer('osm') ? t('map_data.autohidden') : t('map_data.osmhidden'); + tip += '
' + msg + '
'; + } + return uiTooltipHtml(tip); + }) + .placement('top') + ); + + var label = enter + .append('label'); + + label + .append('input') + .attr('type', type) + .attr('name', name) + .on('change', change); + + label + .append('span') + .text(function(d) { return t(name + '.' + d + '.description'); }); + + // Update + items = items + .merge(enter); + + items + .classed('active', active) + .selectAll('input') + .property('checked', active) + .property('indeterminate', autoHiddenFeature); + } + + function autoHiddenFeature(d) { + return context.features().autoHidden(d); + } + + function showsFeature(d) { + return context.features().enabled(d); + } + + function clickFeature(d) { + context.features().toggle(d); + } + + function showsLayer(id) { + var layer = context.layers().layer(id); + return layer && layer.enabled(); + } + + // add listeners + context.features() + .on('change.map_features', section.rerenderContent); + + return section; +} diff --git a/modules/ui/sections/map_style_options.js b/modules/ui/sections/map_style_options.js new file mode 100644 index 000000000..5dadb75f1 --- /dev/null +++ b/modules/ui/sections/map_style_options.js @@ -0,0 +1,101 @@ +import { + event as d3_event +} from 'd3-selection'; + +import { t } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; +import { uiSection } from '../section'; +import { uiTooltipHtml } from '../tooltipHtml'; + +export function uiSectionMapStyleOptions(context) { + + var section = uiSection('fill-area', context) + .title(t('map_data.style_options')) + .expandedByDefault(false); + + section.renderDisclosureContent = function(selection) { + var container = selection.selectAll('.layer-fill-list') + .data([0]); + + container.enter() + .append('ul') + .attr('class', 'layer-list layer-fill-list') + .merge(container) + .call(drawListItems, context.map().areaFillOptions, 'radio', 'area_fill', setFill, isActiveFill); + + var container2 = selection.selectAll('.layer-visual-diff-list') + .data([0]); + + container2.enter() + .append('ul') + .attr('class', 'layer-list layer-visual-diff-list') + .merge(container2) + .call(drawListItems, ['highlight_edits'], 'checkbox', 'visual_diff', toggleHighlightEdited, function() { + return context.surface().classed('highlight-edited'); + }); + }; + + function drawListItems(selection, data, type, name, change, active) { + var items = selection.selectAll('li') + .data(data); + + // Exit + items.exit() + .remove(); + + // Enter + var enter = items.enter() + .append('li') + .call(tooltip() + .html(true) + .title(function(d) { + var tip = t(name + '.' + d + '.tooltip'); + var key = (d === 'wireframe' ? t('area_fill.wireframe.key') : null); + if (d === 'highlight_edits') key = t('map_data.highlight_edits.key'); + return uiTooltipHtml(tip, key); + }) + .placement('top') + ); + + var label = enter + .append('label'); + + label + .append('input') + .attr('type', type) + .attr('name', name) + .on('change', change); + + label + .append('span') + .text(function(d) { return t(name + '.' + d + '.description'); }); + + // Update + items = items + .merge(enter); + + items + .classed('active', active) + .selectAll('input') + .property('checked', active) + .property('indeterminate', false); + } + + function isActiveFill(d) { + return context.map().activeAreaFill() === d; + } + + function toggleHighlightEdited() { + d3_event.preventDefault(); + context.map().toggleHighlightEdited(); + } + + function setFill(d) { + context.map().activeAreaFill(d); + } + + context.map() + .on('changeHighlighting.ui_style, changeAreaFill.ui_style', section.rerenderContent); + + return section; +} From c6129a0493696fb10a14634f6b8a26b36a74c5cb Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 19 Feb 2020 11:28:11 -0800 Subject: [PATCH 020/127] Remove unused autohidden errors code path --- modules/ui/panes/map_data.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/modules/ui/panes/map_data.js b/modules/ui/panes/map_data.js index 0649a5f18..773ad9777 100644 --- a/modules/ui/panes/map_data.js +++ b/modules/ui/panes/map_data.js @@ -31,11 +31,6 @@ export function uiPaneMapData(context) { var _mapStyleOptionsSection = uiSectionMapStyleOptions(context); var _mapFeaturesSection = uiSectionMapFeatures(context); - function autoHiddenFeature(d) { - if (d.type === 'kr_error') return context.errors().autoHidden(d); - return context.features().autoHidden(d); - } - function showsQA(d) { var QAKeys = [d]; @@ -574,10 +569,6 @@ export function uiPaneMapData(context) { .html(true) .title(function(d) { var tip = t(name + '.' + d + '.tooltip'); - if (name === 'keepRight' && autoHiddenFeature(d)) { - var msg = showsLayer('osm') ? t('map_data.autohidden') : t('map_data.osmhidden'); - tip += '
' + msg + '
'; - } return uiTooltipHtml(tip); }) .placement('top') @@ -604,9 +595,7 @@ export function uiPaneMapData(context) { .classed('active', active) .selectAll('input') .property('checked', active) - .property('indeterminate', function(d) { - return name === 'keepRight' && autoHiddenFeature(d); - }); + .property('indeterminate', false); } From ff4ac253da289ac0e56d65899d4edd76193e72a6 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 19 Feb 2020 12:08:13 -0800 Subject: [PATCH 021/127] Make photo overlays its own section object (re: #7368) --- modules/ui/panes/map_data.js | 209 ++------------------------ modules/ui/sections/photo_overlays.js | 201 +++++++++++++++++++++++++ 2 files changed, 216 insertions(+), 194 deletions(-) create mode 100644 modules/ui/sections/photo_overlays.js diff --git a/modules/ui/panes/map_data.js b/modules/ui/panes/map_data.js index 773ad9777..5797edbd9 100644 --- a/modules/ui/panes/map_data.js +++ b/modules/ui/panes/map_data.js @@ -16,6 +16,7 @@ import { uiPane } from '../pane'; import { uiSectionMapFeatures } from '../sections/map_features'; import { uiSectionMapStyleOptions } from '../sections/map_style_options'; +import { uiSectionPhotoOverlays } from '../sections/photo_overlays'; export function uiPaneMapData(context) { var osmDataToggleKey = uiCmd('⌥' + t('area_fill.wireframe.key')); @@ -25,9 +26,9 @@ export function uiPaneMapData(context) { .on('change', customChanged); var _dataLayerContainer = d3_select(null); - var _photoOverlayContainer = d3_select(null); var _QAList = d3_select(null); + var _photoOverlaysSection = uiSectionPhotoOverlays(context); var _mapStyleOptionsSection = uiSectionMapStyleOptions(context); var _mapFeaturesSection = uiSectionMapFeatures(context); @@ -48,12 +49,6 @@ export function uiPaneMapData(context) { } - function toggleHighlightEdited() { - d3_event.preventDefault(); - context.map().toggleHighlightEdited(); - } - - function showsLayer(which) { var layer = layers.layer(which); if (layer) { @@ -86,156 +81,6 @@ export function uiPaneMapData(context) { } - function drawPhotoItems(selection) { - var photoKeys = context.photos().overlayLayerIDs(); - var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; }); - var data = photoLayers.filter(function(obj) { return obj.layer.supported(); }); - - function layerSupported(d) { - return d.layer && d.layer.supported(); - } - function layerEnabled(d) { - return layerSupported(d) && d.layer.enabled(); - } - - var ul = selection - .selectAll('.layer-list-photos') - .data([0]); - - ul = ul.enter() - .append('ul') - .attr('class', 'layer-list layer-list-photos') - .merge(ul); - - var li = ul.selectAll('.list-item-photos') - .data(data); - - li.exit() - .remove(); - - var liEnter = li.enter() - .append('li') - .attr('class', function(d) { - var classes = 'list-item-photos list-item-' + d.id; - if (d.id === 'mapillary-signs' || d.id === 'mapillary-map-features') { - classes += ' indented'; - } - return classes; - }); - - var labelEnter = liEnter - .append('label') - .each(function(d) { - var titleID; - if (d.id === 'mapillary-signs') titleID = 'mapillary.signs.tooltip'; - else if (d.id === 'mapillary') titleID = 'mapillary_images.tooltip'; - else if (d.id === 'openstreetcam') titleID = 'openstreetcam_images.tooltip'; - else titleID = d.id.replace(/-/g, '_') + '.tooltip'; - d3_select(this) - .call(tooltip() - .title(t(titleID)) - .placement('top') - ); - }); - - labelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function(d) { toggleLayer(d.id); }); - - labelEnter - .append('span') - .text(function(d) { - var id = d.id; - if (id === 'mapillary-signs') id = 'photo_overlays.traffic_signs'; - return t(id.replace(/-/g, '_') + '.title'); - }); - - labelEnter - .filter(function(d) { return d.id === 'mapillary-map-features'; }) - .append('a') - .attr('class', 'request-data-link') - .attr('target', '_blank') - .attr('tabindex', -1) - .call(svgIcon('#iD-icon-out-link', 'inline')) - .attr('href', 'https://mapillary.github.io/mapillary_solutions/data-request') - .append('span') - .text(t('mapillary_map_features.request_data')); - - - // Update - li - .merge(liEnter) - .classed('active', layerEnabled) - .selectAll('input') - .property('checked', layerEnabled); - } - - function drawPhotoTypeItems(selection) { - var data = context.photos().allPhotoTypes(); - - function typeEnabled(d) { - return context.photos().showsPhotoType(d); - } - - var ul = selection - .selectAll('.layer-list-photo-types') - .data(context.photos().shouldFilterByPhotoType() ? [0] : []); - - ul.exit() - .remove(); - - ul = ul.enter() - .append('ul') - .attr('class', 'layer-list layer-list-photo-types') - .merge(ul); - - var li = ul.selectAll('.list-item-photo-types') - .data(data); - - li.exit() - .remove(); - - var liEnter = li.enter() - .append('li') - .attr('class', function(d) { - return 'list-item-photo-types list-item-' + d; - }); - - var labelEnter = liEnter - .append('label') - .each(function(d) { - d3_select(this) - .call(tooltip() - .title(t('photo_overlays.photo_type.' + d + '.tooltip')) - .placement('top') - ); - }); - - labelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function(d) { - context.photos().togglePhotoType(d); - update(); - }); - - labelEnter - .append('span') - .text(function(d) { - return t('photo_overlays.photo_type.' + d + '.title'); - }); - - - // Update - li - .merge(liEnter) - .classed('active', typeEnabled) - .selectAll('input') - .property('checked', typeEnabled); - } - - function drawOsmItems(selection) { var osmKeys = ['osm', 'notes']; var osmLayers = layers.all().filter(function(obj) { return osmKeys.indexOf(obj.id) !== -1; }); @@ -611,24 +456,6 @@ export function uiPaneMapData(context) { updateDataLayers(); } - function renderPhotoOverlays(selection) { - var container = selection.selectAll('.photo-overlay-container') - .data([0]); - - _photoOverlayContainer = container.enter() - .append('div') - .attr('class', 'photo-overlay-container') - .merge(container); - - updatePhotoOverlays(); - } - - function updatePhotoOverlays() { - _photoOverlayContainer - .call(drawPhotoItems) - .call(drawPhotoTypeItems); - } - function updateDataLayers() { _dataLayerContainer .call(drawOsmItems) @@ -645,9 +472,9 @@ export function uiPaneMapData(context) { _QAList .call(drawListItems, ['keep-right'], 'checkbox', 'QA', function(d) { toggleLayer(d); }, showsQA); } - if (!mapDataPane.selection().select('.disclosure-wrap-photo_overlays').classed('hide')) { - updatePhotoOverlays(); - } + + mapDataPane.selection().select('.map-data-photo-overlays') + .call(_photoOverlaysSection.render); mapDataPane.selection().select('.map-data-area-fills') .call(_mapStyleOptionsSection.render); @@ -656,15 +483,6 @@ export function uiPaneMapData(context) { .call(_mapFeaturesSection.render); } - - function toggleWireframe() { - if (d3_event) { - d3_event.preventDefault(); - d3_event.stopPropagation(); - } - context.map().toggleWireframe(); - } - var mapDataPane = uiPane('map-data', context) .key(t('map_data.key')) .title(t('map_data.title')) @@ -685,11 +503,7 @@ export function uiPaneMapData(context) { // photo overlays content .append('div') - .attr('class', 'map-data-photo-overlays') - .call(uiDisclosure(context, 'photo_overlays', false) - .title(t('photo_overlays.title')) - .content(renderPhotoOverlays) - ); + .attr('class', 'map-data-photo-overlays'); // area fills content @@ -705,13 +519,20 @@ export function uiPaneMapData(context) { }; context.keybinding() - .on(t('area_fill.wireframe.key'), toggleWireframe) + .on(t('area_fill.wireframe.key'), function toggleWireframe() { + d3_event.preventDefault(); + d3_event.stopPropagation(); + context.map().toggleWireframe(); + }) .on(osmDataToggleKey, function() { d3_event.preventDefault(); d3_event.stopPropagation(); toggleLayer('osm'); }) - .on(t('map_data.highlight_edits.key'), toggleHighlightEdited); + .on(t('map_data.highlight_edits.key'), function toggleHighlightEdited() { + d3_event.preventDefault(); + context.map().toggleHighlightEdited(); + }); return mapDataPane; } diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js new file mode 100644 index 000000000..e0545ade1 --- /dev/null +++ b/modules/ui/sections/photo_overlays.js @@ -0,0 +1,201 @@ +import { + select as d3_select +} from 'd3-selection'; + +import { t } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; +import { svgIcon } from '../../svg/icon'; +import { uiSection } from '../section'; + +export function uiSectionPhotoOverlays(context) { + + var layers = context.layers(); + + var section = uiSection('photo-overlays', context) + .title(t('photo_overlays.title')) + .expandedByDefault(false); + + section.renderDisclosureContent = function(selection) { + var container = selection.selectAll('.photo-overlay-container') + .data([0]); + + container.enter() + .append('div') + .attr('class', 'photo-overlay-container') + .merge(container) + .call(drawPhotoItems) + .call(drawPhotoTypeItems); + }; + + function drawPhotoItems(selection) { + var photoKeys = context.photos().overlayLayerIDs(); + var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; }); + var data = photoLayers.filter(function(obj) { return obj.layer.supported(); }); + + function layerSupported(d) { + return d.layer && d.layer.supported(); + } + function layerEnabled(d) { + return layerSupported(d) && d.layer.enabled(); + } + + var ul = selection + .selectAll('.layer-list-photos') + .data([0]); + + ul = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-photos') + .merge(ul); + + var li = ul.selectAll('.list-item-photos') + .data(data); + + li.exit() + .remove(); + + var liEnter = li.enter() + .append('li') + .attr('class', function(d) { + var classes = 'list-item-photos list-item-' + d.id; + if (d.id === 'mapillary-signs' || d.id === 'mapillary-map-features') { + classes += ' indented'; + } + return classes; + }); + + var labelEnter = liEnter + .append('label') + .each(function(d) { + var titleID; + if (d.id === 'mapillary-signs') titleID = 'mapillary.signs.tooltip'; + else if (d.id === 'mapillary') titleID = 'mapillary_images.tooltip'; + else if (d.id === 'openstreetcam') titleID = 'openstreetcam_images.tooltip'; + else titleID = d.id.replace(/-/g, '_') + '.tooltip'; + d3_select(this) + .call(tooltip() + .title(t(titleID)) + .placement('top') + ); + }); + + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function(d) { toggleLayer(d.id); }); + + labelEnter + .append('span') + .text(function(d) { + var id = d.id; + if (id === 'mapillary-signs') id = 'photo_overlays.traffic_signs'; + return t(id.replace(/-/g, '_') + '.title'); + }); + + labelEnter + .filter(function(d) { return d.id === 'mapillary-map-features'; }) + .append('a') + .attr('class', 'request-data-link') + .attr('target', '_blank') + .attr('tabindex', -1) + .call(svgIcon('#iD-icon-out-link', 'inline')) + .attr('href', 'https://mapillary.github.io/mapillary_solutions/data-request') + .append('span') + .text(t('mapillary_map_features.request_data')); + + + // Update + li + .merge(liEnter) + .classed('active', layerEnabled) + .selectAll('input') + .property('checked', layerEnabled); + } + + function drawPhotoTypeItems(selection) { + var data = context.photos().allPhotoTypes(); + + function typeEnabled(d) { + return context.photos().showsPhotoType(d); + } + + var ul = selection + .selectAll('.layer-list-photo-types') + .data(context.photos().shouldFilterByPhotoType() ? [0] : []); + + ul.exit() + .remove(); + + ul = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-photo-types') + .merge(ul); + + var li = ul.selectAll('.list-item-photo-types') + .data(data); + + li.exit() + .remove(); + + var liEnter = li.enter() + .append('li') + .attr('class', function(d) { + return 'list-item-photo-types list-item-' + d; + }); + + var labelEnter = liEnter + .append('label') + .each(function(d) { + d3_select(this) + .call(tooltip() + .title(t('photo_overlays.photo_type.' + d + '.tooltip')) + .placement('top') + ); + }); + + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function(d) { + context.photos().togglePhotoType(d); + }); + + labelEnter + .append('span') + .text(function(d) { + return t('photo_overlays.photo_type.' + d + '.title'); + }); + + + // Update + li + .merge(liEnter) + .classed('active', typeEnabled) + .selectAll('input') + .property('checked', typeEnabled); + } + + function toggleLayer(which) { + setLayer(which, !showsLayer(which)); + } + + function showsLayer(which) { + var layer = layers.layer(which); + if (layer) { + return layer.enabled(); + } + return false; + } + + function setLayer(which, enabled) { + var layer = layers.layer(which); + if (layer) { + layer.enabled(enabled); + } + } + + context.layers().on('change.uiSectionPhotoOverlays', section.rerenderContent); + context.photos().on('change.uiSectionPhotoOverlays', section.rerenderContent); + + return section; +} From 68c2b9f1a83926fb28b83daeef0933252b1436ac Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 19 Feb 2020 14:31:32 -0800 Subject: [PATCH 022/127] Move the data layers section to its own object (re: #7368) --- modules/ui/panes/map_data.js | 537 ++--------------------------- modules/ui/sections/data_layers.js | 391 +++++++++++++++++++++ 2 files changed, 419 insertions(+), 509 deletions(-) create mode 100644 modules/ui/sections/data_layers.js diff --git a/modules/ui/panes/map_data.js b/modules/ui/panes/map_data.js index 5797edbd9..11d2d7051 100644 --- a/modules/ui/panes/map_data.js +++ b/modules/ui/panes/map_data.js @@ -1,522 +1,18 @@ import { - event as d3_event, - select as d3_select + event as d3_event } from 'd3-selection'; -import { svgIcon } from '../../svg/icon'; -import { t, textDirection } from '../../util/locale'; -import { tooltip } from '../../util/tooltip'; -import { geoExtent } from '../../geo'; +import { t } from '../../util/locale'; import { modeBrowse } from '../../modes/browse'; -import { uiDisclosure } from '../disclosure'; -import { uiSettingsCustomData } from '../settings/custom_data'; -import { uiTooltipHtml } from '../tooltipHtml'; import { uiCmd } from '../cmd'; import { uiPane } from '../pane'; +import { uiSectionDataLayers } from '../sections/data_layers'; import { uiSectionMapFeatures } from '../sections/map_features'; import { uiSectionMapStyleOptions } from '../sections/map_style_options'; import { uiSectionPhotoOverlays } from '../sections/photo_overlays'; export function uiPaneMapData(context) { - var osmDataToggleKey = uiCmd('⌥' + t('area_fill.wireframe.key')); - var layers = context.layers(); - - var settingsCustomData = uiSettingsCustomData(context) - .on('change', customChanged); - - var _dataLayerContainer = d3_select(null); - var _QAList = d3_select(null); - - var _photoOverlaysSection = uiSectionPhotoOverlays(context); - var _mapStyleOptionsSection = uiSectionMapStyleOptions(context); - var _mapFeaturesSection = uiSectionMapFeatures(context); - - - function showsQA(d) { - var QAKeys = [d]; - var QALayers = layers.all().filter(function(obj) { return QAKeys.indexOf(obj.id) !== -1; }); - var data = QALayers.filter(function(obj) { return obj.layer.supported(); }); - - function layerSupported(d) { - return d.layer && d.layer.supported(); - } - function layerEnabled(d) { - return layerSupported(d) && d.layer.enabled(); - } - - return layerEnabled(data[0]); - } - - - function showsLayer(which) { - var layer = layers.layer(which); - if (layer) { - return layer.enabled(); - } - return false; - } - - - function setLayer(which, enabled) { - // Don't allow layer changes while drawing - #6584 - var mode = context.mode(); - if (mode && /^draw/.test(mode.id)) return; - - var layer = layers.layer(which); - if (layer) { - layer.enabled(enabled); - - if (!enabled && (which === 'osm' || which === 'notes')) { - context.enter(modeBrowse(context)); - } - - update(); - } - } - - - function toggleLayer(which) { - setLayer(which, !showsLayer(which)); - } - - - function drawOsmItems(selection) { - var osmKeys = ['osm', 'notes']; - var osmLayers = layers.all().filter(function(obj) { return osmKeys.indexOf(obj.id) !== -1; }); - - var ul = selection - .selectAll('.layer-list-osm') - .data([0]); - - ul = ul.enter() - .append('ul') - .attr('class', 'layer-list layer-list-osm') - .merge(ul); - - var li = ul.selectAll('.list-item') - .data(osmLayers); - - li.exit() - .remove(); - - var liEnter = li.enter() - .append('li') - .attr('class', function(d) { return 'list-item list-item-' + d.id; }); - - var labelEnter = liEnter - .append('label') - .each(function(d) { - if (d.id === 'osm') { - d3_select(this) - .call(tooltip() - .html(true) - .title(uiTooltipHtml(t('map_data.layers.' + d.id + '.tooltip'), osmDataToggleKey)) - .placement('bottom') - ); - } else { - d3_select(this) - .call(tooltip() - .title(t('map_data.layers.' + d.id + '.tooltip')) - .placement('bottom') - ); - } - }); - - labelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function(d) { toggleLayer(d.id); }); - - labelEnter - .append('span') - .text(function(d) { return t('map_data.layers.' + d.id + '.title'); }); - - - // Update - li - .merge(liEnter) - .classed('active', function (d) { return d.layer.enabled(); }) - .selectAll('input') - .property('checked', function (d) { return d.layer.enabled(); }); - } - - - function drawQAItems(selection) { - var qaKeys = ['keepRight', 'improveOSM', 'osmose']; - var qaLayers = layers.all().filter(function(obj) { return qaKeys.indexOf(obj.id) !== -1; }); - - var ul = selection - .selectAll('.layer-list-qa') - .data([0]); - - ul = ul.enter() - .append('ul') - .attr('class', 'layer-list layer-list-qa') - .merge(ul); - - var li = ul.selectAll('.list-item') - .data(qaLayers); - - li.exit() - .remove(); - - var liEnter = li.enter() - .append('li') - .attr('class', function(d) { return 'list-item list-item-' + d.id; }); - - var labelEnter = liEnter - .append('label') - .each(function(d) { - d3_select(this) - .call(tooltip() - .title(t('map_data.layers.' + d.id + '.tooltip')) - .placement('bottom') - ); - }); - - labelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function(d) { toggleLayer(d.id); }); - - labelEnter - .append('span') - .text(function(d) { return t('map_data.layers.' + d.id + '.title'); }); - - - // Update - li - .merge(liEnter) - .classed('active', function (d) { return d.layer.enabled(); }) - .selectAll('input') - .property('checked', function (d) { return d.layer.enabled(); }); - } - - - // Beta feature - sample vector layers to support Detroit Mapping Challenge - // https://github.com/osmus/detroit-mapping-challenge - function drawVectorItems(selection) { - var dataLayer = layers.layer('data'); - var vtData = [ - { - name: 'Detroit Neighborhoods/Parks', - src: 'neighborhoods-parks', - tooltip: 'Neighborhood boundaries and parks as compiled by City of Detroit in concert with community groups.', - template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmur6x34562qp9iv1u3ksf-54hev,jonahadkins.cjksmqxdx33jj2wp90xd9x2md-4e5y2/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA' - }, { - name: 'Detroit Composite POIs', - src: 'composite-poi', - tooltip: 'Fire Inspections, Business Licenses, and other public location data collated from the City of Detroit.', - template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmm6a02sli31myxhsr7zf3-2sw8h/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA' - }, { - name: 'Detroit All-The-Places POIs', - src: 'alltheplaces-poi', - tooltip: 'Public domain business location data created by web scrapers.', - template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmswgk340g2vo06p1w9w0j-8fjjc/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA' - } - ]; - - // Only show this if the map is around Detroit.. - var detroit = geoExtent([-83.5, 42.1], [-82.8, 42.5]); - var showVectorItems = (context.map().zoom() > 9 && detroit.contains(context.map().center())); - - var container = selection.selectAll('.vectortile-container') - .data(showVectorItems ? [0] : []); - - container.exit() - .remove(); - - var containerEnter = container.enter() - .append('div') - .attr('class', 'vectortile-container'); - - containerEnter - .append('h4') - .attr('class', 'vectortile-header') - .text('Detroit Vector Tiles (Beta)'); - - containerEnter - .append('ul') - .attr('class', 'layer-list layer-list-vectortile'); - - containerEnter - .append('div') - .attr('class', 'vectortile-footer') - .append('a') - .attr('target', '_blank') - .attr('tabindex', -1) - .call(svgIcon('#iD-icon-out-link', 'inline')) - .attr('href', 'https://github.com/osmus/detroit-mapping-challenge') - .append('span') - .text('About these layers'); - - container = container - .merge(containerEnter); - - - var ul = container.selectAll('.layer-list-vectortile'); - - var li = ul.selectAll('.list-item') - .data(vtData); - - li.exit() - .remove(); - - var liEnter = li.enter() - .append('li') - .attr('class', function(d) { return 'list-item list-item-' + d.src; }); - - var labelEnter = liEnter - .append('label') - .each(function(d) { - d3_select(this).call( - tooltip().title(d.tooltip).placement('top') - ); - }); - - labelEnter - .append('input') - .attr('type', 'radio') - .attr('name', 'vectortile') - .on('change', selectVTLayer); - - labelEnter - .append('span') - .text(function(d) { return d.name; }); - - // Update - li - .merge(liEnter) - .classed('active', isVTLayerSelected) - .selectAll('input') - .property('checked', isVTLayerSelected); - - - function isVTLayerSelected(d) { - return dataLayer && dataLayer.template() === d.template; - } - - function selectVTLayer(d) { - context.storage('settings-custom-data-url', d.template); - if (dataLayer) { - dataLayer.template(d.template, d.src); - dataLayer.enabled(true); - } - } - } - - - function drawCustomDataItems(selection) { - var dataLayer = layers.layer('data'); - var hasData = dataLayer && dataLayer.hasData(); - var showsData = hasData && dataLayer.enabled(); - - var ul = selection - .selectAll('.layer-list-data') - .data(dataLayer ? [0] : []); - - // Exit - ul.exit() - .remove(); - - // Enter - var ulEnter = ul.enter() - .append('ul') - .attr('class', 'layer-list layer-list-data'); - - var liEnter = ulEnter - .append('li') - .attr('class', 'list-item-data'); - - var labelEnter = liEnter - .append('label') - .call(tooltip() - .title(t('map_data.layers.custom.tooltip')) - .placement('top') - ); - - labelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function() { toggleLayer('data'); }); - - labelEnter - .append('span') - .text(t('map_data.layers.custom.title')); - - liEnter - .append('button') - .call(tooltip() - .title(t('settings.custom_data.tooltip')) - .placement((textDirection === 'rtl') ? 'right' : 'left') - ) - .on('click', editCustom) - .call(svgIcon('#iD-icon-more')); - - liEnter - .append('button') - .call(tooltip() - .title(t('map_data.layers.custom.zoom')) - .placement((textDirection === 'rtl') ? 'right' : 'left') - ) - .on('click', function() { - d3_event.preventDefault(); - d3_event.stopPropagation(); - dataLayer.fitZoom(); - }) - .call(svgIcon('#iD-icon-search')); - - // Update - ul = ul - .merge(ulEnter); - - ul.selectAll('.list-item-data') - .classed('active', showsData) - .selectAll('label') - .classed('deemphasize', !hasData) - .selectAll('input') - .property('disabled', !hasData) - .property('checked', showsData); - } - - - function editCustom() { - d3_event.preventDefault(); - context.container() - .call(settingsCustomData); - } - - - function customChanged(d) { - var dataLayer = layers.layer('data'); - - if (d && d.url) { - dataLayer.url(d.url); - } else if (d && d.fileList) { - dataLayer.fileList(d.fileList); - } - } - - - function drawListItems(selection, data, type, name, change, active) { - var items = selection.selectAll('li') - .data(data); - - // Exit - items.exit() - .remove(); - - // Enter - var enter = items.enter() - .append('li') - .call(tooltip() - .html(true) - .title(function(d) { - var tip = t(name + '.' + d + '.tooltip'); - return uiTooltipHtml(tip); - }) - .placement('top') - ); - - var label = enter - .append('label'); - - label - .append('input') - .attr('type', type) - .attr('name', name) - .on('change', change); - - label - .append('span') - .text(function(d) { return t(name + '.' + d + '.description'); }); - - // Update - items = items - .merge(enter); - - items - .classed('active', active) - .selectAll('input') - .property('checked', active) - .property('indeterminate', false); - } - - - function renderDataLayers(selection) { - var container = selection.selectAll('.data-layer-container') - .data([0]); - - _dataLayerContainer = container.enter() - .append('div') - .attr('class', 'data-layer-container') - .merge(container); - - updateDataLayers(); - } - - function updateDataLayers() { - _dataLayerContainer - .call(drawOsmItems) - .call(drawQAItems) - .call(drawCustomDataItems) - .call(drawVectorItems); // Beta - Detroit mapping challenge - } - - function update() { - - if (!mapDataPane.selection().select('.disclosure-wrap-data_layers').classed('hide')) { - updateDataLayers(); - - _QAList - .call(drawListItems, ['keep-right'], 'checkbox', 'QA', function(d) { toggleLayer(d); }, showsQA); - } - - mapDataPane.selection().select('.map-data-photo-overlays') - .call(_photoOverlaysSection.render); - - mapDataPane.selection().select('.map-data-area-fills') - .call(_mapStyleOptionsSection.render); - - mapDataPane.selection().select('.map-data-feature-filters') - .call(_mapFeaturesSection.render); - } - - var mapDataPane = uiPane('map-data', context) - .key(t('map_data.key')) - .title(t('map_data.title')) - .description(t('map_data.description')) - .iconName('iD-icon-data'); - - mapDataPane.renderContent = function(content) { - - // data layers - content - .append('div') - .attr('class', 'map-data-data-layers') - .call(uiDisclosure(context, 'data_layers', true) - .title(t('map_data.data_layers')) - .content(renderDataLayers) - ); - - // photo overlays - content - .append('div') - .attr('class', 'map-data-photo-overlays'); - - // area fills - content - .append('div') - .attr('class', 'map-data-area-fills'); - - // feature filters - content - .append('div') - .attr('class', 'map-data-feature-filters'); - - update(); - }; context.keybinding() .on(t('area_fill.wireframe.key'), function toggleWireframe() { @@ -524,15 +20,38 @@ export function uiPaneMapData(context) { d3_event.stopPropagation(); context.map().toggleWireframe(); }) - .on(osmDataToggleKey, function() { + .on(uiCmd('⌥' + t('area_fill.wireframe.key')), function toggleOsmData() { d3_event.preventDefault(); d3_event.stopPropagation(); - toggleLayer('osm'); + + // Don't allow layer changes while drawing - #6584 + var mode = context.mode(); + if (mode && /^draw/.test(mode.id)) return; + + var layer = context.layers().layer('osm'); + if (layer) { + layer.enabled(!layer.enabled()); + if (!layer.enabled()) { + context.enter(modeBrowse(context)); + } + } }) .on(t('map_data.highlight_edits.key'), function toggleHighlightEdited() { d3_event.preventDefault(); context.map().toggleHighlightEdited(); }); + var mapDataPane = uiPane('map-data', context) + .key(t('map_data.key')) + .title(t('map_data.title')) + .description(t('map_data.description')) + .iconName('iD-icon-data') + .sections([ + uiSectionDataLayers(context), + uiSectionPhotoOverlays(context), + uiSectionMapStyleOptions(context), + uiSectionMapFeatures(context) + ]); + return mapDataPane; } diff --git a/modules/ui/sections/data_layers.js b/modules/ui/sections/data_layers.js new file mode 100644 index 000000000..db603c4e1 --- /dev/null +++ b/modules/ui/sections/data_layers.js @@ -0,0 +1,391 @@ +import _debounce from 'lodash-es/debounce'; +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { t, textDirection } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; +import { svgIcon } from '../../svg/icon'; +import { geoExtent } from '../../geo'; +import { modeBrowse } from '../../modes/browse'; +import { uiCmd } from '../cmd'; +import { uiSection } from '../section'; +import { uiSettingsCustomData } from '../settings/custom_data'; +import { uiTooltipHtml } from '../tooltipHtml'; + +export function uiSectionDataLayers(context) { + + var settingsCustomData = uiSettingsCustomData(context) + .on('change', customChanged); + + var layers = context.layers(); + + var section = uiSection('data-layers', context) + .title(t('map_data.data_layers')); + + section.renderDisclosureContent = function(selection) { + var container = selection.selectAll('.data-layer-container') + .data([0]); + + container.enter() + .append('div') + .attr('class', 'data-layer-container') + .merge(container) + .call(drawOsmItems) + .call(drawQAItems) + .call(drawCustomDataItems) + .call(drawVectorItems); // Beta - Detroit mapping challenge + }; + + function showsLayer(which) { + var layer = layers.layer(which); + if (layer) { + return layer.enabled(); + } + return false; + } + + function setLayer(which, enabled) { + // Don't allow layer changes while drawing - #6584 + var mode = context.mode(); + if (mode && /^draw/.test(mode.id)) return; + + var layer = layers.layer(which); + if (layer) { + layer.enabled(enabled); + + if (!enabled && (which === 'osm' || which === 'notes')) { + context.enter(modeBrowse(context)); + } + } + } + + function toggleLayer(which) { + setLayer(which, !showsLayer(which)); + } + + function drawOsmItems(selection) { + var osmKeys = ['osm', 'notes']; + var osmLayers = layers.all().filter(function(obj) { return osmKeys.indexOf(obj.id) !== -1; }); + + var ul = selection + .selectAll('.layer-list-osm') + .data([0]); + + ul = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-osm') + .merge(ul); + + var li = ul.selectAll('.list-item') + .data(osmLayers); + + li.exit() + .remove(); + + var liEnter = li.enter() + .append('li') + .attr('class', function(d) { return 'list-item list-item-' + d.id; }); + + var labelEnter = liEnter + .append('label') + .each(function(d) { + if (d.id === 'osm') { + d3_select(this) + .call(tooltip() + .html(true) + .title(uiTooltipHtml(t('map_data.layers.' + d.id + '.tooltip'), uiCmd('⌥' + t('area_fill.wireframe.key')))) + .placement('bottom') + ); + } else { + d3_select(this) + .call(tooltip() + .title(t('map_data.layers.' + d.id + '.tooltip')) + .placement('bottom') + ); + } + }); + + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function(d) { toggleLayer(d.id); }); + + labelEnter + .append('span') + .text(function(d) { return t('map_data.layers.' + d.id + '.title'); }); + + + // Update + li + .merge(liEnter) + .classed('active', function (d) { return d.layer.enabled(); }) + .selectAll('input') + .property('checked', function (d) { return d.layer.enabled(); }); + } + + function drawQAItems(selection) { + var qaKeys = ['keepRight', 'improveOSM', 'osmose']; + var qaLayers = layers.all().filter(function(obj) { return qaKeys.indexOf(obj.id) !== -1; }); + + var ul = selection + .selectAll('.layer-list-qa') + .data([0]); + + ul = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-qa') + .merge(ul); + + var li = ul.selectAll('.list-item') + .data(qaLayers); + + li.exit() + .remove(); + + var liEnter = li.enter() + .append('li') + .attr('class', function(d) { return 'list-item list-item-' + d.id; }); + + var labelEnter = liEnter + .append('label') + .each(function(d) { + d3_select(this) + .call(tooltip() + .title(t('map_data.layers.' + d.id + '.tooltip')) + .placement('bottom') + ); + }); + + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function(d) { toggleLayer(d.id); }); + + labelEnter + .append('span') + .text(function(d) { return t('map_data.layers.' + d.id + '.title'); }); + + + // Update + li + .merge(liEnter) + .classed('active', function (d) { return d.layer.enabled(); }) + .selectAll('input') + .property('checked', function (d) { return d.layer.enabled(); }); + } + + // Beta feature - sample vector layers to support Detroit Mapping Challenge + // https://github.com/osmus/detroit-mapping-challenge + function drawVectorItems(selection) { + var dataLayer = layers.layer('data'); + var vtData = [ + { + name: 'Detroit Neighborhoods/Parks', + src: 'neighborhoods-parks', + tooltip: 'Neighborhood boundaries and parks as compiled by City of Detroit in concert with community groups.', + template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmur6x34562qp9iv1u3ksf-54hev,jonahadkins.cjksmqxdx33jj2wp90xd9x2md-4e5y2/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA' + }, { + name: 'Detroit Composite POIs', + src: 'composite-poi', + tooltip: 'Fire Inspections, Business Licenses, and other public location data collated from the City of Detroit.', + template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmm6a02sli31myxhsr7zf3-2sw8h/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA' + }, { + name: 'Detroit All-The-Places POIs', + src: 'alltheplaces-poi', + tooltip: 'Public domain business location data created by web scrapers.', + template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmswgk340g2vo06p1w9w0j-8fjjc/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA' + } + ]; + + // Only show this if the map is around Detroit.. + var detroit = geoExtent([-83.5, 42.1], [-82.8, 42.5]); + var showVectorItems = (context.map().zoom() > 9 && detroit.contains(context.map().center())); + + var container = selection.selectAll('.vectortile-container') + .data(showVectorItems ? [0] : []); + + container.exit() + .remove(); + + var containerEnter = container.enter() + .append('div') + .attr('class', 'vectortile-container'); + + containerEnter + .append('h4') + .attr('class', 'vectortile-header') + .text('Detroit Vector Tiles (Beta)'); + + containerEnter + .append('ul') + .attr('class', 'layer-list layer-list-vectortile'); + + containerEnter + .append('div') + .attr('class', 'vectortile-footer') + .append('a') + .attr('target', '_blank') + .attr('tabindex', -1) + .call(svgIcon('#iD-icon-out-link', 'inline')) + .attr('href', 'https://github.com/osmus/detroit-mapping-challenge') + .append('span') + .text('About these layers'); + + container = container + .merge(containerEnter); + + + var ul = container.selectAll('.layer-list-vectortile'); + + var li = ul.selectAll('.list-item') + .data(vtData); + + li.exit() + .remove(); + + var liEnter = li.enter() + .append('li') + .attr('class', function(d) { return 'list-item list-item-' + d.src; }); + + var labelEnter = liEnter + .append('label') + .each(function(d) { + d3_select(this).call( + tooltip().title(d.tooltip).placement('top') + ); + }); + + labelEnter + .append('input') + .attr('type', 'radio') + .attr('name', 'vectortile') + .on('change', selectVTLayer); + + labelEnter + .append('span') + .text(function(d) { return d.name; }); + + // Update + li + .merge(liEnter) + .classed('active', isVTLayerSelected) + .selectAll('input') + .property('checked', isVTLayerSelected); + + + function isVTLayerSelected(d) { + return dataLayer && dataLayer.template() === d.template; + } + + function selectVTLayer(d) { + context.storage('settings-custom-data-url', d.template); + if (dataLayer) { + dataLayer.template(d.template, d.src); + dataLayer.enabled(true); + } + } + } + + function drawCustomDataItems(selection) { + var dataLayer = layers.layer('data'); + var hasData = dataLayer && dataLayer.hasData(); + var showsData = hasData && dataLayer.enabled(); + + var ul = selection + .selectAll('.layer-list-data') + .data(dataLayer ? [0] : []); + + // Exit + ul.exit() + .remove(); + + // Enter + var ulEnter = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-data'); + + var liEnter = ulEnter + .append('li') + .attr('class', 'list-item-data'); + + var labelEnter = liEnter + .append('label') + .call(tooltip() + .title(t('map_data.layers.custom.tooltip')) + .placement('top') + ); + + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function() { toggleLayer('data'); }); + + labelEnter + .append('span') + .text(t('map_data.layers.custom.title')); + + liEnter + .append('button') + .call(tooltip() + .title(t('settings.custom_data.tooltip')) + .placement((textDirection === 'rtl') ? 'right' : 'left') + ) + .on('click', editCustom) + .call(svgIcon('#iD-icon-more')); + + liEnter + .append('button') + .call(tooltip() + .title(t('map_data.layers.custom.zoom')) + .placement((textDirection === 'rtl') ? 'right' : 'left') + ) + .on('click', function() { + d3_event.preventDefault(); + d3_event.stopPropagation(); + dataLayer.fitZoom(); + }) + .call(svgIcon('#iD-icon-search')); + + // Update + ul = ul + .merge(ulEnter); + + ul.selectAll('.list-item-data') + .classed('active', showsData) + .selectAll('label') + .classed('deemphasize', !hasData) + .selectAll('input') + .property('disabled', !hasData) + .property('checked', showsData); + } + + function editCustom() { + d3_event.preventDefault(); + context.container() + .call(settingsCustomData); + } + + function customChanged(d) { + var dataLayer = layers.layer('data'); + + if (d && d.url) { + dataLayer.url(d.url); + } else if (d && d.fileList) { + dataLayer.fileList(d.fileList); + } + } + + context.layers().on('change.uiSectionDataLayers', section.rerenderContent); + + context.map() + .on('move.uiSectionDataLayers', + _debounce(function() { + // Detroit layers may have moved in or out of view + window.requestIdleCallback(section.rerenderContent); + }, 1000) + ); + + return section; +} From e42bc34e4b1ae7867c71c5b2f3c0d24385ffed9d Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 19 Feb 2020 17:28:51 -0800 Subject: [PATCH 023/127] Move validation warnings and errors to their own section objects Allow function parameters for disclosure titles --- modules/ui/disclosure.js | 7 +- modules/ui/panes/issues.js | 288 +++-------------------- modules/ui/section.js | 8 +- modules/ui/sections/validation_issues.js | 238 +++++++++++++++++++ modules/ui/sections/validation_rules.js | 2 +- 5 files changed, 286 insertions(+), 257 deletions(-) create mode 100644 modules/ui/sections/validation_issues.js diff --git a/modules/ui/disclosure.js b/modules/ui/disclosure.js index 17392d856..83828379d 100644 --- a/modules/ui/disclosure.js +++ b/modules/ui/disclosure.js @@ -2,6 +2,7 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; import { event as d3_event } from 'd3-selection'; import { svgIcon } from '../svg/icon'; +import { utilFunctor } from '../util'; import { utilRebind } from '../util/rebind'; import { uiToggle } from './toggle'; import { textDirection } from '../util/locale'; @@ -11,7 +12,7 @@ export function uiDisclosure(context, key, expandedDefault) { var dispatch = d3_dispatch('toggled'); var _preference = (context.storage('disclosure.' + key + '.expanded')); var _expanded = (_preference === null ? !!expandedDefault : (_preference === 'true')); - var _title; + var _title = utilFunctor(''); var _updatePreference = true; var _content = function () {}; @@ -40,7 +41,7 @@ export function uiDisclosure(context, key, expandedDefault) { .classed('expanded', _expanded); hideToggle.selectAll('.hide-toggle-text') - .text(_title); + .text(_title()); hideToggle.selectAll('.hide-toggle-icon') .attr('xlink:href', _expanded ? '#iD-icon-down' @@ -96,7 +97,7 @@ export function uiDisclosure(context, key, expandedDefault) { disclosure.title = function(val) { if (!arguments.length) return _title; - _title = val; + _title = utilFunctor(val); return disclosure; }; diff --git a/modules/ui/panes/issues.js b/modules/ui/panes/issues.js index 9088697fd..84753b9b0 100644 --- a/modules/ui/panes/issues.js +++ b/modules/ui/panes/issues.js @@ -1,208 +1,40 @@ import _debounce from 'lodash-es/debounce'; - -import { event as d3_event, select as d3_select } from 'd3-selection'; +import { event as d3_event } from 'd3-selection'; import { t } from '../../util/locale'; - -//import { actionNoop } from '../actions/noop'; -import { geoSphericalDistance } from '../../geo'; import { svgIcon } from '../../svg/icon'; -import { uiDisclosure } from '../disclosure'; -import { utilHighlightEntities } from '../../util'; import { uiPane } from '../pane'; -import { uiValidationRules } from '../sections/validation_rules'; +import { uiSectionValidationIssues } from '../sections/validation_issues'; +import { uiSectionValidationRules } from '../sections/validation_rules'; export function uiPaneIssues(context) { - var _errorsSelection = d3_select(null); - var _warningsSelection = d3_select(null); + var _validationRules = uiSectionValidationRules(context); + var _validationErrors = uiSectionValidationIssues('issues-errors', 'error', context); + var _validationWarnings = uiSectionValidationIssues('issues-warnings', 'warning', context); - var _rulesListContainer = d3_select(null); + function getOptions() { + return { + what: context.storage('validate-what') || 'edited', // 'all', 'edited' + where: context.storage('validate-where') || 'all' // 'all', 'visible' + }; + } - var _validationRules = uiValidationRules(context); - - var _errors = []; - var _warnings = []; - var _options = { - what: context.storage('validate-what') || 'edited', // 'all', 'edited' - where: context.storage('validate-where') || 'all' // 'all', 'visible' - }; - - // listeners - context.validator().on('validated.uiIssues', + // listen for updates that affect the "no issues" box + context.validator().on('validated.uiPaneIssues', function() { window.requestIdleCallback(update); } ); - context.map().on('move.uiIssues', + context.map().on('move.uiPaneIssues', _debounce(function() { window.requestIdleCallback(update); }, 1000) ); - function renderErrorsList(selection) { - _errorsSelection = selection - .call(drawIssuesList, 'errors', _errors); - } - - - function renderWarningsList(selection) { - _warningsSelection = selection - .call(drawIssuesList, 'warnings', _warnings); - } - - - function drawIssuesList(selection, which, issues) { - var list = selection.selectAll('.issues-list') - .data([0]); - - list = list.enter() - .append('ul') - .attr('class', 'layer-list issues-list ' + which + '-list') - .merge(list); - - - var items = list.selectAll('li') - .data(issues, function(d) { return d.id; }); - - // Exit - items.exit() - .remove(); - - // Enter - var itemsEnter = items.enter() - .append('li') - .attr('class', function (d) { return 'issue severity-' + d.severity; }) - .on('click', function(d) { - context.validator().focusIssue(d); - }) - .on('mouseover', function(d) { - utilHighlightEntities(d.entityIds, true, context); - }) - .on('mouseout', function(d) { - utilHighlightEntities(d.entityIds, false, context); - }); - - - var labelsEnter = itemsEnter - .append('div') - .attr('class', 'issue-label'); - - var textEnter = labelsEnter - .append('span') - .attr('class', 'issue-text'); - - textEnter - .append('span') - .attr('class', 'issue-icon') - .each(function(d) { - var iconName = '#iD-icon-' + (d.severity === 'warning' ? 'alert' : 'error'); - d3_select(this) - .call(svgIcon(iconName)); - }); - - textEnter - .append('span') - .attr('class', 'issue-message'); - - /* - labelsEnter - .append('span') - .attr('class', 'issue-autofix') - .each(function(d) { - if (!d.autoFix) return; - - d3_select(this) - .append('button') - .attr('title', t('issues.fix_one.title')) - .datum(d.autoFix) // set button datum to the autofix - .attr('class', 'autofix action') - .on('click', function(d) { - d3_event.preventDefault(); - d3_event.stopPropagation(); - - var issuesEntityIDs = d.issue.entityIds; - utilHighlightEntities(issuesEntityIDs.concat(d.entityIds), false, context); - - context.perform.apply(context, d.autoArgs); - context.validator().validate(); - }) - .call(svgIcon('#iD-icon-wrench')); - }); - */ - - // Update - items = items - .merge(itemsEnter) - .order(); - - items.selectAll('.issue-message') - .text(function(d) { - return d.message(context); - }); - - /* - // autofix - var canAutoFix = issues.filter(function(issue) { return issue.autoFix; }); - - var autoFixAll = selection.selectAll('.autofix-all') - .data(canAutoFix.length ? [0] : []); - - // exit - autoFixAll.exit() - .remove(); - - // enter - var autoFixAllEnter = autoFixAll.enter() - .insert('div', '.issues-list') - .attr('class', 'autofix-all'); - - var linkEnter = autoFixAllEnter - .append('a') - .attr('class', 'autofix-all-link') - .attr('href', '#'); - - linkEnter - .append('span') - .attr('class', 'autofix-all-link-text') - .text(t('issues.fix_all.title')); - - linkEnter - .append('span') - .attr('class', 'autofix-all-link-icon') - .call(svgIcon('#iD-icon-wrench')); - - if (which === 'warnings') { - renderIgnoredIssuesReset(selection); - } - - // update - autoFixAll = autoFixAll - .merge(autoFixAllEnter); - - autoFixAll.selectAll('.autofix-all-link') - .on('click', function() { - context.pauseChangeDispatch(); - context.perform(actionNoop()); - canAutoFix.forEach(function(issue) { - var args = issue.autoFix.autoArgs.slice(); // copy - if (typeof args[args.length - 1] !== 'function') { - args.pop(); - } - args.push(t('issues.fix_all.annotation')); - context.replace.apply(context, args); - }); - context.resumeChangeDispatch(); - context.validator().validate(); - }); - */ - } - - function updateOptionValue(d, val) { if (!val && d3_event && d3_event.target) { val = d3_event.target.value; } - _options[d] = val; context.storage('validate-' + d, val); context.validator().validate(); } @@ -246,7 +78,7 @@ export function uiPaneIssues(context) { .attr('type', 'radio') .attr('name', function(d) { return 'issues-option-' + d.key; }) .attr('value', function(d) { return d.value; }) - .property('checked', function(d) { return _options[d.key] === d.value; }) + .property('checked', function(d) { return getOptions()[d.key] === d.value; }) .on('change', function(d) { updateOptionValue(d.key, d.value); }); valuesEnter @@ -314,10 +146,12 @@ export function uiPaneIssues(context) { function setNoIssuesText() { + var opts = getOptions(); + function checkForHiddenIssues(cases) { for (var type in cases) { - var opts = cases[type]; - var hiddenIssues = context.validator().getIssues(opts); + var hiddenOpts = cases[type]; + var hiddenIssues = context.validator().getIssues(hiddenOpts); if (hiddenIssues.length) { issuesPane.selection().select('.issues-none .details') .text(t( @@ -333,7 +167,7 @@ export function uiPaneIssues(context) { var messageType; - if (_options.what === 'edited' && _options.where === 'visible') { + if (opts.what === 'edited' && opts.where === 'visible') { messageType = 'edits_in_view'; @@ -347,7 +181,7 @@ export function uiPaneIssues(context) { ignored_issues_elsewhere: { what: 'edited', where: 'all', includeIgnored: 'only' } }); - } else if (_options.what === 'edited' && _options.where === 'all') { + } else if (opts.what === 'edited' && opts.where === 'all') { messageType = 'edits'; @@ -357,7 +191,7 @@ export function uiPaneIssues(context) { ignored_issues: { what: 'edited', where: 'all', includeIgnored: 'only' } }); - } else if (_options.what === 'all' && _options.where === 'visible') { + } else if (opts.what === 'all' && opts.where === 'visible') { messageType = 'everything_in_view'; @@ -368,7 +202,7 @@ export function uiPaneIssues(context) { ignored_issues: { what: 'all', where: 'visible', includeIgnored: 'only' }, ignored_issues_elsewhere: { what: 'all', where: 'all', includeIgnored: 'only' } }); - } else if (_options.what === 'all' && _options.where === 'all') { + } else if (opts.what === 'all' && opts.where === 'all') { messageType = 'everything'; @@ -378,7 +212,7 @@ export function uiPaneIssues(context) { }); } - if (_options.what === 'edited' && context.history().difference().summary().length === 0) { + if (opts.what === 'edited' && context.history().difference().summary().length === 0) { messageType = 'no_edits'; } @@ -389,47 +223,9 @@ export function uiPaneIssues(context) { function update() { - var issuesBySeverity = context.validator().getIssuesBySeverity(_options); + var issues = context.validator().getIssues(getOptions()); - // sort issues by distance away from the center of the map - var center = context.map().center(); - var graph = context.graph(); - _errors = issuesBySeverity.error.map(withDistance).sort(byDistance); - _warnings = issuesBySeverity.warning.map(withDistance).sort(byDistance); - - // cut off at 1000 - var errorCount = _errors.length > 1000 ? '1000+' : String(_errors.length); - var warningCount = _warnings.length > 1000 ? '1000+' : String(_warnings.length); - _errors = _errors.slice(0, 1000); - _warnings = _warnings.slice(0, 1000); - - - issuesPane.selection().selectAll('.issues-errors') - .classed('hide', _errors.length === 0); - - if (_errors.length > 0) { - issuesPane.selection().selectAll('.hide-toggle-issues_errors .hide-toggle-text') - .text(t('issues.errors.list_title', { count: errorCount })); - if (!issuesPane.selection().select('.disclosure-wrap-issues_errors').classed('hide')) { - _errorsSelection - .call(drawIssuesList, 'errors', _errors); - } - } - - issuesPane.selection().selectAll('.issues-warnings') - .classed('hide', _warnings.length === 0); - - if (_warnings.length > 0) { - issuesPane.selection().selectAll('.hide-toggle-issues_warnings .hide-toggle-text') - .text(t('issues.warnings.list_title', { count: warningCount })); - if (!issuesPane.selection().select('.disclosure-wrap-issues_warnings').classed('hide')) { - _warningsSelection - .call(drawIssuesList, 'warnings', _warnings); - renderIgnoredIssuesReset(_warningsSelection); - } - } - - var hasIssues = _warnings.length > 0 || _errors.length > 0; + var hasIssues = issues.length > 0; var issuesNone = issuesPane.selection().select('.issues-none'); issuesNone.classed('hide', hasIssues); @@ -438,18 +234,14 @@ export function uiPaneIssues(context) { setNoIssuesText(); } - _rulesListContainer + issuesPane.selection().select('.issues-errors') + .call(_validationErrors.render); + + issuesPane.selection().select('.issues-warnings') + .call(_validationWarnings.render); + + issuesPane.selection().select('.issues-rules') .call(_validationRules.render); - - function byDistance(a, b) { - return a.dist - b.dist; - } - - function withDistance(issue) { - var extent = issue.extent(graph); - var dist = extent ? geoSphericalDistance(center, extent.center()) : 0; - return Object.assign(issue, { dist: dist }); - } } @@ -475,21 +267,15 @@ export function uiPaneIssues(context) { // errors content .append('div') - .attr('class', 'issues-errors') - .call(uiDisclosure(context, 'issues_errors', true) - .content(renderErrorsList) - ); + .attr('class', 'issues-errors'); // warnings content .append('div') - .attr('class', 'issues-warnings') - .call(uiDisclosure(context, 'issues_warnings', true) - .content(renderWarningsList) - ); + .attr('class', 'issues-warnings'); // rules list - _rulesListContainer = content + content .append('div') .attr('class', 'issues-rules'); diff --git a/modules/ui/section.js b/modules/ui/section.js index a5f7372f4..27daa814f 100644 --- a/modules/ui/section.js +++ b/modules/ui/section.js @@ -49,13 +49,17 @@ export function uiSection(id, context) { .call(section.renderContent); }; + section.containerSelection = function() { + return _containerSelection; + }; + // may be called multiple times section.renderContent = function(containerSelection) { - if (section.renderDisclosureContent && _title) { + if (section.renderDisclosureContent) { if (!_disclosure) { _disclosure = uiDisclosure(context, id.replace(/-/g, '_'), _expandedByDefault) - .title(_title) + .title(_title || '') .content(section.renderDisclosureContent); } containerSelection diff --git a/modules/ui/sections/validation_issues.js b/modules/ui/sections/validation_issues.js new file mode 100644 index 000000000..eb722b5db --- /dev/null +++ b/modules/ui/sections/validation_issues.js @@ -0,0 +1,238 @@ +import _debounce from 'lodash-es/debounce'; +import { + select as d3_select +} from 'd3-selection'; + +//import { actionNoop } from '../actions/noop'; +import { geoSphericalDistance } from '../../geo'; +import { svgIcon } from '../../svg/icon'; +import { t } from '../../util/locale'; +import { utilHighlightEntities } from '../../util'; +import { uiSection } from '../section'; + +export function uiSectionValidationIssues(id, severity, context) { + + var _issues = []; + + var section = uiSection(id, context) + .title(function() { + if (!_issues) return ''; + var issueCountText = _issues.length > 1000 ? '1000+' : String(_issues.length); + return t('issues.' + severity + 's.list_title', { count: issueCountText }); + }); + + function getOptions() { + return { + what: context.storage('validate-what') || 'edited', + where: context.storage('validate-where') || 'all' + }; + } + + // get and cache the issues to display, unordered + function reloadIssues() { + _issues = context.validator().getIssuesBySeverity(getOptions())[severity]; + } + + var _parentRenderContent = section.renderContent; + + section.renderContent = function(selection) { + + var isHidden = !_issues || !_issues.length; + + selection.classed('hide', isHidden); + + if (!isHidden) { + selection.call(_parentRenderContent); + } + }; + + section.renderDisclosureContent = function(selection) { + + var center = context.map().center(); + var graph = context.graph(); + + // sort issues by distance away from the center of the map + var issues = _issues.map(function withDistance(issue) { + var extent = issue.extent(graph); + var dist = extent ? geoSphericalDistance(center, extent.center()) : 0; + return Object.assign(issue, { dist: dist }); + }) + .sort(function byDistance(a, b) { + return a.dist - b.dist; + }); + + // cut off at 1000 + issues = issues.slice(0, 1000); + + //renderIgnoredIssuesReset(_warningsSelection); + + selection + .call(drawIssuesList, issues); + }; + + function drawIssuesList(selection, issues) { + var list = selection.selectAll('.issues-list') + .data([0]); + + list = list.enter() + .append('ul') + .attr('class', 'layer-list issues-list ' + severity + 's-list') + .merge(list); + + + var items = list.selectAll('li') + .data(issues, function(d) { return d.id; }); + + // Exit + items.exit() + .remove(); + + // Enter + var itemsEnter = items.enter() + .append('li') + .attr('class', function (d) { return 'issue severity-' + d.severity; }) + .on('click', function(d) { + context.validator().focusIssue(d); + }) + .on('mouseover', function(d) { + utilHighlightEntities(d.entityIds, true, context); + }) + .on('mouseout', function(d) { + utilHighlightEntities(d.entityIds, false, context); + }); + + + var labelsEnter = itemsEnter + .append('div') + .attr('class', 'issue-label'); + + var textEnter = labelsEnter + .append('span') + .attr('class', 'issue-text'); + + textEnter + .append('span') + .attr('class', 'issue-icon') + .each(function(d) { + var iconName = '#iD-icon-' + (d.severity === 'warning' ? 'alert' : 'error'); + d3_select(this) + .call(svgIcon(iconName)); + }); + + textEnter + .append('span') + .attr('class', 'issue-message'); + + /* + labelsEnter + .append('span') + .attr('class', 'issue-autofix') + .each(function(d) { + if (!d.autoFix) return; + + d3_select(this) + .append('button') + .attr('title', t('issues.fix_one.title')) + .datum(d.autoFix) // set button datum to the autofix + .attr('class', 'autofix action') + .on('click', function(d) { + d3_event.preventDefault(); + d3_event.stopPropagation(); + + var issuesEntityIDs = d.issue.entityIds; + utilHighlightEntities(issuesEntityIDs.concat(d.entityIds), false, context); + + context.perform.apply(context, d.autoArgs); + context.validator().validate(); + }) + .call(svgIcon('#iD-icon-wrench')); + }); + */ + + // Update + items = items + .merge(itemsEnter) + .order(); + + items.selectAll('.issue-message') + .text(function(d) { + return d.message(context); + }); + + /* + // autofix + var canAutoFix = issues.filter(function(issue) { return issue.autoFix; }); + + var autoFixAll = selection.selectAll('.autofix-all') + .data(canAutoFix.length ? [0] : []); + + // exit + autoFixAll.exit() + .remove(); + + // enter + var autoFixAllEnter = autoFixAll.enter() + .insert('div', '.issues-list') + .attr('class', 'autofix-all'); + + var linkEnter = autoFixAllEnter + .append('a') + .attr('class', 'autofix-all-link') + .attr('href', '#'); + + linkEnter + .append('span') + .attr('class', 'autofix-all-link-text') + .text(t('issues.fix_all.title')); + + linkEnter + .append('span') + .attr('class', 'autofix-all-link-icon') + .call(svgIcon('#iD-icon-wrench')); + + if (severity === 'warning') { + renderIgnoredIssuesReset(selection); + } + + // update + autoFixAll = autoFixAll + .merge(autoFixAllEnter); + + autoFixAll.selectAll('.autofix-all-link') + .on('click', function() { + context.pauseChangeDispatch(); + context.perform(actionNoop()); + canAutoFix.forEach(function(issue) { + var args = issue.autoFix.autoArgs.slice(); // copy + if (typeof args[args.length - 1] !== 'function') { + args.pop(); + } + args.push(t('issues.fix_all.annotation')); + context.replace.apply(context, args); + }); + context.resumeChangeDispatch(); + context.validator().validate(); + }); + */ + } + + context.validator().on('validated.uiSectionValidationIssues' + id, function() { + window.requestIdleCallback(function() { + reloadIssues(); + section.rerenderContent(); + }); + }); + + context.map().on('move.uiSectionValidationIssues' + id, + _debounce(function() { + if (getOptions().where === 'visible') { + // must refetch issues if they are viewport-dependent + reloadIssues(); + } + // always reload list to re-sort-by-distance + window.requestIdleCallback(section.rerenderContent); + }, 1000) + ); + + return section; +} diff --git a/modules/ui/sections/validation_rules.js b/modules/ui/sections/validation_rules.js index ca8eb532a..28601222a 100644 --- a/modules/ui/sections/validation_rules.js +++ b/modules/ui/sections/validation_rules.js @@ -8,7 +8,7 @@ import { utilGetSetValue, utilNoAuto } from '../../util'; import { tooltip } from '../../util/tooltip'; import { uiSection } from '../section'; -export function uiValidationRules(context) { +export function uiSectionValidationRules(context) { var MINSQUARE = 0; var MAXSQUARE = 20; From 3ccf0e6ccbb2504e88bd790b1a3b44d08e8c2dc4 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 20 Feb 2020 08:51:44 -0500 Subject: [PATCH 024/127] Preload intro graph data while the user is viewing the splash screen --- modules/ui/splash.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/ui/splash.js b/modules/ui/splash.js index 11e395eac..d07c50980 100644 --- a/modules/ui/splash.js +++ b/modules/ui/splash.js @@ -23,6 +23,9 @@ export function uiSplash(context) { context.storage('sawSplash', true); context.storage('sawPrivacyVersion', context.privacyVersion); + // fetch intro graph data now, while user is looking at the splash screen + context.data().get('intro_graph'); + let modalSelection = uiModal(selection); modalSelection.select('.modal') From 503d28799578fdc7d7558d5f44b323c2f434087c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 20 Feb 2020 10:26:09 -0500 Subject: [PATCH 025/127] ES6ify locale.js --- modules/util/locale.js | 174 ++++++++++++++++++++--------------------- 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/modules/util/locale.js b/modules/util/locale.js index e003b759b..dd5ceed96 100644 --- a/modules/util/locale.js +++ b/modules/util/locale.js @@ -1,21 +1,21 @@ -var translations = Object.create(null); -var _dataLanguages = {}; +let _translations = Object.create(null); +let _dataLanguages = {}; -export var currentLocale = 'en'; -export var textDirection = 'ltr'; -export var languageNames = {}; -export var scriptNames = {}; +export let currentLocale = 'en'; +export let textDirection = 'ltr'; +export let languageNames = {}; +export let scriptNames = {}; export function setLocale(val) { - if (translations[val] !== undefined) { - currentLocale = val; - } else if (translations[val.split('-')[0]]) { - currentLocale = val.split('-')[0]; - } + if (_translations[val] !== undefined) { + currentLocale = val; + } else if (_translations[val.split('-')[0]]) { + currentLocale = val.split('-')[0]; + } } export function addTranslation(id, value) { - translations[id] = value; + _translations[id] = value; } /** @@ -23,48 +23,48 @@ export function addTranslation(id, value) { * language, and return it. This function will be called recursively * with locale `en` if a string can not be found in the requested language. * - * @param {string} s string identifier - * @param {object?} o object of token replacements and default string - * @param {string?} loc locale to use - * @returns {string?} locale string + * @param {string} s string identifier + * @param {object?} replacements token replacements and default string + * @param {string?} locale locale to use (defaults to currentLocale) + * @returns {string?} localized string */ -export function t(s, o, loc) { - loc = loc || currentLocale; +export function t(s, replacements, locale) { + locale = locale || currentLocale; - var path = s - .split('.') - .map(function (s) { return s.replace(//g, '.'); }) - .reverse(); + let path = s + .split('.') + .map(s => s.replace(//g, '.')) + .reverse(); - var rep = translations[loc]; + let result = _translations[locale]; - while (rep !== undefined && path.length) { - rep = rep[path.pop()]; + while (result !== undefined && path.length) { + result = result[path.pop()]; + } + + if (result !== undefined) { + if (replacements) { + for (let k in replacements) { + const token = `{${k}}`; + const regex = new RegExp(token, 'g'); + result = result.replace(regex, replacements[k]); + } } + return result; + } - if (rep !== undefined) { - if (o) { - for (var k in o) { - var variable = '\\{' + k + '\\}'; - var re = new RegExp(variable, 'g'); // check globally for variables - rep = rep.replace(re, o[k]); - } - } - return rep; - } + if (locale !== 'en') { + return t(s, replacements, 'en'); // fallback - recurse with 'en' + } - if (loc !== 'en') { - return t(s, o, 'en'); - } + if (replacements && 'default' in replacements) { + return replacements.default; // fallback - replacements.default + } - if (o && 'default' in o) { - return o.default; - } + const missing = `Missing ${locale} translation: ${s}`; + if (typeof console !== 'undefined') console.error(missing); // eslint-disable-line - var missing = 'Missing ' + loc + ' translation: ' + s; - if (typeof console !== 'undefined') console.error(missing); // eslint-disable-line - - return missing; + return missing; } /** @@ -73,55 +73,55 @@ export function t(s, o, loc) { * @param {string} dir ltr or rtl */ export function setTextDirection(dir) { - textDirection = dir; + textDirection = dir; } export function setLanguageNames(obj) { - languageNames = obj; + languageNames = obj; } export function setScriptNames(obj) { - scriptNames = obj; + scriptNames = obj; } export function languageName(context, code, options) { - // Data access is async now, which makes this complicated. - // If _dataLanguages haven't been loaded yet, try to load them. - // Worst case, we fallback to the code until the file has been loaded. - if (!Object.keys(_dataLanguages).length) { - context.data().get('languages') - .then(function(d) { _dataLanguages = d; }) - .catch(function() { /* ignore */ }); + // Data access is async now, which makes this complicated. + // If _dataLanguages haven't been loaded yet, try to load them. + // Worst case, we fallback to the code until the file has been loaded. + if (!Object.keys(_dataLanguages).length) { + context.data().get('languages') + .then(d => _dataLanguages = d) + .catch(() => { /* ignore */ }); + } + + if (languageNames[code]) { // name in locale langauge + // e.g. "German" + return languageNames[code]; + } + + // sometimes we only want the local name + if (options && options.localOnly) return null; + + const langInfo = _dataLanguages[code]; + if (langInfo) { + if (langInfo.nativeName) { // name in native language + // e.g. "Deutsch (de)" + return t('translate.language_and_code', { language: langInfo.nativeName, code: code }); + + } else if (langInfo.base && langInfo.script) { + const base = langInfo.base; // the code of the langauge this is based on + + if (languageNames[base]) { // base language name in locale langauge + const scriptCode = langInfo.script; + const script = scriptNames[scriptCode] || scriptCode; + // e.g. "Serbian (Cyrillic)" + return t('translate.language_and_code', { language: languageNames[base], code: script }); + + } else if (_dataLanguages[base] && _dataLanguages[base].nativeName) { + // e.g. "српски (sr-Cyrl)" + return t('translate.language_and_code', { language: _dataLanguages[base].nativeName, code: code }); + } } - - if (languageNames[code]) { // name in locale langauge - // e.g. German - return languageNames[code]; - } - - // sometimes we only want the local name - if (options && options.localOnly) return null; - - var langInfo = _dataLanguages[code]; - if (langInfo) { - if (langInfo.nativeName) { // name in native language - // e.g. Deutsch (de) - return t('translate.language_and_code', { language: langInfo.nativeName, code: code }); - - } else if (langInfo.base && langInfo.script) { - var base = langInfo.base; // the code of the langauge this is based on - - if (languageNames[base]) { // base language name in locale langauge - var scriptCode = langInfo.script; - var script = scriptNames[scriptCode] || scriptCode; - // e.g. Serbian (Cyrillic) - return t('translate.language_and_code', { language: languageNames[base], code: script }); - - } else if (_dataLanguages[base] && _dataLanguages[base].nativeName) { - // e.g. српски (sr-Cyrl) - return t('translate.language_and_code', { language: _dataLanguages[base].nativeName, code: code }); - } - } - } - return code; // if not found, use the code -} \ No newline at end of file + } + return code; // if not found, use the code +} From 4fe9057eb762515d71cbeb39cb05efd1eb394dc2 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Thu, 20 Feb 2020 13:02:09 -0800 Subject: [PATCH 026/127] Move remaining issues pane sections to their own objects --- css/80_app.css | 4 +- modules/ui/panes/issues.js | 284 +--------------------- modules/ui/section.js | 40 ++- modules/ui/sections/validation_issues.js | 30 +-- modules/ui/sections/validation_options.js | 76 ++++++ modules/ui/sections/validation_status.js | 174 +++++++++++++ 6 files changed, 305 insertions(+), 303 deletions(-) create mode 100644 modules/ui/sections/validation_options.js create mode 100644 modules/ui/sections/validation_status.js diff --git a/css/80_app.css b/css/80_app.css index 72ba5f369..a930b4c73 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -3258,14 +3258,14 @@ button.autofix.action.active { padding: 5px; } -.issues-none .box { +.section-issues-status .box { border-radius: 4px; border: 1px solid #72d979; background: #c6ffca; padding: 5px !important; display: flex; } -.issues-none .icon { +.section-issues-status .icon { color: #05ac10; } diff --git a/modules/ui/panes/issues.js b/modules/ui/panes/issues.js index 84753b9b0..03b47b218 100644 --- a/modules/ui/panes/issues.js +++ b/modules/ui/panes/issues.js @@ -1,286 +1,26 @@ -import _debounce from 'lodash-es/debounce'; -import { event as d3_event } from 'd3-selection'; import { t } from '../../util/locale'; -import { svgIcon } from '../../svg/icon'; import { uiPane } from '../pane'; -import { uiSectionValidationIssues } from '../sections/validation_issues'; -import { uiSectionValidationRules } from '../sections/validation_rules'; +import { uiSectionValidationIssues } from '../sections/validation_issues'; +import { uiSectionValidationOptions } from '../sections/validation_options'; +import { uiSectionValidationRules } from '../sections/validation_rules'; +import { uiSectionValidationStatus } from '../sections/validation_status'; export function uiPaneIssues(context) { - var _validationRules = uiSectionValidationRules(context); - var _validationErrors = uiSectionValidationIssues('issues-errors', 'error', context); - var _validationWarnings = uiSectionValidationIssues('issues-warnings', 'warning', context); - - function getOptions() { - return { - what: context.storage('validate-what') || 'edited', // 'all', 'edited' - where: context.storage('validate-where') || 'all' // 'all', 'visible' - }; - } - - // listen for updates that affect the "no issues" box - context.validator().on('validated.uiPaneIssues', - function() { window.requestIdleCallback(update); } - ); - context.map().on('move.uiPaneIssues', - _debounce(function() { window.requestIdleCallback(update); }, 1000) - ); - - - function updateOptionValue(d, val) { - if (!val && d3_event && d3_event.target) { - val = d3_event.target.value; - } - - context.storage('validate-' + d, val); - context.validator().validate(); - } - - - function renderIssuesOptions(selection) { - var container = selection.selectAll('.issues-options-container') - .data([0]); - - container = container.enter() - .append('div') - .attr('class', 'issues-options-container') - .merge(container); - - var data = [ - { key: 'what', values: ['edited', 'all'] }, - { key: 'where', values: ['visible', 'all'] } - ]; - - var options = container.selectAll('.issues-option') - .data(data, function(d) { return d.key; }); - - var optionsEnter = options.enter() - .append('div') - .attr('class', function(d) { return 'issues-option issues-option-' + d.key; }); - - optionsEnter - .append('div') - .attr('class', 'issues-option-title') - .text(function(d) { return t('issues.options.' + d.key + '.title'); }); - - var valuesEnter = optionsEnter.selectAll('label') - .data(function(d) { - return d.values.map(function(val) { return { value: val, key: d.key }; }); - }) - .enter() - .append('label'); - - valuesEnter - .append('input') - .attr('type', 'radio') - .attr('name', function(d) { return 'issues-option-' + d.key; }) - .attr('value', function(d) { return d.value; }) - .property('checked', function(d) { return getOptions()[d.key] === d.value; }) - .on('change', function(d) { updateOptionValue(d.key, d.value); }); - - valuesEnter - .append('span') - .text(function(d) { return t('issues.options.' + d.key + '.' + d.value); }); - } - - - function renderNoIssuesBox(selection) { - - var box = selection.append('div') - .attr('class', 'box'); - - box - .append('div') - .call(svgIcon('#iD-icon-apply', 'pre-text')); - - var noIssuesMessage = box - .append('span'); - - noIssuesMessage - .append('strong') - .attr('class', 'message'); - - noIssuesMessage - .append('br'); - - noIssuesMessage - .append('span') - .attr('class', 'details'); - } - - function renderIgnoredIssuesReset(selection) { - - var ignoredIssues = context.validator() - .getIssues({ what: 'all', where: 'all', includeDisabledRules: true, includeIgnored: 'only' }); - - var resetIgnored = selection.selectAll('.reset-ignored') - .data(ignoredIssues.length ? [0] : []); - - // exit - resetIgnored.exit() - .remove(); - - // enter - var resetIgnoredEnter = resetIgnored.enter() - .append('div') - .attr('class', 'reset-ignored section-footer'); - - resetIgnoredEnter - .append('a') - .attr('href', '#'); - - // update - resetIgnored = resetIgnored - .merge(resetIgnoredEnter); - - resetIgnored.select('a') - .text(t('issues.reset_ignored', { count: ignoredIssues.length.toString() })); - - resetIgnored.on('click', function() { - context.validator().resetIgnoredIssues(); - }); - } - - function setNoIssuesText() { - - var opts = getOptions(); - - function checkForHiddenIssues(cases) { - for (var type in cases) { - var hiddenOpts = cases[type]; - var hiddenIssues = context.validator().getIssues(hiddenOpts); - if (hiddenIssues.length) { - issuesPane.selection().select('.issues-none .details') - .text(t( - 'issues.no_issues.hidden_issues.' + type, - { count: hiddenIssues.length.toString() } - )); - return; - } - } - issuesPane.selection().select('.issues-none .details') - .text(t('issues.no_issues.hidden_issues.none')); - } - - var messageType; - - if (opts.what === 'edited' && opts.where === 'visible') { - - messageType = 'edits_in_view'; - - checkForHiddenIssues({ - elsewhere: { what: 'edited', where: 'all' }, - everything_else: { what: 'all', where: 'visible' }, - disabled_rules: { what: 'edited', where: 'visible', includeDisabledRules: 'only' }, - everything_else_elsewhere: { what: 'all', where: 'all' }, - disabled_rules_elsewhere: { what: 'edited', where: 'all', includeDisabledRules: 'only' }, - ignored_issues: { what: 'edited', where: 'visible', includeIgnored: 'only' }, - ignored_issues_elsewhere: { what: 'edited', where: 'all', includeIgnored: 'only' } - }); - - } else if (opts.what === 'edited' && opts.where === 'all') { - - messageType = 'edits'; - - checkForHiddenIssues({ - everything_else: { what: 'all', where: 'all' }, - disabled_rules: { what: 'edited', where: 'all', includeDisabledRules: 'only' }, - ignored_issues: { what: 'edited', where: 'all', includeIgnored: 'only' } - }); - - } else if (opts.what === 'all' && opts.where === 'visible') { - - messageType = 'everything_in_view'; - - checkForHiddenIssues({ - elsewhere: { what: 'all', where: 'all' }, - disabled_rules: { what: 'all', where: 'visible', includeDisabledRules: 'only' }, - disabled_rules_elsewhere: { what: 'all', where: 'all', includeDisabledRules: 'only' }, - ignored_issues: { what: 'all', where: 'visible', includeIgnored: 'only' }, - ignored_issues_elsewhere: { what: 'all', where: 'all', includeIgnored: 'only' } - }); - } else if (opts.what === 'all' && opts.where === 'all') { - - messageType = 'everything'; - - checkForHiddenIssues({ - disabled_rules: { what: 'all', where: 'all', includeDisabledRules: 'only' }, - ignored_issues: { what: 'all', where: 'all', includeIgnored: 'only' } - }); - } - - if (opts.what === 'edited' && context.history().difference().summary().length === 0) { - messageType = 'no_edits'; - } - - issuesPane.selection().select('.issues-none .message') - .text(t('issues.no_issues.message.' + messageType)); - - } - - - function update() { - var issues = context.validator().getIssues(getOptions()); - - var hasIssues = issues.length > 0; - - var issuesNone = issuesPane.selection().select('.issues-none'); - issuesNone.classed('hide', hasIssues); - if (!hasIssues) { - renderIgnoredIssuesReset(issuesNone); - setNoIssuesText(); - } - - issuesPane.selection().select('.issues-errors') - .call(_validationErrors.render); - - issuesPane.selection().select('.issues-warnings') - .call(_validationWarnings.render); - - issuesPane.selection().select('.issues-rules') - .call(_validationRules.render); - } - - var issuesPane = uiPane('issues', context) .key(t('issues.key')) .title(t('issues.title')) .description(t('issues.title')) - .iconName('iD-icon-alert'); - - - issuesPane.renderContent = function(content) { - - content - .append('div') - .attr('class', 'issues-options') - .call(renderIssuesOptions); - - content - .append('div') - .attr('class', 'issues-none') - .call(renderNoIssuesBox); - - // errors - content - .append('div') - .attr('class', 'issues-errors'); - - // warnings - content - .append('div') - .attr('class', 'issues-warnings'); - - // rules list - content - .append('div') - .attr('class', 'issues-rules'); - - update(); - }; + .iconName('iD-icon-alert') + .sections([ + uiSectionValidationOptions(context), + uiSectionValidationStatus(context), + uiSectionValidationIssues('issues-errors', 'error', context), + uiSectionValidationIssues('issues-warnings', 'warning', context), + uiSectionValidationRules(context) + ]); return issuesPane; } diff --git a/modules/ui/section.js b/modules/ui/section.js index 27daa814f..c629fe00b 100644 --- a/modules/ui/section.js +++ b/modules/ui/section.js @@ -3,6 +3,7 @@ import { } from 'd3-selection'; import { uiDisclosure } from './disclosure'; +import { utilFunctor } from '../util'; // A unit of controls or info to be used in a layout, such as within a pane. // Can be labeled and collapsible. @@ -10,7 +11,8 @@ export function uiSection(id, context) { var _disclosure; var _title; - var _expandedByDefault = true; + var _expandedByDefault = utilFunctor(true); + var _shouldDisplay; var _containerSelection = d3_select(null); @@ -20,13 +22,19 @@ export function uiSection(id, context) { section.title = function(val) { if (!arguments.length) return _title; - _title = val; + _title = utilFunctor(val); return section; }; section.expandedByDefault = function(val) { if (!arguments.length) return _expandedByDefault; - _expandedByDefault = val; + _expandedByDefault = utilFunctor(val); + return section; + }; + + section.shouldDisplay = function(val) { + if (!arguments.length) return _shouldDisplay; + _shouldDisplay = utilFunctor(val); return section; }; @@ -46,19 +54,31 @@ export function uiSection(id, context) { .merge(_containerSelection); _containerSelection - .call(section.renderContent); + .call(renderContent); }; section.containerSelection = function() { return _containerSelection; }; + function renderContent(selection) { + if (_shouldDisplay) { + var shouldDisplay = _shouldDisplay(); + selection.classed('hide', !shouldDisplay); + if (!shouldDisplay) { + selection.html(''); + return; + } + } + section.renderContent(selection); + } + // may be called multiple times section.renderContent = function(containerSelection) { if (section.renderDisclosureContent) { if (!_disclosure) { - _disclosure = uiDisclosure(context, id.replace(/-/g, '_'), _expandedByDefault) + _disclosure = uiDisclosure(context, id.replace(/-/g, '_'), _expandedByDefault()) .title(_title || '') .content(section.renderDisclosureContent); } @@ -67,13 +87,13 @@ export function uiSection(id, context) { } }; + section.rerenderContent = function() { + _containerSelection + .call(renderContent); + }; + // override to enable disclosure section.renderDisclosureContent = undefined; - section.rerenderContent = function() { - _containerSelection - .call(section.renderContent); - }; - return section; } diff --git a/modules/ui/sections/validation_issues.js b/modules/ui/sections/validation_issues.js index eb722b5db..2fd0672a0 100644 --- a/modules/ui/sections/validation_issues.js +++ b/modules/ui/sections/validation_issues.js @@ -19,6 +19,9 @@ export function uiSectionValidationIssues(id, severity, context) { if (!_issues) return ''; var issueCountText = _issues.length > 1000 ? '1000+' : String(_issues.length); return t('issues.' + severity + 's.list_title', { count: issueCountText }); + }) + .shouldDisplay(function() { + return _issues && _issues.length; }); function getOptions() { @@ -33,19 +36,6 @@ export function uiSectionValidationIssues(id, severity, context) { _issues = context.validator().getIssuesBySeverity(getOptions())[severity]; } - var _parentRenderContent = section.renderContent; - - section.renderContent = function(selection) { - - var isHidden = !_issues || !_issues.length; - - selection.classed('hide', isHidden); - - if (!isHidden) { - selection.call(_parentRenderContent); - } - }; - section.renderDisclosureContent = function(selection) { var center = context.map().center(); @@ -225,12 +215,14 @@ export function uiSectionValidationIssues(id, severity, context) { context.map().on('move.uiSectionValidationIssues' + id, _debounce(function() { - if (getOptions().where === 'visible') { - // must refetch issues if they are viewport-dependent - reloadIssues(); - } - // always reload list to re-sort-by-distance - window.requestIdleCallback(section.rerenderContent); + window.requestIdleCallback(function() { + if (getOptions().where === 'visible') { + // must refetch issues if they are viewport-dependent + reloadIssues(); + } + // always reload list to re-sort-by-distance + section.rerenderContent(); + }); }, 1000) ); diff --git a/modules/ui/sections/validation_options.js b/modules/ui/sections/validation_options.js new file mode 100644 index 000000000..18158297a --- /dev/null +++ b/modules/ui/sections/validation_options.js @@ -0,0 +1,76 @@ +import { + event as d3_event +} from 'd3-selection'; + +import { t } from '../../util/locale'; +import { uiSection } from '../section'; + +export function uiSectionValidationOptions(context) { + + var section = uiSection('issues-options', context); + + section.renderContent = function(selection) { + + var container = selection.selectAll('.issues-options-container') + .data([0]); + + container = container.enter() + .append('div') + .attr('class', 'issues-options-container') + .merge(container); + + var data = [ + { key: 'what', values: ['edited', 'all'] }, + { key: 'where', values: ['visible', 'all'] } + ]; + + var options = container.selectAll('.issues-option') + .data(data, function(d) { return d.key; }); + + var optionsEnter = options.enter() + .append('div') + .attr('class', function(d) { return 'issues-option issues-option-' + d.key; }); + + optionsEnter + .append('div') + .attr('class', 'issues-option-title') + .text(function(d) { return t('issues.options.' + d.key + '.title'); }); + + var valuesEnter = optionsEnter.selectAll('label') + .data(function(d) { + return d.values.map(function(val) { return { value: val, key: d.key }; }); + }) + .enter() + .append('label'); + + valuesEnter + .append('input') + .attr('type', 'radio') + .attr('name', function(d) { return 'issues-option-' + d.key; }) + .attr('value', function(d) { return d.value; }) + .property('checked', function(d) { return getOptions()[d.key] === d.value; }) + .on('change', function(d) { updateOptionValue(d.key, d.value); }); + + valuesEnter + .append('span') + .text(function(d) { return t('issues.options.' + d.key + '.' + d.value); }); + }; + + function getOptions() { + return { + what: context.storage('validate-what') || 'edited', // 'all', 'edited' + where: context.storage('validate-where') || 'all' // 'all', 'visible' + }; + } + + function updateOptionValue(d, val) { + if (!val && d3_event && d3_event.target) { + val = d3_event.target.value; + } + + context.storage('validate-' + d, val); + context.validator().validate(); + } + + return section; +} diff --git a/modules/ui/sections/validation_status.js b/modules/ui/sections/validation_status.js new file mode 100644 index 000000000..6b90e5d66 --- /dev/null +++ b/modules/ui/sections/validation_status.js @@ -0,0 +1,174 @@ +import _debounce from 'lodash-es/debounce'; + +import { svgIcon } from '../../svg/icon'; +import { t } from '../../util/locale'; +import { uiSection } from '../section'; + +export function uiSectionValidationStatus(context) { + + var section = uiSection('issues-status', context) + .shouldDisplay(function() { + var issues = context.validator().getIssues(getOptions()); + return issues.length === 0; + }); + + function getOptions() { + return { + what: context.storage('validate-what') || 'edited', + where: context.storage('validate-where') || 'all' + }; + } + + section.renderContent = function(selection) { + + var box = selection.selectAll('.box') + .data([0]); + + var boxEnter = box.enter() + .append('div') + .attr('class', 'box'); + + boxEnter + .append('div') + .call(svgIcon('#iD-icon-apply', 'pre-text')); + + var noIssuesMessage = boxEnter + .append('span'); + + noIssuesMessage + .append('strong') + .attr('class', 'message'); + + noIssuesMessage + .append('br'); + + noIssuesMessage + .append('span') + .attr('class', 'details'); + + renderIgnoredIssuesReset(selection); + setNoIssuesText(selection); + }; + + function renderIgnoredIssuesReset(selection) { + + var ignoredIssues = context.validator() + .getIssues({ what: 'all', where: 'all', includeDisabledRules: true, includeIgnored: 'only' }); + + var resetIgnored = selection.selectAll('.reset-ignored') + .data(ignoredIssues.length ? [0] : []); + + // exit + resetIgnored.exit() + .remove(); + + // enter + var resetIgnoredEnter = resetIgnored.enter() + .append('div') + .attr('class', 'reset-ignored section-footer'); + + resetIgnoredEnter + .append('a') + .attr('href', '#'); + + // update + resetIgnored = resetIgnored + .merge(resetIgnoredEnter); + + resetIgnored.select('a') + .text(t('issues.reset_ignored', { count: ignoredIssues.length.toString() })); + + resetIgnored.on('click', function() { + context.validator().resetIgnoredIssues(); + }); + } + + function setNoIssuesText(selection) { + + var opts = getOptions(); + + function checkForHiddenIssues(cases) { + for (var type in cases) { + var hiddenOpts = cases[type]; + var hiddenIssues = context.validator().getIssues(hiddenOpts); + if (hiddenIssues.length) { + selection.select('.box .details') + .text(t( + 'issues.no_issues.hidden_issues.' + type, + { count: hiddenIssues.length.toString() } + )); + return; + } + } + selection.select('.box .details') + .text(t('issues.no_issues.hidden_issues.none')); + } + + var messageType; + + if (opts.what === 'edited' && opts.where === 'visible') { + + messageType = 'edits_in_view'; + + checkForHiddenIssues({ + elsewhere: { what: 'edited', where: 'all' }, + everything_else: { what: 'all', where: 'visible' }, + disabled_rules: { what: 'edited', where: 'visible', includeDisabledRules: 'only' }, + everything_else_elsewhere: { what: 'all', where: 'all' }, + disabled_rules_elsewhere: { what: 'edited', where: 'all', includeDisabledRules: 'only' }, + ignored_issues: { what: 'edited', where: 'visible', includeIgnored: 'only' }, + ignored_issues_elsewhere: { what: 'edited', where: 'all', includeIgnored: 'only' } + }); + + } else if (opts.what === 'edited' && opts.where === 'all') { + + messageType = 'edits'; + + checkForHiddenIssues({ + everything_else: { what: 'all', where: 'all' }, + disabled_rules: { what: 'edited', where: 'all', includeDisabledRules: 'only' }, + ignored_issues: { what: 'edited', where: 'all', includeIgnored: 'only' } + }); + + } else if (opts.what === 'all' && opts.where === 'visible') { + + messageType = 'everything_in_view'; + + checkForHiddenIssues({ + elsewhere: { what: 'all', where: 'all' }, + disabled_rules: { what: 'all', where: 'visible', includeDisabledRules: 'only' }, + disabled_rules_elsewhere: { what: 'all', where: 'all', includeDisabledRules: 'only' }, + ignored_issues: { what: 'all', where: 'visible', includeIgnored: 'only' }, + ignored_issues_elsewhere: { what: 'all', where: 'all', includeIgnored: 'only' } + }); + } else if (opts.what === 'all' && opts.where === 'all') { + + messageType = 'everything'; + + checkForHiddenIssues({ + disabled_rules: { what: 'all', where: 'all', includeDisabledRules: 'only' }, + ignored_issues: { what: 'all', where: 'all', includeIgnored: 'only' } + }); + } + + if (opts.what === 'edited' && context.history().difference().summary().length === 0) { + messageType = 'no_edits'; + } + + selection.select('.box .message') + .text(t('issues.no_issues.message.' + messageType)); + + } + + context.validator().on('validated.uiSectionValidationStatus', function() { + window.requestIdleCallback(section.rerenderContent); + }); + + context.map().on('move.uiSectionValidationStatus', + _debounce(function() { + window.requestIdleCallback(section.rerenderContent); + }, 1000) + ); + + return section; +} From 1f4fe57d8bf1c839c7ee7af9d61a8f71b65534b2 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Thu, 20 Feb 2020 13:47:11 -0800 Subject: [PATCH 027/127] Use d3-style API for section contents --- modules/ui/panes/background.js | 16 +++---- modules/ui/section.js | 48 ++++++++++++------- .../ui/sections/background_display_options.js | 15 +++--- modules/ui/sections/background_list.js | 11 +++-- modules/ui/sections/background_offset.js | 7 +-- modules/ui/sections/data_layers.js | 11 +++-- modules/ui/sections/map_features.js | 7 +-- modules/ui/sections/map_style_options.js | 7 +-- modules/ui/sections/overlay_list.js | 11 +++-- modules/ui/sections/photo_overlays.js | 9 ++-- modules/ui/sections/privacy.js | 7 +-- modules/ui/sections/validation_issues.js | 9 ++-- modules/ui/sections/validation_options.js | 7 +-- modules/ui/sections/validation_rules.js | 5 +- modules/ui/sections/validation_status.js | 9 ++-- 15 files changed, 101 insertions(+), 78 deletions(-) diff --git a/modules/ui/panes/background.js b/modules/ui/panes/background.js index ddc894c86..5a96dfd4c 100644 --- a/modules/ui/panes/background.js +++ b/modules/ui/panes/background.js @@ -4,10 +4,10 @@ import { t } from '../../util/locale'; import { uiCmd } from '../cmd'; import { uiPane } from '../pane'; -import { uiBackgroundDisplayOptions } from '../sections/background_display_options'; -import { uiBackgroundList } from '../sections/background_list'; -import { uiBackgroundOffset } from '../sections/background_offset'; -import { uiOverlayList } from '../sections/overlay_list'; +import { uiSectionBackgroundDisplayOptions } from '../sections/background_display_options'; +import { uiSectionBackgroundList } from '../sections/background_list'; +import { uiSectionBackgroundOffset } from '../sections/background_offset'; +import { uiSectionOverlayList } from '../sections/overlay_list'; export function uiPaneBackground(context) { @@ -34,10 +34,10 @@ export function uiPaneBackground(context) { .description(t('background.description')) .iconName('iD-icon-layers') .sections([ - uiBackgroundList(context), - uiOverlayList(context), - uiBackgroundDisplayOptions(context), - uiBackgroundOffset(context) + uiSectionBackgroundList(context), + uiSectionOverlayList(context), + uiSectionBackgroundDisplayOptions(context), + uiSectionBackgroundOffset(context) ]); return backgroundPane; diff --git a/modules/ui/section.js b/modules/ui/section.js index c629fe00b..9eb752dc3 100644 --- a/modules/ui/section.js +++ b/modules/ui/section.js @@ -13,6 +13,8 @@ export function uiSection(id, context) { var _title; var _expandedByDefault = utilFunctor(true); var _shouldDisplay; + var _content; + var _disclosureContent; var _containerSelection = d3_select(null); @@ -38,6 +40,18 @@ export function uiSection(id, context) { return section; }; + section.content = function(val) { + if (!arguments.length) return _content; + _content = val; + return section; + }; + + section.disclosureContent = function(val) { + if (!arguments.length) return _disclosureContent; + _disclosureContent = val; + return section; + }; + // may be called multiple times section.render = function(selection) { @@ -57,10 +71,16 @@ export function uiSection(id, context) { .call(renderContent); }; - section.containerSelection = function() { + section.reRender = function() { + _containerSelection + .call(renderContent); + }; + + section.selection = function() { return _containerSelection; }; + // may be called multiple times function renderContent(selection) { if (_shouldDisplay) { var shouldDisplay = _shouldDisplay(); @@ -70,30 +90,24 @@ export function uiSection(id, context) { return; } } - section.renderContent(selection); - } - // may be called multiple times - section.renderContent = function(containerSelection) { - - if (section.renderDisclosureContent) { + if (_disclosureContent) { if (!_disclosure) { _disclosure = uiDisclosure(context, id.replace(/-/g, '_'), _expandedByDefault()) .title(_title || '') - .content(section.renderDisclosureContent); + .content(_disclosureContent); } - containerSelection + selection .call(_disclosure); + + return; } - }; - section.rerenderContent = function() { - _containerSelection - .call(renderContent); - }; - - // override to enable disclosure - section.renderDisclosureContent = undefined; + if (_content) { + selection + .call(_content); + } + } return section; } diff --git a/modules/ui/sections/background_display_options.js b/modules/ui/sections/background_display_options.js index 7113caedc..145b41616 100644 --- a/modules/ui/sections/background_display_options.js +++ b/modules/ui/sections/background_display_options.js @@ -9,10 +9,11 @@ import { uiSection } from '../section'; import { utilDetect } from '../../util/detect'; -export function uiBackgroundDisplayOptions(context) { +export function uiSectionBackgroundDisplayOptions(context) { var section = uiSection('background-display-options', context) - .title(t('background.display_options')); + .title(t('background.display_options')) + .disclosureContent(renderDisclosureContent); var _detected = utilDetect(); var _storedOpacity = context.storage('background-opacity'); @@ -30,12 +31,10 @@ export function uiBackgroundDisplayOptions(context) { sharpness: 1 }; - function clamp(x, min, max) { return Math.max(min, Math.min(x, max)); } - function updateValue(d, val) { if (!val && d3_event && d3_event.target) { val = d3_event.target.value; @@ -50,11 +49,10 @@ export function uiBackgroundDisplayOptions(context) { context.storage('background-opacity', val); } - section.rerenderContent(); + section.reRender(); } - - section.renderDisclosureContent = function(selection) { + function renderDisclosureContent(selection) { var container = selection.selectAll('.display-options-container') .data([0]); @@ -126,8 +124,7 @@ export function uiBackgroundDisplayOptions(context) { if (containerEnter.size() && _options.brightness !== 1) { context.background().brightness(_options.brightness); } - }; - + } return section; } diff --git a/modules/ui/sections/background_list.js b/modules/ui/sections/background_list.js index 20c9ed340..1d8a946ec 100644 --- a/modules/ui/sections/background_list.js +++ b/modules/ui/sections/background_list.js @@ -14,7 +14,7 @@ import { uiMapInMap } from '../map_in_map'; import { uiSection } from '../section'; import { uiTooltipHtml } from '../tooltipHtml'; -export function uiBackgroundList(context) { +export function uiSectionBackgroundList(context) { var _backgroundList = d3_select(null); @@ -24,13 +24,14 @@ export function uiBackgroundList(context) { .on('change', customChanged); var section = uiSection('background-list', context) - .title(t('background.backgrounds')); + .title(t('background.backgrounds')) + .disclosureContent(renderDisclosureContent); function previousBackgroundID() { return context.storage('background-last-used-toggle'); } - section.renderDisclosureContent = function(selection) { + function renderDisclosureContent(selection) { // the background list var container = selection.selectAll('.layer-background-list') @@ -111,7 +112,7 @@ export function uiBackgroundList(context) { _backgroundList .call(drawListItems, 'radio', chooseBackground, function(d) { return !d.isHidden() && !d.overlay; }); - }; + } function setTooltips(selection) { selection.each(function(d, i, nodes) { @@ -260,7 +261,7 @@ export function uiBackgroundList(context) { .on('move.background_list', _debounce(function() { // layers in-view may have changed due to map move - window.requestIdleCallback(section.rerenderContent); + window.requestIdleCallback(section.reRender); }, 1000) ); diff --git a/modules/ui/sections/background_offset.js b/modules/ui/sections/background_offset.js index 4bbcac373..53adba172 100644 --- a/modules/ui/sections/background_offset.js +++ b/modules/ui/sections/background_offset.js @@ -10,10 +10,11 @@ import { svgIcon } from '../../svg/icon'; import { uiSection } from '../section'; -export function uiBackgroundOffset(context) { +export function uiSectionBackgroundOffset(context) { var section = uiSection('background-offset', context) .title(t('background.fix_misalignment')) + .disclosureContent(renderDisclosureContent) .expandedByDefault(false); var _directions = [ @@ -134,7 +135,7 @@ export function uiBackgroundOffset(context) { } - section.renderDisclosureContent = function(selection) { + function renderDisclosureContent(selection) { var container = selection.selectAll('.nudge-container') .data([0]); @@ -182,7 +183,7 @@ export function uiBackgroundOffset(context) { .call(svgIcon('#iD-icon-' + (textDirection === 'rtl' ? 'redo' : 'undo'))); updateValue(); - }; + } context.background() .on('change.backgroundOffset-update', updateValue); diff --git a/modules/ui/sections/data_layers.js b/modules/ui/sections/data_layers.js index db603c4e1..d31a609e0 100644 --- a/modules/ui/sections/data_layers.js +++ b/modules/ui/sections/data_layers.js @@ -22,9 +22,10 @@ export function uiSectionDataLayers(context) { var layers = context.layers(); var section = uiSection('data-layers', context) - .title(t('map_data.data_layers')); + .title(t('map_data.data_layers')) + .disclosureContent(renderDisclosureContent); - section.renderDisclosureContent = function(selection) { + function renderDisclosureContent(selection) { var container = selection.selectAll('.data-layer-container') .data([0]); @@ -36,7 +37,7 @@ export function uiSectionDataLayers(context) { .call(drawQAItems) .call(drawCustomDataItems) .call(drawVectorItems); // Beta - Detroit mapping challenge - }; + } function showsLayer(which) { var layer = layers.layer(which); @@ -377,13 +378,13 @@ export function uiSectionDataLayers(context) { } } - context.layers().on('change.uiSectionDataLayers', section.rerenderContent); + context.layers().on('change.uiSectionDataLayers', section.reRender); context.map() .on('move.uiSectionDataLayers', _debounce(function() { // Detroit layers may have moved in or out of view - window.requestIdleCallback(section.rerenderContent); + window.requestIdleCallback(section.reRender); }, 1000) ); diff --git a/modules/ui/sections/map_features.js b/modules/ui/sections/map_features.js index 54e2c27f0..8fd9a0e27 100644 --- a/modules/ui/sections/map_features.js +++ b/modules/ui/sections/map_features.js @@ -9,9 +9,10 @@ export function uiSectionMapFeatures(context) { var section = uiSection('map-features', context) .title(t('map_data.map_features')) + .disclosureContent(renderDisclosureContent) .expandedByDefault(false); - section.renderDisclosureContent = function(selection) { + function renderDisclosureContent(selection) { var container = selection.selectAll('.layer-feature-list-container') .data([0]); @@ -52,7 +53,7 @@ export function uiSectionMapFeatures(context) { container.selectAll('.layer-feature-list') .call(drawListItems, _features, 'checkbox', 'feature', clickFeature, showsFeature); - }; + } function drawListItems(selection, data, type, name, change, active) { var items = selection.selectAll('li') @@ -121,7 +122,7 @@ export function uiSectionMapFeatures(context) { // add listeners context.features() - .on('change.map_features', section.rerenderContent); + .on('change.map_features', section.reRender); return section; } diff --git a/modules/ui/sections/map_style_options.js b/modules/ui/sections/map_style_options.js index 5dadb75f1..2649f55fb 100644 --- a/modules/ui/sections/map_style_options.js +++ b/modules/ui/sections/map_style_options.js @@ -11,9 +11,10 @@ export function uiSectionMapStyleOptions(context) { var section = uiSection('fill-area', context) .title(t('map_data.style_options')) + .disclosureContent(renderDisclosureContent) .expandedByDefault(false); - section.renderDisclosureContent = function(selection) { + function renderDisclosureContent(selection) { var container = selection.selectAll('.layer-fill-list') .data([0]); @@ -33,7 +34,7 @@ export function uiSectionMapStyleOptions(context) { .call(drawListItems, ['highlight_edits'], 'checkbox', 'visual_diff', toggleHighlightEdited, function() { return context.surface().classed('highlight-edited'); }); - }; + } function drawListItems(selection, data, type, name, change, active) { var items = selection.selectAll('li') @@ -95,7 +96,7 @@ export function uiSectionMapStyleOptions(context) { } context.map() - .on('changeHighlighting.ui_style, changeAreaFill.ui_style', section.rerenderContent); + .on('changeHighlighting.ui_style, changeAreaFill.ui_style', section.reRender); return section; } diff --git a/modules/ui/sections/overlay_list.js b/modules/ui/sections/overlay_list.js index f8797dce0..2fe0c10bd 100644 --- a/modules/ui/sections/overlay_list.js +++ b/modules/ui/sections/overlay_list.js @@ -9,10 +9,11 @@ import { t } from '../../util/locale'; import { tooltip } from '../../util/tooltip'; import { uiSection } from '../section'; -export function uiOverlayList(context) { +export function uiSectionOverlayList(context) { var section = uiSection('overlay-list', context) - .title(t('background.overlays')); + .title(t('background.overlays')) + .disclosureContent(renderDisclosureContent); var _overlayList = d3_select(null); @@ -97,7 +98,7 @@ export function uiOverlayList(context) { } } - section.renderDisclosureContent = function(selection) { + function renderDisclosureContent(selection) { var container = selection.selectAll('.layer-overlay-list') .data([0]); @@ -110,13 +111,13 @@ export function uiOverlayList(context) { _overlayList .call(drawListItems, 'checkbox', chooseOverlay, function(d) { return !d.isHidden() && d.overlay; }); - }; + } context.map() .on('move.overlay_list', _debounce(function() { // layers in-view may have changed due to map move - window.requestIdleCallback(section.rerenderContent); + window.requestIdleCallback(section.reRender); }, 1000) ); diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index e0545ade1..2c3415bb1 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -13,9 +13,10 @@ export function uiSectionPhotoOverlays(context) { var section = uiSection('photo-overlays', context) .title(t('photo_overlays.title')) + .disclosureContent(renderDisclosureContent) .expandedByDefault(false); - section.renderDisclosureContent = function(selection) { + function renderDisclosureContent(selection) { var container = selection.selectAll('.photo-overlay-container') .data([0]); @@ -25,7 +26,7 @@ export function uiSectionPhotoOverlays(context) { .merge(container) .call(drawPhotoItems) .call(drawPhotoTypeItems); - }; + } function drawPhotoItems(selection) { var photoKeys = context.photos().overlayLayerIDs(); @@ -194,8 +195,8 @@ export function uiSectionPhotoOverlays(context) { } } - context.layers().on('change.uiSectionPhotoOverlays', section.rerenderContent); - context.photos().on('change.uiSectionPhotoOverlays', section.rerenderContent); + context.layers().on('change.uiSectionPhotoOverlays', section.reRender); + context.photos().on('change.uiSectionPhotoOverlays', section.reRender); return section; } diff --git a/modules/ui/sections/privacy.js b/modules/ui/sections/privacy.js index ffb168f50..3b33b693c 100644 --- a/modules/ui/sections/privacy.js +++ b/modules/ui/sections/privacy.js @@ -10,11 +10,12 @@ import { uiSection } from '../section'; export function uiSectionPrivacy(context) { let section = uiSection('preferences-third-party', context) - .title(t('preferences.privacy.title')); + .title(t('preferences.privacy.title')) + .disclosureContent(renderDisclosureContent); let _showThirdPartyIcons = context.storage('preferences.privacy.thirdpartyicons') || 'true'; - section.renderDisclosureContent = function(selection) { + function renderDisclosureContent(selection) { // enter let privacyOptionsListEnter = selection.selectAll('.privacy-options-list') .data([0]) @@ -68,7 +69,7 @@ export function uiSectionPrivacy(context) { .select('input') .property('checked', (_showThirdPartyIcons === 'true')); } - }; + } return section; } diff --git a/modules/ui/sections/validation_issues.js b/modules/ui/sections/validation_issues.js index 2fd0672a0..7824060bb 100644 --- a/modules/ui/sections/validation_issues.js +++ b/modules/ui/sections/validation_issues.js @@ -20,6 +20,7 @@ export function uiSectionValidationIssues(id, severity, context) { var issueCountText = _issues.length > 1000 ? '1000+' : String(_issues.length); return t('issues.' + severity + 's.list_title', { count: issueCountText }); }) + .disclosureContent(renderDisclosureContent) .shouldDisplay(function() { return _issues && _issues.length; }); @@ -36,7 +37,7 @@ export function uiSectionValidationIssues(id, severity, context) { _issues = context.validator().getIssuesBySeverity(getOptions())[severity]; } - section.renderDisclosureContent = function(selection) { + function renderDisclosureContent(selection) { var center = context.map().center(); var graph = context.graph(); @@ -58,7 +59,7 @@ export function uiSectionValidationIssues(id, severity, context) { selection .call(drawIssuesList, issues); - }; + } function drawIssuesList(selection, issues) { var list = selection.selectAll('.issues-list') @@ -209,7 +210,7 @@ export function uiSectionValidationIssues(id, severity, context) { context.validator().on('validated.uiSectionValidationIssues' + id, function() { window.requestIdleCallback(function() { reloadIssues(); - section.rerenderContent(); + section.reRender(); }); }); @@ -221,7 +222,7 @@ export function uiSectionValidationIssues(id, severity, context) { reloadIssues(); } // always reload list to re-sort-by-distance - section.rerenderContent(); + section.reRender(); }); }, 1000) ); diff --git a/modules/ui/sections/validation_options.js b/modules/ui/sections/validation_options.js index 18158297a..ed4197874 100644 --- a/modules/ui/sections/validation_options.js +++ b/modules/ui/sections/validation_options.js @@ -7,9 +7,10 @@ import { uiSection } from '../section'; export function uiSectionValidationOptions(context) { - var section = uiSection('issues-options', context); + var section = uiSection('issues-options', context) + .content(renderContent); - section.renderContent = function(selection) { + function renderContent(selection) { var container = selection.selectAll('.issues-options-container') .data([0]); @@ -54,7 +55,7 @@ export function uiSectionValidationOptions(context) { valuesEnter .append('span') .text(function(d) { return t('issues.options.' + d.key + '.' + d.value); }); - }; + } function getOptions() { return { diff --git a/modules/ui/sections/validation_rules.js b/modules/ui/sections/validation_rules.js index 28601222a..6004bf13d 100644 --- a/modules/ui/sections/validation_rules.js +++ b/modules/ui/sections/validation_rules.js @@ -15,9 +15,10 @@ export function uiSectionValidationRules(context) { var DEFAULTSQUARE = 5; // see also unsquare_way.js var section = uiSection('issues-rules', context) + .disclosureContent(renderDisclosureContent) .title(t('issues.rules.title')); - section.renderDisclosureContent = function(selection) { + function renderDisclosureContent(selection) { var container = selection.selectAll('.issues-rulelist-container') .data([0]); @@ -61,7 +62,7 @@ export function uiSectionValidationRules(context) { container.selectAll('.issue-rules-list') .call(drawListItems, ruleKeys, 'checkbox', 'rule', toggleRule, isRuleEnabled); - }; + } function drawListItems(selection, data, type, name, change, active) { var items = selection.selectAll('li') diff --git a/modules/ui/sections/validation_status.js b/modules/ui/sections/validation_status.js index 6b90e5d66..ae3bbd151 100644 --- a/modules/ui/sections/validation_status.js +++ b/modules/ui/sections/validation_status.js @@ -7,6 +7,7 @@ import { uiSection } from '../section'; export function uiSectionValidationStatus(context) { var section = uiSection('issues-status', context) + .content(renderContent) .shouldDisplay(function() { var issues = context.validator().getIssues(getOptions()); return issues.length === 0; @@ -19,7 +20,7 @@ export function uiSectionValidationStatus(context) { }; } - section.renderContent = function(selection) { + function renderContent(selection) { var box = selection.selectAll('.box') .data([0]); @@ -48,7 +49,7 @@ export function uiSectionValidationStatus(context) { renderIgnoredIssuesReset(selection); setNoIssuesText(selection); - }; + } function renderIgnoredIssuesReset(selection) { @@ -161,12 +162,12 @@ export function uiSectionValidationStatus(context) { } context.validator().on('validated.uiSectionValidationStatus', function() { - window.requestIdleCallback(section.rerenderContent); + window.requestIdleCallback(section.reRender); }); context.map().on('move.uiSectionValidationStatus', _debounce(function() { - window.requestIdleCallback(section.rerenderContent); + window.requestIdleCallback(section.reRender); }, 1000) ); From dc7fba4bf8eb1dcad4bb16313a10d64f779bfdb0 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 20 Feb 2020 17:09:54 -0500 Subject: [PATCH 028/127] Have utilStringQs advance past any leading '?' or '#' characters This lets us remove a bunch of substring(1) and +1 from the code. --- modules/behavior/hash.js | 4 ++-- modules/renderer/background.js | 32 +++++++++++++++--------------- modules/renderer/features.js | 14 ++++++------- modules/renderer/photos.js | 14 ++++++------- modules/util/util.js | 4 ++++ test/spec/services/nominatim.js | 2 +- test/spec/services/osm_wikibase.js | 2 +- test/spec/services/taginfo.js | 2 +- test/spec/spec_helpers.js | 2 ++ test/spec/util/util.js | 23 ++++++++++++++++++--- 10 files changed, 61 insertions(+), 38 deletions(-) diff --git a/modules/behavior/hash.js b/modules/behavior/hash.js index 853b8f42c..6b3937654 100644 --- a/modules/behavior/hash.js +++ b/modules/behavior/hash.js @@ -39,7 +39,7 @@ export function behaviorHash(context) { var center = map.center(); var zoom = map.zoom(); var precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)); - var q = utilObjectOmit(utilStringQs(window.location.hash.substring(1)), + var q = utilObjectOmit(utilStringQs(window.location.hash), ['comment', 'source', 'hashtags', 'walkthrough'] ); var newParams = {}; @@ -91,7 +91,7 @@ export function behaviorHash(context) { .on('hashchange.hash', hashchange); if (window.location.hash) { - var q = utilStringQs(window.location.hash.substring(1)); + var q = utilStringQs(window.location.hash); if (q.id) { context.zoomToEntity(q.id.split(',')[0], !q.map); diff --git a/modules/renderer/background.js b/modules/renderer/background.js index e9e5ecdfc..9fd07463a 100644 --- a/modules/renderer/background.js +++ b/modules/renderer/background.js @@ -203,7 +203,7 @@ export function rendererBackground(context) { const EPSILON = 0.01; const x = +meters[0].toFixed(2); const y = +meters[1].toFixed(2); - let q = utilStringQs(window.location.hash.substring(1)); + let hash = utilStringQs(window.location.hash); let id = currSource.id; if (id === 'custom') { @@ -211,25 +211,25 @@ export function rendererBackground(context) { } if (id) { - q.background = id; + hash.background = id; } else { - delete q.background; + delete hash.background; } if (o) { - q.overlays = o; + hash.overlays = o; } else { - delete q.overlays; + delete hash.overlays; } if (Math.abs(x) > EPSILON || Math.abs(y) > EPSILON) { - q.offset = `${x},${y}`; + hash.offset = `${x},${y}`; } else { - delete q.offset; + delete hash.offset; } if (!window.mocha) { - window.location.replace('#' + utilQsString(q, true)); + window.location.replace('#' + utilQsString(hash, true)); } let imageryUsed = []; @@ -444,9 +444,9 @@ export function rendererBackground(context) { return geoExtent([params[2], params[1]]); // lon,lat } - const q = utilStringQs(window.location.hash.substring(1)); - const requested = q.background || q.layer; - let extent = parseMapParams(q.map); + const hash = utilStringQs(window.location.hash); + const requested = hash.background || hash.layer; + let extent = parseMapParams(hash.map); ensureImageryIndex() .then(imageryIndex => { @@ -479,7 +479,7 @@ export function rendererBackground(context) { background.toggleOverlayLayer(locator); } - const overlays = (q.overlays || '').split(','); + const overlays = (hash.overlays || '').split(','); overlays.forEach(overlay => { overlay = background.findSource(overlay); if (overlay) { @@ -487,15 +487,15 @@ export function rendererBackground(context) { } }); - if (q.gpx) { // todo: move elsewhere - this doesn't belong in background + if (hash.gpx) { // todo: move elsewhere - this doesn't belong in background const gpx = context.layers().layer('data'); if (gpx) { - gpx.url(q.gpx, '.gpx'); + gpx.url(hash.gpx, '.gpx'); } } - if (q.offset) { - const offset = q.offset + if (hash.offset) { + const offset = hash.offset .replace(/;/g, ',') .split(',') .map(n => !isNaN(n) && n); diff --git a/modules/renderer/features.js b/modules/renderer/features.js index fba1b2268..e908df604 100644 --- a/modules/renderer/features.js +++ b/modules/renderer/features.js @@ -64,14 +64,14 @@ export function rendererFeatures(context) { function update() { if (!window.mocha) { - var q = utilStringQs(window.location.hash.substring(1)); + var hash = utilStringQs(window.location.hash); var disabled = features.disabled(); if (disabled.length) { - q.disable_features = disabled.join(','); + hash.disable_features = disabled.join(','); } else { - delete q.disable_features; + delete hash.disable_features; } - window.location.replace('#' + utilQsString(q, true)); + window.location.replace('#' + utilQsString(hash, true)); context.storage('disabled-features', disabled.join(',')); } _hidden = features.hidden(); @@ -579,9 +579,9 @@ export function rendererFeatures(context) { storageDisabled.forEach(features.disable); } - var q = utilStringQs(window.location.hash.substring(1)); - if (q.disable_features) { - var hashDisabled = q.disable_features.replace(/;/g, ',').split(','); + var hash = utilStringQs(window.location.hash); + if (hash.disable_features) { + var hashDisabled = hash.disable_features.replace(/;/g, ',').split(','); hashDisabled.forEach(features.disable); } }; diff --git a/modules/renderer/photos.js b/modules/renderer/photos.js index f1751d03b..62eb89ce9 100644 --- a/modules/renderer/photos.js +++ b/modules/renderer/photos.js @@ -15,18 +15,18 @@ export function rendererPhotos(context) { function updateStorage() { if (window.mocha) return; - var q = utilStringQs(window.location.hash.substring(1)); + var hash = utilStringQs(window.location.hash); var enabled = context.layers().all().filter(function(d) { return _layerIDs.indexOf(d.id) !== -1 && d.layer && d.layer.supported() && d.layer.enabled(); }).map(function(d) { return d.id; }); if (enabled.length) { - q.photo_overlay = enabled.join(','); + hash.photo_overlay = enabled.join(','); } else { - delete q.photo_overlay; + delete hash.photo_overlay; } - window.location.replace('#' + utilQsString(q, true)); + window.location.replace('#' + utilQsString(hash, true)); } photos.overlayLayerIDs = function() { @@ -73,9 +73,9 @@ export function rendererPhotos(context) { }; photos.init = function() { - var q = utilStringQs(window.location.hash.substring(1)); - if (q.photo_overlay) { - var hashOverlayIDs = q.photo_overlay.replace(/;/g, ',').split(','); + var hash = utilStringQs(window.location.hash); + if (hash.photo_overlay) { + var hashOverlayIDs = hash.photo_overlay.replace(/;/g, ',').split(','); hashOverlayIDs.forEach(function(id) { var layer = context.layers().layer(id); if (layer) layer.enabled(true); diff --git a/modules/util/util.js b/modules/util/util.js index a7ea8675e..84204680d 100644 --- a/modules/util/util.js +++ b/modules/util/util.js @@ -318,6 +318,10 @@ export function utilCombinedTags(entityIDs, graph) { export function utilStringQs(str) { + var i = 0; // advance past any leading '?' or '#' characters + while (i < str.length && (str[i] === '?' || str[i] === '#')) i++; + str = str.slice(i); + return str.split('&').reduce(function(obj, pair){ var parts = pair.split('='); if (parts.length === 2) { diff --git a/test/spec/services/nominatim.js b/test/spec/services/nominatim.js index 8773018a9..f28982cd0 100644 --- a/test/spec/services/nominatim.js +++ b/test/spec/services/nominatim.js @@ -21,7 +21,7 @@ describe('iD.serviceNominatim', function() { }); function query(url) { - return iD.utilStringQs(url.substring(url.indexOf('?') + 1)); + return iD.utilStringQs(url.substring(url.indexOf('?'))); } diff --git a/test/spec/services/osm_wikibase.js b/test/spec/services/osm_wikibase.js index ae05015d3..58689647f 100644 --- a/test/spec/services/osm_wikibase.js +++ b/test/spec/services/osm_wikibase.js @@ -21,7 +21,7 @@ describe('iD.serviceOsmWikibase', function () { function query(url) { - return iD.utilStringQs(url.substring(url.indexOf('?') + 1)); + return iD.utilStringQs(url.substring(url.indexOf('?'))); } function adjust(params, data) { diff --git a/test/spec/services/taginfo.js b/test/spec/services/taginfo.js index 0f6843e88..80d46cd7f 100644 --- a/test/spec/services/taginfo.js +++ b/test/spec/services/taginfo.js @@ -31,7 +31,7 @@ describe('iD.serviceTaginfo', function() { }); function query(url) { - return iD.utilStringQs(url.substring(url.indexOf('?') + 1)); + return iD.utilStringQs(url.substring(url.indexOf('?'))); } diff --git a/test/spec/spec_helpers.js b/test/spec/spec_helpers.js index 490275e86..0dfeebe5e 100644 --- a/test/spec/spec_helpers.js +++ b/test/spec/spec_helpers.js @@ -7,6 +7,8 @@ for (var k in iD.services) { delete iD.services[k]; } // Run without data for speed (tests which need data can set it up themselves) +// Initializing `coreContext` will try loading the English locale strings: +iD.data.locale_en = { en: {} }; // Initializing `coreContext` initializes `_background`, which tries loading: iD.data.imagery = []; // Initializing `coreContext` initializes `_presets`, which tries loading: diff --git a/test/spec/util/util.js b/test/spec/util/util.js index 63b999955..b71736ac8 100644 --- a/test/spec/util/util.js +++ b/test/spec/util/util.js @@ -79,9 +79,26 @@ describe('iD.util', function() { }); it('utilStringQs', function() { - expect(iD.utilStringQs('foo=bar')).to.eql({foo: 'bar'}); - expect(iD.utilStringQs('foo=bar&one=2')).to.eql({foo: 'bar', one: '2' }); - expect(iD.utilStringQs('')).to.eql({}); + it('splits a parameter string into k=v pairs', function() { + expect(iD.utilStringQs('foo=bar')).to.eql({foo: 'bar'}); + expect(iD.utilStringQs('foo=bar&one=2')).to.eql({foo: 'bar', one: '2' }); + expect(iD.utilStringQs('')).to.eql({}); + }); + it('trims leading # if present', function() { + expect(iD.utilStringQs('#foo=bar')).to.eql({foo: 'bar'}); + expect(iD.utilStringQs('#foo=bar&one=2')).to.eql({foo: 'bar', one: '2' }); + expect(iD.utilStringQs('#')).to.eql({}); + }); + it('trims leading ? if present', function() { + expect(iD.utilStringQs('?foo=bar')).to.eql({foo: 'bar'}); + expect(iD.utilStringQs('?foo=bar&one=2')).to.eql({foo: 'bar', one: '2' }); + expect(iD.utilStringQs('?')).to.eql({}); + }); + it('trims leading #? if present', function() { + expect(iD.utilStringQs('#?foo=bar')).to.eql({foo: 'bar'}); + expect(iD.utilStringQs('#?foo=bar&one=2')).to.eql({foo: 'bar', one: '2' }); + expect(iD.utilStringQs('#?')).to.eql({}); + }); }); it('utilQsString', function() { From 61c86a352731dc87f402b13ce5899f658ab823b7 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 20 Feb 2020 17:11:47 -0500 Subject: [PATCH 029/127] Don't rely on the `dataEn` export for checking if a string exists Instead use the special `default` key for supplying a fallback string --- modules/ui/improveOSM_details.js | 23 ++++++----------------- modules/ui/improveOSM_header.js | 25 +++++++++---------------- modules/ui/keepRight_details.js | 21 ++++++++------------- modules/ui/keepRight_header.js | 25 +++++++++++-------------- 4 files changed, 34 insertions(+), 60 deletions(-) diff --git a/modules/ui/improveOSM_details.js b/modules/ui/improveOSM_details.js index 9e5b43080..64dae3faf 100644 --- a/modules/ui/improveOSM_details.js +++ b/modules/ui/improveOSM_details.js @@ -3,7 +3,6 @@ import { select as d3_select } from 'd3-selection'; -import { dataEn } from '../../data'; import { modeSelect } from '../modes/select'; import { t } from '../util/locale'; import { utilDisplayName, utilHighlightEntities, utilEntityRoot } from '../util'; @@ -11,26 +10,16 @@ import { utilDisplayName, utilHighlightEntities, utilEntityRoot } from '../util' export function uiImproveOsmDetails(context) { let _qaItem; + function issueDetail(d) { - const unknown = t('inspector.unknown'); - - if (!d) return unknown; - if (d.desc) return d.desc; - - const itemType = d.issueKey; - const et = dataEn.QA.improveOSM.error_types[itemType]; - - let detail; - if (et && et.description) { - detail = t(`QA.improveOSM.error_types.${itemType}.description`, d.replacements); - } else { - detail = unknown; - } - - return detail; + const issueKey = d.issueKey; + d.replacements = d.replacements || {}; + d.replacements.default = t('inspector.unknown'); // special key `default` works as a fallback string + return t(`QA.improveOSM.error_types.${issueKey}.description`, d.replacements); } + function improveOsmDetails(selection) { const details = selection.selectAll('.error-details') .data( diff --git a/modules/ui/improveOSM_header.js b/modules/ui/improveOSM_header.js index a38aae4ef..259a34c57 100644 --- a/modules/ui/improveOSM_header.js +++ b/modules/ui/improveOSM_header.js @@ -1,24 +1,18 @@ -import { dataEn } from '../../data'; import { t } from '../util/locale'; export function uiImproveOsmHeader() { let _qaItem; + function issueTitle(d) { - const unknown = t('inspector.unknown'); - - if (!d) return unknown; - const { issueKey } = d; - const et = dataEn.QA.improveOSM.error_types[issueKey]; - - if (et && et.title) { - return t(`QA.improveOSM.error_types.${issueKey}.title`); - } else { - return unknown; - } + const issueKey = d.issueKey; + d.replacements = d.replacements || {}; + d.replacements.default = t('inspector.unknown'); // special key `default` works as a fallback string + return t(`QA.improveOSM.error_types.${issueKey}.title`, d.replacements); } + function improveOsmHeader(selection) { const header = selection.selectAll('.qa-header') .data( @@ -57,12 +51,11 @@ export function uiImproveOsmHeader() { .attr('transform', 'translate(3.5, 5)') .attr('xlink:href', d => { const picon = d.icon; - if (!picon) { - return ''; + return ''; } else { - const isMaki = /^maki-/.test(picon); - return `#${picon}${isMaki ? '-11' : ''}`; + const isMaki = /^maki-/.test(picon); + return `#${picon}${isMaki ? '-11' : ''}`; } }); diff --git a/modules/ui/keepRight_details.js b/modules/ui/keepRight_details.js index 007c6fa42..e90c9d8cb 100644 --- a/modules/ui/keepRight_details.js +++ b/modules/ui/keepRight_details.js @@ -3,34 +3,29 @@ import { select as d3_select } from 'd3-selection'; -import { dataEn } from '../../data'; import { modeSelect } from '../modes/select'; import { t } from '../util/locale'; import { utilDisplayName, utilHighlightEntities, utilEntityRoot } from '../util'; + export function uiKeepRightDetails(context) { let _qaItem; + function issueDetail(d) { + const { itemType, parentIssueType } = d; const unknown = t('inspector.unknown'); - if (!d) return unknown; + let replacements = d.replacements || {}; + replacements.default = unknown; // special key `default` works as a fallback string - const { itemType, parentIssueType, replacements } = d; - const et = dataEn.QA.keepRight.errorTypes[itemType]; - const pt = dataEn.QA.keepRight.errorTypes[parentIssueType]; - - let detail; - if (et && et.description) { - detail = t(`QA.keepRight.errorTypes.${itemType}.description`, replacements); - } else if (pt && pt.description) { + let detail = t(`QA.keepRight.errorTypes.${itemType}.description`, replacements); + if (detail === unknown) { detail = t(`QA.keepRight.errorTypes.${parentIssueType}.description`, replacements); - } else { - detail = unknown; } - return detail; } + function keepRightDetails(selection) { const details = selection.selectAll('.error-details') .data( diff --git a/modules/ui/keepRight_header.js b/modules/ui/keepRight_header.js index 9fdce6399..c29f2d175 100644 --- a/modules/ui/keepRight_header.js +++ b/modules/ui/keepRight_header.js @@ -1,4 +1,3 @@ -import { dataEn } from '../../data'; import { svgIcon } from '../svg/icon'; import { t } from '../util/locale'; @@ -6,24 +5,21 @@ import { t } from '../util/locale'; export function uiKeepRightHeader() { let _qaItem; + function issueTitle(d) { - const unknown = t('inspector.unknown'); - - if (!d) return unknown; const { itemType, parentIssueType } = d; + const unknown = t('inspector.unknown'); + let replacements = d.replacements || {}; + replacements.default = unknown; // special key `default` works as a fallback string - const et = dataEn.QA.keepRight.errorTypes[itemType]; - const pt = dataEn.QA.keepRight.errorTypes[parentIssueType]; - - if (et && et.title) { - return t(`QA.keepRight.errorTypes.${itemType}.title`); - } else if (pt && pt.title) { - return t(`QA.keepRight.errorTypes.${parentIssueType}.title`); - } else { - return unknown; + let title = t(`QA.keepRight.errorTypes.${itemType}.title`, replacements); + if (title === unknown) { + title = t(`QA.keepRight.errorTypes.${parentIssueType}.title`, replacements); } + return title; } + function keepRightHeader(selection) { const header = selection.selectAll('.qa-header') .data( @@ -54,7 +50,8 @@ export function uiKeepRightHeader() { .text(issueTitle); } - keepRightHeader.issue = val => { + + keepRightHeader.issue = function(val) { if (!arguments.length) return _qaItem; _qaItem = val; return keepRightHeader; From fc0d87ce10b17a240769a7240371323196df450e Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Thu, 20 Feb 2020 16:43:43 -0800 Subject: [PATCH 030/127] Make bridge support field a typeCombo --- data/presets/fields.json | 2 +- data/presets/fields/bridge/support.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/presets/fields.json b/data/presets/fields.json index 5d9fb7557..0434ee86c 100644 --- a/data/presets/fields.json +++ b/data/presets/fields.json @@ -51,7 +51,7 @@ "brand": {"key": "brand", "type": "text", "label": "Brand"}, "brewery": {"key": "brewery", "type": "semiCombo", "label": "Draft Beers", "terms": ["on tap"]}, "bridge": {"key": "bridge", "type": "typeCombo", "label": "Type", "placeholder": "Default"}, - "bridge/support": {"key": "bridge:support", "type": "combo", "label": "Type"}, + "bridge/support": {"key": "bridge:support", "type": "typeCombo", "label": "Type"}, "building_area": {"key": "building", "type": "combo", "default": "yes", "geometry": "area", "label": "Building"}, "building": {"key": "building", "type": "combo", "label": "Building", "terms": ["structure"]}, "building/levels_building": {"key": "building:levels", "type": "number", "minValue": 0, "label": "Building Levels", "placeholder": "2, 4, 6...", "prerequisiteTag": {"key": "building", "valueNot": "no"}}, diff --git a/data/presets/fields/bridge/support.json b/data/presets/fields/bridge/support.json index 7d7563c04..e4795ad59 100644 --- a/data/presets/fields/bridge/support.json +++ b/data/presets/fields/bridge/support.json @@ -1,5 +1,5 @@ { "key": "bridge:support", - "type": "combo", + "type": "typeCombo", "label": "Type" } From c0090929e0833fa12bf21e215082b681ea07a4be Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 21 Feb 2020 10:32:27 -0500 Subject: [PATCH 031/127] Remove no longer needed substring - see dc7fba4bf --- dist/index.html | 2 +- index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/index.html b/dist/index.html index d3b539eaf..1250162af 100644 --- a/dist/index.html +++ b/dist/index.html @@ -37,7 +37,7 @@ context.init(); // disable boundaries (unless we have an explicit disable_features list) - var q = iD.utilStringQs(window.location.hash.substring(1)); + var q = iD.utilStringQs(window.location.hash); if (!q.hasOwnProperty('disable_features')) { context.features().disable('boundaries'); } diff --git a/index.html b/index.html index 1d6f99187..3d202a2b6 100644 --- a/index.html +++ b/index.html @@ -37,7 +37,7 @@ context.init(); // disable boundaries (unless we have an explicit disable_features list) - var q = iD.utilStringQs(window.location.hash.substring(1)); + var q = iD.utilStringQs(window.location.hash); if (!q.hasOwnProperty('disable_features')) { context.features().disable('boundaries'); } From ed58c8f62b7145dbed996cfc7096fb31a1f4163f Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 21 Feb 2020 10:42:58 -0500 Subject: [PATCH 032/127] ES6ify detect.js --- modules/util/detect.js | 257 +++++++++++++++++++++-------------------- 1 file changed, 130 insertions(+), 127 deletions(-) diff --git a/modules/util/detect.js b/modules/util/detect.js index da90d607d..8a08dcb8e 100644 --- a/modules/util/detect.js +++ b/modules/util/detect.js @@ -2,146 +2,149 @@ import { currentLocale, setTextDirection, setLanguageNames, setScriptNames } fro import { dataLocales } from '../../data/index'; import { utilStringQs } from './util'; -var detected; +let _detected; export function utilDetect(force) { - if (detected && !force) return detected; - detected = {}; + if (_detected && !force) return _detected; + _detected = {}; - var ua = navigator.userAgent, - m = null, - q = utilStringQs(window.location.hash.substring(1)); + const ua = navigator.userAgent; + const hash = utilStringQs(window.location.hash); + let m = null; - m = ua.match(/(edge)\/?\s*(\.?\d+(\.\d+)*)/i); // Edge + /* Browser */ + m = ua.match(/(edge)\/?\s*(\.?\d+(\.\d+)*)/i); // Edge + if (m !== null) { + _detected.browser = m[1]; + _detected.version = m[2]; + } + if (!_detected.browser) { + m = ua.match(/Trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/i); // IE11 if (m !== null) { - detected.browser = m[1]; - detected.version = m[2]; + _detected.browser = 'msie'; + _detected.version = m[1]; } - if (!detected.browser) { - m = ua.match(/Trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/i); // IE11 - if (m !== null) { - detected.browser = 'msie'; - detected.version = m[1]; - } + } + if (!_detected.browser) { + m = ua.match(/(opr)\/?\s*(\.?\d+(\.\d+)*)/i); // Opera 15+ + if (m !== null) { + _detected.browser = 'Opera'; + _detected.version = m[2]; } - if (!detected.browser) { - m = ua.match(/(opr)\/?\s*(\.?\d+(\.\d+)*)/i); // Opera 15+ - if (m !== null) { - detected.browser = 'Opera'; - detected.version = m[2]; - } + } + if (!_detected.browser) { + m = ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i); + if (m !== null) { + _detected.browser = m[1]; + _detected.version = m[2]; + m = ua.match(/version\/([\.\d]+)/i); + if (m !== null) _detected.version = m[1]; } - if (!detected.browser) { - m = ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i); - if (m !== null) { - detected.browser = m[1]; - detected.version = m[2]; - m = ua.match(/version\/([\.\d]+)/i); - if (m !== null) detected.version = m[1]; - } - } - if (!detected.browser) { - detected.browser = navigator.appName; - detected.version = navigator.appVersion; + } + if (!_detected.browser) { + _detected.browser = navigator.appName; + _detected.version = navigator.appVersion; + } + + // keep major.minor version only.. + _detected.version = _detected.version.split(/\W/).slice(0,2).join('.'); + + // detect other browser capabilities + // Legacy Opera has incomplete svg style support. See #715 + _detected.opera = (_detected.browser.toLowerCase() === 'opera' && parseFloat(_detected.version) < 15 ); + + if (_detected.browser.toLowerCase() === 'msie') { + _detected.ie = true; + _detected.browser = 'Internet Explorer'; + _detected.support = parseFloat(_detected.version) >= 11; + } else { + _detected.ie = false; + _detected.support = true; + } + + _detected.filedrop = (window.FileReader && 'ondrop' in window); + _detected.download = !(_detected.ie || _detected.browser.toLowerCase() === 'edge'); + _detected.cssfilters = !(_detected.ie || _detected.browser.toLowerCase() === 'edge'); + + + /* Platform */ + if (/Win/.test(ua)) { + _detected.os = 'win'; + _detected.platform = 'Windows'; + } else if (/Mac/.test(ua)) { + _detected.os = 'mac'; + _detected.platform = 'Macintosh'; + } else if (/X11/.test(ua) || /Linux/.test(ua)) { + _detected.os = 'linux'; + _detected.platform = 'Linux'; + } else { + _detected.os = 'win'; + _detected.platform = 'Unknown'; + } + + + /* Locale, Language */ + // The locale and language specified in the url hash + if (hash.locale) { + _detected.hashLocale = hash.locale; + _detected.hashLanguage = hash.locale.split('-')[0]; + } + + // The locale and language specified by the user's browser + _detected.browserLocale = (navigator.language || navigator.userLanguage || 'en-US'); + _detected.browserLanguage = _detected.browserLocale.split('-')[0]; + + // Search `navigator.languages` for a better locale. Prefer the first language, + // unless the second language is a culture-specific version of the first one, see #3842 + if (navigator.languages && navigator.languages.length > 0) { + const code0 = navigator.languages[0]; + const parts0 = code0.split('-'); + + _detected.browserLocale = code0; + _detected.browserLanguage = parts0[0]; + + if (navigator.languages.length > 1 && parts0.length === 1) { + const code1 = navigator.languages[1]; + const parts1 = code1.split('-'); + + if (parts1[0] === parts0[0]) { + _detected.browserLocale = code1; + } } + } - // keep major.minor version only.. - detected.version = detected.version.split(/\W/).slice(0,2).join('.'); + // The locale and language actually being used by iD. + // This can be changed at any time and is stored in the `currentLocale` export. + // So report those instead (except in the situation where 'en' might override 'en-US') + const current = currentLocale || 'en'; + if (current === 'en') { + _detected.locale = _detected.hashLocale || _detected.browserLocale; + _detected.language = _detected.hashLanguage || _detected.browserLanguage; + } else { + _detected.locale = current; + _detected.language = current.split('-')[0]; + } - if (detected.browser.toLowerCase() === 'msie') { - detected.ie = true; - detected.browser = 'Internet Explorer'; - detected.support = parseFloat(detected.version) >= 11; - } else { - detected.ie = false; - detected.support = true; - } + // detect text direction + const lang = dataLocales[_detected.locale] || dataLocales[_detected.language]; + if ((lang && lang.rtl) || (hash.rtl === 'true')) { + _detected.textDirection = 'rtl'; + } else { + _detected.textDirection = 'ltr'; + } + setTextDirection(_detected.textDirection); + setLanguageNames((lang && lang.languageNames) || {}); + setScriptNames((lang && lang.scriptNames) || {}); - // Added due to incomplete svg style support. See #715 - detected.opera = (detected.browser.toLowerCase() === 'opera' && parseFloat(detected.version) < 15 ); + /* Host */ + const loc = window.top.location; + let origin = loc.origin; + if (!origin) { // for unpatched IE11 + origin = loc.protocol + '//' + loc.hostname + (loc.port ? ':' + loc.port: ''); + } - // Set locale based on url param (format 'en-US') or browser lang (default) - if (q.hasOwnProperty('locale')) { - detected.locale = q.locale; - detected.language = q.locale.split('-')[0]; - } else { - detected.locale = (navigator.language || navigator.userLanguage || 'en-US'); - detected.language = detected.locale.split('-')[0]; + _detected.host = origin + loc.pathname; - // Search `navigator.languages` for a better locale. Prefer the first language, - // unless the second language is a culture-specific version of the first one, see #3842 - if (navigator.languages && navigator.languages.length > 0) { - var code0 = navigator.languages[0], - parts0 = code0.split('-'); - detected.locale = code0; - detected.language = parts0[0]; - - if (navigator.languages.length > 1 && parts0.length === 1) { - var code1 = navigator.languages[1], - parts1 = code1.split('-'); - - if (parts1[0] === parts0[0]) { - detected.locale = code1; - } - } - } - } - - // Loaded locale is stored in currentLocale - // return that instead (except in the situation where 'en' might override 'en-US') - var loadedLocale = currentLocale || 'en'; - if (loadedLocale !== 'en') { - detected.locale = loadedLocale; - detected.language = detected.locale.split('-')[0]; - } - - // detect text direction - var lang = dataLocales[detected.locale] || dataLocales[detected.language]; - if ((lang && lang.rtl) || (q.rtl === 'true')) { - detected.textDirection = 'rtl'; - } else { - detected.textDirection = 'ltr'; - } - setTextDirection(detected.textDirection); - setLanguageNames((lang && lang.languageNames) || {}); - setScriptNames((lang && lang.scriptNames) || {}); - - // detect host - var loc = window.top.location; - var origin = loc.origin; - if (!origin) { // for unpatched IE11 - origin = loc.protocol + '//' + loc.hostname + (loc.port ? ':' + loc.port: ''); - } - - detected.host = origin + loc.pathname; - - detected.filedrop = (window.FileReader && 'ondrop' in window); - - detected.download = !(detected.ie || detected.browser.toLowerCase() === 'edge'); - - detected.cssfilters = !(detected.ie || detected.browser.toLowerCase() === 'edge'); - - function nav(x) { - return navigator.userAgent.indexOf(x) !== -1; - } - - if (nav('Win')) { - detected.os = 'win'; - detected.platform = 'Windows'; - } - else if (nav('Mac')) { - detected.os = 'mac'; - detected.platform = 'Macintosh'; - } - else if (nav('X11') || nav('Linux')) { - detected.os = 'linux'; - detected.platform = 'Linux'; - } - else { - detected.os = 'win'; - detected.platform = 'Unknown'; - } - - return detected; + return _detected; } From e8f4452d4916fff684d5700ea4308f19d3c4029c Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 21 Feb 2020 11:09:59 -0500 Subject: [PATCH 033/127] Don't bundle the en.json strings anymore, Promisify locale loading (re: #4994) --- data/index.js | 1 - docs/statistics.html | 2 +- modules/core/context.js | 139 +++++++++++++++++++++------------------- modules/ui/init.js | 47 ++++++++++---- modules/util/locale.js | 39 ++++++----- 5 files changed, 128 insertions(+), 100 deletions(-) diff --git a/data/index.js b/data/index.js index a94be0cfa..03c73d59b 100644 --- a/data/index.js +++ b/data/index.js @@ -1,2 +1 @@ export { dataLocales } from './locales.json'; -export { en as dataEn } from '../dist/locales/en.json'; diff --git a/docs/statistics.html b/docs/statistics.html index 80bededbf..cf5fe38bb 100644 --- a/docs/statistics.html +++ b/docs/statistics.html @@ -3346,7 +3346,7 @@ main { - - diff --git a/test/bench/node-xml.html b/test/bench/node-xml.html deleted file mode 100644 index 8fc9d33d0..000000000 --- a/test/bench/node-xml.html +++ /dev/null @@ -1,99 +0,0 @@ - - - - Node XML - - - -
-
-
-

-        
-    
-    
-
diff --git a/test/bench/removing.html b/test/bench/removing.html
deleted file mode 100644
index 24c8140f7..000000000
--- a/test/bench/removing.html
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-    
-        Event binding cost
-        
-    
-    
-        
-
-

-    
-    
-
diff --git a/test/bench/translate-rounding.html b/test/bench/translate-rounding.html
deleted file mode 100644
index 41eaaf189..000000000
--- a/test/bench/translate-rounding.html
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-    
-        Rounded coordinates
-        
-    
-    
-        
-
-
-

-    
-    
-
diff --git a/test/bench/translate-vs-matrix-webkit.html b/test/bench/translate-vs-matrix-webkit.html
deleted file mode 100644
index c4ed26aec..000000000
--- a/test/bench/translate-vs-matrix-webkit.html
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-    
-        Rounded coordinates
-        
-    
-    
-        
-
-
-

-    
-    
-
diff --git a/test/bench/translate-vs-matrix.html b/test/bench/translate-vs-matrix.html
deleted file mode 100644
index aa6363c04..000000000
--- a/test/bench/translate-vs-matrix.html
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-    
-        Rounded coordinates
-        
-    
-    
-        
-
-
-

-    
-    
-

From 4d0ef1bafc74adaafab183386b277c83be19137f Mon Sep 17 00:00:00 2001
From: Bryan Housel 
Date: Sat, 22 Feb 2020 15:07:09 -0500
Subject: [PATCH 042/127] Export only the d3 functions we use in tests (re:
 #4379)

This trims a bit more off the iD bundle size
---
 docs/statistics.html               |  2 +-
 modules/index.js                   | 19 +++++-
 test/spec/actions/orthogonalize.js |  2 +-
 test/spec/osm/changeset.js         | 96 ++++++++++++++----------------
 test/spec/spec_helpers.js          |  2 +-
 test/spec/ui/combobox.js           |  2 +-
 6 files changed, 66 insertions(+), 57 deletions(-)

diff --git a/docs/statistics.html b/docs/statistics.html
index a08c112eb..18c637c5b 100644
--- a/docs/statistics.html
+++ b/docs/statistics.html
@@ -3346,7 +3346,7 @@ main {