diff --git a/build.js b/build.js
index 7f77b6de1..e05dd40e2 100644
--- a/build.js
+++ b/build.js
@@ -2,7 +2,10 @@ var fs = require('fs'),
path = require('path'),
glob = require('glob'),
YAML = require('js-yaml'),
- _ = require('./js/lib/lodash');
+ _ = require('./js/lib/lodash'),
+ jsonschema = require('jsonschema'),
+ fieldSchema = require('./data/presets/schema/field.json'),
+ presetSchema = require('./data/presets/schema/preset.json');
function read(f) {
return JSON.parse(fs.readFileSync(f));
@@ -16,6 +19,21 @@ function rp(f) {
return r('presets/' + f);
}
+function validate(file, instance, schema) {
+ var result = jsonschema.validate(instance, schema);
+ if (result.length) {
+ console.error(file + ": ");
+ result.forEach(function(error) {
+ if (error.property) {
+ console.error(error.property + ' ' + error.message);
+ } else {
+ console.error(error);
+ }
+ });
+ process.exit(1);
+ }
+}
+
var translations = {
fields: {},
presets: {}
@@ -25,12 +43,16 @@ var fields = {};
glob.sync(__dirname + '/data/presets/fields/*.json').forEach(function(file) {
var field = read(file),
id = path.basename(file, '.json');
+
+ validate(file, field, fieldSchema);
+
translations.fields[id] = {label: field.label};
if (field.strings) {
for (var i in field.strings) {
translations.fields[id][i] = field.strings[i];
}
}
+
fields[id] = field;
});
fs.writeFileSync('data/presets/fields.json', JSON.stringify(fields, null, 4));
@@ -39,10 +61,14 @@ var presets = {};
glob.sync(__dirname + '/data/presets/presets/**/*.json').forEach(function(file) {
var preset = read(file),
id = file.match(/presets\/presets\/([^.]*)\.json/)[1];
+
+ validate(file, preset, presetSchema);
+
translations.presets[id] = {
name: preset.name,
terms: (preset.terms || []).join(',')
};
+
presets[id] = preset;
});
fs.writeFileSync('data/presets/presets.json', JSON.stringify(presets, null, 4));
diff --git a/css/app.css b/css/app.css
index 8ab94f302..50b48896c 100644
--- a/css/app.css
+++ b/css/app.css
@@ -283,7 +283,7 @@ ul.link-list li:last-child {
div.hide,
form.hide {
- display:none;
+ display: none;
}
.deemphasize {
@@ -1147,12 +1147,11 @@ div.combobox {
/* tag editor */
.inspector-inner.additional-tags {
- border-bottom: 1px solid #ccc;
border-top: 1px solid #ccc;
}
.tag-list {
- margin-top: 10px;
+ padding-top: 20px;
}
.tag-row {
diff --git a/data/locales.js b/data/locales.js
index 9b5ac3b98..6958b09ef 100644
--- a/data/locales.js
+++ b/data/locales.js
@@ -588,6 +588,10 @@ locale.en = {
"name": "Building",
"terms": ""
},
+ "building/entrance": {
+ "name": "Entrance",
+ "terms": ""
+ },
"entrance": {
"name": "Entrance",
"terms": ""
@@ -1047,6 +1051,7 @@ locale.en = {
}
}
};
+
locale.zh = {
"modes": {
"add_area": {
diff --git a/data/presets.yaml b/data/presets.yaml
index 155258ef0..e2666770f 100644
--- a/data/presets.yaml
+++ b/data/presets.yaml
@@ -256,6 +256,9 @@ en:
building:
name: Building
terms: ""
+ building/entrance:
+ name: Entrance
+ terms: ""
entrance:
name: Entrance
terms: ""
diff --git a/data/presets/categories.json b/data/presets/categories.json
index bd10ddad6..8638106b2 100644
--- a/data/presets/categories.json
+++ b/data/presets/categories.json
@@ -3,15 +3,14 @@
"icon": "highway",
"id": "Road",
"members": [
+ "highway/residential",
"highway/motorway",
"highway/trunk",
"highway/primary",
"highway/secondary",
"highway/tertiary",
"highway/unclassified",
- "highway/residential",
"highway/service",
- "highway/track",
- "highway"
+ "highway/track"
]
}]
diff --git a/data/presets/fields.json b/data/presets/fields.json
index 74175fc21..01032ec86 100644
--- a/data/presets/fields.json
+++ b/data/presets/fields.json
@@ -368,7 +368,7 @@
"no"
],
"icon": "wheelchair",
- "universal": "true",
+ "universal": true,
"label": "Wheelchair Access"
},
"wikipedia": {
diff --git a/data/presets/fields/wheelchair.json b/data/presets/fields/wheelchair.json
index 7003c7c17..a029dbd6b 100644
--- a/data/presets/fields/wheelchair.json
+++ b/data/presets/fields/wheelchair.json
@@ -7,6 +7,6 @@
"no"
],
"icon": "wheelchair",
- "universal": "true",
+ "universal": true,
"label": "Wheelchair Access"
}
diff --git a/data/presets/presets.json b/data/presets/presets.json
index 4406b34c8..fed256cf0 100644
--- a/data/presets/presets.json
+++ b/data/presets/presets.json
@@ -828,14 +828,6 @@
"building_yes",
"levels"
],
- "additional": [
- "address",
- "phone",
- "website",
- "wikipedia",
- "elevation",
- "source"
- ],
"geometry": [
"area"
],
@@ -845,6 +837,16 @@
"terms": [],
"name": "Building"
},
+ "building/entrance": {
+ "geometry": [
+ "vertex"
+ ],
+ "tags": {
+ "building": "entrance"
+ },
+ "name": "Entrance",
+ "searchable": false
+ },
"entrance": {
"geometry": [
"vertex"
diff --git a/data/presets/presets/building.json b/data/presets/presets/building.json
index 13b7c32dc..daa8a2cb5 100644
--- a/data/presets/presets/building.json
+++ b/data/presets/presets/building.json
@@ -4,14 +4,6 @@
"building_yes",
"levels"
],
- "additional": [
- "address",
- "phone",
- "website",
- "wikipedia",
- "elevation",
- "source"
- ],
"geometry": [
"area"
],
diff --git a/data/presets/presets/building/entrance.json b/data/presets/presets/building/entrance.json
new file mode 100644
index 000000000..e4a1152b7
--- /dev/null
+++ b/data/presets/presets/building/entrance.json
@@ -0,0 +1,10 @@
+{
+ "geometry": [
+ "vertex"
+ ],
+ "tags": {
+ "building": "entrance"
+ },
+ "name": "Entrance",
+ "searchable": false
+}
\ No newline at end of file
diff --git a/data/presets/schema/field.json b/data/presets/schema/field.json
new file mode 100644
index 000000000..f852288c5
--- /dev/null
+++ b/data/presets/schema/field.json
@@ -0,0 +1,67 @@
+{
+ "title": "Field",
+ "description": "A reusable form element for presets",
+ "type": "object",
+ "properties": {
+ "key": {
+ "description": "Tag key whose value is to be displayed",
+ "type": "string"
+ },
+ "keys": {
+ "description": "Tag keys whose value is to be displayed",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "type": {
+ "description": "Type of field",
+ "type": "string",
+ "enum": [
+ "address",
+ "check",
+ "combo",
+ "defaultcheck",
+ "text",
+ "number",
+ "tel",
+ "email",
+ "url",
+ "radio",
+ "textarea"
+ ],
+ "required": true
+ },
+ "label": {
+ "description": "English label for the form",
+ "type": "string",
+ "required": true
+ },
+ "geometry": {
+ "type": "string"
+ },
+ "default": {
+ "type": "string"
+ },
+ "indexed": {
+ "type": "boolean"
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "universal": {
+ "type": "boolean",
+ "default": false
+ },
+ "icon": {
+ "type": "string"
+ },
+ "strings": {
+ "type": "object"
+ }
+ },
+ "additionalProperties": false
+}
\ No newline at end of file
diff --git a/data/presets/schema/preset.json b/data/presets/schema/preset.json
new file mode 100644
index 000000000..f2466671a
--- /dev/null
+++ b/data/presets/schema/preset.json
@@ -0,0 +1,55 @@
+{
+ "title": "Preset",
+ "description": "Associates an icon, form fields, and other UI with a set of OSM tags",
+ "type": "object",
+ "properties": {
+ "name": {
+ "description": "The English name for the feature",
+ "type": "string",
+ "required": true
+ },
+ "geometry": {
+ "description": "Valid geometry types for the feature",
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "enum": ["point", "vertex", "line", "area"]
+ },
+ "required": true
+ },
+ "tags": {
+ "description": "Tags that must be present for the preset to match",
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ },
+ "required": true
+ },
+ "fields": {
+ "description": "Form fields that are displayed for the preset",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "icon": {
+ "description": "Name of preset icon which represents this preset",
+ "type": "string"
+ },
+ "terms": {
+ "description": "English synonyms or related terms",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "searchable": {
+ "description": "Whether or not the preset will be suggested via search",
+ "type": "boolean",
+ "default": true
+ }
+ },
+ "additionalProperties": false
+}
\ No newline at end of file
diff --git a/img/source/sprite.svg b/img/source/sprite.svg
index d84000b2c..98d742f06 100644
--- a/img/source/sprite.svg
+++ b/img/source/sprite.svg
@@ -13,7 +13,7 @@
height="220"
id="svg12393"
version="1.1"
- inkscape:version="0.48.1 r9760"
+ inkscape:version="0.48.2 r9819"
sodipodi:docname="sprite.svg"
inkscape:export-filename="/Users/saman/work_repos/iD/img/sprite.png"
inkscape:export-xdpi="90"
@@ -38,12 +38,12 @@
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
- inkscape:zoom="1"
- inkscape:cx="-49.735643"
- inkscape:cy="134.55123"
+ inkscape:zoom="1.4142136"
+ inkscape:cx="2.3449562"
+ inkscape:cy="33.776287"
inkscape:document-units="px"
inkscape:current-layer="layer12"
- showgrid="true"
+ showgrid="false"
inkscape:window-width="1280"
inkscape:window-height="700"
inkscape:window-x="361"
@@ -53,7 +53,7 @@
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
- showguides="true"
+ showguides="false"
inkscape:guide-bbox="true"
inkscape:snap-bbox="true"
inkscape:snap-nodes="false">
@@ -201,7 +201,7 @@
image/svg+xml
-
+
diff --git a/img/source/ui-mockups.svg b/img/source/ui-mockups.svg
new file mode 100644
index 000000000..1bce1829d
--- /dev/null
+++ b/img/source/ui-mockups.svg
@@ -0,0 +1,727 @@
+
+
+
+
diff --git a/js/id/actions/circularize.js b/js/id/actions/circularize.js
index 4dcfc0a21..dc936d48f 100644
--- a/js/id/actions/circularize.js
+++ b/js/id/actions/circularize.js
@@ -21,13 +21,14 @@ iD.actions.Circularize = function(wayId, projection, count) {
radius = d3.median(points, function(p) {
return iD.geo.dist(centroid, p);
}),
- ids = [];
+ ids = [],
+ sign = d3.geom.polygon(points).area() > 0 ? -1 : 1;
for (var i = 0; i < count; i++) {
var node,
loc = projection.invert([
- centroid[0] + Math.cos((i / 12) * Math.PI * 2) * radius,
- centroid[1] + Math.sin((i / 12) * Math.PI * 2) * radius]);
+ centroid[0] + Math.cos(sign * (i / 12) * Math.PI * 2) * radius,
+ centroid[1] + Math.sin(sign * (i / 12) * Math.PI * 2) * radius]);
if (nodes.length) {
var idx = closestIndex(nodes, loc);
diff --git a/js/id/presets/preset.js b/js/id/presets/preset.js
index 477c13777..8b8ba456f 100644
--- a/js/id/presets/preset.js
+++ b/js/id/presets/preset.js
@@ -3,7 +3,6 @@ iD.presets.Preset = function(id, preset, fields) {
preset.id = id;
preset.fields = (preset.fields || []).map(getFields);
- preset.additional = (preset.additional || []).map(getFields);
function getFields(f) {
return fields[f];
diff --git a/js/id/ui/tag_editor.js b/js/id/ui/tag_editor.js
index 9c6ed6edc..847d99bc2 100644
--- a/js/id/ui/tag_editor.js
+++ b/js/id/ui/tag_editor.js
@@ -98,7 +98,7 @@ iD.ui.TagEditor = function(context, entity) {
}
editorwrap.append('div')
- .attr('class','inspector-inner col12 fillL additional-tags')
+ .attr('class','inspector-inner col12 fillL2 additional-tags')
.call(tagList, preset.id === 'other');
// Don't add for created entities
diff --git a/package.json b/package.json
index 33b2b4079..4cd2317a8 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,8 @@
"mocha-phantomjs": "~1.1.1",
"glob": "~3.1.21",
"js-yaml": "~2.0.3",
- "request": "~2.16.2"
+ "request": "~2.16.2",
+ "jsonschema": "~0.3.2"
},
"engines": {
"node": "~0.8.20"
diff --git a/test/spec/actions/circularize.js b/test/spec/actions/circularize.js
index 1b846c6f3..cb63f9ad6 100644
--- a/test/spec/actions/circularize.js
+++ b/test/spec/actions/circularize.js
@@ -26,7 +26,7 @@ describe("iD.actions.Circularize", function () {
graph = iD.actions.Circularize('-', projection)(graph);
- expect(graph.entity('-').nodes.slice(0, 4)).to.eql(['c', 'b', 'a', 'd']);
+ expect(graph.entity('-').nodes.slice(0, 4).sort()).to.eql(['a', 'b', 'c', 'd']);
});
it("deletes unused nodes that are not members of other ways", function () {
@@ -40,7 +40,7 @@ describe("iD.actions.Circularize", function () {
graph = iD.actions.Circularize('-', projection, 3)(graph);
- expect(graph.entity('d')).to.be.undefined;
+ expect(graph.entity('a')).to.be.undefined;
});
it("reconnects unused nodes that are members of other ways", function () {
@@ -51,12 +51,48 @@ describe("iD.actions.Circularize", function () {
'd': iD.Node({id: 'd', loc: [0, 2]}),
'e': iD.Node({id: 'e', loc: [1, 1]}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'a']}),
- '=': iD.Way({id: '=', nodes: ['d']})
+ '=': iD.Way({id: '=', nodes: ['a']})
});
graph = iD.actions.Circularize('-', projection, 3)(graph);
- expect(graph.entity('d')).to.be.undefined;
+ expect(graph.entity('a')).to.be.undefined;
expect(graph.entity('=').nodes).to.eql(['c']);
});
+
+ function area(id, graph) {
+ return d3.geom.polygon(_.pluck(graph.childNodes(graph.entity(id)), 'loc')).area();
+ }
+
+ it("leaves clockwise ways clockwise", function () {
+ var graph = iD.Graph({
+ 'a': iD.Node({id: 'a', loc: [0, 0]}),
+ 'b': iD.Node({id: 'b', loc: [2, 0]}),
+ 'c': iD.Node({id: 'c', loc: [2, 2]}),
+ 'd': iD.Node({id: 'd', loc: [0, 2]}),
+ '+': iD.Way({id: '+', nodes: ['a', 'd', 'c', 'b', 'a']})
+ });
+
+ expect(area('+', graph)).to.be.gt(0);
+
+ graph = iD.actions.Circularize('+', projection)(graph);
+
+ expect(area('+', graph)).to.be.gt(0);
+ });
+
+ it("leaves counter-clockwise ways counter-clockwise", function () {
+ var graph = iD.Graph({
+ 'a': iD.Node({id: 'a', loc: [0, 0]}),
+ 'b': iD.Node({id: 'b', loc: [2, 0]}),
+ 'c': iD.Node({id: 'c', loc: [2, 2]}),
+ 'd': iD.Node({id: 'd', loc: [0, 2]}),
+ '-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']})
+ });
+
+ expect(area('-', graph)).to.be.lt(0);
+
+ graph = iD.actions.Circularize('-', projection)(graph);
+
+ expect(area('-', graph)).to.be.lt(0);
+ });
});