Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add flip horizontal/vertical operations (for issue #3375) #3555

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions data/core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ en:
incomplete_relation: This feature can't be moved because it hasn't been fully downloaded.
too_large: This can't be moved because not enough of it is currently visible.
connected_to_hidden: This can't be moved because it is connected to a hidden feature.
reflect:
title: reflect
description: Reflect this area on the vertical axis.
key: T
annotation: Reflected an area.
too_large: This can't be reflected because not enough of it is currently visible.
connected_to_hidden: This can't be reflected because it is connected to a hidden feature.
rotate:
title: Rotate
description: Rotate this object around its center point.
Expand Down
8 changes: 8 additions & 0 deletions dist/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@
"too_large": "This can't be moved because not enough of it is currently visible.",
"connected_to_hidden": "This can't be moved because it is connected to a hidden feature."
},
"reflect": {
"title": "reflect",
"description": "Reflect this area on the vertical axis.",
"key": "T",
"annotation": "Reflected an area.",
"too_large": "This can't be reflected because not enough of it is currently visible.",
"connected_to_hidden": "This can't be reflected because it is connected to a hidden feature."
},
"rotate": {
"title": "Rotate",
"description": "Rotate this object around its center point.",
Expand Down
1 change: 1 addition & 0 deletions modules/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ export { actionRotateWay } from './rotate_way';
export { actionSplit } from './split';
export { actionStraighten } from './straighten';
export { actionUnrestrictTurn } from './unrestrict_turn';
export { actionReflect } from './reflect.js';
43 changes: 43 additions & 0 deletions modules/actions/reflect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import _ from 'lodash';
/* Flip the provided way horizontally
Only operates on "area" ways
*/

export function actionReflect(wayId) {

return function (graph) {
const targetWay = graph.entity(wayId);

// If the way is not an area, we will not process it
if (!targetWay.isArea()) {
// return input graph without changes
return graph;
}
// Get the bounding rectangle of the area
const boundingRect = targetWay.extent(graph).rectangle();
// rectangle returned as [ lon (x) top left, lat (y) top left, lon (x) bottom right, lat (y) bottom right]
// Obtain the left and right lonlat's
const left = boundingRect[0];
const right = boundingRect[2];
// Determine the mid-point that we will flip on
const midPoint = left + ((right - left) / 2);

// Obtain all of the nodes on the way, iterate over them to translate then aggreate up
return _(targetWay.nodes)
.map(function (nodeId) {
return graph.entity(nodeId);
})
// Only process each node once, as the first node will be listed twice in the way
.uniqBy(function (node) { return node.id; })
// Get distance from midPoint and produce a translated node
.map(function (node) {
const delta = node.loc[0] - midPoint;
return node.move([node.loc[0]-(2*delta), node.loc[1]]);
})
// Chain together consecutive updates to the graph for each updated node and return
.reduce(function (accGraph, value) {
return accGraph.replace(value);
}, graph);

};
}
4 changes: 2 additions & 2 deletions modules/behavior/paste.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
geoPointInPolygon
} from '../geo/index';

import { modeMove } from '../modes/index';
import { modePostPaste } from '../modes/index';
import { uiCmd } from '../ui/index';


Expand Down Expand Up @@ -75,7 +75,7 @@ export function behaviorPaste(context) {
delta = [ mouse[0] - center[0], mouse[1] - center[1] ];

context.perform(actionMove(newIDs, delta, projection));
context.enter(modeMove(context, newIDs, baseGraph));
context.enter(modePostPaste(context, newIDs));
}


Expand Down
1 change: 1 addition & 0 deletions modules/modes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { modeMove } from './move';
export { modeRotateWay } from './rotate_way';
export { modeSave } from './save';
export { modeSelect } from './select';
export { modePostPaste} from './post_paste';
172 changes: 172 additions & 0 deletions modules/modes/post_paste.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import * as d3 from 'd3';
import _ from 'lodash';
import { d3keybinding } from '../lib/d3.keybinding.js';
import { behaviorBreathe, behaviorHover, behaviorSelect } from '../behavior/index';
import { geoExtent, geoPointInPolygon } from '../geo/index';
import { modeBrowse } from './browse';
import * as Operations from '../operations/index';
import { uiRadialMenu, uiSelectionList } from '../ui/index';

export function modePostPaste(context, selectedIDs) {
var mode = {
id: 'post-paste',
button: 'browse'
};

var keybinding = d3keybinding('postPaste');
var behaviors = [
behaviorBreathe(context),
behaviorHover(context),
behaviorSelect(context)
];
var radialMenu = null;
var suppressMenu = false;
var timeout = null;

function closeMenu() {
if (radialMenu) {
context.surface().call(radialMenu.close);
}
}

function singular() {
if (selectedIDs.length === 1) {
return context.hasEntity(selectedIDs[0]);
}
}

function positionMenu() {
if (suppressMenu || !radialMenu) {
return;
}

var entity = singular();
if (entity && context.geometry(entity.id) === 'relation') {
suppressMenu = true;
} else if (entity && entity.type === 'node') {
radialMenu.center(context.projection(entity.loc));
} else {
var point = context.mouse(),
viewport = geoExtent(context.projection.clipExtent()).polygon();
if (geoPointInPolygon(point, viewport)) {
radialMenu.center(point);
} else {
suppressMenu = true;
}
}
}

function showMenu() {
closeMenu();
if (!suppressMenu && radialMenu) {
context.surface().call(radialMenu);
}
}

function toggleMenu() {
if (d3.select('.radial-menu').empty()) {
showMenu();
} else {
closeMenu();
}
}

// This is called by the select behavior
mode.suppressMenu = function(_) {
if (!arguments.length) return suppressMenu;
suppressMenu = _;
return mode;
};

mode.reselect = function() {
var surfaceNode = context.surface().node();
if (surfaceNode.focus) { // FF doesn't support it
surfaceNode.focus();
}

positionMenu();
showMenu();
};

mode.selectedIDs = function() {
return selectedIDs;
};

mode.enter = function () {
function esc() {
context.enter(modeBrowse(context));
}

behaviors.forEach(function (behavior) {
context.install(behavior);
});

var operations = _.without(d3.values(Operations), Operations.operationDelete)
.map(function(o) { return o(selectedIDs, context); })
.filter(function(o) { return o.available(); });

operations.unshift(Operations.operationDelete(selectedIDs, context));

keybinding
.on('⎋', esc)
.on('space', toggleMenu);

operations.forEach(function(operation) {
operation.keys.forEach(function(key) {
keybinding.on(key, function() {
if (!(context.inIntro() || operation.disabled())) {
operation();
}
});
});
});

d3.select(document)
.call(keybinding);

radialMenu = uiRadialMenu(context, operations);

// Exit this mode when any movement is made on the map
context.map()
.on('move.select', esc);

var show = d3.event && !suppressMenu;

if (show) {
positionMenu();
}

timeout = window.setTimeout(function() {
if (show) {
showMenu();
}
}, 200);

// Display summary of selected elements in the side bar
if (selectedIDs.length > 1) {
var entities = uiSelectionList(context, selectedIDs);
context.ui().sidebar.show(entities);
}

// Start with menu open
toggleMenu();
};

mode.exit = function() {
if (timeout) window.clearTimeout(timeout);

behaviors.forEach(function(behavior) {
context.uninstall(behavior);
});

keybinding.off();
closeMenu();
radialMenu = undefined;

// Detach event listener
context.map().on('move.select', null);
context.ui().sidebar.hide();
};

return mode;
}
1 change: 1 addition & 0 deletions modules/operations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { operationReverse } from './reverse';
export { operationRotate } from './rotate';
export { operationSplit } from './split';
export { operationStraighten } from './straighten';
export { operationReflect } from './reflect';
49 changes: 49 additions & 0 deletions modules/operations/reflect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { t } from '../util/locale';
import { actionReflect } from '../actions/index';
import _ from 'lodash';

export function operationReflect(selectedIDs, context) {
const entityId = selectedIDs[0];
const entity = context.entity(entityId);
const extent = entity.extent(context.graph());
const action = actionReflect(entityId);

var operation = function() {
context.perform(
action,
t('operations.reflect.annotation')
);
};

operation.available = function() {
// For the passed selectIDs, filter out those relating to area geometries
const areaCount = _(selectedIDs)
.filter(function(s) { return context.geometry(s) === 'area';})
// Only allow reflection if exactly 1 area is selected
.size();
return areaCount === 1;
};

operation.disabled = function() {
if (extent.percentContainedIn(context.extent()) < 0.8) {
return 'too_large';
} else if (context.hasHiddenConnections(entityId)) {
return 'connected_to_hidden';
} else {
return false;
}
};

operation.tooltip = function() {
var disable = operation.disabled();
return disable ?
t('operations.reflect.' + disable) :
t('operations.reflect.description');
};

operation.id = 'reflect';
operation.keys = [t('operations.reflect.key')];
operation.title = t('operations.reflect.title');

return operation;
}
1 change: 1 addition & 0 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
<script src='spec/actions/split.js'></script>
<script src='spec/actions/straighten.js'></script>
<script src='spec/actions/unrestrict_turn.js'></script>
<script src='spec/actions/reflect.js'></script>

<script src='spec/behavior/hash.js'></script>
<script src='spec/behavior/hover.js'></script>
Expand Down
Loading