Merge pull request #2525 from openstreetmap/conflict-resolution

Merge Conflict Resolution
This commit is contained in:
Bryan Housel
2015-03-05 19:49:15 -05:00
36 changed files with 2243 additions and 334 deletions
+1
View File
@@ -13,6 +13,7 @@
"_": false,
"t": false,
"bootstrap": false,
"Diff3": false,
"rbush": false,
"JXON": false,
"osmAuth": false,
+1
View File
@@ -29,6 +29,7 @@ dist/iD.js: \
js/lib/d3.typeahead.js \
js/lib/d3.curtain.js \
js/lib/d3.value.js \
js/lib/diff3.js \
js/lib/jxon.js \
js/lib/lodash.js \
js/lib/osmauth.js \
+110 -135
View File
@@ -140,10 +140,6 @@ strong {
a:visited, a {
color: #7092ff;
-webkit-transition: all 100ms;
-moz-transition: all 100ms;
-o-transition: all 100ms;
transition: all 100ms;
}
a:hover {
@@ -172,10 +168,6 @@ input[type=email] {
width: 100%;
border-radius:4px;
text-overflow: ellipsis;
-webkit-transition: all 200ms;
-moz-transition: all 200ms;
-o-transition: all 200ms;
transition: all 200ms;
}
textarea:focus,
@@ -247,9 +239,6 @@ ul li { list-style: none;}
background-color: white;
color: #7092FF;
cursor: pointer;
-moz-transition: all 100ms;
-o-transition: all 100ms;
transition: all 100ms;
}
.toggle-list > label:hover {
@@ -358,11 +347,6 @@ button {
display: inline-block;
height:40px;
border-radius:4px;
/* Crashes Safari: https://github.com/openstreetmap/iD/issues/1188 */
/*-webkit-transition: all 100ms;*/
-moz-transition: all 100ms;
-o-transition: all 100ms;
transition: all 100ms;
}
button:focus,
@@ -418,6 +402,10 @@ button.minor:hover {
border-right: 1px solid rgba(0,0,0,.5);
}
.fillL .joined button {
border-right: 1px solid white;
}
.joined button:first-child {
border-radius:4px 0 0 4px;
}
@@ -429,6 +417,13 @@ button.minor:hover {
button.action {
background: #7092ff;
color: white;
}
button[disabled].action,
button[disabled].action:hover {
background: #cccccc;
color: #888;
}
button.action:focus,
@@ -436,6 +431,15 @@ button.action:hover {
background: #597BE7;
}
button.secondary-action {
background: #ececec;
}
button.secondary-action:focus,
button.secondary-action:hover {
background: #cccccc;
}
button.save.has-count {
padding: 9px;
}
@@ -565,13 +569,13 @@ button[disabled] .icon.avatar { background-position: -320px -40px;}
button[disabled] .icon.nearby { background-position: -340px -40px;}
button[disabled] .icon.data { background-position: -600px -40px;}
.icon.point.deleted { background-position: -302px -80px;}
.icon.line.deleted { background-position: -320px -80px;}
.icon.area.deleted { background-position: -340px -80px;}
.icon.point.deleted { background-position: -480px -80px;}
.icon.line.deleted { background-position: -500px -80px;}
.icon.area.deleted { background-position: -520px -80px;}
.icon.point.created { background-position: -302px -100px;}
.icon.line.created { background-position: -320px -100px;}
.icon.area.created { background-position: -340px -100px;}
.icon.point.created { background-position: -480px -100px;}
.icon.line.created { background-position: -500px -100px;}
.icon.area.created { background-position: -520px -100px;}
.icon.point.modified { background-position: -22px 0; }
@@ -619,7 +623,7 @@ a:hover .icon.out-link { background-position: -500px -14px;}
}
.header h3 {
text-align: center;
text-align: left;
margin-bottom: 0;
white-space: nowrap;
text-overflow: ellipsis;
@@ -657,7 +661,7 @@ a:hover .icon.out-link { background-position: -500px -14px;}
.footer {
position: absolute;
bottom: 0;
padding: 5px 30px 5px 30px;
padding: 5px 20px 5px 20px;
border-top: 1px solid #ccc;
background-color: #fafafa;
width: 100%;
@@ -816,9 +820,6 @@ a:hover .icon.out-link { background-position: -500px -14px;}
text-overflow: ellipsis;
overflow: hidden;
border-left: 1px solid rgba(0, 0, 0, .1);
-moz-transition: all 100ms;
-o-transition: all 100ms;
transition: all 100ms;
}
.feature-list-item .label .icon {
@@ -898,9 +899,6 @@ a:hover .icon.out-link { background-position: -500px -14px;}
text-overflow: ellipsis;
overflow: hidden;
border-left: 1px solid rgba(0, 0, 0, .1);
-moz-transition: all 100ms;
-o-transition: all 100ms;
transition: all 100ms;
border-radius: 0 3px 3px 0;
}
@@ -1051,6 +1049,10 @@ a:hover .icon.out-link { background-position: -500px -14px;}
text-align: right;
}
.form-label-button-wrap .tag-reference-button {
border-radius: 0 3px 0 0;
}
.form-label-button-wrap .icon {
opacity: .5;
}
@@ -1137,7 +1139,7 @@ a:hover .icon.out-link { background-position: -500px -14px;}
/* Styles for raw tag inspector on hover */
.inspector-hover .tag-row .key-wrap,
.inspector-hover .tag-row .input-wrap-position {
.inspector-hover .tag-row .form-field.input-wrap-position {
width: 50%;
}
@@ -1190,69 +1192,6 @@ a:hover .icon.out-link { background-position: -500px -14px;}
line-height: 0;
display: block;
overflow: hidden;
-webkit-transition: opacity 200ms, width 0 200ms, padding 0 200ms, line-height 0 200ms;
-moz-transition: opacity 200ms, width 0 200ms, padding 0 200ms, line-height 0 200ms;
-o-transition: opacity 200ms, width 0 200ms, padding 0 200ms, line-height 0 200ms;
transition: opacity 200ms, width 0 200ms, padding 0 200ms, line-height 0 200ms;
}
/* first phase hover-to-active animations */
textarea,
.form-label,
.preset-input-wrap,
.preset-input-wrap .label {
-webkit-transition: all 200ms;
-moz-transition: all 200ms;
-o-transition: all 200ms;
transition: all 200ms;
}
/* second phase hover-to-active animations */
input,
.checkselect label:last-of-type {
-webkit-transition: opacity 200ms 200ms, width 200ms 200ms, margin-right 200ms 200ms;
-moz-transition: opacity 200ms 200ms, width 200ms 200ms, margin-right 200ms 200ms;
-o-transition: opacity 200ms 200ms, width 200ms 200ms, margin-right 200ms 200ms;
transition: opacity 200ms 200ms, width 200ms 200ms, margin-right 200ms 200ms;
}
.entity-editor-pane button.minor,
.combobox-caret,
.entity-editor-pane .header button,
.toggle-list label span,
.spin-control,
.more-fields,
.view-on-osm,
.hide-toggle:before,
.entity-editor-pane .toggle-list label::before,
.entity-editor-pane .toggle-list label.remove .icon {
-webkit-transition: opacity 200ms 200ms;
-moz-transition: opacity 200ms 200ms;
-o-transition: opacity 200ms 200ms;
transition: opacity 200ms 200ms;
}
.entity-editor-pane a.hide-toggle {
-webkit-transition: padding-left 200ms 200ms, color 200ms 200ms;
-moz-transition: padding-left 200ms 200ms, color 200ms 200ms;
-o-transition: padding-left 200ms 200ms, color 200ms 200ms;
transition: padding-left 200ms 200ms, color 200ms 200ms;
}
.entity-editor-pane .toggle-list label:not(.active) {
-webkit-transition: height 200ms 200ms, padding 200ms 200ms, border-width 100ms 300ms;
-moz-transition: height 200ms 200ms, padding 200ms 200ms, border-width 100ms 300ms;
-o-transition: height 200ms 200ms, padding 200ms 200ms, border-width 100ms 300ms;
transition: height 200ms 200ms, padding 200ms 200ms, border-width 100ms 300ms;
}
.entity-editor-pane .toggle-list label {
-webkit-transition: border-width 100ms 300ms, padding 200ms 200ms, background-color 200ms 200ms, color 200ms 200ms;
-moz-transition: border-width 100ms 300ms, padding 200ms 200ms, background-color 200ms 200ms, color 200ms 200ms;
-o-transition: border-width 100ms 300ms, padding 200ms 200ms, background-color 200ms 200ms, color 200ms 200ms;
transition: border-width 100ms 300ms, padding 200ms 200ms, background-color 200ms 200ms, color 200ms 200ms;
}
/* adding additional preset fields */
@@ -1260,10 +1199,6 @@ input,
.more-fields {
padding: 0 20px 20px 20px;
font-weight: bold;
-webkit-transition: padding 200ms 200ms, max-height 200ms 200ms;
-moz-transition: padding 200ms 200ms, max-height 200ms 200ms;
-o-transition: padding 200ms 200ms, max-height 200ms 200ms;
transition: padding 200ms 200ms, max-height 200ms 200ms;
}
.more-fields label { padding: 5px 10px 5px 0; }
@@ -1601,10 +1536,6 @@ div.combobox {
width: 40%;
float: left;
height: 30px;
-webkit-transition: width 200ms;
-moz-transition: width 200ms;
-o-transition: width 200ms;
transition: width 200ms;
}
.tag-row input.key {
@@ -2187,10 +2118,14 @@ img.wiki-image {
#attrib {
width: 100%;
height: 20px;
margin-bottom: 5px;
float: left;
clear: both;
pointer-events: none;
}
#attrib * { pointer-events: all; }
.base-layer-attribution,
.overlay-layer-attribution {
position: absolute;
@@ -2298,7 +2233,7 @@ img.wiki-image {
clear: both;
text-align: right;
width: 100%;
padding: 0px 5px;
padding: 0px 10px;
}
.api-status.offline,
@@ -2311,8 +2246,11 @@ img.wiki-image {
------------------------------------------------------- */
.modal {
top: 40px;
display: inline-block;
position:absolute;
border-radius: 3px;
overflow: hidden;
left: 0;
right: 0;
margin: auto;
@@ -2349,8 +2287,19 @@ img.wiki-image {
border-bottom: 1px solid #CCC;
}
.modal-section:last-child {
border-bottom: 0;
.modal-section.header h3 {
padding: 0;
}
.modal-section.buttons {
text-align: center;
}
.modal-section.buttons .action {
display: inline-block;
margin: 0 10px;
text-align: center;
vertical-align: middle;
}
.loading-modal {
@@ -2379,7 +2328,8 @@ img.wiki-image {
display: block;
content: '';
height: 100px;
width: 100px;
width: 100%;
max-width: 100px;
margin: auto;
margin-bottom: 10px;
background:transparent url(img/sprite.svg) no-repeat 0 -220px;
@@ -2389,6 +2339,10 @@ img.wiki-image {
border-right: 1px solid #CCC;
}
.modal-section:last-child {
border-bottom: 0;
}
/* Restore Modal
------------------------------------------------------- */
@@ -2458,14 +2412,6 @@ img.wiki-image {
margin-bottom: 0;
}
.mode-save button.action {
float: none;
margin: auto;
display: block;
color: white;
font-size: 14px;
}
.mode-save .user-info img {
float: left;
}
@@ -2528,6 +2474,56 @@ img.wiki-image {
.changeset-list li:first-child { border-top: 0;}
/* Conflict resolution
------------------------------------------------------- */
.conflicts-help {
padding: 20px;
background-color: #ffffbb;
border-bottom: 1px solid #ccc;
}
.conflicts-buttons {
padding: 20px;
}
.mode-save button.conflicts-button {
float: left;
}
.conflict-container {
border-bottom: 1px solid #ccc;
}
.conflict-description {
padding: 5px 20px;
display: block;
}
.conflicts-done {
padding: 20px 20px 0 20px;
}
.conflict-detail-container {
padding: 10px 20px;
}
.conflict-count {
padding: 10px 20px;
}
.conflict-choices {
margin-top: 10px;
}
.conflict-nav-buttons {
padding: 10px 0 20px 0;
}
.conflict-nav-button {
height: 30px;
}
/* Notices
------------------------------------------------------- */
@@ -2708,28 +2704,7 @@ img.wiki-image {
}
.tooltip-inner .keyhint {
font-size: 10px;
padding: 0 7px;
font-weight: bold;
display: inline-block;
border-radius: 2px;
border: 1px solid #CCC;
position: relative;
z-index: 1;
text-align: left;
}
.tooltip-inner .keyhint::after {
content: "";
position: absolute;
border-radius: 2px;
height: 10px;
width: 100%;
z-index: 0;
bottom: -4px;
left: -1px;
border: 1px solid #CCC;
border-top: 0;
}
/* Exceptions for tooltip layouts */
+25 -1
View File
@@ -313,10 +313,33 @@ en:
title: Save
help: "Save changes to OpenStreetMap, making them visible to other users."
no_changes: No changes to save.
error: An error occurred while trying to save
error: Errors occurred while trying to save
status_code: "Server returned status code {code}"
unknown_error_details: "Please ensure you are connected to the internet."
uploading: Uploading changes to OpenStreetMap.
unsaved_changes: You have unsaved changes
conflict:
header: Resolve conflicting edits
count: 'Conflict {num} of {total}'
previous: '< Previous'
next: 'Next >'
keep_local: Keep mine
keep_remote: Use theirs
restore: Restore
delete: Leave Deleted
download_changes: Or download your changes.
done: "All conflicts resolved!"
help: |
Another user changed some of the same map features you changed.
Click on each item below for more details about the conflict, and choose whether to keep
your changes or the other user's changes.
merge_remote_changes:
conflict:
deleted: 'This object has been deleted by {user}.'
location: 'This object was moved by both you and {user}.'
nodelist: 'Nodes were changed by both you and {user}.'
memberlist: 'Relation members were changed by both you and {user}.'
tags: 'You changed the <b>{tag}</b> tag to "{local}" and {user} changed it to "{remote}".'
success:
edited_osm: "Edited OSM!"
just_edited: "You just edited OpenStreetMap!"
@@ -329,6 +352,7 @@ en:
(<a href='https://help.openstreetmap.org/questions/4705/why-havent-my-changes-appeared-on-the-map' target='_blank'>details</a>).
confirm:
okay: "Okay"
cancel: "Cancel"
splash:
welcome: Welcome to the iD OpenStreetMap editor
text: "iD is a friendly but powerful tool for contributing to the world's best free world map. This is version {version}. For more information see {website} and report bugs at {github}."
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 B

After

Width:  |  Height:  |  Size: 1.4 KiB

+39 -11
View File
@@ -29,13 +29,13 @@
id="namedview392"
showgrid="true"
inkscape:zoom="1"
inkscape:cx="262.65678"
inkscape:cy="510.36274"
inkscape:cx="475.13394"
inkscape:cy="495.7147"
inkscape:window-x="276"
inkscape:window-y="71"
inkscape:window-maximized="0"
inkscape:current-layer="svg12393"
showguides="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-global="true"
inkscape:snap-bbox="true"
@@ -78,6 +78,34 @@
orientation="-0.41576267,-0.90947315"
position="646,553.53846"
id="guide6219" />
<sodipodi:guide
orientation="1,0"
position="480,470"
id="guide3809" />
<sodipodi:guide
orientation="1,0"
position="500,505"
id="guide3811" />
<sodipodi:guide
orientation="0,1"
position="495,480"
id="guide3813" />
<sodipodi:guide
orientation="0,1"
position="515,460"
id="guide3815" />
<sodipodi:guide
orientation="1,0"
position="520,475"
id="guide3817" />
<sodipodi:guide
orientation="0,1"
position="505,440"
id="guide3819" />
<sodipodi:guide
orientation="1,0"
position="540,470"
id="guide3821" />
</sodipodi:namedview>
<defs
id="defs12395">
@@ -98,7 +126,7 @@
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
@@ -1787,13 +1815,7 @@
inkscape:connector-curvature="0"
style="color:#000000;fill:#e06d5f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="path2997-7"
d="m 514,84 -1,1 0,1.59375 L 506.59375,93 505,93 l -1,1 0,2 1,1 2,0 1,-1 0,-1.59375 L 514.40625,88 516,88 l 1,-1 0,-2 -1,-1 z m -22,1 c -2.76142,0 -5,2.23858 -5,5 0,2.76143 5,7 5,7 0,0 5,-4.23857 5,-7 0,-2.76142 -2.23858,-5 -5,-5 z m 23,0 c 0.55228,0 1,0.44772 1,1 0,0.55229 -0.44772,1 -1,1 -0.25152,0 -0.48052,-0.0967 -0.65625,-0.25 -0.0344,-0.03002 -0.0638,-0.05934 -0.0937,-0.09375 -0.15335,-0.175731 -0.25,-0.404729 -0.25,-0.65625 0,-0.55228 0.44772,-1 1,-1 z m 10,0 -1,1 0,2 1,1 0,4 -1,1 0,2 1,1 2,0 1,-1 4,0 1,1 2,0 1,-1 0,-2 -1,-1 0,-4 1,-1 0,-2 -1,-1 -2,0 -1,1 -4,0 -1,-1 z m 1,1 c 0.55228,0 1,0.447715 1,1 0,0.552285 -0.44772,1 -1,1 -0.55228,0 -1,-0.447715 -1,-1 0,-0.552285 0.44772,-1 1,-1 z m 8,0 c 0.55228,0 1,0.447715 1,1 0,0.552285 -0.44772,1 -1,1 -0.55228,0 -1,-0.447715 -1,-1 0,-0.552285 0.44772,-1 1,-1 z m -41.84375,2 c 1.10457,0 2,0.89543 2,2 0,1.10457 -0.89543,2 -2,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 0.89543,-2 2,-2 z M 528,88 l 4,0 1,1 0,4 -1,1 -4,0 -1,-1 0,-4 z m -22,6 c 0.25152,0 0.48052,0.0967 0.65625,0.25 l 0.0937,0.09375 c 0.15335,0.175731 0.25,0.404734 0.25,0.65625 0,0.55229 -0.44772,1 -1,1 -0.55228,0 -1,-0.44771 -1,-1 0,-0.55228 0.44772,-1 1,-1 z m 20,0 c 0.55228,0 1,0.447715 1,1 0,0.552285 -0.44772,1 -1,1 -0.55228,0 -1,-0.447715 -1,-1 0,-0.552285 0.44772,-1 1,-1 z m 8,0 c 0.55228,0 1,0.447715 1,1 0,0.552285 -0.44772,1 -1,1 -0.55228,0 -1,-0.447715 -1,-1 0,-0.552285 0.44772,-1 1,-1 z" />
<path
d="m 514,104 -1,1 0,1.59375 L 506.59375,113 505,113 l -1,1 0,2 1,1 2,0 1,-1 0,-1.59375 L 514.40625,108 516,108 l 1,-1 0,-2 -1,-1 z m -22,1 c -2.76142,0 -5,2.23858 -5,5 0,2.76143 5,7 5,7 0,0 5,-4.23857 5,-7 0,-2.76142 -2.23858,-5 -5,-5 z m 23,0 c 0.55228,0 1,0.44772 1,1 0,0.55229 -0.44772,1 -1,1 -0.25152,0 -0.48052,-0.0967 -0.65625,-0.25 -0.0344,-0.03 -0.0638,-0.0593 -0.0937,-0.0937 -0.15335,-0.17573 -0.25,-0.40473 -0.25,-0.65625 0,-0.55228 0.44772,-1 1,-1 z m 10,0 -1,1 0,2 1,1 0,4 -1,1 0,2 1,1 2,0 1,-1 4,0 1,1 2,0 1,-1 0,-2 -1,-1 0,-4 1,-1 0,-2 -1,-1 -2,0 -1,1 -4,0 -1,-1 z m 1,1 c 0.55228,0 1,0.44771 1,1 0,0.55228 -0.44772,1 -1,1 -0.55228,0 -1,-0.44772 -1,-1 0,-0.55229 0.44772,-1 1,-1 z m 8,0 c 0.55228,0 1,0.44771 1,1 0,0.55228 -0.44772,1 -1,1 -0.55228,0 -1,-0.44772 -1,-1 0,-0.55229 0.44772,-1 1,-1 z m -41.84375,2 c 1.10457,0 2,0.89543 2,2 0,1.10457 -0.89543,2 -2,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 0.89543,-2 2,-2 z M 528,108 l 4,0 1,1 0,4 -1,1 -4,0 -1,-1 0,-4 z m -22,6 c 0.25152,0 0.48052,0.0967 0.65625,0.25 l 0.0937,0.0937 c 0.15335,0.17573 0.25,0.40473 0.25,0.65625 0,0.55229 -0.44772,1 -1,1 -0.55228,0 -1,-0.44771 -1,-1 0,-0.55228 0.44772,-1 1,-1 z m 20,0 c 0.55228,0 1,0.44771 1,1 0,0.55228 -0.44772,1 -1,1 -0.55228,0 -1,-0.44772 -1,-1 0,-0.55229 0.44772,-1 1,-1 z m 8,0 c 0.55228,0 1,0.44771 1,1 0,0.55228 -0.44772,1 -1,1 -0.55228,0 -1,-0.44772 -1,-1 0,-0.55229 0.44772,-1 1,-1 z"
id="path3444"
style="color:#000000;fill:#8cd05f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccccccccsscsscssccscccccccccccccccccccccccccccssssssssssssssscccccccccsccsssssssssssssss" />
d="m 514,83 -1,1 0,1.59375 L 506.59375,92 505,92 l -1,1 0,2 1,1 2,0 1,-1 0,-1.59375 L 514.40625,87 516,87 l 1,-1 0,-2 -1,-1 z m -24,1 c -2.76142,0 -5,2.23858 -5,5 0,2.76143 5,7 5,7 0,0 5,-4.23857 5,-7 0,-2.76142 -2.23858,-5 -5,-5 z m 25,0 c 0.55228,0 1,0.44772 1,1 0,0.55229 -0.44772,1 -1,1 -0.25152,0 -0.48052,-0.0967 -0.65625,-0.25 -0.0344,-0.03002 -0.0638,-0.05934 -0.0937,-0.09375 -0.15335,-0.175731 -0.25,-0.404729 -0.25,-0.65625 0,-0.55228 0.44772,-1 1,-1 z m 10,0 -1,1 0,2 1,1 0,4 -1,1 0,2 1,1 2,0 1,-1 4,0 1,1 2,0 1,-1 0,-2 -1,-1 0,-4 1,-1 0,-2 -1,-1 -2,0 -1,1 -4,0 -1,-1 z m 1,1 c 0.55228,0 1,0.447715 1,1 0,0.552285 -0.44772,1 -1,1 -0.55228,0 -1,-0.447715 -1,-1 0,-0.552285 0.44772,-1 1,-1 z m 8,0 c 0.55228,0 1,0.447715 1,1 0,0.552285 -0.44772,1 -1,1 -0.55228,0 -1,-0.447715 -1,-1 0,-0.552285 0.44772,-1 1,-1 z m -43.84375,2 c 1.10457,0 2,0.89543 2,2 0,1.10457 -0.89543,2 -2,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 0.89543,-2 2,-2 z M 528,87 l 4,0 1,1 0,4 -1,1 -4,0 -1,-1 0,-4 z m -22,6 c 0.25152,0 0.48052,0.0967 0.65625,0.25 l 0.0937,0.09375 c 0.15335,0.175731 0.25,0.404734 0.25,0.65625 0,0.55229 -0.44772,1 -1,1 -0.55228,0 -1,-0.44771 -1,-1 0,-0.55228 0.44772,-1 1,-1 z m 20,0 c 0.55228,0 1,0.447715 1,1 0,0.552285 -0.44772,1 -1,1 -0.55228,0 -1,-0.447715 -1,-1 0,-0.552285 0.44772,-1 1,-1 z m 8,0 c 0.55228,0 1,0.447715 1,1 0,0.552285 -0.44772,1 -1,1 -0.55228,0 -1,-0.447715 -1,-1 0,-0.552285 0.44772,-1 1,-1 z" />
<rect
width="2"
height="5.9999971"
@@ -2078,4 +2100,10 @@
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccccccccccccccccccccccccc" />
</g>
<path
d="m 514,104 -1,1 0,1.59375 L 506.59375,113 505,113 l -1,1 0,2 1,1 2,0 1,-1 0,-1.59375 L 514.40625,108 516,108 l 1,-1 0,-2 -1,-1 z m -24,1 c -2.76142,0 -5,2.23858 -5,5 0,2.76143 5,7 5,7 0,0 5,-4.23857 5,-7 0,-2.76142 -2.23858,-5 -5,-5 z m 25,0 c 0.55228,0 1,0.44772 1,1 0,0.55229 -0.44772,1 -1,1 -0.25152,0 -0.48052,-0.0967 -0.65625,-0.25 -0.0344,-0.03 -0.0638,-0.0593 -0.0937,-0.0937 -0.15335,-0.17573 -0.25,-0.40473 -0.25,-0.65625 0,-0.55228 0.44772,-1 1,-1 z m 10,0 -1,1 0,2 1,1 0,4 -1,1 0,2 1,1 2,0 1,-1 4,0 1,1 2,0 1,-1 0,-2 -1,-1 0,-4 1,-1 0,-2 -1,-1 -2,0 -1,1 -4,0 -1,-1 z m 1,1 c 0.55228,0 1,0.44772 1,1 0,0.55228 -0.44772,1 -1,1 -0.55228,0 -1,-0.44772 -1,-1 0,-0.55228 0.44772,-1 1,-1 z m 8,0 c 0.55228,0 1,0.44772 1,1 0,0.55228 -0.44772,1 -1,1 -0.55228,0 -1,-0.44772 -1,-1 0,-0.55228 0.44772,-1 1,-1 z m -43.84375,2 c 1.10457,0 2,0.89543 2,2 0,1.10457 -0.89543,2 -2,2 -1.10457,0 -2,-0.89543 -2,-2 0,-1.10457 0.89543,-2 2,-2 z M 528,108 l 4,0 1,1 0,4 -1,1 -4,0 -1,-1 0,-4 z m -22,6 c 0.25152,0 0.48052,0.0967 0.65625,0.25 l 0.0937,0.0937 c 0.15335,0.17573 0.25,0.40473 0.25,0.65625 0,0.55229 -0.44772,1 -1,1 -0.55228,0 -1,-0.44771 -1,-1 0,-0.55228 0.44772,-1 1,-1 z m 20,0 c 0.55228,0 1,0.44772 1,1 0,0.55228 -0.44772,1 -1,1 -0.55228,0 -1,-0.44772 -1,-1 0,-0.55228 0.44772,-1 1,-1 z m 8,0 c 0.55228,0 1,0.44772 1,1 0,0.55228 -0.44772,1 -1,1 -0.55228,0 -1,-0.44772 -1,-1 0,-0.55228 0.44772,-1 1,-1 z"
id="path3823"
style="color:#000000;fill:#8cd05f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccccccccsscsscssccscccccccccccccccccccccccccccssssssssssssssscccccccccsccsssssssssssssss" />
</svg>

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 179 KiB

+27 -3
View File
@@ -387,10 +387,33 @@
"title": "Save",
"help": "Save changes to OpenStreetMap, making them visible to other users.",
"no_changes": "No changes to save.",
"error": "An error occurred while trying to save",
"error": "Errors occurred while trying to save",
"status_code": "Server returned status code {code}",
"unknown_error_details": "Please ensure you are connected to the internet.",
"uploading": "Uploading changes to OpenStreetMap.",
"unsaved_changes": "You have unsaved changes"
"unsaved_changes": "You have unsaved changes",
"conflict": {
"header": "Resolve conflicting edits",
"count": "Conflict {num} of {total}",
"previous": "< Previous",
"next": "Next >",
"keep_local": "Keep mine",
"keep_remote": "Use theirs",
"restore": "Restore",
"delete": "Leave Deleted",
"download_changes": "Or download your changes.",
"done": "All conflicts resolved!",
"help": "Another user changed some of the same map features you changed.\nClick on each item below for more details about the conflict, and choose whether to keep\nyour changes or the other user's changes.\n"
}
},
"merge_remote_changes": {
"conflict": {
"deleted": "This object has been deleted by {user}.",
"location": "This object was moved by both you and {user}.",
"nodelist": "Nodes were changed by both you and {user}.",
"memberlist": "Relation members were changed by both you and {user}.",
"tags": "You changed the <b>{tag}</b> tag to \"{local}\" and {user} changed it to \"{remote}\"."
}
},
"success": {
"edited_osm": "Edited OSM!",
@@ -402,7 +425,8 @@
"help_html": "Your changes should appear in the \"Standard\" layer in a few minutes. Other layers, and certain features, may take longer\n(<a href='https://help.openstreetmap.org/questions/4705/why-havent-my-changes-appeared-on-the-map' target='_blank'>details</a>).\n"
},
"confirm": {
"okay": "Okay"
"okay": "Okay",
"cancel": "Cancel"
},
"splash": {
"welcome": "Welcome to the iD OpenStreetMap editor",
+4
View File
@@ -30,6 +30,7 @@
<script src='js/lib/d3.value.js'></script>
<script src='js/lib/d3-compat.js'></script>
<script src='js/lib/bootstrap-tooltip.js'></script>
<script src='js/lib/diff3.js'></script>
<script src='js/lib/rbush.js'></script>
<script src='js/lib/sexagesimal.js'></script>
<script src='js/lib/togeojson.js'></script>
@@ -81,6 +82,7 @@
<script src='js/id/ui/modal.js'></script>
<script src='js/id/ui/cmd.js'></script>
<script src='js/id/ui/confirm.js'></script>
<script src='js/id/ui/conflicts.js'></script>
<script src='js/id/ui/commit.js'></script>
<script src='js/id/ui/success.js'></script>
<script src='js/id/ui/loading.js'></script>
@@ -158,6 +160,7 @@
<script src='js/id/actions/join.js'></script>
<script src='js/id/actions/merge.js'></script>
<script src='js/id/actions/merge_polygon.js'></script>
<script src='js/id/actions/merge_remote_changes.js'></script>
<script src='js/id/actions/move_node.js'></script>
<script src='js/id/actions/move.js'></script>
<script src='js/id/actions/noop.js'></script>
@@ -259,6 +262,7 @@
"oauth_secret": "aMnOOCwExO2XYtRVWJ1bI9QOdqh1cay2UgpbhA6p"
}
]));
});
</script>
</body>
+254
View File
@@ -0,0 +1,254 @@
iD.actions.MergeRemoteChanges = function(id, localGraph, remoteGraph, formatUser) {
var option = 'safe', // 'safe', 'force_local', 'force_remote'
conflicts = [];
function user(d) {
return _.isFunction(formatUser) ? formatUser(d) : d;
}
function mergeLocation(remote, target) {
function pointEqual(a, b) {
var epsilon = 1e-6;
return (Math.abs(a[0] - b[0]) < epsilon) && (Math.abs(a[1] - b[1]) < epsilon);
}
if (option === 'force_local' || pointEqual(target.loc, remote.loc)) {
return target;
}
if (option === 'force_remote') {
return target.update({loc: remote.loc});
}
conflicts.push(t('merge_remote_changes.conflict.location', { user: user(remote.user) }));
return target;
}
function mergeNodes(base, remote, target) {
if (option === 'force_local' || _.isEqual(target.nodes, remote.nodes)) {
return target;
}
if (option === 'force_remote') {
return target.update({nodes: remote.nodes});
}
var ccount = conflicts.length,
o = base.nodes || [],
a = target.nodes || [],
b = remote.nodes || [],
nodes = [],
hunks = Diff3.diff3_merge(a, o, b, true);
for (var i = 0; i < hunks.length; i++) {
var hunk = hunks[i];
if (hunk.ok) {
nodes.push.apply(nodes, hunk.ok);
} else {
// for all conflicts, we can assume c.a !== c.b
// because `diff3_merge` called with `true` option to exclude false conflicts..
var c = hunk.conflict;
if (_.isEqual(c.o, c.a)) { // only changed remotely
nodes.push.apply(nodes, c.b);
} else if (_.isEqual(c.o, c.b)) { // only changed locally
nodes.push.apply(nodes, c.a);
} else { // changed both locally and remotely
conflicts.push(t('merge_remote_changes.conflict.nodelist', { user: user(remote.user) }));
break;
}
}
}
return (conflicts.length === ccount) ? target.update({nodes: nodes}) : target;
}
function mergeChildren(targetWay, children, updates, graph) {
function isUsed(node, targetWay) {
var parentWays = _.pluck(graph.parentWays(node), 'id');
return node.hasInterestingTags() ||
_.without(parentWays, targetWay.id).length > 0 ||
graph.parentRelations(node).length > 0;
}
var ccount = conflicts.length;
for (var i = 0; i < children.length; i++) {
var id = children[i],
node = graph.hasEntity(id);
// remove unused childNodes..
if (targetWay.nodes.indexOf(id) === -1) {
if (node && !isUsed(node, targetWay)) {
updates.removeIds.push(id);
}
continue;
}
// restore used childNodes..
var local = localGraph.hasEntity(id),
remote = remoteGraph.hasEntity(id),
target;
if (!remote) continue;
if (option === 'force_remote' && remote.visible) {
updates.replacements.push(remote);
}
if (option === 'force_local' && local) {
target = iD.Entity(local, { version: remote.version });
updates.replacements.push(target);
}
if (option === 'safe' && local && remote) {
target = iD.Entity(local, { version: remote.version });
if (remote.visible) {
target = mergeLocation(remote, target);
} else {
conflicts.push(t('merge_remote_changes.conflict.deleted', { user: user(remote.user) }));
}
if (conflicts.length !== ccount) break;
updates.replacements.push(target);
}
}
return targetWay;
}
function updateChildren(updates, graph) {
for (var i = 0; i < updates.replacements.length; i++) {
graph = graph.replace(updates.replacements[i]);
}
if (updates.removeIds.length) {
graph = iD.actions.DeleteMultiple(updates.removeIds)(graph);
}
return graph;
}
function mergeMembers(remote, target) {
if (option === 'force_local' || _.isEqual(target.members, remote.members)) {
return target;
}
if (option === 'force_remote') {
return target.update({members: remote.members});
}
conflicts.push(t('merge_remote_changes.conflict.memberlist', { user: user(remote.user) }));
return target;
}
function mergeTags(base, remote, target) {
function ignoreKey(k) {
return _.contains(iD.data.discarded, k);
}
if (option === 'force_local' || _.isEqual(target.tags, remote.tags)) {
return target;
}
if (option === 'force_remote') {
return target.update({tags: remote.tags});
}
var ccount = conflicts.length,
o = base.tags || {},
a = target.tags || {},
b = remote.tags || {},
keys = _.reject(_.union(_.keys(o), _.keys(a), _.keys(b)), ignoreKey),
tags = _.clone(a),
changed = false;
for (var i = 0; i < keys.length; i++) {
var k = keys[i];
if (o[k] !== b[k] && a[k] !== b[k]) { // changed remotely..
if (o[k] !== a[k]) { // changed locally..
conflicts.push(t('merge_remote_changes.conflict.tags',
{ tag: k, local: a[k], remote: b[k], user: user(remote.user) }));
} else { // unchanged locally, accept remote change..
if (b.hasOwnProperty(k)) {
tags[k] = b[k];
} else {
delete tags[k];
}
changed = true;
}
}
}
return (changed && conflicts.length === ccount) ? target.update({tags: tags}) : target;
}
// `graph.base()` is the common ancestor of the two graphs.
// `localGraph` contains user's edits up to saving
// `remoteGraph` contains remote edits to modified nodes
// `graph` must be a descendent of `localGraph` and may include
// some conflict resolution actions performed on it.
//
// --- ... --- `localGraph` -- ... -- `graph`
// /
// `graph.base()` --- ... --- `remoteGraph`
//
var action = function(graph) {
var updates = { replacements: [], removeIds: [] },
base = graph.base().entities[id],
local = localGraph.entity(id),
remote = remoteGraph.entity(id),
target = iD.Entity(local, { version: remote.version });
// delete/undelete
if (!remote.visible) {
if (option === 'force_remote') {
return iD.actions.DeleteMultiple([id])(graph);
} else if (option === 'force_local') {
if (target.type === 'way') {
target = mergeChildren(target, _.uniq(local.nodes), updates, graph);
graph = updateChildren(updates, graph);
}
return graph.replace(target);
} else {
conflicts.push(t('merge_remote_changes.conflict.deleted', { user: user(remote.user) }));
return graph; // do nothing
}
}
// merge
if (target.type === 'node') {
target = mergeLocation(remote, target);
} else if (target.type === 'way') {
// pull in any child nodes that may not be present locally..
graph.rebase(remoteGraph.childNodes(remote), [graph], false);
target = mergeNodes(base, remote, target);
target = mergeChildren(target, _.union(local.nodes, remote.nodes), updates, graph);
} else if (target.type === 'relation') {
target = mergeMembers(remote, target);
}
target = mergeTags(base, remote, target);
if (!conflicts.length) {
graph = updateChildren(updates, graph).replace(target);
}
return graph;
};
action.withOption = function(opt) {
option = opt;
return action;
};
action.conflicts = function() {
return conflicts;
};
return action;
};
+1 -1
View File
@@ -65,7 +65,7 @@ iD.behavior.Hash = function(context) {
if (location.hash) {
var q = iD.util.stringQs(location.hash.substring(1));
if (q.id) context.loadEntity(q.id.split(',')[0], !q.map);
if (q.id) context.zoomToEntity(q.id.split(',')[0], !q.map);
if (q.comment) context.storage('comment', q.comment);
hashchange();
if (q.map) hash.hadHash = true;
+49 -14
View File
@@ -1,6 +1,5 @@
iD.Connection = function() {
var event = d3.dispatch('authenticating', 'authenticated', 'auth', 'loading', 'load', 'loaded'),
var event = d3.dispatch('authenticating', 'authenticated', 'auth', 'loading', 'loaded'),
url = 'http://www.openstreetmap.org',
connection = {},
inflight = {},
@@ -42,10 +41,10 @@ iD.Connection = function() {
};
connection.loadFromURL = function(url, callback) {
function done(dom) {
return callback(null, parse(dom));
function done(err, dom) {
return callback(err, parse(dom));
}
return d3.xml(url).get().on('load', done);
return d3.xml(url).get(done);
};
connection.loadEntity = function(id, callback) {
@@ -55,11 +54,34 @@ iD.Connection = function() {
connection.loadFromURL(
url + '/api/0.6/' + type + '/' + osmID + (type !== 'node' ? '/full' : ''),
function(err, entities) {
event.load(err, {data: entities});
if (callback) callback(err, entities && _.find(entities, function(e) { return e.id === id; }));
if (callback) callback(err, {data: entities});
});
};
connection.loadMultiple = function(ids, callback) {
// TODO: upgrade lodash and just use _.chunk
function chunk(arr, chunkSize) {
var result = [];
for (var i = 0; i < arr.length; i += chunkSize) {
result.push(arr.slice(i, i + chunkSize));
}
return result;
}
_.each(_.groupBy(ids, iD.Entity.id.type), function(v, k) {
var type = k + 's',
osmIDs = _.map(v, iD.Entity.id.toOSM);
_.each(chunk(osmIDs, 150), function(arr) {
connection.loadFromURL(
url + '/api/0.6/' + type + '?' + type + '=' + arr.join(),
function(err, entities) {
if (callback) callback(err, {data: entities});
});
});
});
};
function authenticating() {
event.authenticating();
}
@@ -68,6 +90,12 @@ iD.Connection = function() {
event.authenticated();
}
function getLoc(attrs) {
var lon = attrs.lon && attrs.lon.value,
lat = attrs.lat && attrs.lat.value;
return [parseFloat(lon), parseFloat(lat)];
}
function getNodes(obj) {
var elems = obj.getElementsByTagName(ndStr),
nodes = new Array(elems.length);
@@ -101,15 +129,20 @@ iD.Connection = function() {
return members;
}
function getVisible(attrs) {
return (!attrs.visible || attrs.visible.value !== 'false');
}
var parsers = {
node: function nodeData(obj) {
var attrs = obj.attributes;
return new iD.Node({
id: iD.Entity.id.fromOSM(nodeStr, attrs.id.value),
loc: [parseFloat(attrs.lon.value), parseFloat(attrs.lat.value)],
loc: getLoc(attrs),
version: attrs.version.value,
user: attrs.user && attrs.user.value,
tags: getTags(obj)
tags: getTags(obj),
visible: getVisible(attrs)
});
},
@@ -120,7 +153,8 @@ iD.Connection = function() {
version: attrs.version.value,
user: attrs.user && attrs.user.value,
tags: getTags(obj),
nodes: getNodes(obj)
nodes: getNodes(obj),
visible: getVisible(attrs)
});
},
@@ -131,13 +165,14 @@ iD.Connection = function() {
version: attrs.version.value,
user: attrs.user && attrs.user.value,
tags: getTags(obj),
members: getMembers(obj)
members: getMembers(obj),
visible: getVisible(attrs)
});
}
};
function parse(dom) {
if (!dom || !dom.childNodes) return new Error('Bad request');
if (!dom || !dom.childNodes) return;
var root = dom.childNodes[0],
children = root.childNodes,
@@ -292,7 +327,7 @@ iD.Connection = function() {
return connection;
};
connection.loadTiles = function(projection, dimensions) {
connection.loadTiles = function(projection, dimensions, callback) {
if (off) return;
@@ -345,7 +380,7 @@ iD.Connection = function() {
loadedTiles[id] = true;
delete inflight[id];
event.load(err, _.extend({data: parsed}, tile));
if (callback) callback(err, _.extend({data: parsed}, tile));
if (_.isEmpty(inflight)) {
event.loaded();
+3
View File
@@ -56,6 +56,9 @@ iD.Entity.prototype = {
if (!this.id && this.type) {
this.id = iD.Entity.id(this.type);
}
if (!this.hasOwnProperty('visible')) {
this.visible = true;
}
if (iD.debug) {
Object.freeze(this);
+6 -16
View File
@@ -16,10 +16,7 @@ iD.Graph = function(other, mutable) {
this.transients = {};
this._childNodes = {};
if (!mutable) {
this.freeze();
}
this.frozen = !mutable;
};
iD.Graph.prototype = {
@@ -112,20 +109,19 @@ iD.Graph.prototype = {
// is used only during the history operation that merges newly downloaded
// data into each state. To external consumers, it should appear as if the
// graph always contained the newly downloaded data.
rebase: function(entities, stack) {
rebase: function(entities, stack, force) {
var base = this.base(),
i, j, k, id;
for (i = 0; i < entities.length; i++) {
var entity = entities[i];
if (base.entities[entity.id])
if (!entity.visible || (!force && base.entities[entity.id]))
continue;
// Merging data into the base graph
base.entities[entity.id] = entity;
this._updateCalculated(undefined, entity,
base.parentWays, base.parentRels);
this._updateCalculated(undefined, entity, base.parentWays, base.parentRels);
// Restore provisionally-deleted nodes that are discovered to have an extant parent
if (entity.type === 'way') {
@@ -262,15 +258,9 @@ iD.Graph.prototype = {
arguments[i].call(graph, graph);
}
return this.frozen ? graph.freeze() : this;
},
if (this.frozen) graph.frozen = true;
freeze: function() {
this.frozen = true;
// No longer freezing entities here due to in-place updates needed in rebase.
return this;
return graph;
},
// Obliterates any existing entities
+14 -4
View File
@@ -41,9 +41,13 @@ iD.History = function(context) {
return stack[index].graph;
},
base: function() {
return stack[0].graph;
},
merge: function(entities, extent) {
stack[0].graph.rebase(entities, _.pluck(stack, 'graph'));
tree.rebase(entities);
stack[0].graph.rebase(entities, _.pluck(stack, 'graph'), false);
tree.rebase(entities, false);
dispatch.change(undefined, extent);
},
@@ -195,6 +199,12 @@ iD.History = function(context) {
if (id in base.graph.entities) {
baseEntities[id] = base.graph.entities[id];
}
// get originals of parent entities too
_.forEach(base.graph._parentWays[id], function(parentId) {
if (parentId in base.graph.entities) {
baseEntities[parentId] = base.graph.entities[parentId];
}
});
});
var x = {};
@@ -237,8 +247,8 @@ iD.History = function(context) {
var baseEntities = h.baseEntities.map(function(entity) {
return iD.Entity(entity);
});
stack[0].graph.rebase(baseEntities, _.pluck(stack, 'graph'));
tree.rebase(baseEntities);
stack[0].graph.rebase(baseEntities, _.pluck(stack, 'graph'), true);
tree.rebase(baseEntities, true);
}
stack = h.stack.map(function(d) {
+2 -2
View File
@@ -39,13 +39,13 @@ iD.Tree = function(head) {
var tree = {};
tree.rebase = function(entities) {
tree.rebase = function(entities, force) {
var insertions = {};
for (var i = 0; i < entities.length; i++) {
var entity = entities[i];
if (head.entities.hasOwnProperty(entity.id) || rectangles[entity.id])
if (!entity.visible || (!force && (head.entities.hasOwnProperty(entity.id) || rectangles[entity.id])))
continue;
insertions[entity.id] = entity;
+46 -30
View File
@@ -53,10 +53,6 @@ window.iD = function () {
locale = locale.split('-')[0];
}
connection.on('load.context', function loadContext(err, result) {
history.merge(result.data, result.extent);
});
context.preauth = function(options) {
connection.switch(options);
return context;
@@ -86,6 +82,51 @@ window.iD = function () {
context.connection = function() { return connection; };
context.history = function() { return history; };
/* Connection */
function entitiesLoaded(err, result) {
if (!err) history.merge(result.data, result.extent);
}
context.loadTiles = function(projection, dimensions, callback) {
function done(err, result) {
entitiesLoaded(err, result);
if (callback) callback(err, result);
}
connection.loadTiles(projection, dimensions, done);
};
context.loadEntity = function(id, callback) {
function done(err, result) {
entitiesLoaded(err, result);
if (callback) callback(err, result);
}
connection.loadEntity(id, done);
};
context.zoomToEntity = function(id, zoomTo) {
if (zoomTo !== false) {
this.loadEntity(id, function(err, result) {
if (err) return;
var entity = _.find(result.data, function(e) { return e.id === id; });
if (entity) { map.zoomTo(entity); }
});
}
map.on('drawn.zoomToEntity', function() {
if (!context.hasEntity(id)) return;
map.on('drawn.zoomToEntity', null);
context.on('enter.zoomToEntity', null);
context.enter(iD.modes.Select(context, [id]));
});
context.on('enter.zoomToEntity', function() {
if (mode.id !== 'browse') {
map.on('drawn.zoomToEntity', null);
context.on('enter.zoomToEntity', null);
}
});
};
/* History */
context.graph = history.graph;
context.changes = history.changes;
@@ -100,7 +141,7 @@ window.iD = function () {
};
context.save = function() {
if (inIntro) return;
if (inIntro || (mode && mode.id === 'save')) return;
history.save();
if (history.hasChanges()) return t('save.unsaved_changes');
};
@@ -170,31 +211,6 @@ window.iD = function () {
}
};
context.loadEntity = function(id, zoomTo) {
if (zoomTo !== false) {
connection.loadEntity(id, function(error, entity) {
if (entity) {
map.zoomTo(entity);
}
});
}
map.on('drawn.loadEntity', function() {
if (!context.hasEntity(id)) return;
map.on('drawn.loadEntity', null);
context.on('enter.loadEntity', null);
context.enter(iD.modes.Select(context, [id]));
});
context.on('enter.loadEntity', function() {
if (mode.id !== 'browse') {
map.on('drawn.loadEntity', null);
context.on('enter.loadEntity', null);
}
});
};
/* Behaviors */
context.install = function(behavior) {
context.surface().call(behavior);
+233 -41
View File
@@ -1,43 +1,249 @@
iD.modes.Save = function(context) {
var ui = iD.ui.Commit(context)
.on('cancel', cancel)
.on('save', save);
.on('cancel', cancel)
.on('save', save);
function cancel() {
context.enter(iD.modes.Browse(context));
}
function save(e) {
var loading = iD.ui.Loading(context)
.message(t('save.uploading'))
.blocking(true);
function save(e, tryAgain) {
function withChildNodes(ids) {
return _.uniq(_.reduce(toCheck, function(result, id) {
var e = context.entity(id);
if (e.type === 'way') result.push.apply(result, e.nodes);
return result;
}, _.clone(ids)));
}
context.container()
.call(loading);
var loading = iD.ui.Loading(context).message(t('save.uploading')).blocking(true),
history = context.history(),
origChanges = history.changes(iD.actions.DiscardTags(history.difference())),
localGraph = context.graph(),
remoteGraph = iD.Graph(history.base(), true),
modified = _.filter(history.difference().summary(), {changeType: 'modified'}),
toCheck = _.pluck(_.pluck(modified, 'entity'), 'id'),
toLoad = withChildNodes(toCheck),
conflicts = [],
errors = [];
context.connection().putChangeset(
context.history().changes(iD.actions.DiscardTags(context.history().difference())),
e.comment,
context.history().imageryUsed(),
function(err, changeset_id) {
loading.close();
if (err) {
var confirm = iD.ui.confirm(context.container());
confirm
.select('.modal-section.header')
.append('h3')
.text(t('save.error'));
confirm
.select('.modal-section.message-text')
.append('p')
.text(err.responseText || t('save.unknown_error_details'));
} else {
context.flush();
success(e, changeset_id);
if (!tryAgain) history.perform(iD.actions.Noop()); // checkpoint
context.container().call(loading);
if (toCheck.length) {
context.connection().loadMultiple(toLoad, loaded);
} else {
finalize();
}
// Reload modified entities into an alternate graph and check for conflicts..
function loaded(err, result) {
if (errors.length) return;
if (err) {
errors.push({
msg: err.responseText,
details: [ t('save.status_code', { code: err.status }) ]
});
showErrors();
} else {
_.each(result.data, function(entity) {
remoteGraph.replace(entity);
toLoad = _.without(toLoad, entity.id);
});
if (!toLoad.length) {
checkConflicts();
}
}
}
function checkConflicts() {
function choice(id, text, action) {
return { id: id, text: text, action: function() { history.replace(action); } };
}
function formatUser(d) {
return '<a href="' + context.connection().userURL(d) + '" target="_blank">' + d + '</a>';
}
function entityName(entity) {
return iD.util.displayName(entity) || (iD.util.displayType(entity.id) + ' ' + entity.id);
}
function compareVersions(local, remote) {
if (local.version !== remote.version) return false;
if (local.type === 'way') {
var children = _.union(local.nodes, remote.nodes);
for (var i = 0; i < children.length; i++) {
var a = localGraph.hasEntity(children[i]),
b = remoteGraph.hasEntity(children[i]);
if (!a || !b || a.version !== b.version) return false;
}
}
return true;
}
_.each(toCheck, function(id) {
var local = localGraph.entity(id),
remote = remoteGraph.entity(id);
if (compareVersions(local, remote)) return;
var action = iD.actions.MergeRemoteChanges,
merge = action(id, localGraph, remoteGraph, formatUser),
diff = history.replace(merge);
if (diff.length()) return; // merged safely
var forceLocal = action(id, localGraph, remoteGraph).withOption('force_local'),
forceRemote = action(id, localGraph, remoteGraph).withOption('force_remote'),
keepMine = t('save.conflict.' + (remote.visible ? 'keep_local' : 'restore')),
keepTheirs = t('save.conflict.' + (remote.visible ? 'keep_remote' : 'delete'));
conflicts.push({
id: id,
name: entityName(local),
details: merge.conflicts(),
chosen: 1,
choices: [
choice(id, keepMine, forceLocal),
choice(id, keepTheirs, forceRemote)
]
});
});
finalize();
}
function finalize() {
if (conflicts.length) {
conflicts.sort(function(a,b) { return b.id.localeCompare(a.id); });
showConflicts();
} else if (errors.length) {
showErrors();
} else {
context.connection().putChangeset(
history.changes(iD.actions.DiscardTags(history.difference())),
e.comment,
history.imageryUsed(),
function(err, changeset_id) {
if (err) {
errors.push({
msg: err.responseText,
details: [ t('save.status_code', { code: err.status }) ]
});
showErrors();
} else {
loading.close();
context.flush();
success(e, changeset_id);
}
});
}
}
function showConflicts() {
var selection = context.container()
.select('#sidebar')
.append('div')
.attr('class','sidebar-component');
loading.close();
selection.call(iD.ui.Conflicts(context)
.list(conflicts)
.on('download', function() {
var data = JXON.stringify(context.connection().osmChangeJXON('CHANGEME', origChanges)),
win = window.open('data:text/xml,' + encodeURIComponent(data), '_blank');
win.focus();
})
.on('cancel', function() {
history.pop();
selection.remove();
})
.on('save', function() {
selection.remove();
save(e, true);
})
);
}
function showErrors() {
var selection = iD.ui.confirm(context.container());
history.pop();
loading.close();
selection
.select('.modal-section.header')
.append('h3')
.text(t('save.error'));
addErrors(selection, errors);
selection.okButton();
}
function addErrors(selection, data) {
var message = selection
.select('.modal-section.message-text');
var items = message
.selectAll('.error-container')
.data(data);
var enter = items.enter()
.append('div')
.attr('class', 'error-container');
enter
.append('a')
.attr('class', 'error-description')
.attr('href', '#')
.classed('hide-toggle', true)
.text(function(d) { return d.msg || t('save.unknown_error_details'); })
.on('click', function() {
var error = d3.select(this),
detail = d3.select(this.nextElementSibling),
exp = error.classed('expanded');
detail.style('display', exp ? 'none' : 'block');
error.classed('expanded', !exp);
d3.event.preventDefault();
});
var details = enter
.append('div')
.attr('class', 'error-detail-container')
.style('display', 'none');
details
.append('ul')
.attr('class', 'error-detail-list')
.selectAll('li')
.data(function(d) { return d.details || []; })
.enter()
.append('li')
.attr('class', 'error-detail-item')
.text(function(d) { return d; });
items.exit()
.remove();
}
}
function success(e, changeset_id) {
context.enter(iD.modes.Browse(context)
.sidebar(iD.ui.Success(context)
@@ -54,27 +260,13 @@ iD.modes.Save = function(context) {
id: 'save'
};
var behaviors = [
iD.behavior.Hover(context),
iD.behavior.Select(context),
iD.behavior.Lasso(context),
iD.modes.DragNode(context).behavior];
mode.enter = function() {
behaviors.forEach(function(behavior) {
context.install(behavior);
});
context.connection().authenticate(function() {
context.ui().sidebar.show(ui);
});
};
mode.exit = function() {
behaviors.forEach(function(behavior) {
context.uninstall(behavior);
});
context.ui().sidebar.hide(ui);
};
+4 -2
View File
@@ -151,14 +151,16 @@ iD.Background = function(context) {
background.zoomToGpxLayer = function() {
if (background.hasGpxLayer()) {
var viewport = context.map().extent().polygon(),
var map = context.map(),
viewport = map.trimmedExtent().polygon(),
coords = _.reduce(gpxLayer.geojson().features, function(coords, feature) {
var c = feature.geometry.coordinates;
return _.union(coords, feature.geometry.type === 'Point' ? [c] : c);
}, []);
if (!iD.geo.polygonIntersectsPolygon(viewport, coords)) {
context.map().extent(d3.geo.bounds(gpxLayer.geojson()));
var extent = iD.geo.Extent(d3.geo.bounds(gpxLayer.geojson()));
map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));
}
}
};
+19 -8
View File
@@ -205,7 +205,7 @@ iD.Map = function(context) {
}
if (map.editable()) {
context.connection().loadTiles(projection, dimensions);
context.loadTiles(projection, dimensions);
drawVector(difference, extent);
} else {
editOff();
@@ -341,8 +341,10 @@ iD.Map = function(context) {
};
map.zoomTo = function(entity, zoomLimits) {
var extent = entity.extent(context.graph()),
zoom = map.extentZoom(extent);
var extent = entity.extent(context.graph());
if (!isFinite(extent.area())) return;
var zoom = map.trimmedExtentZoom(extent);
zoomLimits = zoomLimits || [context.minEditableZoom(), 20];
map.centerZoom(extent.center(), Math.min(Math.max(zoom, zoomLimits[0]), zoomLimits[1]));
};
@@ -391,19 +393,28 @@ iD.Map = function(context) {
projection.invert([dimensions[0] - pad, headerY + pad]));
};
map.extentZoom = function(_) {
var extent = iD.geo.Extent(_),
tl = projection([extent[0][0], extent[1][1]]),
function calcZoom(extent, dim) {
var tl = projection([extent[0][0], extent[1][1]]),
br = projection([extent[1][0], extent[0][1]]);
// Calculate maximum zoom that fits extent
var hFactor = (br[0] - tl[0]) / dimensions[0],
vFactor = (br[1] - tl[1]) / dimensions[1],
var hFactor = (br[0] - tl[0]) / dim[0],
vFactor = (br[1] - tl[1]) / dim[1],
hZoomDiff = Math.log(Math.abs(hFactor)) / Math.LN2,
vZoomDiff = Math.log(Math.abs(vFactor)) / Math.LN2,
newZoom = map.zoom() - Math.max(hZoomDiff, vZoomDiff);
return newZoom;
}
map.extentZoom = function(_) {
return calcZoom(iD.geo.Extent(_), dimensions);
};
map.trimmedExtentZoom = function(_) {
var trimY = 120, trimX = 40,
trimmed = [dimensions[0] - trimX, dimensions[1] - trimY];
return calcZoom(iD.geo.Extent(_), trimmed);
};
map.editable = function() {
+4 -4
View File
@@ -88,6 +88,10 @@ iD.ui = function(context) {
.attr('id', 'footer')
.attr('class', 'fillD');
footer.append('div')
.attr('class', 'api-status')
.call(iD.ui.Status(context));
footer.append('div')
.attr('id', 'scale-block')
.call(iD.ui.Scale(context));
@@ -132,10 +136,6 @@ iD.ui = function(context) {
.attr('tabindex', -1)
.call(iD.ui.Contributors(context));
footer.append('div')
.attr('class', 'api-status')
.call(iD.ui.Status(context));
window.onbeforeunload = function() {
return context.save();
};
+2 -1
View File
@@ -6,6 +6,7 @@ iD.ui.Commit = function(context) {
summary = context.history().difference().summary();
function zoomToEntity(change) {
var entity = change.entity;
if (change.changeType !== 'deleted' &&
context.graph().entity(entity.id).geometry(context.graph()) !== 'vertex') {
@@ -116,7 +117,7 @@ iD.ui.Commit = function(context) {
// Confirm Button
var saveButton = saveSection.append('button')
.attr('class', 'action col4 button')
.attr('class', 'action col6 button')
.on('click.save', function() {
event.save({
comment: commentField.node().value
+12 -7
View File
@@ -12,15 +12,20 @@ iD.ui.confirm = function(selection) {
section.append('div')
.attr('class', 'modal-section message-text');
var buttonwrap = section.append('div')
var buttons = section.append('div')
.attr('class', 'modal-section buttons cf');
buttonwrap.append('button')
.attr('class', 'col2 action')
.on('click.confirm', function() {
modal.remove();
})
.text(t('confirm.okay'));
modal.okButton = function() {
buttons
.append('button')
.attr('class', 'action col4')
.on('click.confirm', function() {
modal.remove();
})
.text(t('confirm.okay'));
return modal;
};
return modal;
};
+237
View File
@@ -0,0 +1,237 @@
iD.ui.Conflicts = function(context) {
var dispatch = d3.dispatch('download', 'cancel', 'save'),
list;
function conflicts(selection) {
var header = selection
.append('div')
.attr('class', 'header fillL');
header
.append('button')
.attr('class', 'fr')
.on('click', function() { dispatch.cancel(); })
.append('span')
.attr('class', 'icon close');
header
.append('h3')
.text(t('save.conflict.header'));
var body = selection
.append('div')
.attr('class', 'body fillL');
body
.append('div')
.attr('class', 'conflicts-help')
.text(t('save.conflict.help'))
.append('a')
.attr('class', 'conflicts-download')
.text(t('save.conflict.download_changes'))
.on('click.download', function() { dispatch.download(); });
body
.append('div')
.attr('class', 'conflict-container fillL3')
.call(showConflict, 0);
body
.append('div')
.attr('class', 'conflicts-done')
.attr('opacity', 0)
.style('display', 'none')
.text(t('save.conflict.done'));
var buttons = body
.append('div')
.attr('class','buttons col12 joined conflicts-buttons');
buttons
.append('button')
.attr('disabled', list.length > 1)
.attr('class', 'action conflicts-button col6')
.text(t('save.title'))
.on('click.try_again', function() { dispatch.save(); });
buttons
.append('button')
.attr('class', 'secondary-action conflicts-button col6')
.text(t('confirm.cancel'))
.on('click.cancel', function() { dispatch.cancel(); });
}
function showConflict(selection, index) {
if (index < 0 || index >= list.length) return;
var parent = d3.select(selection.node().parentElement);
// enable save button if this is the last conflict being reviewed..
if (index === list.length - 1) {
window.setTimeout(function() {
parent.select('.conflicts-button')
.attr('disabled', null);
parent.select('.conflicts-done')
.transition()
.attr('opacity', 1)
.style('display', 'block');
}, 250);
}
var item = selection
.selectAll('.conflict')
.data([list[index]]);
var enter = item.enter()
.append('div')
.attr('class', 'conflict');
enter
.append('h4')
.attr('class', 'conflict-count')
.text(t('save.conflict.count', { num: index + 1, total: list.length }));
enter
.append('a')
.attr('class', 'conflict-description')
.attr('href', '#')
.text(function(d) { return d.name; })
.on('click', function(d) {
zoomToEntity(d.id);
d3.event.preventDefault();
});
var details = enter
.append('div')
.attr('class', 'conflict-detail-container');
details
.append('ul')
.attr('class', 'conflict-detail-list')
.selectAll('li')
.data(function(d) { return d.details || []; })
.enter()
.append('li')
.attr('class', 'conflict-detail-item')
.html(function(d) { return d; });
details
.append('div')
.attr('class', 'conflict-choices')
.call(addChoices);
details
.append('div')
.attr('class', 'conflict-nav-buttons joined cf')
.selectAll('button')
.data(['previous', 'next'])
.enter()
.append('button')
.text(function(d) { return t('save.conflict.' + d); })
.attr('class', 'conflict-nav-button action col6')
.attr('disabled', function(d, i) {
return (i === 0 && index === 0) ||
(i === 1 && index === list.length - 1) || null;
})
.on('click', function(d, i) {
var container = parent.select('.conflict-container'),
sign = (i === 0 ? -1 : 1);
container
.selectAll('.conflict')
.remove();
container
.call(showConflict, index + sign);
d3.event.preventDefault();
});
item.exit()
.remove();
}
function addChoices(selection) {
var choices = selection
.append('ul')
.attr('class', 'layer-list')
.selectAll('li')
.data(function(d) { return d.choices || []; });
var enter = choices.enter()
.append('li')
.attr('class', 'layer');
var label = enter
.append('label');
label
.append('input')
.attr('type', 'radio')
.attr('name', function(d) { return d.id; })
.on('change', function(d, i) {
var ul = this.parentElement.parentElement.parentElement;
ul.__data__.chosen = i;
choose(ul, d);
});
label
.append('span')
.text(function(d) { return d.text; });
choices
.each(function(d, i) {
var ul = this.parentElement;
if (ul.__data__.chosen === i) choose(ul, d);
});
}
function choose(ul, datum) {
if (d3.event) d3.event.preventDefault();
d3.select(ul)
.selectAll('li')
.classed('active', function(d) { return d === datum; })
.selectAll('input')
.property('checked', function(d) { return d === datum; });
datum.action();
zoomToEntity(datum.id);
}
function zoomToEntity(id) {
context.surface().selectAll('.hover')
.classed('hover', false);
var entity = context.graph().hasEntity(id);
if (entity) {
context.map().zoomTo(entity);
context.surface().selectAll(
iD.util.entityOrMemberSelector([entity.id], context.graph()))
.classed('hover', true);
}
}
// The conflict list should be an array of objects like:
// {
// id: id,
// name: entityName(local),
// details: merge.conflicts(),
// chosen: 1,
// choices: [
// choice(id, keepMine, forceLocal),
// choice(id, keepTheirs, forceRemote)
// ]
// }
conflicts.list = function(_) {
if (!arguments.length) return list;
list = _;
return conflicts;
};
return d3.rebind(conflicts, dispatch, 'on');
};
+1 -1
View File
@@ -55,7 +55,7 @@ iD.ui.Contributors = function(context) {
return function(selection) {
update(selection);
context.connection().on('load.contributors', function() {
context.connection().on('loaded.contributors', function() {
update(selection);
});
+1 -1
View File
@@ -223,7 +223,7 @@ iD.ui.FeatureList = function(context) {
else if (d.entity) {
context.enter(iD.modes.Select(context, [d.entity.id]));
} else {
context.loadEntity(d.id);
context.zoomToEntity(d.id);
}
}
-6
View File
@@ -52,15 +52,9 @@ iD.ui.modal = function(selection, blocking) {
if (animate) {
shaded.transition().style('opacity', 1);
modal
.style('top','0px')
.transition()
.duration(200)
.style('top','40px');
} else {
shaded.style('opacity', 1);
}
return shaded;
};
+8
View File
@@ -30,6 +30,14 @@ iD.util.displayName = function(entity) {
return entity.tags[localeName] || entity.tags.name || entity.tags.ref;
};
iD.util.displayType = function(id) {
return {
n: t('inspector.node'),
w: t('inspector.way'),
r: t('inspector.relation')
}[id.charAt(0)];
};
iD.util.stringQs = function(str) {
return str.split('&').reduce(function(obj, pair){
var parts = pair.split('=');
+431
View File
@@ -0,0 +1,431 @@
// Copyright (c) 2006, 2008 Tony Garnock-Jones <tonyg@lshift.net>
// Copyright (c) 2006, 2008 LShift Ltd. <query@lshift.net>
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation files
// (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software,
// and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
// BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// source: https://bitbucket.org/lshift/synchrotron/src
Diff3 = (function() {
'use strict';
var diff3 = {
longest_common_subsequence: function(file1, file2) {
/* Text diff algorithm following Hunt and McIlroy 1976.
* J. W. Hunt and M. D. McIlroy, An algorithm for differential file
* comparison, Bell Telephone Laboratories CSTR #41 (1976)
* http://www.cs.dartmouth.edu/~doug/
*
* Expects two arrays of strings.
*/
var equivalenceClasses;
var file2indices;
var newCandidate;
var candidates;
var line;
var c, i, j, jX, r, s;
equivalenceClasses = {};
for (j = 0; j < file2.length; j++) {
line = file2[j];
if (equivalenceClasses[line]) {
equivalenceClasses[line].push(j);
} else {
equivalenceClasses[line] = [j];
}
}
candidates = [{file1index: -1,
file2index: -1,
chain: null}];
for (i = 0; i < file1.length; i++) {
line = file1[i];
file2indices = equivalenceClasses[line] || [];
r = 0;
c = candidates[0];
for (jX = 0; jX < file2indices.length; jX++) {
j = file2indices[jX];
for (s = 0; s < candidates.length; s++) {
if ((candidates[s].file2index < j) &&
((s == candidates.length - 1) ||
(candidates[s + 1].file2index > j)))
break;
}
if (s < candidates.length) {
newCandidate = {file1index: i,
file2index: j,
chain: candidates[s]};
if (r == candidates.length) {
candidates.push(c);
} else {
candidates[r] = c;
}
r = s + 1;
c = newCandidate;
if (r == candidates.length) {
break; // no point in examining further (j)s
}
}
}
candidates[r] = c;
}
// At this point, we know the LCS: it's in the reverse of the
// linked-list through .chain of
// candidates[candidates.length - 1].
return candidates[candidates.length - 1];
},
diff_comm: function(file1, file2) {
// We apply the LCS to build a "comm"-style picture of the
// differences between file1 and file2.
var result = [];
var tail1 = file1.length;
var tail2 = file2.length;
var common = {common: []};
function processCommon() {
if (common.common.length) {
common.common.reverse();
result.push(common);
common = {common: []};
}
}
for (var candidate = Diff3.longest_common_subsequence(file1, file2);
candidate !== null;
candidate = candidate.chain)
{
var different = {file1: [], file2: []};
while (--tail1 > candidate.file1index) {
different.file1.push(file1[tail1]);
}
while (--tail2 > candidate.file2index) {
different.file2.push(file2[tail2]);
}
if (different.file1.length || different.file2.length) {
processCommon();
different.file1.reverse();
different.file2.reverse();
result.push(different);
}
if (tail1 >= 0) {
common.common.push(file1[tail1]);
}
}
processCommon();
result.reverse();
return result;
},
diff_patch: function(file1, file2) {
// We apply the LCD to build a JSON representation of a
// diff(1)-style patch.
var result = [];
var tail1 = file1.length;
var tail2 = file2.length;
function chunkDescription(file, offset, length) {
var chunk = [];
for (var i = 0; i < length; i++) {
chunk.push(file[offset + i]);
}
return {offset: offset,
length: length,
chunk: chunk};
}
for (var candidate = Diff3.longest_common_subsequence(file1, file2);
candidate !== null;
candidate = candidate.chain)
{
var mismatchLength1 = tail1 - candidate.file1index - 1;
var mismatchLength2 = tail2 - candidate.file2index - 1;
tail1 = candidate.file1index;
tail2 = candidate.file2index;
if (mismatchLength1 || mismatchLength2) {
result.push({file1: chunkDescription(file1,
candidate.file1index + 1,
mismatchLength1),
file2: chunkDescription(file2,
candidate.file2index + 1,
mismatchLength2)});
}
}
result.reverse();
return result;
},
strip_patch: function(patch) {
// Takes the output of Diff3.diff_patch(), and removes
// information from it. It can still be used by patch(),
// below, but can no longer be inverted.
var newpatch = [];
for (var i = 0; i < patch.length; i++) {
var chunk = patch[i];
newpatch.push({file1: {offset: chunk.file1.offset,
length: chunk.file1.length},
file2: {chunk: chunk.file2.chunk}});
}
return newpatch;
},
invert_patch: function(patch) {
// Takes the output of Diff3.diff_patch(), and inverts the
// sense of it, so that it can be applied to file2 to give
// file1 rather than the other way around.
for (var i = 0; i < patch.length; i++) {
var chunk = patch[i];
var tmp = chunk.file1;
chunk.file1 = chunk.file2;
chunk.file2 = tmp;
}
},
patch: function (file, patch) {
// Applies a patch to a file.
//
// Given file1 and file2, Diff3.patch(file1,
// Diff3.diff_patch(file1, file2)) should give file2.
var result = [];
var commonOffset = 0;
function copyCommon(targetOffset) {
while (commonOffset < targetOffset) {
result.push(file[commonOffset]);
commonOffset++;
}
}
for (var chunkIndex = 0; chunkIndex < patch.length; chunkIndex++) {
var chunk = patch[chunkIndex];
copyCommon(chunk.file1.offset);
for (var lineIndex = 0; lineIndex < chunk.file2.chunk.length; lineIndex++) {
result.push(chunk.file2.chunk[lineIndex]);
}
commonOffset += chunk.file1.length;
}
copyCommon(file.length);
return result;
},
diff_indices: function(file1, file2) {
// We apply the LCS to give a simple representation of the
// offsets and lengths of mismatched chunks in the input
// files. This is used by diff3_merge_indices below.
var result = [];
var tail1 = file1.length;
var tail2 = file2.length;
for (var candidate = Diff3.longest_common_subsequence(file1, file2);
candidate !== null;
candidate = candidate.chain)
{
var mismatchLength1 = tail1 - candidate.file1index - 1;
var mismatchLength2 = tail2 - candidate.file2index - 1;
tail1 = candidate.file1index;
tail2 = candidate.file2index;
if (mismatchLength1 || mismatchLength2) {
result.push({file1: [tail1 + 1, mismatchLength1],
file2: [tail2 + 1, mismatchLength2]});
}
}
result.reverse();
return result;
},
diff3_merge_indices: function (a, o, b) {
// Given three files, A, O, and B, where both A and B are
// independently derived from O, returns a fairly complicated
// internal representation of merge decisions it's taken. The
// interested reader may wish to consult
//
// Sanjeev Khanna, Keshav Kunal, and Benjamin C. Pierce. "A
// Formal Investigation of Diff3." In Arvind and Prasad,
// editors, Foundations of Software Technology and Theoretical
// Computer Science (FSTTCS), December 2007.
//
// (http://www.cis.upenn.edu/~bcpierce/papers/diff3-short.pdf)
var i;
var m1 = Diff3.diff_indices(o, a);
var m2 = Diff3.diff_indices(o, b);
var hunks = [];
function addHunk(h, side) {
hunks.push([h.file1[0], side, h.file1[1], h.file2[0], h.file2[1]]);
}
for (i = 0; i < m1.length; i++) { addHunk(m1[i], 0); }
for (i = 0; i < m2.length; i++) { addHunk(m2[i], 2); }
hunks.sort();
var result = [];
var commonOffset = 0;
function copyCommon(targetOffset) {
if (targetOffset > commonOffset) {
result.push([1, commonOffset, targetOffset - commonOffset]);
commonOffset = targetOffset;
}
}
for (var hunkIndex = 0; hunkIndex < hunks.length; hunkIndex++) {
var firstHunkIndex = hunkIndex;
var hunk = hunks[hunkIndex];
var regionLhs = hunk[0];
var regionRhs = regionLhs + hunk[2];
while (hunkIndex < hunks.length - 1) {
var maybeOverlapping = hunks[hunkIndex + 1];
var maybeLhs = maybeOverlapping[0];
if (maybeLhs > regionRhs) break;
regionRhs = maybeLhs + maybeOverlapping[2];
hunkIndex++;
}
copyCommon(regionLhs);
if (firstHunkIndex == hunkIndex) {
// The "overlap" was only one hunk long, meaning that
// there's no conflict here. Either a and o were the
// same, or b and o were the same.
if (hunk[4] > 0) {
result.push([hunk[1], hunk[3], hunk[4]]);
}
} else {
// A proper conflict. Determine the extents of the
// regions involved from a, o and b. Effectively merge
// all the hunks on the left into one giant hunk, and
// do the same for the right; then, correct for skew
// in the regions of o that each side changed, and
// report appropriate spans for the three sides.
var regions = {
0: [a.length, -1, o.length, -1],
2: [b.length, -1, o.length, -1]
};
for (i = firstHunkIndex; i <= hunkIndex; i++) {
hunk = hunks[i];
var side = hunk[1];
var r = regions[side];
var oLhs = hunk[0];
var oRhs = oLhs + hunk[2];
var abLhs = hunk[3];
var abRhs = abLhs + hunk[4];
r[0] = Math.min(abLhs, r[0]);
r[1] = Math.max(abRhs, r[1]);
r[2] = Math.min(oLhs, r[2]);
r[3] = Math.max(oRhs, r[3]);
}
var aLhs = regions[0][0] + (regionLhs - regions[0][2]);
var aRhs = regions[0][1] + (regionRhs - regions[0][3]);
var bLhs = regions[2][0] + (regionLhs - regions[2][2]);
var bRhs = regions[2][1] + (regionRhs - regions[2][3]);
result.push([-1,
aLhs, aRhs - aLhs,
regionLhs, regionRhs - regionLhs,
bLhs, bRhs - bLhs]);
}
commonOffset = regionRhs;
}
copyCommon(o.length);
return result;
},
diff3_merge: function (a, o, b, excludeFalseConflicts) {
// Applies the output of Diff3.diff3_merge_indices to actually
// construct the merged file; the returned result alternates
// between "ok" and "conflict" blocks.
var result = [];
var files = [a, o, b];
var indices = Diff3.diff3_merge_indices(a, o, b);
var okLines = [];
function flushOk() {
if (okLines.length) {
result.push({ok: okLines});
}
okLines = [];
}
function pushOk(xs) {
for (var j = 0; j < xs.length; j++) {
okLines.push(xs[j]);
}
}
function isTrueConflict(rec) {
if (rec[2] != rec[6]) return true;
var aoff = rec[1];
var boff = rec[5];
for (var j = 0; j < rec[2]; j++) {
if (a[j + aoff] != b[j + boff]) return true;
}
return false;
}
for (var i = 0; i < indices.length; i++) {
var x = indices[i];
var side = x[0];
if (side == -1) {
if (excludeFalseConflicts && !isTrueConflict(x)) {
pushOk(files[0].slice(x[1], x[1] + x[2]));
} else {
flushOk();
result.push({conflict: {a: a.slice(x[1], x[1] + x[2]),
aIndex: x[1],
o: o.slice(x[3], x[3] + x[4]),
oIndex: x[3],
b: b.slice(x[5], x[5] + x[6]),
bIndex: x[5]}});
}
} else {
pushOk(files[side].slice(x[1], x[1] + x[2]));
}
}
flushOk();
return result;
}
};
return diff3;
})();
if (typeof module !== 'undefined') module.exports = Diff3;
+7 -2
View File
@@ -31,6 +31,7 @@
<script src='../js/lib/d3.value.js'></script>
<script src='../js/lib/d3-compat.js'></script>
<script src='../js/lib/bootstrap-tooltip.js'></script>
<script src='../js/lib/diff3.js'></script>
<script src='../js/lib/rbush.js'></script>
<script src='../js/lib/togeojson.js'></script>
<script src='../js/lib/osmauth.js'></script>
@@ -76,6 +77,7 @@
<script src='../js/id/ui/modal.js'></script>
<script src='../js/id/ui/cmd.js'></script>
<script src='../js/id/ui/confirm.js'></script>
<script src='../js/id/ui/conflicts.js'></script>
<script src='../js/id/ui/commit.js'></script>
<script src='../js/id/ui/success.js'></script>
<script src='../js/id/ui/loading.js'></script>
@@ -117,9 +119,9 @@
<script src='../js/id/ui/preset/wikipedia.js'></script>
<script src='../js/id/actions.js'></script>
<script src='../js/id/actions/add_entity.js'></script>
<script src='../js/id/actions/add_member.js'></script>
<script src="../js/id/actions/add_midpoint.js"></script>
<script src='../js/id/actions/add_entity.js'></script>
<script src='../js/id/actions/add_vertex.js'></script>
<script src='../js/id/actions/change_member.js'></script>
<script src='../js/id/actions/change_preset.js'></script>
@@ -137,8 +139,9 @@
<script src='../js/id/actions/join.js'></script>
<script src='../js/id/actions/merge.js'></script>
<script src='../js/id/actions/merge_polygon.js'></script>
<script src='../js/id/actions/move_node.js'></script>
<script src='../js/id/actions/merge_remote_changes.js'></script>
<script src='../js/id/actions/move.js'></script>
<script src='../js/id/actions/move_node.js'></script>
<script src='../js/id/actions/noop.js'></script>
<script src='../js/id/actions/orthogonalize.js'></script>
<script src='../js/id/actions/restrict_turn.js'></script>
@@ -216,6 +219,7 @@
<!-- include spec files here... -->
<script src="spec/lib/d3.combobox.js"></script>
<script src="spec/lib/d3.keybinding.js"></script>
<script src="spec/lib/diff3.js"></script>
<script src="spec/lib/locale.js"></script>
<script src="spec/actions/add_member.js"></script>
@@ -239,6 +243,7 @@
<script src="spec/actions/join.js"></script>
<script src='spec/actions/merge.js'></script>
<script src="spec/actions/merge_polygon.js"></script>
<script src="spec/actions/merge_remote_changes.js"></script>
<script src="spec/actions/move_node.js"></script>
<script src="spec/actions/move.js"></script>
<script src="spec/actions/noop.js"></script>
+8 -2
View File
@@ -25,11 +25,12 @@
<!-- include spec files here... -->
<script src="spec/lib/d3.keybinding.js"></script>
<script src="spec/lib/diff3.js"></script>
<script src="spec/lib/locale.js"></script>
<script src="spec/actions/add_entity.js"></script>
<script src="spec/actions/add_member.js"></script>
<script src="spec/actions/add_midpoint.js"></script>
<script src="spec/actions/add_entity.js"></script>
<script src="spec/actions/change_member.js"></script>
<script src="spec/actions/change_preset.js"></script>
<script src="spec/actions/change_tags.js"></script>
@@ -45,11 +46,16 @@
<script src='spec/actions/disconnect.js'></script>
<script src="spec/actions/join.js"></script>
<script src='spec/actions/merge.js'></script>
<script src="spec/actions/move_node.js"></script>
<script src='spec/actions/merge_polygon.js'></script>
<script src='spec/actions/merge_remote_changes.js'></script>
<script src="spec/actions/move.js"></script>
<script src="spec/actions/move_node.js"></script>
<script src="spec/actions/noop.js"></script>
<script src="spec/actions/orthogonalize.js"></script>
<script src="spec/actions/restrict_turn.js"></script>
<script src="spec/actions/reverse.js"></script>
<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/geo/extent.js"></script>
+16 -16
View File
@@ -56,21 +56,21 @@ describe("iD.actions.CopyEntity", function () {
expect(created[0]).to.be.an.instanceof(iD.Relation);
});
it("deep copies a Relation, member Ways, and child Nodes and adds them to the graph", function () {
var a = iD.Node({id: 'a'}),
b = iD.Node({id: 'b'}),
w = iD.Way({id: 'w', nodes: ['a', 'b']}),
r = iD.Relation({id: 'r', members: [{id: 'w'}]}),
base = iD.Graph([a, b, w, r]),
head = iD.actions.CopyEntity(r, true)(base),
diff = iD.Difference(base, head),
created = diff.created();
it("deep copies a Relation, member Ways, and child Nodes and adds them to the graph");//, function () {
// var a = iD.Node({id: 'a'}),
// b = iD.Node({id: 'b'}),
// w = iD.Way({id: 'w', nodes: ['a', 'b']}),
// r = iD.Relation({id: 'r', members: [{id: 'w'}]}),
// base = iD.Graph([a, b, w, r]),
// head = iD.actions.CopyEntity(r, true)(base),
// diff = iD.Difference(base, head),
// created = diff.created();
expect(head.hasEntity('r')).to.be.ok;
expect(created).to.have.length(4);
expect(created[0]).to.be.an.instanceof(iD.Relation);
expect(created[1]).to.be.an.instanceof(iD.Way);
expect(created[2]).to.be.an.instanceof(iD.Node);
expect(created[3]).to.be.an.instanceof(iD.Node);
});
// expect(head.hasEntity('r')).to.be.ok;
// expect(created).to.have.length(4);
// expect(created[0]).to.be.an.instanceof(iD.Relation);
// expect(created[1]).to.be.an.instanceof(iD.Way);
// expect(created[2]).to.be.an.instanceof(iD.Node);
// expect(created[3]).to.be.an.instanceof(iD.Node);
// });
});
+545
View File
@@ -0,0 +1,545 @@
describe("iD.actions.MergeRemoteChanges", function () {
var base = iD.Graph([
iD.Node({id: 'a', loc: [1, 1], version: '1', tags: {foo: 'foo'}}),
iD.Node({id: 'p1', loc: [ 10, 10], version: '1'}),
iD.Node({id: 'p2', loc: [ 10, -10], version: '1'}),
iD.Node({id: 'p3', loc: [-10, -10], version: '1'}),
iD.Node({id: 'p4', loc: [-10, 10], version: '1'}),
iD.Way({
id: 'w1',
nodes: ['p1', 'p2', 'p3', 'p4', 'p1'],
version: '1',
tags: {foo: 'foo', area: 'yes'}
}),
iD.Node({id: 'q1', loc: [ 5, 5], version: '1'}),
iD.Node({id: 'q2', loc: [ 5, -5], version: '1'}),
iD.Node({id: 'q3', loc: [-5, -5], version: '1'}),
iD.Node({id: 'q4', loc: [-5, 5], version: '1'}),
iD.Way({
id: 'w2',
nodes: ['q1', 'q2', 'q3', 'q4', 'q1'],
version: '1',
tags: {foo: 'foo', area: 'yes'}
}),
iD.Relation({
id: 'r',
members: [{id: 'w1', role: 'outer'}, {id: 'w2', role: 'inner'}],
version: '1',
tags: {type: 'multipolygon', foo: 'foo'}
})
]),
// some new objects not in the graph yet..
r1 = iD.Node({id: 'r1', loc: [ 12, 12], version: '1'}),
r2 = iD.Node({id: 'r2', loc: [ 12, -12], version: '1'}),
r3 = iD.Node({id: 'r3', loc: [-12, -12], version: '1'}),
r4 = iD.Node({id: 'r4', loc: [-12, 12], version: '1'}),
w3 = iD.Way({
id: 'w3',
nodes: ['r1', 'r2', 'r3', 'r4', 'r1'],
version: '1',
tags: {foo: 'foo_new', area: 'yes'}
}),
s1 = iD.Node({id: 's1', loc: [ 6, 6], version: '1'}),
s2 = iD.Node({id: 's2', loc: [ 6, -6], version: '1'}),
s3 = iD.Node({id: 's3', loc: [-6, -6], version: '1'}),
s4 = iD.Node({id: 's4', loc: [-6, 6], version: '1'}),
w4 = iD.Way({
id: 'w4',
nodes: ['s1', 's2', 's3', 's4', 's1'],
version: '1',
tags: {foo: 'foo_new', area: 'yes'}
}),
saved, error;
// setup mock locale object..
beforeEach(function() {
saved = locale;
error = console.error;
console.error = function () {};
locale = {
_current: 'en',
en: {
'merge_remote_changes': {
"annotation": "Merged remote changes from server.",
"conflict": {
"deleted": "This object has been deleted by {user}.",
"location": "This object was moved by both you and {user}.",
"nodelist": "Nodes were changed by both you and {user}.",
"memberlist": "Relation members were changed by both you and {user}.",
"tags": "You changed the <b>{tag}</b> tag to \"{local}\" and {user} changed it to \"{remote}\"."
}
}
}
};
});
afterEach(function() {
locale = saved;
console.error = error;
});
function makeGraph(entities) {
return _.reduce(entities, function(graph, entity) {
return graph.replace(entity);
}, iD.Graph(base));
}
describe("non-destuctive merging", function () {
describe("tags", function() {
it("doesn't merge tags if conflict (local change, remote change)", function () {
var localTags = {foo: 'foo_local'}, // changed foo
remoteTags = {foo: 'foo_remote'}, // changed foo
local = base.entity('a').update({tags: localTags}),
remote = base.entity('a').update({tags: remoteTags, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('a', localGraph, remoteGraph),
result = action(localGraph);
expect(result).to.eql(localGraph);
});
it("doesn't merge tags if conflict (local change, remote delete)", function () {
var localTags = {foo: 'foo_local'}, // changed foo
remoteTags = {}, // deleted foo
local = base.entity('a').update({tags: localTags}),
remote = base.entity('a').update({tags: remoteTags, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('a', localGraph, remoteGraph),
result = action(localGraph);
expect(result).to.eql(localGraph);
});
it("doesn't merge tags if conflict (local delete, remote change)", function () {
var localTags = {}, // deleted foo
remoteTags = {foo: 'foo_remote'}, // changed foo
local = base.entity('a').update({tags: localTags}),
remote = base.entity('a').update({tags: remoteTags, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('a', localGraph, remoteGraph),
result = action(localGraph);
expect(result).to.eql(localGraph);
});
it("doesn't merge tags if conflict (local add, remote add)", function () {
var localTags = {foo: 'foo', bar: 'bar_local'}, // same foo, added bar
remoteTags = {foo: 'foo', bar: 'bar_remote'}, // same foo, added bar
local = base.entity('a').update({tags: localTags}),
remote = base.entity('a').update({tags: remoteTags, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('a', localGraph, remoteGraph),
result = action(localGraph);
expect(result).to.eql(localGraph);
});
it("merges tags if no conflict (remote delete)", function () {
var localTags = {foo: 'foo', bar: 'bar_local'}, // same foo, added bar
remoteTags = {}, // deleted foo
mergedTags = {bar: 'bar_local'},
local = base.entity('a').update({tags: localTags}),
remote = base.entity('a').update({tags: remoteTags, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('a', localGraph, remoteGraph),
result = action(localGraph);
expect(result.entity('a').version).to.eql('2');
expect(result.entity('a').tags).to.eql(mergedTags);
});
it("merges tags if no conflict (local delete)", function () {
var localTags = {}, // deleted foo
remoteTags = {foo: 'foo', bar: 'bar_remote'}, // same foo, added bar
mergedTags = {bar: 'bar_remote'},
local = base.entity('a').update({tags: localTags}),
remote = base.entity('a').update({tags: remoteTags, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('a', localGraph, remoteGraph),
result = action(localGraph);
expect(result.entity('a').version).to.eql('2');
expect(result.entity('a').tags).to.eql(mergedTags);
});
});
describe("nodes", function () {
it("doesn't merge nodes if location is different", function () {
var localTags = {foo: 'foo_local'}, // changed foo
remoteTags = {foo: 'foo', bar: 'bar_remote'}, // same foo, added bar
localLoc = [2, 2], // moved node
remoteLoc = [3, 3], // moved node
local = base.entity('a').update({tags: localTags, loc: localLoc}),
remote = base.entity('a').update({tags: remoteTags, loc: remoteLoc, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('a', localGraph, remoteGraph),
result = action(localGraph);
expect(result).to.eql(localGraph);
});
it("merges nodes if location is same", function () {
var localTags = {foo: 'foo_local'}, // changed foo
remoteTags = {foo: 'foo', bar: 'bar_remote'}, // same foo, added bar
mergedTags = {foo: 'foo_local', bar: 'bar_remote'},
localLoc = [2, 2], // moved node
remoteLoc = [2, 2], // moved node
local = base.entity('a').update({tags: localTags, loc: localLoc}),
remote = base.entity('a').update({tags: remoteTags, loc: remoteLoc, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('a', localGraph, remoteGraph),
result = action(localGraph);
expect(result.entity('a').version).to.eql('2');
expect(result.entity('a').tags).to.eql(mergedTags);
expect(result.entity('a').loc).to.eql([2, 2]);
});
});
describe("ways", function () {
it("merges ways if nodelist is same", function () {
var localTags = {foo: 'foo_local', area: 'yes'}, // changed foo
remoteTags = {foo: 'foo', bar: 'bar_remote', area: 'yes'}, // same foo, added bar
mergedTags = {foo: 'foo_local', bar: 'bar_remote', area: 'yes'},
local = base.entity('w1').update({tags: localTags}),
remote = base.entity('w1').update({tags: remoteTags, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('w1', localGraph, remoteGraph),
result = action(localGraph);
expect(result.entity('w1').version).to.eql('2');
expect(result.entity('w1').tags).to.eql(mergedTags);
});
it("merges ways if nodelist changed only remotely", function () {
var localTags = {foo: 'foo_local', area: 'yes'}, // changed foo
remoteTags = {foo: 'foo', bar: 'bar_remote', area: 'yes'}, // same foo, added bar
mergedTags = {foo: 'foo_local', bar: 'bar_remote', area: 'yes'},
localNodes = ['p1', 'p2', 'p3', 'p4', 'p1'], // didn't change nodes
remoteNodes = ['p1', 'r2', 'r3', 'p4', 'p1'], // changed nodes
local = base.entity('w1').update({tags: localTags, nodes: localNodes}),
remote = base.entity('w1').update({tags: remoteTags, nodes: remoteNodes, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote, r2, r3]),
action = iD.actions.MergeRemoteChanges('w1', localGraph, remoteGraph),
result = action(localGraph);
expect(result.entity('w1').version).to.eql('2');
expect(result.entity('w1').tags).to.eql(mergedTags);
expect(result.entity('w1').nodes).to.eql(remoteNodes);
expect(result.hasEntity('r2')).to.eql(r2);
expect(result.hasEntity('r3')).to.eql(r3);
});
it("merges ways if nodelist changed only locally", function () {
var localTags = {foo: 'foo_local', area: 'yes'}, // changed foo
remoteTags = {foo: 'foo', bar: 'bar_remote', area: 'yes'}, // same foo, added bar
mergedTags = {foo: 'foo_local', bar: 'bar_remote', area: 'yes'},
localNodes = ['p1', 'r2', 'r3', 'p4', 'p1'], // changed nodes
remoteNodes = ['p1', 'p2', 'p3', 'p4', 'p1'], // didn't change nodes
local = base.entity('w1').update({tags: localTags, nodes: localNodes}),
remote = base.entity('w1').update({tags: remoteTags, nodes: remoteNodes, version: '2'}),
localGraph = makeGraph([local, r2, r3]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('w1', localGraph, remoteGraph),
result = action(localGraph);
expect(result.entity('w1').version).to.eql('2');
expect(result.entity('w1').tags).to.eql(mergedTags);
expect(result.entity('w1').nodes).to.eql(localNodes);
});
it("merges ways if nodelist changes don't overlap", function () {
var localTags = {foo: 'foo_local', area: 'yes'}, // changed foo
remoteTags = {foo: 'foo', bar: 'bar_remote', area: 'yes'}, // same foo, added bar
mergedTags = {foo: 'foo_local', bar: 'bar_remote', area: 'yes'},
localNodes = ['p1', 'r1', 'r2', 'p3', 'p4', 'p1'], // changed p2 -> r1, r2
remoteNodes = ['p1', 'p2', 'p3', 'r3', 'r4', 'p1'], // changed p4 -> r3, r4
mergedNodes = ['p1', 'r1', 'r2', 'p3', 'r3', 'r4', 'p1'],
local = base.entity('w1').update({tags: localTags, nodes: localNodes}),
remote = base.entity('w1').update({tags: remoteTags, nodes: remoteNodes, version: '2'}),
localGraph = makeGraph([local, r1, r2]),
remoteGraph = makeGraph([remote, r3, r4]),
action = iD.actions.MergeRemoteChanges('w1', localGraph, remoteGraph),
result = action(localGraph);
expect(result.entity('w1').version).to.eql('2');
expect(result.entity('w1').tags).to.eql(mergedTags);
expect(result.entity('w1').nodes).to.eql(mergedNodes);
expect(result.hasEntity('r3')).to.eql(r3);
expect(result.hasEntity('r4')).to.eql(r4);
});
it("doesn't merge ways if nodelist changes overlap", function () {
var localTags = {foo: 'foo_local', area: 'yes'}, // changed foo
remoteTags = {foo: 'foo', bar: 'bar_remote', area: 'yes'}, // same foo, added bar
localNodes = ['p1', 'r1', 'r2', 'p3', 'p4', 'p1'], // changed p2 -> r1, r2
remoteNodes = ['p1', 'r3', 'r4', 'p3', 'p4', 'p1'], // changed p2 -> r3, r4
local = base.entity('w1').update({tags: localTags, nodes: localNodes}),
remote = base.entity('w1').update({tags: remoteTags, nodes: remoteNodes, version: '2'}),
localGraph = makeGraph([local, r1, r2]),
remoteGraph = makeGraph([remote, r3, r4]),
action = iD.actions.MergeRemoteChanges('w1', localGraph, remoteGraph),
result = action(localGraph);
expect(result).to.eql(localGraph);
});
it("merges ways if childNode location is same", function () {
var localLoc = [12, 12], // moved node
remoteLoc = [12, 12], // moved node
local = base.entity('p1').update({loc: localLoc}),
remote = base.entity('p1').update({loc: remoteLoc, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('w1', localGraph, remoteGraph),
result = action(localGraph);
expect(result.entity('p1').version).to.eql('2');
expect(result.entity('p1').loc).to.eql(remoteLoc);
});
it("doesn't merge ways if childNode location is different", function () {
var localLoc = [12, 12], // moved node
remoteLoc = [13, 13], // moved node
local = base.entity('p1').update({loc: localLoc}),
remote = base.entity('p1').update({loc: remoteLoc, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('w1', localGraph, remoteGraph),
result = action(localGraph);
expect(result).to.eql(localGraph);
});
});
describe("relations", function () {
it("doesn't merge relations if members have changed", function () {
var localTags = {foo: 'foo_local', type: 'multipolygon'}, // changed foo
remoteTags = {foo: 'foo', bar: 'bar_remote', type: 'multipolygon'}, // same foo, added bar
localMembers = [{id: 'w1', role: 'outer'}, {id: 'w2', role: 'inner'}], // same members
remoteMembers = [{id: 'w1', role: 'outer'}, {id: 'w4', role: 'inner'}], // changed inner to w4
local = base.entity('r').update({tags: localTags, members: localMembers}),
remote = base.entity('r').update({tags: remoteTags, members: remoteMembers, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote, s1, s2, s3, s4, w4]),
action = iD.actions.MergeRemoteChanges('r', localGraph, remoteGraph),
result = action(localGraph);
expect(result).to.eql(localGraph);
});
it("merges relations if members are same and changed tags don't conflict", function () {
var localTags = {foo: 'foo_local', type: 'multipolygon'}, // changed foo
remoteTags = {foo: 'foo', bar: 'bar_remote', type: 'multipolygon'}, // same foo, added bar
mergedTags = {foo: 'foo_local', bar: 'bar_remote', type: 'multipolygon'},
localMembers = [{id: 'w1', role: 'outer'}, {id: 'w2', role: 'inner'}], // same members
remoteMembers = [{id: 'w1', role: 'outer'}, {id: 'w2', role: 'inner'}], // same members
local = base.entity('r').update({tags: localTags, members: localMembers}),
remote = base.entity('r').update({tags: remoteTags, members: remoteMembers, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('r', localGraph, remoteGraph),
result = action(localGraph);
expect(result.entity('r').version).to.eql('2');
expect(result.entity('r').tags).to.eql(mergedTags);
});
});
describe("#conflicts", function () {
it("returns conflict details", function () {
var localTags = {foo: 'foo_local'}, // changed foo
remoteTags = {foo: 'foo', bar: 'bar_remote'}, // same foo, added bar
remoteLoc = [2, 2], // moved node
local = base.entity('a').update({tags: localTags}),
remote = base.entity('a').update({tags: remoteTags, loc: remoteLoc, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('a', localGraph, remoteGraph),
result = action(localGraph);
expect(action.conflicts()).not.to.be.empty;
});
});
});
describe("destuctive merging", function () {
describe("nodes", function () {
it("merges nodes with 'force_local' option", function () {
var localTags = {foo: 'foo_local'}, // changed foo
remoteTags = {foo: 'foo_remote'}, // changed foo
localLoc = [2, 2], // moved node
remoteLoc = [3, 3], // moved node
local = base.entity('a').update({tags: localTags, loc: localLoc}),
remote = base.entity('a').update({tags: remoteTags, loc: remoteLoc, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('a', localGraph, remoteGraph).withOption('force_local'),
result = action(localGraph);
expect(result.entity('a').version).to.eql('2');
expect(result.entity('a').tags).to.eql(localTags);
expect(result.entity('a').loc).to.eql(localLoc);
});
it("merges nodes with 'force_remote' option", function () {
var localTags = {foo: 'foo_local'}, // changed foo
remoteTags = {foo: 'foo_remote'}, // changed foo
localLoc = [2, 2], // moved node
remoteLoc = [3, 3], // moved node
local = base.entity('a').update({tags: localTags, loc: localLoc}),
remote = base.entity('a').update({tags: remoteTags, loc: remoteLoc, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('a', localGraph, remoteGraph).withOption('force_remote'),
result = action(localGraph);
expect(result.entity('a').version).to.eql('2');
expect(result.entity('a').tags).to.eql(remoteTags);
expect(result.entity('a').loc).to.eql(remoteLoc);
});
});
describe("ways", function () {
it("merges ways with 'force_local' option", function () {
var localTags = {foo: 'foo_local', area: 'yes'}, // changed foo
remoteTags = {foo: 'foo_remote', area: 'yes'}, // changed foo
localNodes = ['p1', 'r1', 'r2', 'p3', 'p4', 'p1'], // changed p2 -> r1, r2
remoteNodes = ['p1', 'r3', 'r4', 'p3', 'p4', 'p1'], // changed p2 -> r3, r4
local = base.entity('w1').update({tags: localTags, nodes: localNodes}),
remote = base.entity('w1').update({tags: remoteTags, nodes: remoteNodes, version: '2'}),
localGraph = makeGraph([local, r1, r2]),
remoteGraph = makeGraph([remote, r3, r4]),
action = iD.actions.MergeRemoteChanges('w1', localGraph, remoteGraph).withOption('force_local'),
result = action(localGraph);
expect(result.entity('w1').version).to.eql('2');
expect(result.entity('w1').tags).to.eql(localTags);
expect(result.entity('w1').nodes).to.eql(localNodes);
});
it("merges ways with 'force_remote' option", function () {
var localTags = {foo: 'foo_local', area: 'yes'}, // changed foo
remoteTags = {foo: 'foo_remote', area: 'yes'}, // changed foo
localNodes = ['p1', 'r1', 'r2', 'p3', 'p4', 'p1'], // changed p2 -> r1, r2
remoteNodes = ['p1', 'r3', 'r4', 'p3', 'p4', 'p1'], // changed p2 -> r3, r4
local = base.entity('w1').update({tags: localTags, nodes: localNodes}),
remote = base.entity('w1').update({tags: remoteTags, nodes: remoteNodes, version: '2'}),
localGraph = makeGraph([local, r1, r2]),
remoteGraph = makeGraph([remote, r3, r4]),
action = iD.actions.MergeRemoteChanges('w1', localGraph, remoteGraph).withOption('force_remote'),
result = action(localGraph);
expect(result.entity('w1').version).to.eql('2');
expect(result.entity('w1').tags).to.eql(remoteTags);
expect(result.entity('w1').nodes).to.eql(remoteNodes);
expect(result.hasEntity('r3')).to.eql(r3);
expect(result.hasEntity('r4')).to.eql(r4);
});
it("merges way childNodes with 'force_local' option", function () {
var localLoc = [12, 12], // moved node
remoteLoc = [13, 13], // moved node
local = base.entity('p1').update({loc: localLoc}),
remote = base.entity('p1').update({loc: remoteLoc, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('w1', localGraph, remoteGraph).withOption('force_local'),
result = action(localGraph);
expect(result.entity('p1').version).to.eql('2');
expect(result.entity('p1').loc).to.eql(localLoc);
});
it("merges way childNodes with 'force_remote' option", function () {
var localLoc = [12, 12], // moved node
remoteLoc = [13, 13], // moved node
local = base.entity('p1').update({loc: localLoc}),
remote = base.entity('p1').update({loc: remoteLoc, version: '2'}),
localGraph = makeGraph([local]),
remoteGraph = makeGraph([remote]),
action = iD.actions.MergeRemoteChanges('w1', localGraph, remoteGraph).withOption('force_remote'),
result = action(localGraph);
expect(result.entity('p1').version).to.eql('2');
expect(result.entity('p1').loc).to.eql(remoteLoc);
});
it("keeps only important childNodes when merging", function () {
var localNodes = ['p1', 'r1', 'r2', 'p3', 'p4', 'p1'], // changed p2 -> r1, r2
remoteNodes = ['p1', 'r3', 'r4', 'p3', 'p4', 'p1'], // changed p2 -> r3, r4
localr1 = r1.update({tags: {highway: 'traffic_signals'}}), // r1 has interesting tags
local = base.entity('w1').update({nodes: localNodes}),
remote = base.entity('w1').update({nodes: remoteNodes, version: '2'}),
localGraph = makeGraph([local, localr1, r2]),
remoteGraph = makeGraph([remote, r3, r4]),
action = iD.actions.MergeRemoteChanges('w1', localGraph, remoteGraph).withOption('force_remote'),
result = action(localGraph);
expect(result.entity('w1').nodes).to.eql(remoteNodes);
expect(result.hasEntity('r1')).to.eql(localr1);
expect(result.hasEntity('r2')).to.be.not.ok;
});
});
describe("relations", function () {
it("merges relations with 'force_local' option", function () {
var localTags = {foo: 'foo_local', type: 'multipolygon'}, // changed foo
remoteTags = {foo: 'foo_remote', type: 'multipolygon'}, // changed foo
localMembers = [{id: 'w3', role: 'outer'}, {id: 'w2', role: 'inner'}], // changed outer to w3
remoteMembers = [{id: 'w1', role: 'outer'}, {id: 'w4', role: 'inner'}], // changed inner to w4
local = base.entity('r').update({tags: localTags, members: localMembers}),
remote = base.entity('r').update({tags: remoteTags, members: remoteMembers, version: '2'}),
localGraph = makeGraph([local, r1, r2, r3, r4, w3]),
remoteGraph = makeGraph([remote, s1, s2, s3, s4, w4]),
action = iD.actions.MergeRemoteChanges('r', localGraph, remoteGraph).withOption('force_local'),
result = action(localGraph);
expect(result.entity('r').version).to.eql('2');
expect(result.entity('r').tags).to.eql(localTags);
expect(result.entity('r').members).to.eql(localMembers);
});
it("merges relations with 'force_remote' option", function () {
var localTags = {foo: 'foo_local', type: 'multipolygon'}, // changed foo
remoteTags = {foo: 'foo_remote', type: 'multipolygon'}, // changed foo
localMembers = [{id: 'w3', role: 'outer'}, {id: 'w2', role: 'inner'}], // changed outer to w3
remoteMembers = [{id: 'w1', role: 'outer'}, {id: 'w4', role: 'inner'}], // changed inner to w4
local = base.entity('r').update({tags: localTags, members: localMembers}),
remote = base.entity('r').update({tags: remoteTags, members: remoteMembers, version: '2'}),
localGraph = makeGraph([local, r1, r2, r3, r4, w3]),
remoteGraph = makeGraph([remote, s1, s2, s3, s4, w4]),
action = iD.actions.MergeRemoteChanges('r', localGraph, remoteGraph).withOption('force_remote'),
result = action(localGraph);
expect(result.entity('r').version).to.eql('2');
expect(result.entity('r').tags).to.eql(remoteTags);
expect(result.entity('r').members).to.eql(remoteMembers);
});
});
});
});
+21 -16
View File
@@ -90,9 +90,10 @@ describe('iD.Connection', function () {
});
it('loads a node', function(done) {
c.loadEntity('n1', function(error, entity) {
var id = 'n1';
c.loadEntity(id, function(err, result) {
var entity = _.find(result.data, function(e) { return e.id === id; });
expect(entity).to.be.an.instanceOf(iD.Node);
expect(entity.id).to.eql('n1');
done();
});
@@ -102,9 +103,10 @@ describe('iD.Connection', function () {
});
it('loads a way', function(done) {
c.loadEntity('w1', function(error, entity) {
var id = 'w1';
c.loadEntity(id, function(err, result) {
var entity = _.find(result.data, function(e) { return e.id === id; });
expect(entity).to.be.an.instanceOf(iD.Way);
expect(entity.id).to.eql('w1');
done();
});
@@ -112,20 +114,23 @@ describe('iD.Connection', function () {
[200, { "Content-Type": "text/xml" }, wayXML]);
server.respond();
});
it('emits a load event', function(done) {
c.loadEntity('n1');
c.on('load', function(error, result) {
expect(result.data[0]).to.be.an.instanceOf(iD.Node);
done();
});
server.respondWith("GET", "http://www.openstreetmap.org/api/0.6/node/1",
[200, { "Content-Type": "text/xml" }, nodeXML]);
server.respond();
});
});
describe('#loadMultiple', function () {
beforeEach(function() {
server = sinon.fakeServer.create();
});
afterEach(function() {
server.restore();
});
it('loads nodes');
it('loads ways');
});
describe('#osmChangeJXON', function() {
it('converts change data to JXON', function() {
var jxon = c.osmChangeJXON('1234', {created: [], modified: [], deleted: []});
+21 -8
View File
@@ -30,7 +30,7 @@ describe('iD.Graph', function() {
});
it("remains mutable if passed true as second argument", function () {
expect(iD.Graph([], true).frozen).not.to.be.true;
expect(iD.Graph([], true).frozen).to.be.false;
});
});
@@ -58,12 +58,6 @@ describe('iD.Graph', function() {
});
});
describe("#freeze", function () {
it("sets the frozen flag", function () {
expect(iD.Graph([], true).freeze().frozen).to.be.true;
});
});
describe("#rebase", function () {
it("preserves existing entities", function () {
var node = iD.Node({id: 'n'}),
@@ -83,6 +77,15 @@ describe('iD.Graph', function() {
expect(graph.entity('n')).to.equal(node);
});
it("doesn't rebase deleted entities", function () {
var node = iD.Node({id: 'n', visible: false}),
graph = iD.Graph();
graph.rebase([node], [graph]);
expect(graph.hasEntity('n')).to.be.not.ok;
});
it("gives precedence to existing entities", function () {
var a = iD.Node({id: 'n'}),
b = iD.Node({id: 'n'}),
@@ -93,10 +96,20 @@ describe('iD.Graph', function() {
expect(graph.entity('n')).to.equal(a);
});
it("gives precedence to new entities when force = true", function () {
var a = iD.Node({id: 'n'}),
b = iD.Node({id: 'n'}),
graph = iD.Graph([a]);
graph.rebase([b], [graph], true);
expect(graph.entity('n')).to.equal(b);
});
it("inherits entities from base prototypally", function () {
var graph = iD.Graph();
graph.rebase([iD.Node()], [graph]);
graph.rebase([iD.Node({id: 'n'})], [graph]);
expect(graph.entities).not.to.have.ownProperty('n');
});
+2 -2
View File
@@ -91,7 +91,7 @@ describe('iD.Relation', function () {
expect(r1_copy.members[1].id).to.equal(r2_copy.members[0].id);
});
// it("deep copies cyclical relation graphs without issue", function () {
it("deep copies cyclical relation graphs without issue"); //, function () {
// var r1 = iD.Relation({id: 'r1', members: [{id: 'r2'}]}),
// r2 = iD.Relation({id: 'r2', members: [{id: 'r1'}]}),
// graph = iD.Graph([r1, r2]),
@@ -109,7 +109,7 @@ describe('iD.Relation', function () {
// expect(r2_copy.members[0].id).to.equal(r1_copy.id, msg);
// });
// it("deep copies self-refrencing relations without issue", function () {
it("deep copies self-refrencing relations without issue"); //, function () {
// var r1 = iD.Relation({id: 'r1', members: [{id: 'r1'}]}),
// graph = iD.Graph([r1]),
// result = r1.copy(true, graph),
+89
View File
@@ -0,0 +1,89 @@
describe("diff3", function() {
function split(s) {
return s ? s.split(/ /) : [];
}
it('performs diff3 merge', function() {
var o = split('AA ZZ 00 M 99'),
a = split('AA a b c ZZ new 00 a a M 99'),
b = split('AA a d c ZZ 11 M z z 99'),
res = Diff3.diff3_merge(a, o, b);
/*
AA
<<<<<<< a
a
b
c
||||||| o
=======
a
d
c
>>>>>>> b
ZZ
<<<<<<< a
new
00
a
a
||||||| o
00
=======
11
>>>>>>> b
M
z
z
99
*/
expect(res[0].ok).to.eql(['AA']);
expect(res[0].conflict).to.be.undefined;
expect(res[1].ok).to.be.undefined;
expect(res[1].conflict.o).to.eql([]);
expect(res[1].conflict.a).to.eql(['a', 'b', 'c']);
expect(res[1].conflict.b).to.eql(['a', 'd', 'c']);
expect(res[2].ok).to.eql(['ZZ']);
expect(res[2].conflict).to.be.undefined;
expect(res[3].ok).to.be.undefined;
expect(res[3].conflict.o).to.eql(['00']);
expect(res[3].conflict.a).to.eql(['new', '00', 'a', 'a']);
expect(res[3].conflict.b).to.eql(['11']);
expect(res[4].ok).to.eql(['M', 'z', 'z', '99']);
expect(res[4].conflict).to.be.undefined;
});
it('can include false conflicts', function() {
var o = split('AA ZZ'),
a = split('AA a b c ZZ'),
b = split('AA a b c ZZ'),
res = Diff3.diff3_merge(a, o, b, false);
expect(res[0].ok).to.eql(['AA']);
expect(res[0].conflict).to.be.undefined;
expect(res[1].ok).to.be.undefined;
expect(res[1].conflict.o).to.eql([]);
expect(res[1].conflict.a).to.eql(['a', 'b', 'c']);
expect(res[1].conflict.b).to.eql(['a', 'b', 'c']);
expect(res[2].ok).to.eql(['ZZ']);
expect(res[2].conflict).to.be.undefined;
});
it('can exclude false conflicts', function() {
var o = split('AA ZZ'),
a = split('AA a b c ZZ'),
b = split('AA a b c ZZ'),
res = Diff3.diff3_merge(a, o, b, true);
expect(res[0].ok).to.eql(['AA', 'a', 'b', 'c', 'ZZ']);
expect(res[0].conflict).to.be.undefined;
});
});