Merge branch 'master' into validation_and_change_perf

This commit is contained in:
Bryan Housel
2019-04-10 14:22:20 -04:00
209 changed files with 1871 additions and 1169 deletions
+2 -2
View File
@@ -3,12 +3,12 @@ import { utilArrayUniq } from '../util';
// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteRelationAction.as
export function actionDeleteRelation(relationID) {
export function actionDeleteRelation(relationID, allowUntaggedMembers) {
function canDeleteEntity(entity, graph) {
return !graph.parentWays(entity).length &&
!graph.parentRelations(entity).length &&
!entity.hasInterestingTags();
(!entity.hasInterestingTags() && !allowUntaggedMembers);
}
+4 -1
View File
@@ -59,6 +59,9 @@ export function actionDisconnect(nodeId, newNodeId) {
} else {
way.nodes.forEach(function(waynode, index) {
if (waynode === nodeId) {
if (way.isClosed() && parentWays.length > 1 && wayIds && wayIds.indexOf(way.id) !== -1 && index === way.nodes.length-1) {
return;
}
candidates.push({ wayID: way.id, index: index });
}
});
@@ -71,7 +74,7 @@ export function actionDisconnect(nodeId, newNodeId) {
action.disabled = function(graph) {
var connections = action.connections(graph);
if (connections.length === 0 || (wayIds && wayIds.length !== connections.length))
if (connections.length === 0)
return 'not_connected';
var parentWays = graph.parentWays(graph.entity(nodeId));
+18 -2
View File
@@ -2,7 +2,7 @@ import { event as d3_event, select as d3_select } from 'd3-selection';
import { t } from '../util/locale';
import { actionAddMidpoint } from '../actions';
import { actionAddMidpoint, actionDeleteRelation } from '../actions';
import {
behaviorBreathe, behaviorCopy, behaviorHover,
behaviorLasso, behaviorPaste, behaviorSelect
@@ -57,7 +57,7 @@ export function modeSelect(context, selectedIDs) {
function singular() {
if (selectedIDs.length === 1) {
if (selectedIDs && selectedIDs.length === 1) {
return context.hasEntity(selectedIDs[0]);
}
}
@@ -541,6 +541,22 @@ export function modeSelect(context, selectedIDs) {
context.map().on('drawn.select', null);
context.ui().sidebar.hide();
context.features().forceVisible([]);
var entity = singular();
if (newFeature &&
entity &&
entity.type === 'relation' &&
// no tags
Object.keys(entity.tags).length === 0 &&
// no parent relations
context.graph().parentRelations(entity).length === 0 &&
// no members or one member with no role
(entity.members.length === 0 || (entity.members.length === 1 && !entity.members[0].role))) {
// the user added this relation but didn't edit it at all, so just delete it
var deleteAction = actionDeleteRelation(entity.id, true /* don't delete untagged members */);
context.perform(deleteAction, t('operations.delete.annotation.relation'));
}
};
+47 -11
View File
@@ -4,33 +4,69 @@ import { behaviorOperation } from '../behavior/index';
export function operationDisconnect(selectedIDs, context) {
var vertices = selectedIDs
.filter(function(id) { return context.geometry(id) === 'vertex'; });
var entityID = vertices[0];
var action = actionDisconnect(entityID);
var _disabled;
var vertices = [];
var ways = [];
var others = [];
if (entityID && selectedIDs.length > 1) {
var ids = selectedIDs.filter(function(id) { return id !== entityID; });
action.limitWays(ids);
}
selectedIDs.forEach(function(id) {
if (context.geometry(id) === 'vertex') {
vertices.push(id);
} else if (context.entity(id).type === 'way'){
ways.push(id);
} else {
others.push(id);
}
});
var actions = [];
vertices.forEach(function(vertexID) {
var action = actionDisconnect(vertexID);
if (ways.length > 0) {
var waysIDsForVertex = ways.filter(function(wayID) {
return context.graph().entity(wayID).nodes.includes(vertexID);
});
action.limitWays(waysIDsForVertex);
}
actions.push(action);
});
var operation = function() {
context.perform(action, operation.annotation());
context.perform(function(graph) {
actions.forEach(function(action) {
graph = action(graph);
});
return graph;
}, operation.annotation());
};
operation.available = function() {
return vertices.length === 1;
return vertices.length > 0 &&
others.length === 0 &&
(ways.length === 0 || ways.every(function(way) {
return vertices.some(function(vertex) {
return context.graph().entity(way).nodes.includes(vertex);
});
}));
};
operation.disabled = function() {
if (_disabled !== undefined) return _disabled;
_disabled = action.disabled(context.graph());
for (var actionIndex in actions) {
var action = actions[actionIndex];
var actionReason = action.disabled(context.graph());
if (actionReason) {
_disabled = actionReason;
break;
}
}
if (_disabled) {
return _disabled;
} else if (selectedIDs.some(context.hasHiddenConnections)) {
+1 -1
View File
@@ -17,7 +17,7 @@ export var osmOneWayTags = {
'platter': true,
'rope_tow': true,
't-bar': true,
'zipline': true
'zip_line': true
},
'highway': {
'motorway': true
+5
View File
@@ -114,6 +114,11 @@ export function uiCommit(context) {
}
}
for (var key in tags) {
// remove existing warning counts
if (key.match(/^warnings:/)) delete tags[key];
}
var warningCountsByType = {};
context.validator().getWarnings().forEach(function(warning) {
// deletion count can be derived so don't tag that warning in the changeset
+1 -1
View File
@@ -106,7 +106,7 @@ export function uiCurtain() {
if (options.buttonText && options.buttonCallback) {
html += '<div class="button-section">' +
'<button href="#" class="button action col8">' + options.buttonText + '</button></div>';
'<button href="#" class="button action">' + options.buttonText + '</button></div>';
}
var classes = 'curtain-tooltip tooltip in ' + (options.tooltipClass || '');
+1 -1
View File
@@ -152,7 +152,7 @@ export function uiEntityEditor(context) {
});
body.select('.preset-list-item button')
.call(uiPresetIcon()
.call(uiPresetIcon(context)
.geometry(context.geometry(_entityID))
.preset(_activePreset)
);
+1 -1
View File
@@ -133,7 +133,7 @@ export function uiEntityIssues(context) {
var fixLists = items.selectAll('.issue-fix-list');
var fixes = fixLists.selectAll('.issue-fix-item')
.data(function(d) { return d.fixes; })
.data(function(d) { return d.fixes ? d.fixes : []; })
.enter()
.append('li')
.attr('class', function(d) {
+1 -1
View File
@@ -41,7 +41,7 @@ export function uiModal(selection, blocking) {
var modal = shaded
.append('div')
.attr('class', 'modal fillL col6');
.attr('class', 'modal fillL');
if (!blocking) {
shaded.on('click.remove-modal', function() {
+101 -18
View File
@@ -3,7 +3,7 @@ import { select as d3_select } from 'd3-selection';
import { svgIcon, svgTagClasses } from '../svg';
import { utilFunctor } from '../util';
export function uiPresetIcon() {
export function uiPresetIcon(context) {
var preset, geometry, sizeClass = 'medium';
function isSmall() {
@@ -119,30 +119,87 @@ export function uiPresetIcon() {
.attr('height', h)
.attr('viewBox', '0 0 ' + w + ' ' + h);
lineEnter.append('path')
.attr('d', 'M' + x1 + ' ' + y + ' L' + x2 + ' ' + y)
.attr('class', 'line casing');
lineEnter.append('path')
.attr('d', 'M' + x1 + ' ' + y + ' L' + x2 + ' ' + y)
.attr('class', 'line stroke');
lineEnter.append('circle')
.attr('class', 'vertex')
.attr('cx', x1 - 1)
.attr('cy', y)
.attr('r', r);
lineEnter.append('circle')
.attr('class', 'vertex')
.attr('cx', x2 + 1)
.attr('cy', y)
.attr('r', r);
['casing', 'stroke'].forEach(function(klass) {
lineEnter.append('path')
.attr('d', 'M' + x1 + ' ' + y + ' L' + x2 + ' ' + y)
.attr('class', 'line ' + klass);
});
[[x1 - 1, y], [x2 + 1, y]].forEach(function(loc) {
lineEnter.append('circle')
.attr('class', 'vertex')
.attr('cx', loc[0])
.attr('cy', loc[1])
.attr('r', r);
});
}
function renderRoute(routeEnter) {
var d = isSmall() ? 40 : 60;
// draw the route parametrically
var w = d,
h = d,
y1 = Math.round(d*0.80),
y2 = Math.round(d*0.68),
l = Math.round(d*0.6),
r = 2;
var x1 = (w - l)/2, x2 = x1 + l/3, x3 = x2 + l/3, x4 = x3 + l/3;
routeEnter = routeEnter
.append('svg')
.attr('class', 'preset-icon-route')
.attr('width', w)
.attr('height', h)
.attr('viewBox', '0 0 ' + w + ' ' + h);
['casing', 'stroke'].forEach(function(klass) {
routeEnter.append('path')
.attr('d', 'M' + x1 + ' ' + y1 + ' L' + x2 + ' ' + y2)
.attr('class', 'segment0 line ' + klass);
routeEnter.append('path')
.attr('d', 'M' + x2 + ' ' + y2 + ' L' + x3 + ' ' + y1)
.attr('class', 'segment1 line ' + klass);
routeEnter.append('path')
.attr('d', 'M' + x3 + ' ' + y1 + ' L' + x4 + ' ' + y2)
.attr('class', 'segment2 line ' + klass);
});
[[x1, y1], [x2, y2], [x3, y1], [x4, y2]].forEach(function(loc) {
routeEnter.append('circle')
.attr('class', 'vertex')
.attr('cx', loc[0])
.attr('cy', loc[1])
.attr('r', r);
});
}
var routeSegements = {
bicycle: ['highway/cycleway', 'highway/cycleway', 'highway/cycleway'],
bus: ['highway/unclassified', 'highway/secondary', 'highway/primary'],
detour: ['highway/tertiary', 'highway/residential', 'highway/unclassified'],
ferry: ['route/ferry', 'route/ferry', 'route/ferry'],
foot: ['highway/footway', 'highway/footway', 'highway/footway'],
hiking: ['highway/path', 'highway/path', 'highway/path'],
horse: ['highway/bridleway', 'highway/bridleway', 'highway/bridleway'],
light_rail: ['railway/light_rail', 'railway/light_rail', 'railway/light_rail'],
pipeline: ['man_made/pipeline', 'man_made/pipeline', 'man_made/pipeline'],
piste: ['piste/downhill', 'piste/hike', 'piste/nordic'],
power: ['power/line', 'power/line', 'power/line'],
road: ['highway/secondary', 'highway/primary', 'highway/trunk'],
subway: ['railway/subway', 'railway/subway', 'railway/subway'],
train: ['railway/rail', 'railway/rail', 'railway/rail'],
tram: ['railway/tram', 'railway/tram', 'railway/tram'],
waterway: ['waterway/stream', 'waterway/stream', 'waterway/stream']
};
function render() {
var p = preset.apply(this, arguments);
var isFallback = isSmall() && p.isFallback && p.isFallback();
var geom = geometry ? geometry.apply(this, arguments) : null;
if (geom === 'relation' && p.tags && ((p.tags.type === 'route' && p.tags.route && routeSegements[p.tags.route]) || p.tags.type === 'waterway')) {
geom = 'route';
}
var imageURL = p.imageURL;
var picon = imageURL ? null : getIcon(p, geom);
var isMaki = picon && /^maki-/.test(picon);
@@ -154,7 +211,8 @@ export function uiPresetIcon() {
var drawVertex = picon !== null && geom === 'vertex' && (!isSmall() || !isFallback);
var drawLine = picon && geom === 'line' && !isFallback && !isCategory;
var drawArea = picon && geom === 'area' && !isFallback;
var isFramed = (drawVertex || drawArea || drawLine);
var drawRoute = picon && geom === 'route';
var isFramed = (drawVertex || drawArea || drawLine || drawRoute);
var tags = !isCategory ? p.setTags({}, geom) : {};
for (var k in tags) {
@@ -244,6 +302,31 @@ export function uiPresetIcon() {
line.selectAll('path.casing')
.attr('class', 'line casing ' + tagClasses);
var route = container.selectAll('.preset-icon-route')
.data(drawRoute ? [0] : []);
route.exit()
.remove();
var routeEnter = route.enter();
renderRoute(routeEnter);
route = routeEnter.merge(route);
if (drawRoute) {
var routeType = p.tags.type === 'waterway' ? 'waterway' : p.tags.route;
var segmentPresetIDs = routeSegements[routeType];
for (var segmentIndex in segmentPresetIDs) {
var segmentPreset = context.presets().item(segmentPresetIDs[segmentIndex]);
var segmentTagClasses = svgTagClasses().getClassesString(segmentPreset.tags, '');
route.selectAll('path.stroke.segment' + segmentIndex)
.attr('class', 'segment' + segmentIndex + ' line stroke ' + segmentTagClasses);
route.selectAll('path.casing.segment' + segmentIndex)
.attr('class', 'segment' + segmentIndex + ' line casing ' + segmentTagClasses);
}
}
var icon = container.selectAll('.preset-icon')
.data(picon ? [0] : []);
+10 -5
View File
@@ -105,16 +105,21 @@ export function uiPresetList(context) {
if (geocoder && entity) {
var center = entity.extent(context.graph()).center();
geocoder.countryCode(center, function countryCallback(err, countryCode) {
// get the input value again because it may have changed
var currentValue = search.property('value');
if (!currentValue.length) return;
var results;
if (!err && countryCode) {
countryCode = countryCode.toLowerCase();
results = presets.search(value, geometry, countryCode);
results = presets.search(currentValue, geometry, countryCode);
} else {
results = presets.search(value, geometry);
results = presets.search(currentValue, geometry);
}
message.text(t('inspector.results', {
n: results.collection.length,
search: value
search: currentValue
}));
list.call(drawList, results);
});
@@ -287,7 +292,7 @@ export function uiPresetList(context) {
.append('button')
.attr('class', 'preset-list-button')
.classed('expanded', false)
.call(uiPresetIcon()
.call(uiPresetIcon(context)
.geometry(context.geometry(_entityID))
.preset(preset))
.on('click', click)
@@ -375,7 +380,7 @@ export function uiPresetList(context) {
var button = wrap.append('button')
.attr('class', 'preset-list-button')
.call(uiPresetIcon()
.call(uiPresetIcon(context)
.geometry(context.geometry(_entityID))
.preset(preset))
.on('click', item.choose)
+4 -2
View File
@@ -69,6 +69,10 @@ export function uiRawMemberEditor(context) {
function deleteMember(d) {
// remove the hover-highlight styling
utilHighlightEntities([d.id], false, context);
context.perform(
actionDeleteMember(d.relation.id, d.index),
t('operations.delete_member.annotation')
@@ -77,8 +81,6 @@ export function uiRawMemberEditor(context) {
if (!context.hasEntity(d.relation.id)) {
context.enter(modeBrowse(context));
}
utilHighlightEntities([d.id], false, context);
}
+18 -13
View File
@@ -3,7 +3,7 @@ import {
select as d3_select
} from 'd3-selection';
import { t } from '../util/locale';
import { t, textDirection } from '../util/locale';
import {
actionAddEntity,
@@ -17,6 +17,7 @@ import { osmEntity, osmRelation } from '../osm';
import { services } from '../services';
import { svgIcon } from '../svg';
import { uiCombobox, uiDisclosure } from './index';
import { tooltip } from '../util/tooltip';
import { utilArrayGroupBy, utilDisplayName, utilNoAuto, utilHighlightEntities } from '../util';
@@ -87,6 +88,9 @@ export function uiRawMembershipEditor(context) {
this.blur(); // avoid keeping focus on the button
if (d === 0) return; // called on newrow (shoudn't happen)
// remove the hover-highlight styling
utilHighlightEntities([d.relation.id], false, context);
context.perform(
actionDeleteMember(d.relation.id, d.index),
t('operations.delete_member.annotation')
@@ -184,16 +188,13 @@ export function uiRawMembershipEditor(context) {
.append('li')
.attr('class', 'member-row member-row-normal form-field');
itemsEnter.each(function(d){
// highlight the relation in the map while hovering on the list item
d3_select(this)
.on('mouseover', function() {
utilHighlightEntities([d.relation.id], true, context);
})
.on('mouseout', function() {
utilHighlightEntities([d.relation.id], false, context);
});
});
// highlight the relation in the map while hovering on the list item
itemsEnter.on('mouseover', function(d) {
utilHighlightEntities([d.relation.id], true, context);
})
.on('mouseout', function(d) {
utilHighlightEntities([d.relation.id], false, context);
});
var labelEnter = itemsEnter
.append('label')
@@ -308,10 +309,14 @@ export function uiRawMembershipEditor(context) {
.append('div')
.attr('class', 'add-row');
addRowEnter
var addRelationButton = addRowEnter
.append('button')
.attr('class', 'add-relation')
.attr('class', 'add-relation');
addRelationButton
.call(svgIcon('#iD-icon-plus', 'light'));
addRelationButton
.call(tooltip().title(t('inspector.add_to_relation')).placement(textDirection === 'ltr' ? 'right' : 'left'));
addRowEnter
.append('div')
+4 -7
View File
@@ -11,13 +11,10 @@ export function uiRestore(context) {
var modalSelection = uiModal(selection, true);
modalSelection.select('.modal')
.attr('class', 'modal fillL col6');
.attr('class', 'modal fillL');
var introModal = modalSelection.select('.content');
introModal
.attr('class','cf');
introModal
.append('div')
.attr('class', 'modal-section')
@@ -32,11 +29,11 @@ export function uiRestore(context) {
var buttonWrap = introModal
.append('div')
.attr('class', 'modal-actions cf');
.attr('class', 'modal-actions');
var restore = buttonWrap
.append('button')
.attr('class', 'restore col6')
.attr('class', 'restore')
.on('click', function() {
context.history().restore();
modalSelection.remove();
@@ -54,7 +51,7 @@ export function uiRestore(context) {
var reset = buttonWrap
.append('button')
.attr('class', 'reset col6')
.attr('class', 'reset')
.on('click', function() {
context.history().clearSaved();
modalSelection.remove();
+5 -5
View File
@@ -14,7 +14,7 @@ export function uiSplash(context) {
var modalSelection = uiModal(selection);
modalSelection.select('.modal')
.attr('class', 'modal-splash modal col6');
.attr('class', 'modal-splash modal');
var introModal = modalSelection.select('.content')
.append('div')
@@ -22,7 +22,7 @@ export function uiSplash(context) {
introModal
.append('div')
.attr('class','modal-section cf')
.attr('class','modal-section')
.append('h3').text(t('splash.welcome'));
introModal
@@ -37,11 +37,11 @@ export function uiSplash(context) {
var buttonWrap = introModal
.append('div')
.attr('class', 'modal-actions cf');
.attr('class', 'modal-actions');
var walkthrough = buttonWrap
.append('button')
.attr('class', 'walkthrough col6')
.attr('class', 'walkthrough')
.on('click', function() {
context.container().call(uiIntro(context));
modalSelection.close();
@@ -59,7 +59,7 @@ export function uiSplash(context) {
var startEditing = buttonWrap
.append('button')
.attr('class', 'start-editing col6')
.attr('class', 'start-editing')
.on('click', modalSelection.close);
startEditing
+1 -1
View File
@@ -173,7 +173,7 @@ export function uiToolAddFavorite(context) {
buttonsEnter
.each(function(d) {
d3_select(this)
.call(uiPresetIcon()
.call(uiPresetIcon(context)
.geometry((d.geometry === 'point' && !d.preset.matchGeometry(d.geometry)) ? 'vertex' : d.geometry)
.preset(d.preset)
.sizeClass('small')
+1 -1
View File
@@ -210,7 +210,7 @@ export function uiToolAddRecent(context) {
buttonsEnter
.each(function(d) {
d3_select(this)
.call(uiPresetIcon()
.call(uiPresetIcon(context)
.geometry((d.geometry === 'point' && !d.preset.matchGeometry(d.geometry)) ? 'vertex' : d.geometry)
.preset(d.preset)
.sizeClass('small')
+1 -1
View File
@@ -418,7 +418,7 @@ export function uiToolSearchAdd(context) {
row.each(function(d) {
d3_select(this).call(
uiPresetIcon()
uiPresetIcon(context)
.geometry(d.geometry)
.preset(d.preset || d.presets[0])
.sizeClass('small')
+3 -1
View File
@@ -92,7 +92,9 @@ export function uiToolUndoRedo(context) {
function update() {
buttons
.property('disabled', !editable())
.classed('disabled', function(d) { return !d.annotation(); })
.classed('disabled', function(d) {
return !editable() || !d.annotation();
})
.each(function() {
var selection = d3_select(this);
if (selection.property('tooltipVisible')) {
@@ -0,0 +1,43 @@
import { t } from '../util/locale';
import { utilDisplayLabel } from '../util';
import { validationIssue, validationIssueFix } from '../core/validator';
export function validationIncompatibleSource() {
var type = 'incompatible_source';
var invalidSources = [{id:'google', regex:'google'}];
var validation = function(entity, context) {
var issues = [];
if (entity.tags && entity.tags.source) {
invalidSources.forEach(function(invalidSource) {
var pattern = new RegExp(invalidSource.regex, 'i');
if (entity.tags.source.match(pattern)) {
issues.push(new validationIssue({
type: type,
severity: 'warning',
message: t('issues.incompatible_source.' + invalidSource.id + '.feature.message', {
feature: utilDisplayLabel(entity, context),
}),
tooltip: t('issues.incompatible_source.' + invalidSource.id + '.tip'),
entities: [entity],
fixes: [
new validationIssueFix({
title: t('issues.fix.remove_proprietary_data.title')
})
]
}));
}
});
}
return issues;
};
validation.type = type;
return validation;
}
+1
View File
@@ -2,6 +2,7 @@ export { validationAlmostJunction } from './almost_junction';
export { validationCrossingWays } from './crossing_ways';
export { validationDisconnectedWay } from './disconnected_way';
export { validationGenericName } from './generic_name';
export { validationIncompatibleSource } from './incompatible_source';
export { validationManyDeletions } from './many_deletions';
export { validationMaprules } from './maprules';
export { validationMissingRole } from './missing_role';