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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + Search for forms + + + + + + + + + + + + + + + + + + + + + Attribution + Address + Telephone + Website + + + + + + + Attribution + Address + Telephone + Website + + + + + + Close + + + + + + + + + Add Form + + + + 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); + }); });